为什么这个 ASM 代表 Virtual Function 调用?

逆向工程 拆卸 C++ 静态分析 虚函数
2021-06-26 20:20:57

我指的是之前问过的这个问题: 虚函数调用 asm

我想知道如何才能知道此 ASM 列表是否代表 Virtual Functions 调用?上面链接的问题中提到的 ASM 中有什么内容表明这是虚拟函数调用?

2个回答

间接调用,使用 ecx 作为 this 指针等表明它是一个虚函数调用

让我们以您在查询中引用的示例稍微修改一下并查看反汇编

目录预编译内容

D:\virt>dir /b
virt.cpp

来自示例的源代码经过适当修改

D:\virt>type virt.cpp
01 #include <iostream>
02 class Animal {
03 public:
04     Animal( char *name,char *color) {_name =name; _color=color;};
05     virtual char *getname(){return _name;}
06     virtual char *makeSound() = 0;
07     virtual char *getColor() {return _color;}
08     virtual ~Animal() {};
09 private:
10     char *_name;
11     char *_color;
12 };
13 class Cat : public Animal{
14 public:
15     Cat(char *name,char* color) : Animal(name,color) {};
16     char *makeSound() { return "meow"; }
17     ~Cat() {};
18 };
19 Animal* animals[] = {
20     new Cat("fluffy","  red  "), new Cat("gruffy"," green "),
22     new Cat("mooody","magenta")
23 };
24 int main(){
25     for (int i = 0; i < 3; i++)
26     if (animals[i]){
27         std::cout << animals[i]->getname()      << " is ";
28         std::cout << animals[i]->getColor()     << " and sounds ";
29         std::cout << animals[i]->makeSound()    << std::endl;
30     }
31     for (int i = 0; i < 3; i++)
32     delete animals[i];
33     return 0;
34 }

在 x86 中使用 vs2017 社区编译和执行为 x86

D:\virt>cl /Zi /W4 /analyze /EHsc /nologo /Od virt.cpp /link /release
virt.cpp

D:\virt>virt.exe
fluffy is   red   and sounds meow
gruffy is  green  and sounds meow
mooody is magenta and sounds meow

调用函数的第 26、27、28 行的反汇编注意指针算术
和间接调用,用 <<<<<<<<<<<<<<<

D:\virt>cdb -lines -c "g virt!main;uf .;q" virt.exe | grep -iE " 26| 27| 28"
virt!main+0x33 [d:\virt\virt.cpp @ 26]:
   26 00251223 6800022c00      push    offset virt!__xt_z+0x34 (002c0200)
   26 00251228 8b55fc          mov     edx,dword ptr [ebp-4]
   26 0025122b 8b049528892d00  mov     eax,dword ptr virt!animals (002d8928)[edx*4]
   26 00251232 8b4dfc          mov     ecx,dword ptr [ebp-4]
   26 00251235 8b10            mov     edx,dword ptr [eax]
   26 00251237 8b0c8d28892d00  mov     ecx,dword ptr virt!animals (002d8928)[ecx*4]
   26 0025123e 8b02            mov     eax,dword ptr [edx] <<<<<<<<<
   26 00251240 ffd0            call    eax <<<<<<<<<<<<<<<
   26 00251242 50              push    eax
   26 00251243 68688a2d00      push    offset virt!std::cout (002d8a68)
   26 00251248 e803020000      call    virt!std::operator<<<std::char_traits<char> 
   26 0025124d 83c408          add     esp,8
   26 00251250 50              push    eax
   26 00251251 e8fa010000      call    virt!std::operator<<<std::char_traits<char>
   26 00251256 83c408          add     esp,8
   27 00251259 6808022c00      push    offset virt!__xt_z+0x3c (002c0208)
   27 0025125e 8b4dfc          mov     ecx,dword ptr [ebp-4]
   27 00251261 8b148d28892d00  mov     edx,dword ptr virt!animals (002d8928)[ecx*4]
   27 00251268 8b45fc          mov     eax,dword ptr [ebp-4]
   27 0025126b 8b12            mov     edx,dword ptr [edx]
   27 0025126d 8b0c8528892d00  mov     ecx,dword ptr virt!animals (002d8928)[eax*4]
   27 00251274 8b4208          mov     eax,dword ptr [edx+8] <<<<<<<
   27 00251277 ffd0            call    eax <<<<<<<<<
   27 00251279 50              push    eax
   27 0025127a 68688a2d00      push    offset virt!std::cout (002d8a68)
   27 0025127f e8cc010000      call    virt!std::operator<<<std::char_traits<char>
   27 00251284 83c408          add     esp,8
   27 00251287 50              push    eax
   27 00251288 e8c3010000      call    virt!std::operator<<<std::char_traits<char>
   27 0025128d 83c408          add     esp,8
   28 00251290 68f01b2500      push    offset virt!std::endl<char,std::char_traits<char>
   28 00251295 8b4dfc          mov     ecx,dword ptr [ebp-4]
   28 00251298 8b148d28892d00  mov     edx,dword ptr virt!animals (002d8928)[ecx*4]
   28 0025129f 8b45fc          mov     eax,dword ptr [ebp-4]
   28 002512a2 8b12            mov     edx,dword ptr [edx]
   28 002512a4 8b0c8528892d00  mov     ecx,dword ptr virt!animals (002d8928)[eax*4]
   28 002512ab 8b4204          mov     eax,dword ptr [edx+4] <<<<<<<<<
   28 002512ae ffd0            call    eax <<<<<<<<<
   28 002512b0 50              push    eax
   28 002512b1 68688a2d00      push    offset virt!std::cout (002d8a68)
   28 002512b6 e895010000      call    virt!std::operator<<<std::char_traits<char>
   28 002512bb 83c408          add     esp,8
   28 002512be 8bc8            mov     ecx,eax
   28 002512c0 e8db1a0000      call    ::operator<< (00252da0)

编辑 :

函数指针数组将生成间接调用,如 call qword ptr [reg + offset] 、call qword ptr [mem+offset] 等

如果您正在寻找一种理论方法来区分间接调用的性质,我的回答更具有实用性,下面显示的是函数指针数组的源代码和反汇编

源代码编译和执行

:\>type funarr.cpp
#include <stdio.h>
#include <stdlib.h>
int sum(int a, int b){ return a + b;}
int sub(int a, int b){ return a - b;}
int mul(int a, int b){ return a * b;}
int (*p[3]) (int x, int y) {sum,sub,mul};

int main(int argc,char *argv[])
{
        if(argc == 3)
        {
                int i = atoi(argv[1]);
                int j = atoi(argv[2]);
                int k = (*p[0]) (i, j);
                int l = (*p[1]) (i, j);
                int m = (*p[2]) (i, j);
                printf("%d %d %d", k,l,m );
        }
}
:\>cl /Zi /W4 /analyze /O1 /nologo funarr.cpp /link /release
funarr.cpp

:\>funarr.exe 2 3
5 -1 6

主要拆解

:\>cdb -c "uf funarr!main;q" funarr.exe | awk "/Reading/,/quit/"
0:000> cdb: Reading initial command 'uf funarr!main;q'                                                                  funarr!main:                                                                                                            00007ff7`95f2106c 48895c2408      mov     qword ptr [rsp+8],rbx
00007ff7`95f21071 48896c2410      mov     qword ptr [rsp+10h],rbp
00007ff7`95f21076 4889742418      mov     qword ptr [rsp+18h],rsi
00007ff7`95f2107b 57              push    rdi
00007ff7`95f2107c 4883ec20        sub     rsp,20h
00007ff7`95f21080 488bda          mov     rbx,rdx
00007ff7`95f21083 83f903          cmp     ecx,3
00007ff7`95f21086 754c            jne     funarr!main+0x68 (00007ff7`95f210d4)

funarr!main+0x1c:
00007ff7`95f21088 488b4a08        mov     rcx,qword ptr [rdx+8]
00007ff7`95f2108c e803810200      call    funarr!atoi (00007ff7`95f49194)
00007ff7`95f21091 488b4b10        mov     rcx,qword ptr [rbx+10h]
00007ff7`95f21095 8be8            mov     ebp,eax
00007ff7`95f21097 e8f8800200      call    funarr!atoi (00007ff7`95f49194)
00007ff7`95f2109c 8bd0            mov     edx,eax
00007ff7`95f2109e 8bcd            mov     ecx,ebp
00007ff7`95f210a0 8bf8            mov     edi,eax
00007ff7`95f210a2 ff1558cf0500    call    qword ptr [funarr!p (00007ff7`95f7e000)]
00007ff7`95f210a8 8bd7            mov     edx,edi
00007ff7`95f210aa 8bcd            mov     ecx,ebp
00007ff7`95f210ac 8bf0            mov     esi,eax
00007ff7`95f210ae ff1554cf0500    call    qword ptr [funarr!p+0x8 (00007ff7`95f7e008)]
00007ff7`95f210b4 8bd7            mov     edx,edi
00007ff7`95f210b6 8bcd            mov     ecx,ebp
00007ff7`95f210b8 8bd8            mov     ebx,eax
00007ff7`95f210ba ff1550cf0500    call    qword ptr [funarr!p+0x10 (00007ff7`95f7e010)]
00007ff7`95f210c0 448bc3          mov     r8d,ebx
00007ff7`95f210c3 488d0d76c20400  lea     rcx,[funarr!`string' (00007ff7`95f6d340)]
00007ff7`95f210ca 448bc8          mov     r9d,eax
00007ff7`95f210cd 8bd6            mov     edx,esi
00007ff7`95f210cf e818000000      call    funarr!printf (00007ff7`95f210ec)

funarr!main+0x68:
00007ff7`95f210d4 488b5c2430      mov     rbx,qword ptr [rsp+30h]
00007ff7`95f210d9 33c0            xor     eax,eax
00007ff7`95f210db 488b6c2438      mov     rbp,qword ptr [rsp+38h]
00007ff7`95f210e0 488b742440      mov     rsi,qword ptr [rsp+40h]
00007ff7`95f210e5 4883c420        add     rsp,20h
00007ff7`95f210e9 5f              pop     rdi
00007ff7`95f210ea c3              ret
quit:

没有 100% 确定的方法来区分虚拟调用和函数指针调用,但有一些强有力的提示。

  1. 虚函数表 (vftable)通常位于对象的最开始处,因此假设对象的地址存储在 中objectreg,您应该会看到类似

    mov vftreg, [objectreg]
    
  2. 根据使用的 ABI,对象地址(this指针)被传递给虚方法,通常作为第一个参数或在单独的寄存器中。在 Microsoft x86 中,ecx寄存器用于此目的,因此常见模式是:

    mov ecx, objectreg
    
  3. 调用是使用 vftable 中的一个槽来执行的。它可以直接使用来自保存 vftable 的寄存器的位移来完成:

    call [vftreg+slotoff]
    

或者通过一个中间寄存器:

  mov callreg, [vftreg+slotoff]
  call callreg

slotoff 应该是指针大小的倍数,可能为零。

如果方法有参数,它们也会被加载(通常this参数最后初始化,就在调用之前)。

您还可以在关于该主题的旧文章中找到内容丰富的信息