今天是国庆长假的最后一天,对看雪的几位朋友的承诺顿就在今天对现了,这是我做人原格问题,说实话要想对现一个承诺,真要要做很我,付出才会有回报,是这我一直相信的道理
,虽然前几天感冒了,但为了承诺还是一边用手巾擦鼻涕,一边写着文章,总算是写完了,不管是好是坏,总算是带着一些朋友把PE的相关知识又重新温习了一遍,技术含量肯定没有玩命大哥的高,不过自己也一直在向牛人们学习,希望有一天,也能向他们那样吧!!
我相信每一个牛人都是这样一步一步走上去的,从什么都不会,到技术达人!!依惜记得高中毕业时我还不知道QQ是什么,毕业那会有很多同学说给我QQ号,我没要,因为我根本不知道那个有什么用,无语!! 那时也不会上网,只是听别人说上网像冲浪,其实也没玩过冲浪,所以也不知道冲浪是啥感觉,但几年过去了,不说自己技术有多强,但至少有很多东西,我基本上都掌握了,什么事情都是这样的,只要你想去做,就一定要坚持做下去,不管结局是好是坏,总会有一些意外的收获!相信只要大家努力,总有一天能够和看雪的几位大牛们一样的!!有梦想就一直要大胆去追,这样才活的有意义!!
如果这七天,你都认认真真的所这七篇文章看完了,如果能给你一点小小的收获的话,我倍感兴慰~~~
最后一篇文章,我就作个结尾,讲一个比较简单的应用,相信写过病毒的朋友们一般都会参考过这段代码!!如果还没写过病毒的朋友,一定会觉得很有意思,然后会爱上这个,我以前就是这样的,觉得好好玩的!!!如果不学PE的话,这些东西都很少提到,一般的编程书上也很少讲这方面的知识!!
参考文献《Win32汇编语言程序设计》第二版
在PE文件上添加可执行代码!!!!
本章所涉及到的实例将演示在PE文件上添加一段可执行代码,并且让这段代码在原来的代码之前被执行,经过修改的目标PE文件被运行后,将首先弹出一个带"YES"和"NO"的消息框并提示“一定要运行这个程序吗?”,如果用户选择"YES"的话,原文件被运行,否则程序直接退出。
先讲解一下原理吧!
根据前面对PE文件各个部分进行的分析,可以得到在PE文件中添加代码需要以下几个步骤:
将添加的代码写到目标PE文件中,这段代码既可以插入原代码所处的节的空隙中(由于每个节保存在文件中时是按照FileAlignment的值对齐的,所以节的最后必然会有一些空余的空间),也可以通过添加一个新的节对附在原文件的尾部。
PE文件原来的入口指针必须被保存在添加的代码中,这样,这段代码执行完以后可以转移到原始文件处执行。
PE文件原来的入口指针需要被修改,指向新添加代码的入口地址。
PE文件头中的一些值需要根据情况做相应的修正,以符合修改后PE文件的情况。
另外有一些操作是应该避免的,因为它们是无法实现的,或者实现它们的复杂性远远超过它们带来的好处,这些操作是:
如果节的空隙不足以插入代码的话,应该在文件尾新建一个节而不是去扩大原来的代码节并将它后面的其他节后移,因为程序无法得知整个PE文件中有多少个RVA值会指向这些被移动位置的节,修正所有这些RVA值几乎是不可能的。
如果附加的代码中要用到API函数的话,不要尝试在原始目标文件的导入表添加导入函数名称,因为这样将涉及在目标PE文件的导入表中插入新的模块名和函数名,其结果同样是造成导入表的一些项目被移动位置,修正指向这些项目的RVA同样是很难实现的。
_ProcessPeFile这个源代码中包含了一个_AddCode.asm的源文件,先来看看_AddCode.asm为我们做了些什么?
代码如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 要被添加到目标文件后面的执行代码
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;
;
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 一些函数的原形定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_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 ;如果选择的不是YES,则直接返回,不会返回到原入口点处
ret
.endif
;********************************************************************
; 执行原来的文件 ;这里主要是为了从当前入口点返回到原入口点
;********************************************************************
_ToOldEntry:
db 0e9h ;0e9h是jmp xxxxxxxx的机器码
_dwOldEntry:
dd ? ;用来填入原来的入口地址 ;这里用来填入原程序的入口地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
APPEND_CODE_END equ this byte ;定义这段代码的结束
_AddCode.asm中包含要被添加到其他可执行文件中的代码。这段代码是按照能够自身重定位的方式写的,而且必须按照这种格式书写,因为当它被添加到目标PE文件后,对于不同的PE文件所处的位置肯定是不同的,不进行重定位处理必然无法正常运行。
这段附加代码实现的功能和前一篇文章中NoImport例子大致相同,也是首先使用_GetKernel..asm中提供的两个函数来获取Kernel32.dll模块的基址和GetProcAddress函数的入口地址,并由此最后得到MessageBox函数的入口地址以便显示消息框。
在这段程序的最后_ToOldEntry标号处的数据是0e9h是JMP XXXXXXXX的机器码的第一个字节,它与下面的_dwOldEntry标号处的双字一起组成整个JMP指令,这条JMP指令将在主程序中根据具体情况修正。
现在我们来重点不看看_ProcessPeFile这个源程序的代码吧!!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; AddCode 例子的功能模块
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.const
szErrCreate db '创建文件错误!',0dh,0ah,0
szErrNoRoom db '程序中没有多余的空间可供加入代码!',0dh,0ah,0
szMySection db '.adata',0
szExt db '_new.exe',0
szSuccess db '在文件后附加代码成功,新文件:',0dh,0ah
db '%s',0dh,0ah,0
.code
include _AddCode.asm
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 计算按照指定值对齐后的数值
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Align proc _dwSize,_dwAlign
push edx
mov eax,_dwSize
xor edx,edx
div _dwAlign
.if edx
inc eax
.endif
mul _dwAlign
pop edx
ret
_Align endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile proc _lpFile,_lpPeHead,_dwSize
local @szNewFile[MAX_PATH]:byte
local @hFile,@dwTemp,@dwEntry,@lpMemory
local @dwAddCodeBase,@dwAddCodeFile
local @szBuffer[256]:byte
pushad
;********************************************************************
; (Part 1)准备工作:1-建立新文件,2-打开文件
;********************************************************************
invoke lstrcpy,addr @szNewFile,addr szFileName
invoke lstrlen,addr @szNewFile
lea ecx,@szNewFile
mov byte ptr [ecx+eax-4],0
invoke lstrcat,addr @szNewFile,addr szExt
invoke CopyFile,addr szFileName,addr @szNewFile,FALSE ;从原始PE文件拷贝一个名为“原始文件名_new.exe”的文件
;这个文件将被添加上可执行代码,原来的文件不会被改动
invoke CreateFile,addr @szNewFile,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or \
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
.if eax == INVALID_HANDLE_VALUE ;文件拷贝成功后,打开拷贝生成的新文件,以便进行修改
invoke SetWindowText,hWinEdit,addr szErrCreate
jmp _Ret
.endif
mov @hFile,eax
;********************************************************************
;(Part 2)进行一些准备工作和检测工作
; esi --> 原PeHead,edi --> 新的PeHead
; edx --> 最后一个节表,ebx --> 新加的节表
;********************************************************************
mov esi,_lpPeHead
assume esi:ptr IMAGE_NT_HEADERS,edi:ptr IMAGE_NT_HEADERS
invoke GlobalAlloc,GPTR,[esi].OptionalHeader.SizeOfHeaders ;分配一个等于目标PE文件的文件头大小的内存块
mov @lpMemory,eax
mov edi,eax
invoke RtlMoveMemory,edi,_lpFile,[esi].OptionalHeader.SizeOfHeaders ;并使用RltMoveMemory将PE文件头拷贝到这个内存中
add edi,esi
sub edi,_lpFile
movzx eax,[esi].FileHeader.NumberOfSections ;得到节表的数量
dec eax
mov ecx,sizeof IMAGE_SECTION_HEADER ;节表的长度
mul ecx ;节表的数量*节表的长度
mov edx,edi
add edx,eax
add edx,sizeof IMAGE_NT_HEADERS ;节表尾部的指针
mov ebx,edx
add ebx,sizeof IMAGE_SECTION_HEADER ;节表的最后一项的指针
assume ebx:ptr IMAGE_SECTION_HEADER,edx:ptr IMAGE_SECTION_HEADER
;********************************************************************
; (Part 2.1)检查是否有空闲的位置可供插入节表
;********************************************************************
pushad
mov edi,ebx
xor eax,eax ;将EAX值设为0
mov ecx,IMAGE_SECTION_HEADER
repz scasb ;看节表中是否存在一个全零的位置,进行扫描
popad
.if ! ZERO?
;********************************************************************
; (Part 3.1)如果没有新的节表空间的话,则查看现存代码节的最后
; 是否存在足够的全零空间,如果存在则在此处加入代码
;********************************************************************
xor eax,eax
mov ebx,edi
add ebx,sizeof IMAGE_NT_HEADERS
.while ax <= [esi].FileHeader.NumberOfSections ;扫描现存的节
mov ecx,[ebx].SizeOfRawData ;节在磁盘文件中对齐后的大小
.if ecx && ([ebx].Characteristics & IMAGE_SCN_MEM_EXECUTE) ;节的属性
sub ecx,[ebx].Misc.VirtualSize ;节在磁盘中对齐后的大小减去节的实际大上,得到节空隙大小
.if ecx > offset APPEND_CODE_END-offset APPEND_CODE ;看现存的节空隙的大小是否大于加入的代码长度
or [ebx].Characteristics,IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
add [ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
jmp @F ;如果存在一个节的空隙大于加入到的代码长度,则跳到加入代码处
.endif
.endif
add ebx,IMAGE_SECTION_HEADER ;指向下一个节
inc ax
.endw
invoke CloseHandle,@hFile
invoke DeleteFile,addr @szNewFile
invoke SetWindowText,hWinEdit,addr szErrNoRoom
jmp _Ret
@@:
;********************************************************************
; 将新增代码加入代码节的空隙中
;********************************************************************
mov eax,[ebx].VirtualAddress ;节装载到内存中的偏移地址
add eax,[ebx].Misc.VirtualSize ;加上节的实际大小
mov @dwAddCodeBase,eax ;新添代码在内存中的位置
mov eax,[ebx].PointerToRawData ;在文件中的偏移
add eax,[ebx].Misc.VirtualSize ;加上节的实际大小
mov @dwAddCodeFile,eax ;新添加代码在文件中的位置
invoke SetFilePointer,@hFile,@dwAddCodeFile,NULL,FILE_BEGIN ;定位到加入代码地址处
mov ecx,offset APPEND_CODE_END-offset APPEND_CODE
invoke WriteFile,@hFile,offset APPEND_CODE,ecx,addr @dwTemp,NULL ;将需加入的代码写入空隙处
.else
;********************************************************************
; (Part 3.2)如果有新的节表空间的话,加入一个新的节
;********************************************************************
inc [edi].FileHeader.NumberOfSections ;增加一个新的节,节的数量加1
mov eax,[edx].PointerToRawData
add eax,[edx].SizeOfRawData
mov [ebx].PointerToRawData,eax
mov ecx,offset APPEND_CODE_END-offset APPEND_CODE
invoke _Align,ecx,[esi].OptionalHeader.FileAlignment ;将新添加后的节按指定值对齐
mov [ebx].SizeOfRawData,eax
invoke _Align,ecx,[esi].OptionalHeader.SectionAlignment ;将新添加后的节按指定的值对值
add [edi].OptionalHeader.SizeOfCode,eax ;修正SizeOfCode
add [edi].OptionalHeader.SizeOfImage,eax ;修正SizeOfImage
invoke _Align,[edx].Misc.VirtualSize,[esi].OptionalHeader.SectionAlignment
add eax,[edx].VirtualAddress
mov [ebx].VirtualAddress,eax ;给新添加的节的各个字段赋值
mov [ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
mov [ebx].Characteristics,IMAGE_SCN_CNT_CODE\
or IMAGE_SCN_MEM_EXECUTE or IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
invoke lstrcpy,addr [ebx].Name1,addr szMySection
;********************************************************************
; 将新增代码作为一个新的节写到文件尾部
;********************************************************************
invoke SetFilePointer,@hFile,[ebx].PointerToRawData,NULL,FILE_BEGIN ;定位到新添加的节的位置
invoke WriteFile,@hFile,offset APPEND_CODE,[ebx].Misc.VirtualSize,\ ;将代码写入到新添加的节的位置
addr @dwTemp,NULL
mov eax,[ebx].PointerToRawData
add eax,[ebx].SizeOfRawData
invoke SetFilePointer,@hFile,eax,NULL,FILE_BEGIN ;定位到文件的开始
invoke SetEndOfFile,@hFile ;然后得到文件的结尾位置
;********************************************************************
push [ebx].VirtualAddress ;eax = 新加代码的基地址
pop @dwAddCodeBase
push [ebx].PointerToRawData
pop @dwAddCodeFile
.endif
;********************************************************************
; (Part 4)修正文件入口指针并写入新的文件头
;********************************************************************
mov eax,@dwAddCodeBase
add eax,(offset _NewEntry-offset APPEND_CODE)
mov [edi].OptionalHeader.AddressOfEntryPoint,eax ;将入口地址写入文件头
invoke SetFilePointer,@hFile,0,NULL,FILE_BEGIN ;定位到文件开始,写入新的文件头
invoke WriteFile,@hFile,@lpMemory,[esi].OptionalHeader.SizeOfHeaders,\
addr @dwTemp,NULL
;********************************************************************
; (Part 5)修正新加代码中的 Jmp oldEntry 指令
;********************************************************************
push [esi].OptionalHeader.AddressOfEntryPoint
pop @dwEntry
mov eax,@dwAddCodeBase
add eax,(offset _ToOldEntry-offset APPEND_CODE+5) ;定位到原来入口点代码处+5是因为
sub @dwEntry,eax ;JMP XXXXXXXX占用了五个字节
mov ecx,@dwAddCodeFile
add ecx,(offset _dwOldEntry-offset APPEND_CODE)
invoke SetFilePointer,@hFile,ecx,NULL,FILE_BEGIN
invoke WriteFile,@hFile,addr @dwEntry,4,addr @dwTemp,NULL ;写入跳转到原代码的代码
;********************************************************************
; (Part 6)关闭文件
;********************************************************************
invoke GlobalFree,@lpMemory ;释放内存,关闭文件句柄
invoke CloseHandle,@hFile
invoke wsprintf,addr @szBuffer,Addr szSuccess,addr @szNewFile
invoke SetWindowText,hWinEdit,addr @szBuffer
_Ret:
assume esi:nothing
popad
ret
_ProcessPeFile endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
上面的我作了简单的注释,这里我在具体分析一下这段代码,请大家仔细研究一下,很有意思的!!
Part 1从原始PE文件拷贝一个名为“原始文件名_new.exe”的文件,这个文件将被添加上可执行代码,原来的“原始文件名.exe”文件则不会被改动。当文件成功拷贝后,程序将打开拷贝生成的新文件以便进行修改。
Part 2分配一个等于目标PE文件的文件头大小的内存块,并将文件头拷贝到这个内存块中,所有对PE文件头的修改操作都是在这个内存块中完成的,这个内存块的内容最终将被写到“原始文件名_new.exe”文件中。完成拷贝工作以后,程序计算两个指针以备后用:指向节表最后一项指针和指向节表尾部的指针,这两个指针可以从节表的数量和节表的长度计算而来的,节表的数量是从PE文件头中的FileHeader.NumberOfSections字段获取的。
正如本节的开始所述,新增的代码既可以插入原代码所处的节的空隙中,也可以通过添加一个新的节来附在原文件的尾部,为了增加成功的机会,应该对这两种情况都给予考虑,于是在Part2中对节表的尾部进行全零数据的扫描,如果存在一段全零的位置可供放入一个新的节表,那么采取增加新节的办法(Part3.2),否则采用在代码节的空隙中插入的方法(Part3.1)
Part 3.1 中对所有节表进行循环扫描,以便于找到代码节并检测节的空隙是否可以容纳新增的代码,程序首先判断SizeOfRawData是否为0,这个数值为0,说明这个节是包含未初始化数据的节,不能用于插入代码,如果SizeOfRawData大于0的话,则检测Characteristics字段查看当前节是否为代码节(包含IMAGE_SCN_MEM_EXECUTE标志)。
通过检测后,程序计算空隙的大小(SizeOfRawData和Misc.VirtualSize之差)是否大于插入代码的长度,如果空隙足够大的话,则进行插入操作。在插入代码的同时,这个节的属性中必须被加上IMAGE_SCN_MEM_READ和IMAGE_SCN_MEM_WRITE标志,因为附加代码中使用了对被加入部分进行写操作的指令。另外,VirtualSize字段中的实际数据大小也需要被修正。上面的程序只考虑了在代码节中插入的情况,要是代码节中的空隙大小不够,那么程序就退出了。实际上,程序也可以在其他的节中插入代码,只要将节的属性同时也加上IMAGE_SCN_MEM_EXECUTE标志就可以了,要是进一步将程序完成的话,当单个节的空隙大小不够的时候,也可以将附加代码分块插入多个节中,不过这时附加代码中就必须考虑在执行前将代码重新拼装在一起这个步骤了。
Part 3.2在节表中加入一个新的节表项目,节表项中的VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristics和Name1字段需要被设置。其中Name1中的名称被设置为“.adata”;Characteristics字段中的标志被设置为可执行和可读写,其他几个字段值的算法如下(下面的“上一节”指原始PE文件的最后一节):
● PointerToRawData=(上一节的PointerToRawData)+(上一节的SizeOfRawData)
● SizeOfRawData=附加代码的长度按FileAlignMent值对齐
● VirtualAddress=(上一节的VirtualAddress)+(上一节的VirtualSize按SectionAlignMent的对齐值)
● VirtualSize=附加代码的长度按SectionAlignMent值对齐
其中的对齐算法是用_Align子程序来完成的。在这一部分中,程序还修正了文件头中的SizeOfCode和SizeOfImage的值。如果SizeOfImage的值不被修正的话,Windows将无法装入修改后的PE文件,报的错误为“这不是一个有效的Win32可执行文件”。Part3.2的最后,程序将附加代码写到文件的最后,由于附加代码的长度还没有按FileAlignment的值对齐,所以程序再次使用SetFilePointer函数将文件指针移动到对齐后的位置并用SetFileEnd函数将文件长度扩展到这里。无论是Part3.1还是Part3.2的最后,程序将新增代码在文件中位置和在内存中的位置分别保存在@dwAddCodeFile和@dwAddCodeBase变量中以备后用。
Part4 修改PE文件头中的文件入口地址,并将修改后的整个PE文件头写入到新文件中。
Part 5将原始PE文件的入口地址取出,和附加代码的入口地址计算得出“jmp 原入口地址”这条指令中的二进制码值,并将这个值写到附加代码的对应位置中。JMP指令的编码方式是由一个0e9h字节加上指令执行后的EIP的修正值,也就是说,当JMP指令的下一句指令地址是addr1,而跳转的目标地址是addr2的放在,那么0e9h字节后的双字的值就是addr2-addr1,所以下面的几句就是将指令改成"JMP原入口址"的样子:
push [esi].OptioinalHeader.AddressOfEntryPoint
pop @dwEntry
mov eax,@dwAddCodeBase
add eax,(offset _ToOldEntry--offset APPEND_CODE+5)
sub @dwEntry,eax
在指令列执行前,ESI指向PE文件头,@dwAddCodeBase中保存有新增代码被装载到内存后的起始地址,所以由(1)标出的指令执行后,EAX的值是_ToOldEntry后面的5个字节的位置,或者说是JMP XXXXXXXX后一条指令的位置,也就是上面算式中的addr1。@dwEntry中的原始值是可执行文件原来的入口地址,也即addr2,指令(2)执行后,@dwEntry中的值就是addr2-adr1b ,这就是需要填入_dwOldEntry位置的数据。
接下来程序用SetFilePointer函数将文件指针移动到新增代码中的_dwOldEntry位置,并将上面计算出的结果写入文件中。
Part 6进行扫尾工作,如释放内存、关闭文件和显示成功信息等。至此,程序的所有功能就完成了。
写到这里,基本上也对现了承诺,在这七天的时间里陪大家重新温习了一下PE的相关知识,最近在学习内核和驱动方面的知识,希望想交流的朋友能加我的看雪ID号或QQ号,谢谢!
前面有朋友说作个链接,在这里作了一个小链接,以方便大家参考学习,温故而知新!!!
国庆PE总复习(一)(二) http://bbs.pediy.com/showthread.php?t=121488
国庆PE总复习(三) http://bbs.pediy.com/showthread.php?t=121595
国庆PE总复习(四) http://bbs.pediy.com/showthread.php?t=121672
国庆PE总复习(五) http://bbs.pediy.com/showthread.php?t=121695
国庆PE总复习(六) http://bbs.pediy.com/showthread.php?t=121748
国庆长假结束了,希望大家在看雪都能找一个属于自己的位置,早日成为看雪上的技术牛人!!
我相信每一个牛人都是这样一步一步走上去的,从什么都不会,到技术达人!!依惜记得高中毕业时我还不知道QQ是什么,毕业那会有很多同学说给我QQ号,我没要,因为我根本不知道那个有什么用,无语!! 那时也不会上网,只是听别人说上网像冲浪,其实也没玩过冲浪,所以也不知道冲浪是啥感觉,但几年过去了,不说自己技术有多强,但至少有很多东西,我基本上都掌握了,什么事情都是这样的,只要你想去做,就一定要坚持做下去,不管结局是好是坏,总会有一些意外的收获!相信只要大家努力,总有一天能够和看雪的几位大牛们一样的!!有梦想就一直要大胆去追,这样才活的有意义!!
如果这七天,你都认认真真的所这七篇文章看完了,如果能给你一点小小的收获的话,我倍感兴慰~~~
最后一篇文章,我就作个结尾,讲一个比较简单的应用,相信写过病毒的朋友们一般都会参考过这段代码!!如果还没写过病毒的朋友,一定会觉得很有意思,然后会爱上这个,我以前就是这样的,觉得好好玩的!!!如果不学PE的话,这些东西都很少提到,一般的编程书上也很少讲这方面的知识!!
参考文献《Win32汇编语言程序设计》第二版
在PE文件上添加可执行代码!!!!
本章所涉及到的实例将演示在PE文件上添加一段可执行代码,并且让这段代码在原来的代码之前被执行,经过修改的目标PE文件被运行后,将首先弹出一个带"YES"和"NO"的消息框并提示“一定要运行这个程序吗?”,如果用户选择"YES"的话,原文件被运行,否则程序直接退出。
先讲解一下原理吧!
根据前面对PE文件各个部分进行的分析,可以得到在PE文件中添加代码需要以下几个步骤:
将添加的代码写到目标PE文件中,这段代码既可以插入原代码所处的节的空隙中(由于每个节保存在文件中时是按照FileAlignment的值对齐的,所以节的最后必然会有一些空余的空间),也可以通过添加一个新的节对附在原文件的尾部。
PE文件原来的入口指针必须被保存在添加的代码中,这样,这段代码执行完以后可以转移到原始文件处执行。
PE文件原来的入口指针需要被修改,指向新添加代码的入口地址。
PE文件头中的一些值需要根据情况做相应的修正,以符合修改后PE文件的情况。
另外有一些操作是应该避免的,因为它们是无法实现的,或者实现它们的复杂性远远超过它们带来的好处,这些操作是:
如果节的空隙不足以插入代码的话,应该在文件尾新建一个节而不是去扩大原来的代码节并将它后面的其他节后移,因为程序无法得知整个PE文件中有多少个RVA值会指向这些被移动位置的节,修正所有这些RVA值几乎是不可能的。
如果附加的代码中要用到API函数的话,不要尝试在原始目标文件的导入表添加导入函数名称,因为这样将涉及在目标PE文件的导入表中插入新的模块名和函数名,其结果同样是造成导入表的一些项目被移动位置,修正指向这些项目的RVA同样是很难实现的。
_ProcessPeFile这个源代码中包含了一个_AddCode.asm的源文件,先来看看_AddCode.asm为我们做了些什么?
代码如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 要被添加到目标文件后面的执行代码
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;
;
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 一些函数的原形定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_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 ;如果选择的不是YES,则直接返回,不会返回到原入口点处
ret
.endif
;********************************************************************
; 执行原来的文件 ;这里主要是为了从当前入口点返回到原入口点
;********************************************************************
_ToOldEntry:
db 0e9h ;0e9h是jmp xxxxxxxx的机器码
_dwOldEntry:
dd ? ;用来填入原来的入口地址 ;这里用来填入原程序的入口地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
APPEND_CODE_END equ this byte ;定义这段代码的结束
_AddCode.asm中包含要被添加到其他可执行文件中的代码。这段代码是按照能够自身重定位的方式写的,而且必须按照这种格式书写,因为当它被添加到目标PE文件后,对于不同的PE文件所处的位置肯定是不同的,不进行重定位处理必然无法正常运行。
这段附加代码实现的功能和前一篇文章中NoImport例子大致相同,也是首先使用_GetKernel..asm中提供的两个函数来获取Kernel32.dll模块的基址和GetProcAddress函数的入口地址,并由此最后得到MessageBox函数的入口地址以便显示消息框。
在这段程序的最后_ToOldEntry标号处的数据是0e9h是JMP XXXXXXXX的机器码的第一个字节,它与下面的_dwOldEntry标号处的双字一起组成整个JMP指令,这条JMP指令将在主程序中根据具体情况修正。
现在我们来重点不看看_ProcessPeFile这个源程序的代码吧!!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; AddCode 例子的功能模块
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.const
szErrCreate db '创建文件错误!',0dh,0ah,0
szErrNoRoom db '程序中没有多余的空间可供加入代码!',0dh,0ah,0
szMySection db '.adata',0
szExt db '_new.exe',0
szSuccess db '在文件后附加代码成功,新文件:',0dh,0ah
db '%s',0dh,0ah,0
.code
include _AddCode.asm
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 计算按照指定值对齐后的数值
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Align proc _dwSize,_dwAlign
push edx
mov eax,_dwSize
xor edx,edx
div _dwAlign
.if edx
inc eax
.endif
mul _dwAlign
pop edx
ret
_Align endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile proc _lpFile,_lpPeHead,_dwSize
local @szNewFile[MAX_PATH]:byte
local @hFile,@dwTemp,@dwEntry,@lpMemory
local @dwAddCodeBase,@dwAddCodeFile
local @szBuffer[256]:byte
pushad
;********************************************************************
; (Part 1)准备工作:1-建立新文件,2-打开文件
;********************************************************************
invoke lstrcpy,addr @szNewFile,addr szFileName
invoke lstrlen,addr @szNewFile
lea ecx,@szNewFile
mov byte ptr [ecx+eax-4],0
invoke lstrcat,addr @szNewFile,addr szExt
invoke CopyFile,addr szFileName,addr @szNewFile,FALSE ;从原始PE文件拷贝一个名为“原始文件名_new.exe”的文件
;这个文件将被添加上可执行代码,原来的文件不会被改动
invoke CreateFile,addr @szNewFile,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or \
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
.if eax == INVALID_HANDLE_VALUE ;文件拷贝成功后,打开拷贝生成的新文件,以便进行修改
invoke SetWindowText,hWinEdit,addr szErrCreate
jmp _Ret
.endif
mov @hFile,eax
;********************************************************************
;(Part 2)进行一些准备工作和检测工作
; esi --> 原PeHead,edi --> 新的PeHead
; edx --> 最后一个节表,ebx --> 新加的节表
;********************************************************************
mov esi,_lpPeHead
assume esi:ptr IMAGE_NT_HEADERS,edi:ptr IMAGE_NT_HEADERS
invoke GlobalAlloc,GPTR,[esi].OptionalHeader.SizeOfHeaders ;分配一个等于目标PE文件的文件头大小的内存块
mov @lpMemory,eax
mov edi,eax
invoke RtlMoveMemory,edi,_lpFile,[esi].OptionalHeader.SizeOfHeaders ;并使用RltMoveMemory将PE文件头拷贝到这个内存中
add edi,esi
sub edi,_lpFile
movzx eax,[esi].FileHeader.NumberOfSections ;得到节表的数量
dec eax
mov ecx,sizeof IMAGE_SECTION_HEADER ;节表的长度
mul ecx ;节表的数量*节表的长度
mov edx,edi
add edx,eax
add edx,sizeof IMAGE_NT_HEADERS ;节表尾部的指针
mov ebx,edx
add ebx,sizeof IMAGE_SECTION_HEADER ;节表的最后一项的指针
assume ebx:ptr IMAGE_SECTION_HEADER,edx:ptr IMAGE_SECTION_HEADER
;********************************************************************
; (Part 2.1)检查是否有空闲的位置可供插入节表
;********************************************************************
pushad
mov edi,ebx
xor eax,eax ;将EAX值设为0
mov ecx,IMAGE_SECTION_HEADER
repz scasb ;看节表中是否存在一个全零的位置,进行扫描
popad
.if ! ZERO?
;********************************************************************
; (Part 3.1)如果没有新的节表空间的话,则查看现存代码节的最后
; 是否存在足够的全零空间,如果存在则在此处加入代码
;********************************************************************
xor eax,eax
mov ebx,edi
add ebx,sizeof IMAGE_NT_HEADERS
.while ax <= [esi].FileHeader.NumberOfSections ;扫描现存的节
mov ecx,[ebx].SizeOfRawData ;节在磁盘文件中对齐后的大小
.if ecx && ([ebx].Characteristics & IMAGE_SCN_MEM_EXECUTE) ;节的属性
sub ecx,[ebx].Misc.VirtualSize ;节在磁盘中对齐后的大小减去节的实际大上,得到节空隙大小
.if ecx > offset APPEND_CODE_END-offset APPEND_CODE ;看现存的节空隙的大小是否大于加入的代码长度
or [ebx].Characteristics,IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
add [ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
jmp @F ;如果存在一个节的空隙大于加入到的代码长度,则跳到加入代码处
.endif
.endif
add ebx,IMAGE_SECTION_HEADER ;指向下一个节
inc ax
.endw
invoke CloseHandle,@hFile
invoke DeleteFile,addr @szNewFile
invoke SetWindowText,hWinEdit,addr szErrNoRoom
jmp _Ret
@@:
;********************************************************************
; 将新增代码加入代码节的空隙中
;********************************************************************
mov eax,[ebx].VirtualAddress ;节装载到内存中的偏移地址
add eax,[ebx].Misc.VirtualSize ;加上节的实际大小
mov @dwAddCodeBase,eax ;新添代码在内存中的位置
mov eax,[ebx].PointerToRawData ;在文件中的偏移
add eax,[ebx].Misc.VirtualSize ;加上节的实际大小
mov @dwAddCodeFile,eax ;新添加代码在文件中的位置
invoke SetFilePointer,@hFile,@dwAddCodeFile,NULL,FILE_BEGIN ;定位到加入代码地址处
mov ecx,offset APPEND_CODE_END-offset APPEND_CODE
invoke WriteFile,@hFile,offset APPEND_CODE,ecx,addr @dwTemp,NULL ;将需加入的代码写入空隙处
.else
;********************************************************************
; (Part 3.2)如果有新的节表空间的话,加入一个新的节
;********************************************************************
inc [edi].FileHeader.NumberOfSections ;增加一个新的节,节的数量加1
mov eax,[edx].PointerToRawData
add eax,[edx].SizeOfRawData
mov [ebx].PointerToRawData,eax
mov ecx,offset APPEND_CODE_END-offset APPEND_CODE
invoke _Align,ecx,[esi].OptionalHeader.FileAlignment ;将新添加后的节按指定值对齐
mov [ebx].SizeOfRawData,eax
invoke _Align,ecx,[esi].OptionalHeader.SectionAlignment ;将新添加后的节按指定的值对值
add [edi].OptionalHeader.SizeOfCode,eax ;修正SizeOfCode
add [edi].OptionalHeader.SizeOfImage,eax ;修正SizeOfImage
invoke _Align,[edx].Misc.VirtualSize,[esi].OptionalHeader.SectionAlignment
add eax,[edx].VirtualAddress
mov [ebx].VirtualAddress,eax ;给新添加的节的各个字段赋值
mov [ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
mov [ebx].Characteristics,IMAGE_SCN_CNT_CODE\
or IMAGE_SCN_MEM_EXECUTE or IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
invoke lstrcpy,addr [ebx].Name1,addr szMySection
;********************************************************************
; 将新增代码作为一个新的节写到文件尾部
;********************************************************************
invoke SetFilePointer,@hFile,[ebx].PointerToRawData,NULL,FILE_BEGIN ;定位到新添加的节的位置
invoke WriteFile,@hFile,offset APPEND_CODE,[ebx].Misc.VirtualSize,\ ;将代码写入到新添加的节的位置
addr @dwTemp,NULL
mov eax,[ebx].PointerToRawData
add eax,[ebx].SizeOfRawData
invoke SetFilePointer,@hFile,eax,NULL,FILE_BEGIN ;定位到文件的开始
invoke SetEndOfFile,@hFile ;然后得到文件的结尾位置
;********************************************************************
push [ebx].VirtualAddress ;eax = 新加代码的基地址
pop @dwAddCodeBase
push [ebx].PointerToRawData
pop @dwAddCodeFile
.endif
;********************************************************************
; (Part 4)修正文件入口指针并写入新的文件头
;********************************************************************
mov eax,@dwAddCodeBase
add eax,(offset _NewEntry-offset APPEND_CODE)
mov [edi].OptionalHeader.AddressOfEntryPoint,eax ;将入口地址写入文件头
invoke SetFilePointer,@hFile,0,NULL,FILE_BEGIN ;定位到文件开始,写入新的文件头
invoke WriteFile,@hFile,@lpMemory,[esi].OptionalHeader.SizeOfHeaders,\
addr @dwTemp,NULL
;********************************************************************
; (Part 5)修正新加代码中的 Jmp oldEntry 指令
;********************************************************************
push [esi].OptionalHeader.AddressOfEntryPoint
pop @dwEntry
mov eax,@dwAddCodeBase
add eax,(offset _ToOldEntry-offset APPEND_CODE+5) ;定位到原来入口点代码处+5是因为
sub @dwEntry,eax ;JMP XXXXXXXX占用了五个字节
mov ecx,@dwAddCodeFile
add ecx,(offset _dwOldEntry-offset APPEND_CODE)
invoke SetFilePointer,@hFile,ecx,NULL,FILE_BEGIN
invoke WriteFile,@hFile,addr @dwEntry,4,addr @dwTemp,NULL ;写入跳转到原代码的代码
;********************************************************************
; (Part 6)关闭文件
;********************************************************************
invoke GlobalFree,@lpMemory ;释放内存,关闭文件句柄
invoke CloseHandle,@hFile
invoke wsprintf,addr @szBuffer,Addr szSuccess,addr @szNewFile
invoke SetWindowText,hWinEdit,addr @szBuffer
_Ret:
assume esi:nothing
popad
ret
_ProcessPeFile endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
上面的我作了简单的注释,这里我在具体分析一下这段代码,请大家仔细研究一下,很有意思的!!
Part 1从原始PE文件拷贝一个名为“原始文件名_new.exe”的文件,这个文件将被添加上可执行代码,原来的“原始文件名.exe”文件则不会被改动。当文件成功拷贝后,程序将打开拷贝生成的新文件以便进行修改。
Part 2分配一个等于目标PE文件的文件头大小的内存块,并将文件头拷贝到这个内存块中,所有对PE文件头的修改操作都是在这个内存块中完成的,这个内存块的内容最终将被写到“原始文件名_new.exe”文件中。完成拷贝工作以后,程序计算两个指针以备后用:指向节表最后一项指针和指向节表尾部的指针,这两个指针可以从节表的数量和节表的长度计算而来的,节表的数量是从PE文件头中的FileHeader.NumberOfSections字段获取的。
正如本节的开始所述,新增的代码既可以插入原代码所处的节的空隙中,也可以通过添加一个新的节来附在原文件的尾部,为了增加成功的机会,应该对这两种情况都给予考虑,于是在Part2中对节表的尾部进行全零数据的扫描,如果存在一段全零的位置可供放入一个新的节表,那么采取增加新节的办法(Part3.2),否则采用在代码节的空隙中插入的方法(Part3.1)
Part 3.1 中对所有节表进行循环扫描,以便于找到代码节并检测节的空隙是否可以容纳新增的代码,程序首先判断SizeOfRawData是否为0,这个数值为0,说明这个节是包含未初始化数据的节,不能用于插入代码,如果SizeOfRawData大于0的话,则检测Characteristics字段查看当前节是否为代码节(包含IMAGE_SCN_MEM_EXECUTE标志)。
通过检测后,程序计算空隙的大小(SizeOfRawData和Misc.VirtualSize之差)是否大于插入代码的长度,如果空隙足够大的话,则进行插入操作。在插入代码的同时,这个节的属性中必须被加上IMAGE_SCN_MEM_READ和IMAGE_SCN_MEM_WRITE标志,因为附加代码中使用了对被加入部分进行写操作的指令。另外,VirtualSize字段中的实际数据大小也需要被修正。上面的程序只考虑了在代码节中插入的情况,要是代码节中的空隙大小不够,那么程序就退出了。实际上,程序也可以在其他的节中插入代码,只要将节的属性同时也加上IMAGE_SCN_MEM_EXECUTE标志就可以了,要是进一步将程序完成的话,当单个节的空隙大小不够的时候,也可以将附加代码分块插入多个节中,不过这时附加代码中就必须考虑在执行前将代码重新拼装在一起这个步骤了。
Part 3.2在节表中加入一个新的节表项目,节表项中的VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristics和Name1字段需要被设置。其中Name1中的名称被设置为“.adata”;Characteristics字段中的标志被设置为可执行和可读写,其他几个字段值的算法如下(下面的“上一节”指原始PE文件的最后一节):
● PointerToRawData=(上一节的PointerToRawData)+(上一节的SizeOfRawData)
● SizeOfRawData=附加代码的长度按FileAlignMent值对齐
● VirtualAddress=(上一节的VirtualAddress)+(上一节的VirtualSize按SectionAlignMent的对齐值)
● VirtualSize=附加代码的长度按SectionAlignMent值对齐
其中的对齐算法是用_Align子程序来完成的。在这一部分中,程序还修正了文件头中的SizeOfCode和SizeOfImage的值。如果SizeOfImage的值不被修正的话,Windows将无法装入修改后的PE文件,报的错误为“这不是一个有效的Win32可执行文件”。Part3.2的最后,程序将附加代码写到文件的最后,由于附加代码的长度还没有按FileAlignment的值对齐,所以程序再次使用SetFilePointer函数将文件指针移动到对齐后的位置并用SetFileEnd函数将文件长度扩展到这里。无论是Part3.1还是Part3.2的最后,程序将新增代码在文件中位置和在内存中的位置分别保存在@dwAddCodeFile和@dwAddCodeBase变量中以备后用。
Part4 修改PE文件头中的文件入口地址,并将修改后的整个PE文件头写入到新文件中。
Part 5将原始PE文件的入口地址取出,和附加代码的入口地址计算得出“jmp 原入口地址”这条指令中的二进制码值,并将这个值写到附加代码的对应位置中。JMP指令的编码方式是由一个0e9h字节加上指令执行后的EIP的修正值,也就是说,当JMP指令的下一句指令地址是addr1,而跳转的目标地址是addr2的放在,那么0e9h字节后的双字的值就是addr2-addr1,所以下面的几句就是将指令改成"JMP原入口址"的样子:
push [esi].OptioinalHeader.AddressOfEntryPoint
pop @dwEntry
mov eax,@dwAddCodeBase
add eax,(offset _ToOldEntry--offset APPEND_CODE+5)
sub @dwEntry,eax
在指令列执行前,ESI指向PE文件头,@dwAddCodeBase中保存有新增代码被装载到内存后的起始地址,所以由(1)标出的指令执行后,EAX的值是_ToOldEntry后面的5个字节的位置,或者说是JMP XXXXXXXX后一条指令的位置,也就是上面算式中的addr1。@dwEntry中的原始值是可执行文件原来的入口地址,也即addr2,指令(2)执行后,@dwEntry中的值就是addr2-adr1b ,这就是需要填入_dwOldEntry位置的数据。
接下来程序用SetFilePointer函数将文件指针移动到新增代码中的_dwOldEntry位置,并将上面计算出的结果写入文件中。
Part 6进行扫尾工作,如释放内存、关闭文件和显示成功信息等。至此,程序的所有功能就完成了。
写到这里,基本上也对现了承诺,在这七天的时间里陪大家重新温习了一下PE的相关知识,最近在学习内核和驱动方面的知识,希望想交流的朋友能加我的看雪ID号或QQ号,谢谢!
前面有朋友说作个链接,在这里作了一个小链接,以方便大家参考学习,温故而知新!!!
国庆PE总复习(一)(二) http://bbs.pediy.com/showthread.php?t=121488
国庆PE总复习(三) http://bbs.pediy.com/showthread.php?t=121595
国庆PE总复习(四) http://bbs.pediy.com/showthread.php?t=121672
国庆PE总复习(五) http://bbs.pediy.com/showthread.php?t=121695
国庆PE总复习(六) http://bbs.pediy.com/showthread.php?t=121748
国庆长假结束了,希望大家在看雪都能找一个属于自己的位置,早日成为看雪上的技术牛人!!