游戏函数调用及内嵌CALL细节须知

发表于2016-09-06
评论0 5k浏览
  在针对游戏制作一些辅助工具时,除了修改游戏数据外,很多情况是直接通过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的实现的具体细节基本讲述完毕。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签: