首页
社区
课程
招聘
[原创]一步一步实现在PE文件中添加可执行代码
发表于: 2012-5-28 13:07 26761

[原创]一步一步实现在PE文件中添加可执行代码

2012-5-28 13:07
26761
最近在学习罗云彬老师写的《Windows环境下32位汇编语言程序设计》书中的PE文件,着重分析了下如何在PE文件中添加可执行代码的问题。因为书上对这个程序没有做过多分析,这里我把自己的分析过程及一些问题进行讲解,希望能给初学者一点帮助。
        众所周知,PE文件是Windows下的文件格式,包括exe、dll等,那么如何向PE文件中添加可执行代码呢?我们应该有以下这些问题:
1、        可执行代码应该添加到PE文件的什么位置?
2、        如何让添加的代码最先执行?
3、        执行完添加的代码后如何回到原PE文件的起始地址?
4、        添加的代码如何获取需要用到的API地址?
带着这些问题,我们的主题就开始了。
准备知识:
1、        首先你得知道PE文件结构,不然很多问题你不会很明白,附件中有一张我作的PE文件结构的思维导图,希望能对你有帮助。
2、        新加的代码功能很简单,就是在运行这个PE文件之前弹出一个选择对话框,询问是否继续。代码放在后面附件中,请读者先下载看看,后面的讲解中会涉及到。
3、        了解Windows环境下的程序设计,不然你会不知道我在说什么,呵呵。。
好了,废话不多说,我们进入正题。
1、        使用通用控件GetOpenFileName函数打开一个查找文件的对话框,选择想要添加代码的文件。获得文件句柄后,通过内存映射函数CreateFileMapping和MapViewOfFile获取PE文件的起始地址。然后通过对文件的MZ格式文件头和PE头分析该文件是不是有效的PE文件,如果不是直接退出,如果是则进入下一步。这部分的代码比较简单,就直接写在下面了。
               
local	@stOF:OPENFILENAME
		local	@hFile,@dwFileSize,@hMapFile,@lpMemory

		invoke	RtlZeroMemory,addr @stOF,sizeof @stOF
;初始化OPENFILENAME结构,以便使用
		mov	@stOF.lStructSize,sizeof @stOF
		push	hWinMain
		pop	@stOF.hwndOwner
		mov	@stOF.lpstrFilter,offset szExtPe
		mov	@stOF.lpstrFile,offset szFileName
		mov	@stOF.nMaxFile,MAX_PATH
		mov	@stOF.Flags,OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST
		invoke	GetOpenFileName,addr @stOF
		.if	! eax
			jmp	@F
		.endif
; 打开文件获得文件句柄
			invoke	CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
			FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
		.if	eax !=	INVALID_HANDLE_VALUE
			mov	@hFile,eax
			invoke	GetFileSize,eax,NULL
			mov	@dwFileSize,eax
			.if	eax

;用获得的文件句柄建立文件mapping
				invoke	CreateFileMapping,@hFile,NULL,PAGE_READONLY,0,0,NULL
				.if	eax
					mov	@hMapFile,eax
					invoke	MapViewOfFile,eax,FILE_MAP_READ,0,0,0
					.if	eax
						mov	@lpMemory,eax
; 检测 PE 文件是否有效
						mov	esi,@lpMemory
						assume	esi:ptr IMAGE_DOS_HEADER
;检查MZ格式文件头
						.if	[esi].e_magic != IMAGE_DOS_SIGNATURE
							jmp	_ErrFormat
						.endif
						add	esi,[esi].e_lfanew
						assume	esi:ptr IMAGE_NT_HEADERS
;检查PE文件头
						.if	[esi].Signature != IMAGE_NT_SIGNATURE
							jmp	_ErrFormat
						.endif
						invoke	_ProcessPeFile,@lpMemory,esi,@dwFileSize

        判断完是有效的PE文件后,我们进入ProcessPeFile函数的编写,在这个函数中,我们要完成可执行代码的添加功能,下面的具体操作:
1、        将要添加代码的文件复制一份,并重命名为xxx_new.exe,然后用CreateFile函数打开文件,获取文件句柄,下面在代码中进行讲解。
;获取路径及文件名
		invoke	lstrcpy,addr @szNewFile,addr szFileName		
		invoke	lstrlen,addr @szNewFile
;获取新文件名字符串的地址
		lea	ecx,@szNewFile
;eax是路径及文件名的长度,eax-4就是除去后面的.exe格式后的长度,然后再加上新文件的名字xxx_new.exe构成一个新的.exe文件。					
		mov	byte ptr [ecx+eax-4],0		
		invoke	lstrcat,addr @szNewFile,addr szExt
;复制文件内容,建立新文件成功。
		invoke	CopyFile,addr szFileName,addr @szNewFile,FALSE
;打开文件,获取句柄,并保存到@hFile变量中,以便后面使用。
		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

2、        接下来我们需要用GlobalAlloc函数分配一个等于原始PE文件的文件头大小的内在块,并使用RtlMoveMemory函数将PE文件头复制到这个内存块中,然后利用节的数目定义两个指针,edx指向最后一个节表,ebx指向新加的节表。下面在代码中进行一些分析。
		mov	esi,_lpPeHead
		assume	esi:ptr IMAGE_NT_HEADERS,edi:ptr IMAGE_NT_HEADERS
;分配内存空间
		invoke	GlobalAlloc,GPTR,[esi].OptionalHeader.SizeOfHeaders
		mov	@lpMemory,eax
		mov	edi,eax
;将PE文件头复制到新分配的内存中
		invoke	RtlMoveMemory,edi,_lpFile,[esi].OptionalHeader.SizeOfHeaders
;下面两句是为了让分配的内存指针指跳过DOS块,指向PE文件头。Edi是新分配内存的起始地址,esi是原文件的PE文件头,_lpFile是原文件的起始地址。
		add	edi,esi
		sub	edi,_lpFile
;确定两个指针
		movzx	eax,[esi].FileHeader.NumberOfSections
		dec	eax			;注意这减了1
		mov	ecx,sizeof IMAGE_SECTION_HEADER
		mul	ecx
;现在eax中的值是节的总大小减去一个节的大小
		mov	edx,edi
		add	edx,eax
		add	edx,sizeof IMAGE_NT_HEADERS
;现在edx指向最后一个节的开始
		mov	ebx,edx
		add	ebx,sizeof IMAGE_SECTION_HEADER
;现在ebx指向新加节的开始
		assume	ebx:ptr IMAGE_SECTION_HEADER,edx:ptr IMAGE_SECTION_HEADER
[ATTACH]67747[/ATTACH]


3、        现在回答第一个问题,要添加的代码应该添加到PE文件的什么地方?
1.        因为节中的内容在磁盘文件中是按照FileAlignment对齐的,在内存中是按照SectionAlignment对齐的,所以在一个节中可能存在一个空闲区,能够将要添加的代码装下,但这种方法有很大的局限性。
2.        我们可以在已有节的基础上,添加新的节。
下面先作判断:
		pushad
		mov	edi,ebx
		xor	eax,eax
		mov	ecx,IMAGE_SECTION_HEADER
		repz	scasb		;这句话的意思可以用一个C语言代码来解释

;while(--ecx)
					;{
					;	if(*(edi++)!=al)
					;		break;
					;}

		Popad

下面是在节中插入代码的方法:
1.        依次查找每一个节,判断是否有足够空间。
			xor	eax,eax
			mov	ebx,edi
			add	ebx,sizeof IMAGE_NT_HEADERS
			.while	ax <=	[esi].FileHeader.NumberOfSections
;从第一个节的文件偏移地址开始
				mov	ecx,[ebx].SizeOfRawData
;要判断的内容不仅看ecx是否为空,还要判断这个节的属性是否为可执行,要不然如果把可执行代码写在了一个只读的节中,那会发生内存错误的。
				.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
						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

2.如果其中有可以使用的节,那么就将要添加的代码写到这个节中,如下:
@@:
;获取新加代码的基址,并存入@dwAddCodeBase变量中
			mov	eax,[ebx].VirtualAddress
			add	eax,[ebx].Misc.VirtualSize
			mov	@dwAddCodeBase,eax
;获取新加代码的在磁盘文件中相对于文件头的偏移量
			mov	eax,[ebx].PointerToRawData
			add	eax,[ebx].Misc.VirtualSize
			mov	@dwAddCodeFile,eax
;将这个节的大小加上代码的大小,存在VirtualSize属性中。
			add	[ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
;下面的代码是将要添加的代码写在这个节的空闲处。
			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

        下面是在原PE文件中添加新的节的方法:
        添加了一个节后,首先你在更新节表信息,就像你去银行存了钱,银行的业务员帮你更新你的账单一样。这里需要更新节的一些重要属性,包括PointerToRawData,SizeOfRawData,VirtualAddress,VirtualAddress,VirtualSize,Characteristics,Name1等属性。
下面是完成这些操作的代码:
			inc	[edi].FileHeader.NumberOfSections
			push	edx
			@@:
			mov	eax,[edx].PointerToRawData
; 当最后一个节是未初始化数据时,PointerToRawData和SizeOfRawData等于0
; 这时应该取前一个节的PointerToRawData和SizeOfRawData数据
			.if	! eax
				sub	edx,sizeof IMAGE_SECTION_HEADER
				jmp	@B
			.endif
			add	eax,[edx].SizeOfRawData
			pop	edx
			mov	[ebx].PointerToRawData,eax
;获取按照FileAlignment对齐的磁盘文件大小
			mov	ecx,offset APPEND_CODE_END-offset APPEND_CODE
			invoke	_Align,ecx,[esi].OptionalHeader.FileAlignment
			mov	[ebx].SizeOfRawData,eax
;下面的三句代码是修改OptionHeader中的两个属性SizeOfCode和SizeOfImage,如果不修改SizeOfImage的值,Windows将无法装入修改后的PE文件,系统会报“这不是一个有效的PE文件”。
			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
下面是向新加的节中写入要添加的代码:
;用SetFilePointer设置写入的超始点
			invoke	SetFilePointer,@hFile,[ebx].PointerToRawData,NULL,FILE_BEGIN
			invoke	WriteFile,@hFile,offset APPEND_CODE,[ebx].Misc.VirtualSize,\
				addr @dwTemp,NULL
;因为要写入的代码没有按照FileAlignment对齐,所以再次用SetFilePointer将起始点设置到代码结束的地方
			mov	eax,[ebx].PointerToRawData
			add	eax,[ebx].SizeOfRawData
			invoke	SetFilePointer,@hFile,eax,NULL,FILE_BEGIN
			invoke	SetEndOfFile,@hFile
;保存新加节的内存起始地址和磁盘文件起始地址
			push	[ebx].VirtualAddress
			pop	@dwAddCodeBase
			push	[ebx].PointerToRawData
			pop	@dwAddCodeFile

4、        现在回答第二个问题,如何让添加进来的代码最先执行。PE结构中的OptionHeader数据结构中的AddressOfEntryPoint属性可以设置文件的入口地址,所以我们可以把“新加节的基址+添加代码的起始地址”设为AddressOfEntryPoint的值。
		mov	eax,@dwAddCodeBase
;因为新加的代码中含有数据的定义,占据了一定的内存,所以要用代码开始的地址减去数据开始的地址,得到真正的代码地址,再加上VirtualBase的值,就是文件开始的地址。
		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

5、        接下来回答第三个问题,怎样让程序执行完添加的代码后又从原文件的起始部分开始执行呢,下面介绍实现这个功能的操作。
;取出原文件的入口地址,请注意看清这里是esi,是原文件的PE文件头,我之前由于粗心看成了ebx,结果一直没看懂。
       
	push	[esi].OptionalHeader.AddressOfEntryPoint
		pop	@dwEntry
;这里有点复杂,需要认真分析。首先,要弄清楚jmp指令的机器码是如何生成的,jmp指令的机器码由转移类别0e9h+偏移地址组成,偏移地址就是目的地址与jmp下一条指令地址的差,上面我们已经得到了目的地址(就是原PE文件的入口地址),记为a,现在来找jmp下一条指令的地址,观看添加的代码发现_ToOldEntry标号对应的指令是jmp,那么再加上这条指令的长度5,就可以得到下一条指令的地址,记为b。现在用a-b就能得到偏移量,如下面的第三句代码。
 [ATTACH]67748[/ATTACH]
		mov	eax,@dwAddCodeBase
		add	eax,(offset _ToOldEntry-offset APPEND_CODE+5)
		sub	@dwEntry,eax
;找到偏移量后,将这个值写到jmp后面的双字中,当程序执行到这里后,会把jmp和后面的数据(偏移量)看成一句指令,由此进行跳转。
		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

到这里,添加代码的任务就完成了。当然,最后不要忘记关闭文件句柄。
6、        下面回答第四个问题,添加进来的代码如何获取API地址。有人说,直接在要添加的代码中加入导入库不就行了吗?这是平常的程序的写法,请注意我们这里是要向别的文件中添加代码,所以不能在代码中直接使用API函数,因为函数的地址在不同的进程中会随着DLL装入位置的不同而不同,如果在代码中直接调用API函数,那么系统会按照当前进程(即原PE文件)的DLL装入位置填入函数地址,这显然存在错误。正确的方法是在程序中动态获取API地址,即先用LoadLibrary函数装入需要用到的DLL,然后用GetProcAddress函数获取需要用到的函数地址。具体操作请参看我前几天发的“动态获取API地址详解”,这里我也会把这个文档添加到附件,请读者参考前面的一起学习。

教程到这里也算结束了,希望对大家有帮助。
以前看别人做的技术文章觉得挺辛苦的,今天自己试着写了下,确实有点累呀,昨天下午两点开始写,一直到晚上12点过,写了些又改,今天早上起来又继续写了些,才算基本完成。
附件中是《Windows环境下32位汇编语言程序设计》书中的程序+我做的PE文件结构思维导图+动态获取API地址详解DOC文档+本节的DOC文档。

[课程]Android-CTF解题方法汇总!

上传的附件:
收藏
免费 6
支持
分享
最新回复 (30)
雪    币: 1689
活跃值: (379)
能力值: ( LV15,RANK:440 )
在线值:
发帖
回帖
粉丝
2
沙发……ing
2012-5-28 13:12
0
雪    币: 297
活跃值: (235)
能力值: ( LV4,RANK:55 )
在线值:
发帖
回帖
粉丝
3
挺详细的东西
2012-5-28 13:28
0
雪    币: 458
活跃值: (306)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
4
大家有什么问题可以提出来,一起分析讨论。
2012-5-28 13:48
0
雪    币: 615
活跃值: (172)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
5
前段时间毕业设计我是做PE的研究和加壳机的设计实现,也要做代码移植,对这个还是比较了解的!
2012-5-28 13:55
0
雪    币: 615
活跃值: (172)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
6
太长了,上课去了,回来有时间再看...........
2012-5-28 13:59
0
雪    币: 458
活跃值: (306)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
7
我还要等两年才做毕业设计啊。。
2012-5-28 16:14
0
雪    币: 59
活跃值: (1481)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
呵呵,有本书叫 《Windows PE权威指南 》,上面也有非常详细的介绍在pe中添加代码的章节
2012-5-28 18:53
0
雪    币: 602
活跃值: (45)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
还没看到,不知道有没有下载版本
2012-5-29 05:56
0
雪    币: 221
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
图不错~~~~
2012-5-29 09:09
0
雪    币: 458
活跃值: (306)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
11
谢谢推荐,有空去图书馆找找这本书。。
2012-5-29 09:42
0
雪    币: 1233
活跃值: (907)
能力值: ( LV12,RANK:750 )
在线值:
发帖
回帖
粉丝
12
MARK一下,幸苦了
2012-5-29 09:50
0
雪    币: 105
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
牛人,学习了!
2012-5-29 10:29
0
雪    币: 90
活跃值: (91)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
从没见过这么详细的东东 感谢楼主
2012-5-29 10:51
0
雪    币: 458
活跃值: (306)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
15
我也不常做思维导图,还没有用到思维导图的核心东西----图片
2012-5-29 18:21
0
雪    币: 232
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
太好了,有打包的哇,不知道可以写入多大的数据进去....
2012-5-29 20:03
0
雪    币: 458
活跃值: (306)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
17
mov        [ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
invoke        WriteFile,@hFile,offset APPEND_CODE,[ebx].Misc.VirtualSize,\
                                addr @dwTemp,NULL
理论上应该可以写入足够大的数据,具体没有试过。
2012-5-29 20:46
0
雪    币: 71
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
不错

值得一看
2012-5-29 21:42
0
雪    币: 458
活跃值: (306)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
19
我也只是把我理解的写出来。。
2012-5-30 20:32
0
雪    币: 154
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
流程非常详细。楼主可以考虑在解释完所有流程后,画一个大致的流程图然后总结下使用了哪些API,好文一个,赞!
2012-5-31 11:33
0
雪    币: 458
活跃值: (306)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
21
嗯,这个建议好,下次再写东西的时候添上去。。
2012-5-31 18:22
0
雪    币: 3080
活跃值: (5104)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
有分析这个的精力还不如去自己看看别人的外壳代码,自己写个壳子呢
2012-6-5 01:49
0
雪    币: 458
活跃值: (306)
能力值: ( LV12,RANK:400 )
在线值:
发帖
回帖
粉丝
23
正在研究外壳编写。。
2012-6-5 22:28
0
雪    币: 219
活跃值: (38)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
先顶后看,感谢LZ
2012-6-6 16:55
0
雪    币: 201
活跃值: (16)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
楼主 请问你的图使用什么软件画的
2012-6-9 11:22
0
游客
登录 | 注册 方可回帖
返回
//