游戏函数调用及内嵌CALL细节须知
发表于2016-09-06
在针对游戏制作一些辅助工具时,除了修改游戏数据外,很多情况是直接通过call游戏函数来实现对应功能,例如频道喊话、打坐修炼等功能。那么如何做到正确的游戏函数调用呢?我认为应该从参数确定、堆栈平衡、上下文环境保护、函数返回值等几个方面来考虑。
1、参数确定
参数确定一般需考虑参数个数、否有寄存器传值、是否有this指针以及是否其他隐含信息等几个方面。我们现在对这几个方面逐一讲解。
1)、参数个数
方法一:函数参数个数首先可以通过在调用前push了多少个变量到堆栈中简单确认,但是这样确认不一定准确。还需配合分析游戏函数中使用了堆栈中的哪些值。例如代码清单1所示代码,可以看到call MessageBoxA前面有四个push,并且根据经验判断,基本上可以确认MessageBoxA有四个参数:
代码清单1:
text:00411400 push 0 ; uType
text:00411402 push offset Caption ; "HelloWorld"
text:00411407 push offset Caption ; "HelloWorld"
text:0041140C push 0 ; hWnd
text:0041140E call ds:__imp__MessageBoxA@16 ; MessageBoxA(x,x,x,x)
方法二: 直接通过游戏函数中add esp ,XXX或者retn XXX来确认游戏参数,参数个数一般等于XXX / 4。当然,这种判断方法要首先排除寄存器传值。至于寄存器传值情况则在后面会讲到。代码清单2中 add esp, 8与代码清单3 中retn 8都可以分别判断出cdecladd 与stdcalladd有8/4= 2 个参数。
代码清单2:
text:0041152E push 2 ; b
text:00411530 push 1 ; a
text:00411532 call j_?stdcalladd@@YGHHH@Z ; stdcalladd(int,int)
text:00411537 mov [ebp+sum1], eax
text:0041153A push 3 ; b
text:0041153C push 2 ; a
text:0041153E call j_?cdecladd@@YAHHH@Z ; cdecladd(int,int)
text:00411543 add esp, 8
text:00411546 mov [ebp+sum2], eax
text:00411549 mov esi, esp
代码清单3:
text:004113E0 ; int __stdcall stdcalladd(int a, int b)
text:004113E0 ?stdcalladd@@YGHHH@Z proc near ; CODE XREF: stdcalladd(int,int)j
text:004113E0
……// 省略掉中间代码
text:00411407 mov esp, ebp
text:00411409 pop ebp
text:0041140A retn 8
2)、是否有寄存器传值
这个确认一般比较简单,分析被调用游戏函数代码,假如一个寄存器没有被赋值就被使用(存在读取寄存器中值的操作),那么一般可以确认该函数有通过此寄存器传递值。例如代码清单4中,mov [ebp+b], edx 与mov [ebp+a], ecx两句前面并没有对edx与ecx赋值,但是这里却直接从里面取值,那么基本上可以判断此函数含有寄存器传值,然后通过函数尾部的ret 4可以判断出此函数除了通过寄存器传值外,含通过堆栈传递了一个参数。代码清单5为调用此函数的过程。
代码清单4:
text:00411450 ; int __fastcall fastcalladd(int a, int b, int c)
text:00411450 ?fastcalladd@@YIHHHH@Z proc near ; CODE XREF: fastcalladd(int,int,int)j
text:00411450
text:00411450 var_D8 = byte ptr -0D8h
text:00411450 b = dword ptr -14h
text:00411450 a = dword ptr -8
text:00411450 c = dword ptr 8
text:00411450
text:00411450 push ebp
text:00411451 mov ebp, esp
text:00411453 sub esp, 0D8h
text:00411459 push ebx
text:0041145A push esi
text:0041145B push edi
text:0041145C push ecx
text:0041145D lea edi, [ebp+var_D8]
text:00411463 mov ecx, 36h
text:00411468 mov eax, 0CCCCCCCCh
text:0041146D rep stosd
text:0041146F pop ecx
text:00411470 mov [ebp+b], edx
text:00411473 mov [ebp+a], ecx
text:00411476 mov eax, [ebp+a]
text:00411479 add eax, [ebp+b]
text:0041147C add eax, [ebp+c]
text:0041147F pop edi
text:00411480 pop esi
text:00411481 pop ebx
text:00411482 mov esp, ebp
text:00411484 pop ebp
text:00411485 retn 4
代码清单5:
text:004114C9 push 3 ; c
text:004114CB mov edx, 2 ; b
text:004114D0 mov ecx, 1 ; a
text:004114D5 call j_?fastcalladd@@YIHHHH@Z ; fastcalladd(int,int,int)
text:004114DA mov [ebp+sum3], eax
3)、是否有this指针。
首先,为什么要判断是否有this指针?如果一个函数有this指针,那么你call它的时候也必须设置this指针。否则程序运行时会崩溃;其次,你得先了解thiscall调用约定(针对类成员函数调用约定)。对于this指针,一般情况下有两中传递(根据函数参数个数是否确定)方式:一是当函数参数个数不固定时,将this指针在所有参数压入栈之后将其作为最后一个参数压入栈;二是当函数参数个数固定时,通过ECX寄存器来传递。
接下来,看一下如何确认函数参数是否包含this指针。由于游戏功能函数的参数个数一般都是固定的。因此一般采用的都是通过ECX来传递this指针。那么可以通过判断被调用的游戏函数是否通过ECX寄存器传值且ECX看起来像是一个结构的起始地址(可以通过后文是否有等价于[ecx+XXX]的操作来判断),那么一般情况下此函数便含有this指针。如代码清单7中,首先,ecx没有(等价于)被赋值的情况下被使用,可以判断为是ECX寄存器传值;其次通过后文mov eax, [eax+4]与 add eax, [ecx] 两句可以判断出ecx为某结构的起始地址。一般情况下可以猜测ECX为this指针。
代码清单7:
text:00411C80 push ebp
text:00411C81 mov ebp, esp
text:00411C83 sub esp, 0CCh
text:00411C89 push ebx
text:00411C8A push esi
text:00411C8B push edi
text:00411C8C push ecx
text:00411C8D lea edi, [ebp+var_CC]
text:00411C93 mov ecx, 33h
text:00411C98 mov eax, 0CCCCCCCCh
text:00411C9D rep stosd
text:00411C9F pop ecx
text:00411CA0 mov [ebp+this], ecx
text:00411CA3 mov eax, [ebp+this]
text:00411CA6 mov eax, [eax+4]
text:00411CA9 mov ecx, [ebp+this]
text:00411CAC add eax, [ecx]
text:00411CAE pop edi
text:00411CAF pop esi
text:00411CB0 pop ebx
text:00411CB1 mov esp, ebp
text:00411CB3 pop ebp
text:00411CB4 retn
4)、是否有其它隐含信息
对于一个参数来讲,一般情况下会有数值、buffer、结构体三种情况。对于数值和buffer,利用IDA反汇编一下很容易就可以看出来。对于结构体,IDA往往隐含结构体大小与结构体的每一个参数数据类型。这个对于我们方便调用游戏函数至关重要。一般情况下,看到出现连续的一个“变量+XXX” 这样的汇编代码,应该第一时刻分析其是否为数组或者结构体。例如代码清单8中,参数为a1为int型。但是后面的两个操作*(_DWORD*)a1 = operator new [0xAu]与*(_DWORD*)(a1 + 4) = 10,可以很简单的看出a1为一个结构体指针,此结构体的前两个成员变量为一个指向一段缓冲区的buf指针和此缓冲区的长度。并且也可以看出来sub_4114f0可能为初始化此结构体的函数。
代码清单8:
2、堆栈平衡
当使用内嵌call的方式调用游戏函数时,最重要要考虑的是堆栈平衡。假如堆栈不平衡,那么当你调用玩游戏函数后,程序会崩溃。所以平衡堆栈极其重要。那么如何去平衡堆栈呢。首先你的知道函数平衡堆栈有两种方式:调用者平衡堆栈和被调用者平衡堆栈。如何区分这两种呢?很简单,只要查看游戏中调用对应函数的call后面有没有add esp,XXX 或者查看游戏功能函数后面返回为Retn XXX则表示为被调用这平衡堆栈。否则则说明是调用者平衡堆栈。
对于调用者平衡堆栈,只需将游戏中call对应函数后面add esp,XXX同样写到自己的call后面即可,如代码清单2中的call j_?cdecladd@@YAHHH@Z 后面的add esp, 8便是调用者用来平衡堆栈的。
对于被调用者平衡堆栈,这个自己调用游戏函数就不用考虑了,只需保证参数个数正确即可;如代码清单4中的ret 8为被调用者用来平衡堆栈的。
3、上下文环境保护
对于采用内嵌call的方式调用游戏函数,有可能会对寄存器直接操作。这样就必须确保在自己代码执行前后的寄存器得值不会改变,进而不会影响到其它程序的正常运行。所以必须进行上下文环境保护。
对于上下文环境的保护,如代码清单10所示,一般情况下是通过在执行自己代码之前将所有寄存器的值保存到堆栈中,然后执行完自己程序之后再将寄存器原有的值从堆栈中拿出来重新保存在寄存器中。:
代码清单9:
__asm{
pushad
pushfd
// 你的call程序
popfd
popad
}
4、函数返回值
当一个函数有返回值时有如下几种情况(以下几点情况具体实例在代码清单10中):
1)、Bool 、char等不大于8bit的结构通过al传递,例如如retbool函数返回值;
2)、short等大于8bit但不大于16bit的结构通过ax传递,如retshort;
3)、int、指针大于16bit但不大于32bit的结构通过eax传递;
4)、int64等大于32bit且不大于64bit结构通过edx:eax两个寄存器传递;
5)、其它大小的结构返回时都是将其地址通过eax返回;
6)、注意一点, float/double作为返回值时,不采用eax寄存器;而是采用浮点寄存器ST[0]返回。
代码清单10:
text:00411750 push ebp
text:00411751 mov ebp, esp
text:00411753 sub esp, 1B8h
text:00411759 push ebx
text:0041175A push esi
text:0041175B push edi
text:0041175C lea edi, [ebp+var_1B8]
text:00411762 mov ecx, 6Eh
text:00411767 mov eax, 0CCCCCCCCh
text:0041176C rep stosd
text:0041176E call j_?retbool@@YA_NXZ ; retbool(void)
text:00411773 mov [ebp+ret1], al
text:00411776 call j_?retshort@@YAFXZ ; retshort(void)
text:0041177B mov [ebp+ret2], ax
text:0041177F call j_?retint@@YAHXZ ; retint(void)
text:00411784 mov [ebp+ret3], eax
text:00411787 call j_?retint64@@YA_JXZ ; retint64(void)
text:0041178C mov dword ptr [ebp+ret4], eax
text:0041178F mov dword ptr [ebp+ret4+4], edx
text:00411792 call j_?retdouble@@YANXZ ; retdouble(void)
text:00411797 fstp [ebp+ret5]
text:0041179A lea eax, [ebp+result]
text:004117A0 push eax ; result
text:004117A1 call j_?retstruct@@YA?AURetStrcut@@XZ ; retstruct(void)
text:004117A6 add esp, 4
text:004117A9 mov ecx, 0Dh
text:004117AE mov esi, eax
text:004117B0 lea edi, [ebp+var_1B4]
text:004117B6 rep movsd
text:004117B8 mov ecx, 0Dh
text:004117BD lea esi, [ebp+var_1B4]
text:004117C3 lea edi, [ebp+ret6]
text:004117C6 rep movsd
text:004117C8 push edx
text:004117C9 mov ecx, ebp ; frame
text:004117CB push eax
至此,对于游戏函数调用及内嵌call的实现的具体细节基本讲述完毕。