虚函数逆向分析

虚函数的机制

在C++中的类中有虚函数时,编译器会将该类的所有虚函数首地址保存在一张表里面,这张表被称为虚函数地址表,同时会在类中的首四个字节添加一个虚表指针来指向虚函数表的首地址。

分析构造函数可以很清楚的看到:

 

00F718D0  push        ebp  
00F718D1 mov         ebp,esp  
00F718D3 sub         esp,0CCh  
00F718D9 push       ebx  
00F718DA push       esi  
00F718DB push       edi  
00F718DC push       ecx  
00F718DD lea         edi,[ebp-0CCh]  
00F718E3 mov         ecx,33h  
00F718E8 mov         eax,0CCCCCCCCh  
00F718ED rep stos   dword ptr es:[edi]  
00F718EF pop         ecx  
00F718F0 mov         dword ptr [this],ecx  
00F718F3 mov         eax,dword ptr [this]  
00F718F6 mov         dword ptr [eax],offset CObj::`vftable' (0F79D04h)  
00F718FC mov         eax,dword ptr [this]  
00F718FF mov         dword ptr [eax+4],1  
00F71906 mov         eax,dword ptr [this]

;这里的00F718F6 mov         dword ptr [eax],offset CObj::`vftable' (0F79D04h) 指令,就是把虚函数表的首地址给虚表指针。

虚表初始化

对象的虚表指针初始化是通过编译器自动添加,虚表指针来处理。

虚表信息在编译后会被链接到对应的执行文件中,因此虚表地址是一个相对固定的地址。虚函数表中函数的顺序依据函数的声明顺序处理,先声明的放在前面。

可以通过函数将this指针的首地址初始化为虚表首地址来判断是否为构造函数

虚函数调用流程

在初始化后,当其他代码访问对象的虚函数时,会根据对象的首地址,取出对应虚表元素。当函数被调用时,会间接访问虚表,得到对应的虚函数,并调用执行。是一个需要多次寻址的间接调用过程。

只有使用对象的指针或引用来调用虚函数的时候才会用到该流程,如果是直接用对象来处理就根本用不到虚函数,根本没有构成多态性。

#include<iostream>
using namespace std;

class CObj
{
public:
int a = 1;
virtual void show()
{
printf("虚函数show\n");
}
void fun1()
{
printf("成员函数fun1\n");
}
};
void show2()
{
printf("外部函数\n");
}
int main()
{
CObj obj;
CObj* pobj = &obj;
pobj->show();
pobj->fun1();
return 0;
}
    CObj obj;
00FB1B82 lea         ecx,[obj]  
00FB1B85 call       std::operator<<<std::char_traits<char> > (0FB14B0h)  
CObj* pobj = &obj;
00FB1B8A lea         eax,[obj]  
00FB1B8D mov         dword ptr [pobj],eax  
pobj->show();
00FB1B90 mov         eax,dword ptr [pobj]  
00FB1B93 mov         edx,dword ptr [eax]  
00FB1B95 mov         esi,esp  
00FB1B97 mov         ecx,dword ptr [pobj]  
00FB1B9A mov         eax,dword ptr [edx]  
00FB1B9C call       eax  
00FB1B9E cmp         esi,esp  
00FB1BA0 call       __RTC_CheckEsp (0FB12A3h)  
pobj->fun1();
00FB1BA5 mov         ecx,dword ptr [pobj]  
00FB1BA8 call       CObj::CObj (0FB14FBh)  
return 0;

可以很清楚的看到在通过指针或引用来调用虚函数的时候和直接调用函数非常不一样

    pobj->show();
00FB1B90 mov         eax,dword ptr [pobj]  
;变量的首地址,得到的是虚表指针
00FB1B93 mov         edx,dword ptr [eax]  
;把虚表指针的首个虚函数地址给edx

00FB1B95 mov         esi,esp  
00FB1B97 mov         ecx,dword ptr [pobj]
;传递this指针给ecx,这是thiscall的标志
00FB1B9A mov         eax,dword ptr [edx]  
;拿到虚函数表的第一个地址,也就是第一个虚函数
00FB1B9C call       eax  
;访问eax中对应的虚函数的地址
00FB1B9E cmp         esi,esp  
00FB1BA0 call       __RTC_CheckEsp (0FB12A3h)  

虚函数的调用,先选一个寄存器来存放虚函数的地址,然后把ecx作为this指针传进去再调用虚函数地址。

如何存放虚函数地址:先取指针的首地址的前四位,然后这个前四位是一个虚函数地址指向的是虚函数表的首地址,然后取这个指针的内容,也就是得到虚函数表的首地址,然后再根据偏移得到对应要处理的虚函数。

虚函数的识别

虚函数的特征:1 类中隐藏定义了一个数据成员

2 该数据成员在首地址,而且占4个字节

3 这个数据成员初始化某个数组的首地址

4 地址属于数据区是一个固定的地址

5 数组内每个元素都是函数指针

6 这些函数被调用前都是先传ecx中给this指针

7 还有可能会间接调用this指针