WIN32汇编语言教程:第17章 PE文件 · 17.6 应用实例(5)
另外一个源文件_AddCode.asm中包含了被添加到目标PE文件中的代码,内容如下:
;#################################################################### ; 一些函数的原形定义 ;#################################################################### _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 ;#################################################################### APPEND_CODE equ this byte ;#################################################################### ; 被添加到目标文件中的代码从这里开始 ;#################################################################### include _GetKernel.asm ;#################################################################### hDllKernel32 dd ? hDllUser32 dd ? _GetProcAddress _ApiGetProcAddress ? _LoadLibrary _ApiLoadLibrary ? _MessageBox _ApiMessageBox ? szLoadLibrary db 'LoadLibraryA',0 szGetProcAddress db 'GetProcAddress',0 szUser32 db 'user32',0 szMessageBox db 'MessageBoxA',0 szCaption db '问题提示',0 szText db '一定要运行这个程序吗?',0 ;******************************************************************** ; 新的入口地址 ;******************************************************************** _NewEntry: ;******************************************************************** ; 重定位并获取一些 API 的入口地址 ;******************************************************************** call @F @@: pop ebx sub ebx,offset @B ;******************************************************************** invoke _GetKernelBase,[esp] ;获取Kernel32.dll基址 .if ! eax jmp _ToOldEntry .endif mov [ebx+hDllKernel32],eax ;获取GetProcAddress入口 lea eax,[ebx+szGetProcAddress] invoke _GetApi,[ebx+hDllKernel32],eax .if ! eax jmp _ToOldEntry .endif mov [ebx+_GetProcAddress],eax ;******************************************************************** lea eax,[ebx+szLoadLibrary] ;获取LoadLibrary入口 invoke [ebx+_GetProcAddress],[ebx+hDllKernel32],eax mov [ebx+_LoadLibrary],eax lea eax,[ebx+szUser32] ;获取User32.dll基址 invoke [ebx+_LoadLibrary],eax mov [ebx+hDllUser32],eax lea eax,[ebx+szMessageBox] ;获取MessageBox入口 invoke [ebx+_GetProcAddress],[ebx+hDllUser32],eax mov [ebx+_MessageBox],eax ;******************************************************************** lea ecx,[ebx+szText] lea eax,[ebx+szCaption] invoke [ebx+_MessageBox],NULL,ecx,eax,\ MB_YESNO or MB_ICONQUESTION .if eax != IDYES ret .endif ;******************************************************************** ; 执行原来的文件 ;******************************************************************** _ToOldEntry: db 0e9h ;0e9h是jmp xxxxxxxx的机器吗 _dwOldEntry: dd ? ;用来填入原来的入口地址 ;#################################################################### APPEND_CODE_END equ this byte
这段代码是按照能够自身重定位的方式写的,而且必须按照这种格式书写,因为当它被添加到目标PE文件后,对于不同的PE文件所处的位置肯定是不同的,不进行重定位处理必然无法正常运行。
附加代码实现的功能和17.6.1节的NoImport例子大致相同,也是首先使用17.6.1节中的两个子程序获取Kernel32.dll模块的基址和GetProcAddress函数的入口地址,并由此最后得到MessageBox函数的入口地址以便显示消息框。
程序最后的_ToOldEntry标号处的数据0e9h是jmp xxxxxxxx的机器码的第一个字节,它与下面的_dwOldEntry标号处的双字一起组成整个jmp指令,jmp指令的编码方式是由一个0e9h字节加上指令执行后的EIP的修正值,也就是说,当jmp指令的下一句指令地址是a,而跳转的目标地址是b的话,那么0e9h字节后的双字的值就是b-a,所以主程序中的这几句就是将指令改成“jmp 原入口地址”的样子:
push [esi].OptionalHeader.AddressOfEntryPoint ;esi是PE文件头地址
pop @dwEntry ;现在@dwEntry是原来的入口地址
(1) mov eax,[ebx].VirtualAddress
(2) add eax,(offset _ToOldEntry-offset APPEND_CODE+5)
(3) sub @dwEntry,eax
在指令序列执行前,esi指向PE文件头,ebx指向节表,所以由(1)标出的指令执行后,eax的值是新加代码节的起始地址,(2)指令就是将eax修正到_ToOldEntry后面5字节的位置,也就是jmp xxxxxxxx后一条指令的位置,这样,当指令(3)执行后,@dwEntry中的值就是需要填入_dwOldEntry位置的数据。
好!现在回过头来分析一下_ProcessPeFile.asm文件中的代码。Part 1从原始PE文件拷贝一个名为“原始文件名_new.exe”的文件,这个文件将被添加上可执行代码,原来的“原始文件名.exe”文件则不会被改动。当文件成功拷贝后,程序将打开拷贝生成的新文件以便进行修改。
Part 2分配一个等于目标PE文件的文件头大小的内存块,并将文件头拷贝到这个内存块中,所有对PE文件头的修改操作都是在这个内存块中完成的,这个内存块的内容最终将被写到“原始文件名_new.exe”文件中。
Part 3在节表中加入一个新的节表项目,节表项中的VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristics和Name1字段需要被设置。其中Name1中的名称被设置为“.adata”;Characteristics字段中的标志被设置为可执行和可读写,这是因为需要对这个节中的hDllKernel32等变量进行写入操作。其他几个字段值的算法如下(下面的“上一节”指原始PE文件的最后一节):
● PointerToRawData=(上一节的PointerToRawData)+(上一节的SizeOfRawData)
● SizeOfRawData=附加代码的长度按FileAlignMent值对齐
● VirtualAddress=(上一节的VirtualAddress)+(上一节的VirtualSize按SectionAlignMent的对齐值)
● VirtualSize=附加代码的长度按SectionAlignMent值对齐
其中的对齐算法是用_Align子程序来完成的。在这一部分中,程序还修正了文件头中的SizeOfCode和SizeOfImage的值。如果SizeOfImage的值不被修正的话,Windows将无法装入修改后的PE文件,报的错误为“这不是一个有效的Win32可执行文件”。
Part 5将修改后的文件头写入文件并将附加代码写到文件的最后,由于附加代码的长度还没有按FileAlignMent的值对齐,所以程序再次使用SetFilePointer函数将文件指针移动到对齐后的位置并用SetFileEnd函数将文件长度扩展到这里。
Part 6将原始PE文件的入口地址取出,和附加代码的入口地址计算得出“jmp 原入口地址”这条指令中的二进制码值,并将这个值写到附加代码的对应位置中。
Part 7进行扫尾工作,如释放内存、关闭文件和显示成功信息等。至此,程序的所有功能就完成了。
参考 文 献
1 Matt Pietrek. Windows 95 System programming SECRETS. IDG Books. 1995
2 Jeffrey Richter. Programming Applications for Windows. Microsoft Press. 1999
3 Charles Petzold. Programming Windows 95. Microsoft Press. 1996
4 Charles Petzold. Programming Windows. Microsoft Press. 1998
5 W.Richard Stevens. TCP/IP详解:卷1 协议. 北京:机械工业出版社
6 侯捷. 深入浅出MFC. 1998年. 华中理工大学出版社
7 施炜,李铮,秦颍. Windows Sockets规范及应用
8 北京科海. 80386系统设计手册. 1990
9 吕晓庆. 80386/486系统编程实践. 浙江大学出版社,1993
10 Art of Assemble. http://www.cs.ucr.edu/~rhyde. 1996
11 Iczlion's Win32asm tutorial. http://win32asm.cjb.net
本书的编写过程中还参考了本人长期积累的、来自因特网上的编程资料,出处一时无法考证,在此一并表示感谢!