WIN32汇编语言教程:第17章 PE文件 · 17.6 应用实例(3)
repnz scasb mov ecx,edi sub ecx,_lpszApi mov @dwStringLength,ecx ;******************************************************************** ; 从 PE 文件头的数据目录获取导出表地址 ;******************************************************************** mov esi,_hModule add esi,[esi + 3ch] assume esi:ptr IMAGE_NT_HEADERS mov esi,[esi].OptionalHeader.DataDirectory.VirtualAddress add esi,_hModule assume esi:ptr IMAGE_EXPORT_DIRECTORY ;******************************************************************** ; 查找符合名称的导出函数名 ;******************************************************************** mov ebx,[esi].AddressOfNames add ebx,_hModule xor edx,edx .repeat push esi mov edi,[ebx] add edi,_hModule mov esi,_lpszApi mov ecx,@dwStringLength rep z cmpsb .if ZERO? pop esi jmp @F .endif pop esi add ebx,4 inc edx .until edx >= [esi].NumberOfNames jmp _Error @@: ;******************************************************************** ; API名称索引 --> 序号索引 --> 地址索引 ;******************************************************************** sub ebx,[esi].AddressOfNames sub ebx,_hModule shr ebx,1 add ebx,[esi].AddressOfNameOrdinals add ebx,_hModule movzx eax,word ptr [ebx] shl eax,2 add eax,[esi].AddressOfFunctions add eax,_hModule ;******************************************************************** ; 从地址表得到导出函数地址 ;******************************************************************** mov eax,[eax] add eax,_hModule mov @dwReturn,eax _Error: pop fs:[0] add esp,0ch assume esi:nothing popad mov eax,@dwReturn ret _GetApi endp ;####################################################################
文件中的_GetKernelBase子程序的参数是主程序从堆栈中得到的返回地址,程序首先设置一个SEH异常处理子程序,以免在搜寻内存的过程中访问到无效的页面后出错;接下来将参数中传递过来的目标地址按页对齐(与0ffff0000h进行and操作);然后以每次一个页的间隔在内存中寻找DOS MZ文件头标识和PE文件头标识,如果找到的话,表示这个页的起始地址就是Kernel32.dll模块的基址。
_GetApi子程序从指定的PE内存映像中扫描导出表并获取某个函数的入口地址,这个子程序的结构完全是按照17.3.1节的第4点内容写的,读者可以对比分析一下。另外,这两个子程序是按照能够自定位的方式写的(还记得13.4.2节中的call/pop/sub指令组合吗?),这样就可以将它们使用在任何地方。
当调用_GetApi子程序的时候,传递过来的API名称中不要忘了最后的“A”或“W”字符,比如LoadLibrary函数和MessageBox函数的真实函数名称根据版本的不同分别是“LoadLibraryA”,“MessageBoxA”或者“LoadLibraryW”和“MessageBoxW”,如果仅仅将“LoadLibrary”字符串传递过来的话,在导出表中是找不到这个函数的。
本例将演示如何在PE文件上添加一段可执行代码,并且让这段代码在原来的代码之前被执行,经过修改的目标PE文件被运行后,将首先弹出一个带“Yes”和“No”的消息框并提示“一定要运行这个程序吗?”,如果用户选择“Yes”的话,原文件被运行,否则程序直接退出。
1. 原理
根据前面对PE文件各部分进行的分析,不难写出在PE文件上添加代码所必须的几个步骤,它们是:
● 将添加的代码写到目标PE文件中,这段代码既可以插入原代码所处的节的空隙中(由于每个节保存在文件中时是按照FileAlignMent的值对齐的,所以节的最后必然会有一些空余的空间),也可以通过添加一个新的节来附在原文件的尾部。
● PE文件原来的入口指针必须被保存在添加的代码中,这样,这段代码执行完以后可以转移到原始文件处执行。
● PE文件头中的入口指针需要被修改,指向新添加代码中的入口地址。
● PE文件头中的一些值需要根据情况做相应的修正,以符合修改后PE文件的情况。
另外,有一些操作是应该避免的,因为它们是无法实现的,或者实现它们的复杂性远远超过它们带来的好处,这些操作是:
● 如果节的空隙不足以插入代码的话,应该在文件尾新建一个节而不是去扩大原来的代码节并将它后面的其他节后移,因为程序无法得知整个PE文件中有多少个RVA值会指向这些被移动位置的节,修正所有这些RVA值几乎是不可能的。
● 如果附加的代码中要用到API函数的话,不要尝试在原始目标文件的导入表中添加导入函数名称,因为这样将涉及在目标PE文件的导入表中插入新的模块名和函数名,其结果同样是造成导入表的一些项目被移动位置,修正指向这些项目的RVA同样是很难实现的。
2. 例子
全部的源程序在Chapter17\AddCode目录中,为了节省篇幅,界面文件同样使用17.1节中的Main.asm和Main.rc文件,需要修改的仅仅是由Main.asm文件所包含的_ProcessPeFile.asm文件,其内容如下:
.const
szErrCreate db '创建文件错误!',0dh,0ah,0
szMySection db '.adata',0
szExt db '_new.exe',0
szSuccess db '在文件后附加代码成功,新文件:',0dh,0ah
db '%s',0dh,0ah,0
.code
;####################################################################
; _AddCode.asm 中包含被附加到目标PE文件中的执行代码
;####################################################################
include _AddCode.asm
;####################################################################
; 计算按照指定值对齐后的数值
;####################################################################
_Align proc _dwSize,_dwAlign
push edx