虚函数逆向分析
虚函数的机制
在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