C++对象内存布局结构及虚函数调用实现分析(下)
2.4 基于虚函数调用过程中可被利用的2种hook方法
2.4.1 hook虚函数
从图2.6中我们可以看到,虚函数表都存放在rdata段中,比如函数CPlayerObject::OnDamageHp函数存放在地址00402274中,如果我们把自己的函数地址替换到0x00402274处,那么当调用CPlayerObject::OnDamageHp函数时实际上调用到了我们自己的函数内部。我们就可以在函数内进行数据感知或者数据修改,甚至改变游戏逻辑等。比如通过this指针(ecx)我们可以知道哪个对象在被攻击,通过参数nDamageHp我们知道该次伤害掉血量有多少。如果是一个真实的游戏,那么我们基于这些信息就可以实现无敌(this指针为角色时可设置nDamageHp为0),也可以实现秒怪(如果this指针为怪物那么就可以设置nDamageHp为99999999等高伤害)。
2.4.2 hook虚表起始地址
从头图2.7虚函数调用过程总我们可以看到,虚函数调用是先获取虚函数表起始地址,也就是先mov eax, [ecx];这时候如果我们把虚函数起始地址替换从一个我们自己分配的内存块地址,然后把从地址0x00402250开始到0x00402278截止的内存数据拷贝到我们自己分配的内存中去,那么就相当于挟持了整个类的所有需函数。在我们自己构造的需函数表中想hook哪个函数就可以替换哪个函数的函数地址,替换方法和3.1介绍的方法类似。劫持整个函数虚表的好处是没有对代码段和rdata段进行修改,那么在分析过程中比较难发现。
三、c++函数调用基础
3.1 函数的调用约定
在开始之前,还是以代码说话,一般函数调用说明有如下几种方式,值需要参照c++声明和汇编层实现就可以很好的理解函数调用约定:
图3.1
图3.2
3.1.1 __cdecl调用约定
图3.2
从函数调用约定的声明上来看,int __cdecl TestFunctionC(int a, int b, int c, int d, int e, int f)的调用约定我们声明为__cdecl调用方式。从汇编层实现,我们可以看到该方式具有以下特征:1、参数传递都通过堆栈传递;2、函数入栈顺序从右到左;3、堆栈平衡由调用者平衡,add esp, 18h这行命令就是用来平衡堆栈的。另外在IDA中我们看到int TestFunctionA(int a, int b, int c, int d, int e, int f)的调用方式也符合该原则,这说明在cc++写的函数中如果不是类的成员函数,没做特殊调用声明,那么默认采用__cdecl调用约定。
3.1.2 __stdcall调用约定
图3.4
图3.5
从函数调用约定的声明上看,int __stdcall TestFunctionB(int a, int b, int c, int d, int e, int f)的调用约定我们声明为__stdcall 调用方式。从汇编层实现,我们可以看到该方式具有以下特征:1、参数传递都通过堆栈传递;2、函数入栈顺序从右到左;3、堆栈平衡由被调用者平衡,被调用函数内部函数结尾处retn 18h这行命令就是用来平衡堆栈的。
3.1.3 __fastcall 调用约定
图3.6
3.1.4 thiscall 调用约定
Thiscall 调用约定是唯一一个不需要特殊声明也无法人为指定的调用方式,它是指类的普通成员函数调用方式。该调用方式具有以下特点:1、ecx被使用,用作this指针的传递;2、其它参数一律通过堆栈传递,入栈顺序从右到左;3、堆栈平衡有被调用者平衡。
3.2 函数参数个数的确定技巧
基于堆栈传递的部分,要确定参数个数比较简单,通过堆栈平衡部分就可以快速确定参数个数。不过如果有参数是通过寄存器传递的,那么就需要一定技巧来确定寄存器里是否有参数传递进来。如下截图所示:
图3.8
图3.9
确定一个寄存器是否有被当成参数传递,那么最简单的方式就是看该寄存器在使用之前有没有进行数据保存和赋值。如图3.8所示,在对edi进行使用和赋值之前有进行push edi保存操作,所以edi就没有作为参数传递寄存器。如图3.9所示,edx没有进行赋值和保存数据,直接把edx里面的值复制给ebp+b参数了,所以edx是作为参数传递寄存器了。在分析游戏利用游戏函数实现功能的过程中edx就需要特别处理。