WIN32汇编语言教程:第17章 PE文件 · 17.6 应用实例(1)
本章的前5节介绍了PE文件的结构,所举的例子只涉及对PE文件的静态分析,但在实际的应用中还有很多其他方面的内容,比如对PE文件加密、压缩,编写杀毒软件等都涉及修改及重组PE文件,另外,象API Hook,PE文件的内存映像Dump等应用则涉及分析内存中的PE映像。
本节将用另外的两个例子来简单说明这些方面的应用,17.6.1节将演示如何从内存中动态获取某个API的地址;17.6.2节将演示如何在PE文件上添加一段可执行代码。
学习这个例子是为了加深对PE文件到内存的映射和使用导出表这两方面的知识的理解。
在Win32环境下编程,不使用API几乎是不可能的事情,一般情况下,在代码中使用API不外乎两种办法:第一是编译链接的时候使用导入库,那么生成的PE文件中就会包含导入表,这样程序执行时会由Windows装载器根据导入表中的信息来修正API调用语句中的地址;第二种方法是使用LoadLibrary函数动态装入某个DLL模块,并使用GetProcAddress函数从被装入的模块中获取API函数的地址。
假如有一段代码由于特殊的原因无法(或者实现的难度很大)在PE文件中使用导入表,比如17.6.2节中被加到其他PE文件上的代码就是如此,在这种代码中,如何使用API函数呢?有人可能会说,那就用第二种办法好了!听起来不错,但这里有一个“先有鸡还是先有蛋”的问题,固然所有的API函数都可以用LoadLibrary函数和GetProcAddress函数配合来动态获取,但这两个函数本身也是API,又怎样首先得到它们的地址呢?
本节的内容讲述如何用一种变通的办法来解决这个问题。
1. 原理
在DOS环境下,一个可执行文件既可以用INT 21h/4ch来结束程序,也可以用一个Ret指令来结束程序,实际上,在Win32下也可以用这种方法来结束程序,虽然大部分的Win32程序都使用ExitProcess函数来终止执行,但是使用Ret指令确实也是有效的。
如图17.9所示,当父进程要创建一个子进程的时候,它会调用Kernel32.dll中的CreateProcess函数,CreateProcess函数在完成装载应用程序后,会将一个返回地址压入堆栈并转而执行应用程序,如果应用程序用ExitProcess函数来终止,那么这个返回地址没有什么用途,但如果应用程序使用Ret指令的话,程序就会返回CreateProcess函数设定的地址。也就是说,应用程序的主程序可以看作是被Windows调用的一个子程序。
图17.9 Win32可执行文件退出的示意图
那么Ret指令返回到的地址上究竟有什么指令呢?用Soft-ICE看看就会发现,它包含一句push eax指令和一句call ExitThread,也就是说,假如用Ret指令返回的话,Windows会替程序去调用ExitThread函数,如果这是进程的最后一个线程的话,ExitThread函数又会自动去调用ExitProcess,这样程序就会被终止执行。
从这个过程可以得到一个很重要的数据,那就是堆栈中的返回地址,这个地址只要在程序入口的地方用[esp]就可以将它读出,说它重要是因为它位于Kernel32.dll模块中,而LoadLibrary和GetProcAddress函数正是处于Kernel32.dll模块中,换句话说就是,我们得到的地址和这两个函数近在咫尺,完全可以从这个地址经过某种算法来找到这两个函数的入口地址,得到这两个函数的入口地址以后,什么问题都解决了。
结合本章前面内容中提到过的两个事实,可以确定这种想法是可行的。
首先,PE文件被装入内存后(包括Kernel32.dll文件),除了一些可丢弃的节如重定位节以外,其他的内容都会被装入内存,这样获取导出函数地址所需的PE文件头、导出表等数据都存在于内存中;第二,PE文件被装入内存时是按内存页对齐的,只要从Ret指令返回的地址按照页对齐的边界一页页地向低地址搜寻,就必然可以找到Kernel32.dll文件的文件头位置。
好了,有了Kernel32.dll的基址,接下来的事情就是按照17.3.1节的第4点所列的过程去操作了!
2. 例子
例子演示了上面所设想的功能,全部的源代码包含在本书所附光盘的Chapter17\NoImport目录中,主程序NoImport.asm的内容如下:
.386 .model flat,stdcall option casemap:none ;#################################################################### include windows.inc _ProtoGetProcAddress typedef proto :dword,:dword _ProtoLoadLibrary typedef proto :dword _ProtoMessageBox typedef proto :dword,:dword,:dword,:dword _ApiGetProcAddress typedef ptr _ProtoGetProcAddress _ApiLoadLibrary typedef ptr _ProtoLoadLibrary _ApiMessageBox typedef ptr _ProtoMessageBox ;#################################################################### ; 数据段 ;#################################################################### .data? hDllKernel32 dd ? hDllUser32 dd ? _GetProcAddress _ApiGetProcAddress ? _LoadLibrary _ApiLoadLibrary ? _MessageBox _ApiMessageBox ? .const szLoadLibrary db 'LoadLibraryA',0 szGetProcAddress db 'GetProcAddress',0 szUser32 db 'user32',0 szMessageBox db 'MessageBoxA',0 szCaption db 'A MessageBox !',0 szText db 'Hello, Win32 ASM !',0 ;#################################################################### ; 代码段 ;#################################################################### .code include _GetKernel.asm start: ;******************************************************************** ; 从堆栈中的 Ret 地址转换 Kernel32.dll 的基址,并在 Kernel32.dll ; 的导出表中查找 GetProcAddress 函数的入口地址 ;******************************************************************** invoke _GetKernelBase,[esp] . .if eax mov hDllKernel32,eax invoke _GetApi,hDllKernel32,addr szGetProcAddress mov _GetProcAddress,eax .endif ;******************************************************************** ; 用得到的 GetProcAddress 函数得到 LoadLibrary 函数地址并装入其他 Dll ;******************************************************************** .if _GetProcAddress invoke _GetProcAddress,hDllKernel32,\ addr szLoadLibrary mov _LoadLibrary,eax .if eax invoke _LoadLibrary,addr szUser32 mov hDllUser32,eax invoke _GetProcAddress,hDllUser32,\ addr szMessageBox mov _MessageBox,eax .endif .endif ;******************************************************************** .if _MessageBox invoke _MessageBox,NULL,offset szText,\ offset szCaption,MB_OK .endif ret ;#################################################################### end start