动态获取API入口地址

Home / Article MrLee 2016-6-27 3524

在本文中介绍另一个相对独立的技术:动态获取API 入口地址。文章附带 的源码有两个工程:GetApiAddr 为一个独立的动态获取 API 入口地址的程序;另一个 Virus 为在上一篇文章基础上进行扩展的病毒程序,这个程序把工程目录下的 test.exe 复制一份 为 test_virus.exe并在这个文件上添加动态获取API入口地址的代码,如果程序运行成功, 打开 test_virus.exe后会弹出动态获取的函数MessageBox的内存地址。
为什么要动态获取
在 Windows的安装目录下有许多dll文件,这些dll文件其实就是一些功能模块,说白 了就是里面存放了一些具有一定功能的函数俗称API,应用程序要在windows平台上运行必 须要调用这些函数。Windows在启动的时候会把一些重要的模块(如Kernel32.dll)装入内 存,这些文件装入内存后,里面的每一个功能函数就对应了一个确定的内存地址,应用程序 调用这些函数时只要把用到的参数压入堆栈,call 对应的函数地址就可以了。高级语言编 程时编译器会在生成的可执行文件中保存这些函数的地址信息,一个完整的exe 文件运行时 系统会自动对这些函数地址进行修正,我们不需要关注其底层细节。但作为一个病毒,它的 任务是把自己复制到其它可执行文件中去执行,病毒代码本身并不是一个完整的exe 文件, 但宿主文件并不会自动给病毒代码服务,让病毒代码里的API可以正常调用,所以病毒代码 必须有自己动态获取API地址的能力。 动态获取 API 入口地址也不难,只要调用两个 API 就可以实现:LoadLibrary 和 GetProcAddress,这两个函数都是位于 kernel32.dll 中的,前者为导入 dll 文件,后者通 过函数名在这个dll文件中得到函数地址。但这两个也是系统的API,病毒代码里这两个函 数的地址怎样得到呢?
原理 这个技术的原理是一些前辈们总结的经验,这里简单说一下。Windows在进入应用程序 之前是运行在模块kernel32.dll的某个函数中的(通过对windows的反汇编可以发现)。按 照运行原理,进入应用程序之前要先把返回地址(该地址位于kernel32.dll中)压入栈中, 再转向应用程序去执行,也就是说在刚刚进入应用程序后栈顶是一个位于 kernel32.dll 中 的地址,我们得到这个地址后向可以向系统内存的低地址进行搜索得到kernel32.dll的装 载基址。因为 dll 文件也是标准的 PE 文件,所以得到装载基址后,可以通过搜索 kernel32.dll的导出表得到GetProcAddress的内存地址,然后用这个函数和kernel32.dll 的基址得到LoadLibrary的地址,于是问题解决了。
VC 编译器出现的问题
做底层工作很少不与编译器打交道,上面的原理放在不同的编译器上就要有不同的解决 方法。VC 编译器生成的可执行文件在执行你自己的代码之前已经进行了很多额外的操作, 我们称之为启动函数。如果你试图用[esp]来得到返回地址的话你不会成功,因为执行你的 代码的时候已经经过了多次的压栈操作,栈顶早已不是想要的返回地址了,所以我们必须通 过对可执行文件的反汇编跟踪得到都经过了哪些变换,从而在程序中得到这个地址。 下面对工程目录下的test工程进行反汇编,来介绍一下分析思路。 1. 用OllyDbg载入生成的test.exe文件,指针停在00403510处的启动函数处(这个 地址与大家的可能不一样)。此处你会发现栈顶值为7C817077,这个地址正好位于 kernel32.dll中。开始的几句指令是编译器标志:push ebp?? mov ebp , esp 2. 执行完这两句之后 ebp 所指的内存恰是栈顶,后面又执行了很多代码,这些代码执 行完后 esp早已被改了,但是ebp 没有变(按编译器的原则这个值基本不变)。 3. 向下到004035F4处,这里是一个call,正好去调用我们的主函数代码,跟踪进入。 4. 进入之后发现又执行了一次 push ebp??? mov ebp , esp,再向下,ebp的值又基本 没有变。 好了,我们开始分析一下吧。进入启动函数执行完push ebp?? mov ebp , esp后,ebp 所指向的地址正好是(存放返回地址的地址)-4,也就是在进入主函数之前,我们可以通 过[ebp+4]得到返回地址。进入主函数之后,又执行了一次这个过程,此时[ebp]就是原ebp 的值,我们可以通过[[ebp]+4]得到返回地址。代码如下:
mov eax , [ebp]
  mov eax , [eax+4]
  mov addr,eax

这段代码是工程 GetApiAddr 中的实现方法,因为病毒代码并不需要经过启动代码,所 以工程 virus 的实现方法略有所不同,但跟踪分析的原理是一样的,读者可以去分析练习一 下。  搜索 kernel32.dll 的基址 按 windows系统的设计,装入 dll 文件时要从一个独立页开始,装载基址处的内容也是 一个标准 PE 文件的开始。我们可以搜索返回地址低地址附近几个页的开始位置,看看是否 具有 DOS 文件头,NT 文件头等 PE 文件的起始标志,有这个标志的页就是 kernel32.dll 装 载的起始页。按这个方法,首先要得到返回地址所在页的起始地址,方法如下: KernelBase = KernelBase & 0Xffff0000; //与后面的值进行并操作,得到页起始地址 然后检查本页起始处是否有PE文件的起始标志,如果没有则查找低地址页的起始位置, 循环下去直到找到kernel32.dll的基址。代码如下:
IMAGE_DOS_HEADER *doshead;
while(KernelBase >= 0X70000000) 
寻找 GetProcAddress的内存地址
//如果搜索到0X70000000,还没有找到的话说明已经不可能再找到了,直接退出
//此句防止一个错误的值使程序出现异常
{
 //首先检查dos文件头
  doshead = (IMAGE_DOS_HEADER*)KernelBase;
 if(doshead->e_magic == IMAGE_DOS_SIGNATURE)
 {
  //再检查 NT 文件头
   IMAGE_NT_HEADERS* nthead;
   nthead = (IMAGE_NT_HEADERS*)((LPBYTE)doshead+doshead->e_lfanew);
  if(nthead->Signature == IMAGE_NT_SIGNATURE)
  {
   break; //成功了,退出
  }
 }
 KernelBase-=0x10000; //向低地址移动一个页面
}

寻找 GetProcAddress的内存地址 这个搜索过程的原理是搜索 kernel32.dll 的导出表,把导出函数的名字与字符串 GetProcAddress进行匹配,相等时,就可以得到函数的基址了。学习这部分关键是对PE文 件的导出表格式要有所了解,详细知识可以去参考罗云彬的《windows 环境下 32 位汇编语 言程序设计》,看雪论坛的《加密与解密》等书,这里我就不费篇幅去写了。代码如下:
DWORD AddrOfGetProcAddr , AddrOfLoadLib;
IMAGE_DOS_HEADER* pFile1; //指向dos文件头
IMAGE_NT_HEADERS* pFile2; //指向 nt文件头
pFile1 = (IMAGE_DOS_HEADER* )KernelBase;
pFile2 = (IMAGE_NT_HEADERS*)((PBYTE)pFile1 + pFile1->e_lfanew);
//检查文件的合法性,此处略去,详见源代码
IMAGE_EXPORT_DIRECTORY *pExport;
pExport=(IMAGE_EXPORT_DIRECTORY*)((PBYTE)pFile1+pFile2->OptionalHeader.
DataDirectory[0].VirtualAddress);
//以下在导出表中搜索名字为"GetProcAddress"的导出函数
char *FunName;
DWORD *AddrOfNameRVA;
WORD *AddrOfNameOrRVA;
AddrOfNameRVA = (DWORD*)(KernelBase + pExport->AddressOfNames);
for (int i = 0 ; i < (int)pExport->NumberOfNames ; i++)
{
 //得到一个导出函数的名字  
 
  FunName = (char*)(KernelBase + AddrOfNameRVA[i]);
 //函数名与字符串"GetProcAddress"进行比较
//为避免使用API,此处必须自己实现比较过程
 BOOL eql = 1;  //布尔类型变量
 for (int j = 0 ; j < 15 ; j ++) { if (GetProcAddrName[j] != FunName[j]) { eql = 0; break; } } //如果 eql为 1,说明找到了,再去得到这个导出函数的基址 if (eql) { AddrOfNameOrRVA=(WORD*)(KernelBase+pExport->AddressOfNameOrdinals)
;
 int num = 0;
  num = pExport->Base + AddrOfNameOrRVA[i];
  DWORD *AddrOfFun;
 AddrOfFun = (DWORD*)(KernelBase + pExport->AddressOfFunctions);
 //地址保存在变量AddrOfGetProcAddr中
  AddrOfGetProcAddr = KernelBase + AddrOfFun[num - 1];
 break;
 }

此时我们已经得到了 GetProcAddress 的基址,下面是调用这个函数得到 LoadLibrary 的基址,然后再调用LoadLibrary导入 User32.dll,再用 GetProcAddress得到MessageBox 的基址。这些代码都很简单,这里不再列举了。需要注意的是在Virus工程中的VirusCode 函数中,不能出现任何的函数调用,甚至 cout 都不行。另外,病毒代码中也不能出现任何字符串,因为字符串在赋值时会被放在数据区中,不能随代码一起复制,所有用到的字符串 都要以数组的方式进行赋值,比如下面的赋值就会出错: char *GetProcAddrName = “GetProcAddress”; 我们应该这样赋值: char GetProcAddress[] {'G','e','t','P','r','o','c','A','d','d','r','e','s','s','\0'};   后边这种方法的字符串保存在栈中,并且会在代码运行时对字符串进行初始化。   好了,到这里本技术就介绍完了.

本文链接:https://www.it72.com/9479.htm

推荐阅读
最新回复 (0)
返回