在反外挂系统中,经常会检测函数的返回地址,确认函数的返回地址在规定的范围之内,从而保证,游戏程序中的函数,不被外挂所调用。这种检查方式就涉及到一个基本的技术问题,如何获得函数的返回地址?

例如下面的第一段代码:

#include<stdio.h>
int main()
{
   getchar();
   return 0;
}


 

    非常简单的一段程序,那么我们如何获得该函数的起始地址和返回地址呢?起始地址获取非常容易,如下:

#include<stdio.h>
int main()
{
   printf("%0x\n",main);
   getchar();
   return 0;
}


那么如何获得函数的返回地址呢?这个就相对来说比较困难。我们先看第一段代码反汇编后的结果:

 

#include<stdio.h>
intmain()
{
009919E0  push       ebp  
009919E1  mov        ebp,esp  
009919E3  sub        esp,0C0h  
009919E9  push       ebx  
009919EA  push       esi  
009919EB  push       edi  
009919EC  lea        edi,[ebp-0C0h]  
009919F2  mov        ecx,30h  
009919F7  mov        eax,0CCCCCCCCh  
009919FC  rep stos   dword ptr es:[edi]  
   getchar();
009919FE  mov        esi,esp  
00991A00  call       dword ptr [__imp__getchar (9982B0h)] 
00991A06  cmp        esi,esp  
00991A08  call       @ILT+295(__RTC_CheckEsp) (99112Ch) 
   return 0;
00991A0D  xor        eax,eax  
}
00991A0F  pop        edi  
00991A10  pop        esi  
00991A11  pop        ebx  
00991A12  add        esp,0C0h  
00991A18  cmp        ebp,esp  
00991A1A  call       @ILT+295(__RTC_CheckEsp) (99112Ch) 
00991A1F  mov         esp,ebp 
00991A21  pop        ebp  
00991A22  ret


 

代码开始部分,先保存ebp的内容,然后将ESP的内容写入EBP

009919E0  push       ebp

009919E1  mov        ebp,esp

 

汇编指令call会做两件事情,其一,将call指令后面的一条指令的地址压入栈中,无条件跳转到call指令的调用地指处,开始执行子程序。

 

call指令对应的ret指令,则开始执行call指令后面的一条指令。

 

那么,ret指令如何知道call指令后面一条指令的地址呢?因为call指令已经将这条指令压入到了栈中,所以ret指令可以找到call指令后的一条指令的地址。

 

既然ret指令可以找到call的返回地址,也就是call的下一条指令的地址,那么我们也可以找到!!!

 

main函数在执行前以及执行过程中,栈的分布如下:

 

如何获得C语言函数起始地址和返回地址_技术

 

通过以上几张图,我们可以清楚的看到,main函数的返回地址在[EBP+4]处。所以,获得main函数的返回地址的代码如下:

 

#include<stdio.h>
int main()
{
   int re_addr;
   __asm
   {
      mov eax,dword ptr [ebp+4]
      mov re_addr,eax
   }
   printf("%0X\n",re_addr);
   getchar();
   return 0;
}


 

其中__tmainCRTStartup()函数调用了main函数,调用的汇编代码如下:

            mainret = main(argc, argv, envp);

00B81926  mov        eax,dword ptr [envp (0B87140h)] 

00B8192B  push       eax 

00B8192C  mov        ecx,dword ptr [argv (0B87144h)] 

00B81932  push       ecx 

00B81933  mov        edx,dword ptr [argc (0B8713Ch)] 

00B81939  push       edx 

00B8193A call        @ILT+300(_main)(0B81131h) 

00B8193F  add        esp,0Ch 

00B81942  mov        dword ptr [mainret (0B87154h)],eax 

 

可以看出,call main指令后的一条指令的地址为:00B8193F而我们获得的main的返回地址如下:

B8193F

 

说明我们获得的结果正确。

 

对于其他函数的情况类似,下面笔者就把获得某个函数的返回值得功能,做成一个函数,提供给大家,如下:

 

#include<stdio.h>
 
int Get_return_addr()
{
   int re_addr;
   __asm
   {
      mov eax,dword ptr [ebp]
      mov ebx,dword ptr [eax+4]
      mov re_addr,ebx
   }
   returnre_addr;
 
}
 
int main()
{
   intre_addr=Get_return_addr();
   printf("%0X\n",re_addr);
   getchar();
   return 0;
}


 

Get_return_add函数中,为什么会多几条汇编指令呢?大家可以自行思考。