游戏安全实验室 首页 技术入门 查看内容

 阅读目录

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

发布于:2016-6-28 14:46   |    130380次阅读 作者: 管理员    |   原作者: TP   |   来自: 原创

在针对游戏制作一些辅助工具时,除了修改游戏数据外,很多情况是直接通过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 espXXX同样写到自己的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的实现的具体细节基本讲述完毕。


 *转载请注明来自游戏安全实验室(GSLAB.QQ.COM)

分享到:
踩0 赞0

收藏

上一篇:远线程注入

下一篇:常见的6种外挂获取执行时机方法介绍

最新评论
B Color Image Link Quote Code Smilies

发表评论