本章将研究PE文件的可塑性,通过对PE文件进行变形,看是否能通过操作系统的PE加载器。本章的目标是通过手工打造一些小的PE程序,以便探究PE文件结构与操作系统PE加载器之间的关系。
研究PE变形技术不局限于了解PE加载器加载PE的机制,还在于通过变形可以实现反调试、运行劫持等。
12.1 变形技术的分类
所谓变形是指通过改变链接器生成的PE文件内容,扩大或缩小文件尺寸,用以测试PE加载器的机制及健壮性。本节主要讲述静态PE文件中的四种变形技术,它们依次是:
❑ 结构重叠技术
❑ 空间调整技术
❑ 数据转移技术
❑ 数据压缩技术
下面分别介绍这四种变形技术。
12.1.1 结构重叠技术
结构重叠技术是指在不影响正常性能的前提下,将某些数据结构进行重叠的技术。在缩小PE的变形中将大量使用这种技术。现在举例说明,以下是一个使用了结构重叠的PE文件头部字节码:
00000000 4D 5A 48 65 6C 6C 6F 57 6F 72 6C 64 50 45 00 00 MZHelloWorldPE..
00000010 4C 01 01 00 D6 53 0F 4C 00 00 00 00 00 00 00 00 L...S.L........
00000020 E0 00 0F 01 0B 01 05 0C B0 01 00 00 00 00 00 00 ..............
00000030 00 00 00 00 9C 00 00 00 00 00 00 00 0C 00 00 00 ...............
该文件头部就是典型的IMAGE_DOS_HEADER和IMAGE_NT_HEADERS两个结构的重叠。首先分开来看,如果把这部分数据看成是IMAGE_DOS_HEADER,则各部分的值为:
IMAGE_DOS_HEADER STRUCT
e_magic WORD ? ;“MZ”
e_cblp WORD ? ; 48 65 最后(部分)页中的字节数
e_cp WORD ? ; 6C 6C 文件中的全部和部分页数
e_crlc WORD ? ; 6F 57 重定位表中的指针数
e_cparhdr WORD ? ; 6F 72头部尺寸,以段落为单位
e_minalloc WORD ? ; 6C 64 所需的最小附加段
e_maxalloc WORD ? ; 50 45 所需的最大附加段
e_ss WORD ? ; 00 00 初始的SS值(相对偏移量)
e_sp WORD ? ; 4C 01 初始的SP值
……
e_lfanew DWORD ? ; 0C 00 00 00 PE头相对于文件的偏移地址
可以看到,指向PE文件头部的字段依然是在偏移3Ch处。IMAGE_DOS_HEADER的40个字节一个不缺,所以,它是一个完整的DOS MZ头结构。
由于两个结构并不是从一开始就重叠,所以在IMAGE_DOS_HEADER结构的0ch偏移处两个结构开始重叠。从该位置处开始的IMAGE_NT_HEADERS结构各字段的值分别是:
IMAGE_NT_HEADERS STRUCT
Signature DWORD ? ; 50 45 00 00 PE文件标识,“PE\0\0”
IMAGE_FILE_HEADER.Machine WORD ? ; 4C 01 运行平台
IMAGE_FILE_HEADER.NumberOfSections WORD ? ; 01 00 PE中节的数量
IMAGE_FILE_HEADER.TimeDateStamp DWORD ? ; D6 53 0F 4C - 文件创建日期和时间
IMAGE_FILE_HEADER.PointerToSymbolTable DWORD ? ; 00 00 00 00 指向符号表
……
IMAGE_NT_HEADERS ENDS
从上面的分析可以看出,两个结构从以下字段开始发生重叠:
❑ IMAGE_NT_HEADERS.Signature
❑ IMAGE_DOS_HEADER.e_maxalloc+ IMAGE_DOS_HEADER.e_ss
两个结构中的字段发生了重叠,结构自然也就重叠了。那么为什么结构重叠了却没有发生加载错误呢?得益于以下三点:
1)被覆盖的数据可能是另一个结构中无用的数据。
2)有用的数据可能只对一个结构起作用,但有时被覆盖的数据在两个结构中都有用。此种情况下发生的重叠必须保证重叠的字段在两个结构中拥有相同值。
3)PE加载器并不检测所有的字段。
重叠以后的两个数据结构关系见图12-1。

图12-1 结构重叠示意图
从图中可以看出,重叠以后的数据明显变少了。
12.1.2 空间调整技术
本小节以不固定大小的数据块DOS STUB为例介绍空间调整技术。具体思路是,通过调整字段IMAGE_DOS_HEADER.e_lfanew的值,实现动态地扩充或缩小DOS STUB块空间,从而达到PE变形的目的。
这里以第6章的免导入、免重定位的HelloWorld1_1.exe作为蓝本,目标是将该PE文件扩充一个内存页大小。以下是详细的测试步骤:
使用FlexHex建立一个大小为5120字节的HelloWorld1_10.exe程序,并执行以下操作:
步骤1 修改IMAGE_DOS_HEADER.e_lfanew的值,增加一个页面大小1000h,由原来的000000A8更改为000010A8。

修改为000010A8

步骤2 将HelloWorld1_1.exe的PE标识符起始位置开始的所有非零数据全部复制到000010A8位置(采用覆盖方式)。

将选中数据导出到pe.zim文件

定位到000010A8位置,导入pe.zim的数据

覆盖后:

步骤3 修改字段IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint的值,由原来的00001124更改为00002124。

偏移位置10A8h + 28h = 10D0h (参考第70页 PE结构)

步骤4 修 改 字 段IMAGE_OPTIONAL_HEADER32.SizeOfImage的 值,由 原 来 的00002000更改为00003000。

偏移位置10A8h+50h = 10F8h(参考第70页 PE结构)

步骤5 修改字段IMAGE_OPTIONAL_HEADER32.SizeOfHeaders的值,由原来的00000200更改为00001200。

偏移位置10A8h+54h = 10FCh(参考第70页 PE结构)

步骤6 修改字段IMAGE_SECTION_HEADER.VirtualAddress的值,由原来的00001000更改为00002000。

只有一个.text节,找到对应位置(参数74页偏移位置),修改后:

步骤7 修改字段IMAGE_SECTION_HEADER.PointerToRawData的值,由原来的00000200更改为00001200。

找到对应位置(参数74页偏移位置),修改后:

因为该文件没有重定位信息、没有导入表、没有数据段、没有数据目录项,且只有一个节,所以本测试中所有需要修改的参数都已列出。
运行chapter12\HelloWorld1_10.exe,发现可以正常显示对话框。

在OD中查看内存分配,可以看到文件头部被扩充了一个页面大小,如图12-2所示。在操作系统查看两个文件大小之差为4096,十六进制刚好是1000h,打造HelloWorld1_10的实验证明调整DOS_STUB块空间是可行的。


图12-2 扩大后的HelloWorld1_10.exe文件头部占用的内存空间
该实例演示了PE变形中的扩大技术。可以看到,HelloWorld.exe在被加载到虚拟内存空间的PE文件头占用空间的大小,由原来的00001000h变成了00002000h。
12.1.3 数据转移技术
在编程过程中,出于某种考虑,经常会将PE中的一部分数据转移到另一个位置。比如,将程序中的变量存储到文件头部结构的某个字段中,将代码转移到头部结构的某个连续空间中等,这就是数据转移技术。该技术包括对变量的存储和代码的存储。
1.变量存储
变量存储的例子节选自随书文件chapter12\HelloWorld7.exe的PE头部,如下所示:
00000000 4D 5A 48 65 6C 6C 6F 57 6F 72 6C 64 50 45 00 00 MZHelloWorldPE..
该示例将程序要显示的字符串变量移动到了文件头部的IMAGE_DOS_HEADER中,而且与数据结构IMAGE_NT_HEADERS的PE标识字段自动重合,重合的部分为:
50 45 00 00
该部分既可以认为是IMAGE_NT_HEADERS.Signature,也可以认为是字符串“Hello WorldPE\0\0”的一部分。该部分变量原来的位置是在一个独立的节“.data”中,占据文件中的200h个字节;通过这样的转移,使得PE产生变形,不仅节的内容没有了,节表中也少了一个描述该节信息的表项。
2.代码存储
对代码的转储比较普遍,常见的有:PE压缩、病毒、加密与解密等。文件头部的连续空间被认为是存储代码的好地方,如果连续空间的长度无法容纳所有的代码,则可以将代码分解。例如,看OD对第1章中HelloWorld.exe的反汇编代码:

00401000 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00401002 |. 6A 00 PUSH 0 ; |Title = NULL
00401004 |. 68 00304000 PUSH HelloWor.00403000 ; |Text = "HelloWorld-modified by OD"
00401009 |. 6A 00 PUSH 0 ; |hOwner = NULL
0040100B |. E8 08000000 CALL <JMP.&user32.MessageBoxA> ; \MessageBoxA
00401010 |. 6A 00 PUSH 0 ; /ExitCode = 0
00401012 \. E8 07000000 CALL <JMP.&kernel32.ExitProcess>; \ExitProcess
00401017 CC INT3
00401018 $- FF25 08204000 JMP DWORD PTR DS:[<&user32.MessageBoxA>]
; user32.MessageBoxA
0040101E .- FF25 00204000 JMP DWORD PTR DS:[<&kernel32.ExitProcess>
; kernel32.ExitProcess
指令字节码总长度为36字节。变形空间中能够容纳这些代码的有两处:一个是IMAGE_DOS_HEADER,另一个是数据目录表。
假设以上两处没有空间能存放这些代码,我们也可以将这些代码分开来存储,但分开存储时必须要保证调用指令之间的先后顺序。下面是对HelloWorld.exe反汇编代码的连续指令长度的一个统计:
❑ 长度为6字节的指令有3个
❑ 长度为5字节的指令有2个
❑ 长度为2字节的指令有4个
❑ 长度为1字节的指令有1个
有了以上的统计数据,就可以对比变形空间中描述的可用连续字段,将这些指令分别存储在不同的空间位置。以前三条指令为例:
用字段扩展PE头中的BaseOfCode开始的8字节存储6A 00 6A 00指令,另加一条近跳转指令EB 00。
用扩展PE头中的MajorOperatingSystemVersion字段开始的8个字节存储68 00304000指令,另外加一条近跳转指令EB 00。
然后根据两部分的距离修正跳转指令中的操作数如下:
BaseOfCode DWORD ? ;002ch - 代码的节的起始RVA
BaseOfData DWORD ? ;0030h - 数据的节的起始RVA
ImageBase DWORD ? ;0034h - 程序的建议装载地址
SectionAlignment DWORD ? ;0038h - 内存中的节的对齐粒度
FileAlignment DWORD ? ;003ch - 文件中的节的对齐粒度
MajorOperatingSystemVersion WORD ? ;0040h - 操作系统版本号
如上所示,从字段BaseOfCode到字段MajorOperatingSystemVersion,中间隔了三个双字的字段,所以第一条近跳转指令中的操作数为4*3+2=0Eh;另一个操作数则要根据下一条指令所在字段的位置进行计算。
这样,原来的指令:
PUSH 0
PUSH 0
PUSH HelloWo.00403000
就变成了现在的指令:
PUSH 0
PUSH 0
Jmp Loc1:
……
Loc1:
PUSH HelloWo.00403000
以上方法在构造指令时非常复杂,其实还有一种更好的方法,即通过程序编码,使链接器辅助我们构造指令长度。下面为大家演示,步骤如下。
步骤1 未修正前的指令字节码。
以下内容节选自HelloWorld.exe的代码段:

00000400 6A 00 6A 00 68 00 30 40 00 6A 00 E8 08 00 00 00 j.j.h.0@.j.....
00000410 6A 00 E8 07 00 00 00 CC FF 25 08 20 40 00 FF 25 j..... %. @. %
00000420 00 20 40 00 . @.
以上所列为没有修正前的原始指令字节码。
步骤2 修正以后的程序。
通过程序将原始指令字节码分解为多个小块代码,详情见代码清单12-1。
代码清单12-1 分解原始指令代码(chapter12\exp.asm)
;exp.asm 代码清单12-1
;使用 nmake 或下列命令进行编译和链接:
;ml -c -coff exp.asm
;link -subsystem:windows exp.obj
.386
.model flat,stdcall
option casemap:none ;区分大小写
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
;数据段
.data
szText db 'HelloWorldPE', 0
;代码段
.code
start:
push MB_OK
push NULL
push offset szText
jmp short @next1
db 8 dup(0aah) ;在代码中加入了8个字节
@next1:
push NULL
call MessageBoxA
push NULL
call ExitProcess
end start
行26使用伪指令语句db定义了第一块代码(行22~25)到第二块代码(行28~32)之间的间隔(以字节计)。
步骤3 修正后的代码。
下面是加入了补足数据的字节码:

00000400 6A 00 6A 00 68 00 30 40 00 EB 08 AA AA AA AA AA j.j.h.0@..
00000410 AA AA AA 6A 00 E8 08 00 00 00 6A 00 E8 07 00 00 j.....j....
00000420 00 CC FF 25 08 20 40 00 FF 25 00 20 40 00 . %. @. %. @.
与该字节码对应的汇编代码如下:

00401000 < $ 6A 00 PUSH 0
00401002 . 6A 00 PUSH 0
00401004 . 68 00304000 PUSH exp.00403000 ; ASCII "HelloWorldPE"
00401009 . EB 08 JMP SHORT exp.00401013
0040100B . AA STOS BYTE PTR ES:[EDI]
0040100C . AA STOS BYTE PTR ES:[EDI]
0040100D . AA STOS BYTE PTR ES:[EDI]
0040100E . AA STOS BYTE PTR ES:[EDI]
0040100F . AA STOS BYTE PTR ES:[EDI]
00401010 . AA STOS BYTE PTR ES:[EDI]
00401011 . AA STOS BYTE PTR ES:[EDI]
00401012 . AA STOS BYTE PTR ES:[EDI]
00401013 > 6A 00 PUSH 0 ; |hOwner = NULL
00401015 . E8 08000000 CALL <JMP.&user32.MessageBoxA> ; \MessageBoxA
0040101A . 6A 00 PUSH 0 ; /ExitCode = 0
0040101C . E8 07000000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess
00401021 CC INT3
00401022 $- FF25 08204000 JMP DWORD PTR DS:[<&user32.MessageBoxA>]
00401028 .- FF25 00204000 JMP DWORD PTR DS:[<&kernel32.ExitProcess>
代码清单12-1的行25使用了跳转语句(翻译为指令字节码是EB)。在程序源代码中,开发者只需要简单地使用标号来表明跳转指令要跳转到的位置,以及跳转指令后的操作数,即可由编译程序自动生成。从以上反汇编代码中可以看到,EB指令后的操作数为08,这8个字节是代码清单12-1的行26定义的8个0AAh。通过这种简单的方法就可以让编译器帮助我们计算跳转指令的操作数了。
12.1.4 数据压缩技术
在编程的过程中,如果指令代码比较长,还可以先对代码实施压缩,然后在PE头部找一块比较大的连续区域存放解压缩用的代码。程序被PE加载器加载后,文件头就基本不再使用了。这时,可以将存储的压缩代码通过PE头部的解压缩程序进行解压,解压后即可通过跳转指令实施程序指令的转移。
由于压缩以后的代码不便于通过十六进制直观地看到,所以这种方法在一些病毒程序代码中比较常见;另外,在一些加壳程序中会经常看到数据压缩技术。
先来看一个这种技术的应用,以下是某病毒代码的头部信息:
00000000 4D 5A 50 00 01 02 00 03 04 00 01 0F 00 01 FF FF MZP...........
00000010 00 02 B8 00 07 40 00 01 1A 00 22 01 00 02 BA 10 ..?.@...."...?
00000020 00 01 0E 1F B4 09 CD 21 B8 01 4C CD 21 90 90 54 ....???L? T
00000030 68 69 73 20 70 72 6F 67 72 61 6D 20 6D 75 73 74 his program must
00000040 20 62 65 20 72 75 6E 20 75 6E 64 65 72 20 57 69 be run under Wi
00000050 6E 33 32 0D 0A 24 37 00 88 50 45 00 02 4C 01 04 n32..$7. E..L..
00000060 00 01 B5 2C EF 82 00 08 E0 00 01 8E 81 0B 01 02 ..? ..?. ...
00000070 19 00 01 02 00 03 06 00 07 10 00 03 10 00 03 20 ...............
00000080 00 04 40 00 02 10 00 03 02 00 02 01 00 07 03 00 ..@.............
00000090 01 0A 00 06 50 00 03 04 00 06 02 00 05 10 00 02 ....P...........
000000A0 20 00 04 10 00 02 10 00 06 10 00 0C 30 00 02 4E ...........0..N
000000B0 00 1C 40 00 02 0C 00 53 43 4F 44 45 00 05 10 00 ..@....SCODE....
000000C0 03 10 00 03 02 00 03 06 00 0E 20 00 02 60 44 41 .......... ..`DA
000000D0 54 41 00 05 10 00 03 20 00 03 02 00 03 08 00 0E TA..... ........
000000E0 40 00 02 C0 2E 69 64 61 74 61 00 03 10 00 03 30 @..?idata.....0
000000F0 00 03 02 00 03 0A 00 0E 40 00 02 C0 2E 72 65 6C ........@..?rel
00000100 6F 63 00 03 10 00 03 40 00 03 02 00 03 0C 00 0E oc.....@........
00000110 40 00 02 50 00 FF 00 FF 00 FF 00 6B C3 FF 25 30 @..P. . . .k?%0
该病毒对两个标识字段并没有进行大的改动。注意观察节表部分内容,按照基础知识中所介绍的,每个节表最少应该有40个字节,在这里明显大小并不符合。经过仔细分析之后才知道,病毒程序对这部分数据进行了加密处理。下面详细分析病毒是如何加密该部分数据的。
>>50 45 00 02 4C 01
根据前面所学的知识,PE头部应该有两个“\0”,在这里只是用了00-02来表示这两个“\0”,看起来好像使用了简单的行程压缩算法。凡是有连续“\0”的地方都将0的个数作为紧跟在“\0”后面的一项。来看节SCODE的内容,如下所示:
000000B0 00 1C 40 00 02 0C 00 53 43 4F 44 45 00 05 10 00 ..@....SCODE....
000000C0 03 10 00 03 02 00 03 06 00 0E 20 00 02 60
根据以上的猜测来还原该节的实际内容如下:
000000B0 00 1C 40 00 02 0C 00 53 43 4F 44 45 00 00 00 00 ..@....SCODE....
000000C0 00 10 00 00 00 10 00 00 00 02 00 00 00 06 00 00
000000D0 00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60
根据恢复以后的节来看,对该算法的猜测应该是没有问题的。再仔细分析一下,可以看到,该病毒的作者只对文件头部进行了加密,其他部分还是没有更改的。也就是说,要想恢复这个PE文件的内容,只需要对头部进行处理即可。知道原理之后,接下来的解密工作就容易多了,代码清单12-2是解密的源代码。
代码清单12-2 解压病毒文件头部数据(chapter12\UnEncrpt.asm)
;UnEncrpt.asm for Angry Angel 3.0 unzip File Header
;使用 nmake 或下列命令进行编译和链接:
;ml -c -coff sehKernelBase.asm
;link -subsystem:windows sehKernelBase.obj
.386
.model flat,stdcall
option casemap:none ;区分大小写
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
include C:/masm32/include/comdlg32.inc
includelib C:/masm32/lib/comdlg32.lib
TOTAL_SIZE equ 162h
;数据段
.data
szFileSource db 'C:\worm2.exe', 0
szFileDest db 'C:\worm2_bak.exe', 0
dwTotalSize dd 0
hFileSrc dd 0
hFileDst dd 0
dwTemp dd 0
dwTemp1 dd 0
dwTemp2 dd 0
szCaption db 'Got you', 0
szText db 'OK!?^_^', 0
szBuffer db TOTAL_SIZE dup(0)
szBuffer1 db 0ffffh dup(0)
;代码段
.code
start:
;打开文件worm2.exe
invoke CreateFile, addr szFileSource, GENERIC_READ, \
FILE_SHARE_READ, \
0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0
mov hFileSrc, eax
;创建另外一个文件worm2_bak.exe
invoke CreateFile, addr szFileDest, GENERIC_WRITE, \
FILE_SHARE_READ, \
0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0
mov hFileDst, eax
;解压缩头部
invoke ReadFile, hFileSrc, addr szBuffer, \
TOTAL_SIZE, addr dwTemp, 0
mov esi, offset szBuffer
mov edi, offset szBuffer1
mov ecx, TOTAL_SIZE
mov dwTemp2, 0
@@0:
lodsb
mov bl, al
sub bl,0
jz @@1
stosb
inc dwTemp2
dec ecx
jecxz @F
jmp @@0
@@1:
dec ecx
jecxz @F
lodsb
push ecx
xor ecx, ecx
mov cl, al
add dwTemp2, ecx
mov al, 0
rep stosb
pop ecx
dec ecx
jecxz @F
jmp @@0
@@:
invoke WriteFile, hFileDst, addr szBuffer1, \
dwTemp2, addr dwTemp1, NULL
;关闭文件
invoke CloseHandle, hFileDst
invoke CloseHandle, hFileSrc
invoke MessageBox, NULL, offset szText, \
offset szCaption, MB_OK
invoke ExitProcess, NULL
end start
程序首先打开两个文件,一个是待解压的文件,用来读;另一个是解压后的文件,用来写。解压缩的代码在行56~84。行61取出一个字节,然后判断是否为0,如果是则跳转到标号@@1处执行;否则将字节原样写入目标缓冲区szBuffer1。
如果取到的是0,则再取一个字节,该字节记录了0的个数。将该值赋给cl寄存器,使用语句rep stosb将指定个数的0存入目标缓冲区;然后,调整循环次数,并跳转到标号@@0处继续执行下一个循环。
最后,将目标缓冲区中已经解压的字节写入目标文件(行86)。
以上描述了四种基本的PE变形技术,下面探讨PE变形时需要遵循的一些原则,以确保最终变形后的PE能被Windows加载器顺利加载而不发生错误。
12.2 变形技术可用的空间
要想对PE进行变形,需要掌握PE文件中每个位置的数据的可替换特性,即该位置数据是否可以被替换为别的值,某段数据是否可以被其他用途利用等。总体上讲,PE中可以用作变形的空间有以下四类。
12.2.1 文件头部未用的字段
通过基础知识部分的学习我们知道,在PE文件头部有许多字段的值可以被修改和利用。也就是说,出于兼容上的考虑,PE头部的数据结构中为将来预留了很多的字段,这些字段现在有的被强制设置为0,有的则未加任何限制。这些可以被替换的数据见表12-1(以下结论是笔者测试得出的,并不保证能适应所有的场合)。
表12-1 文件头部可用字段

续表

由于文件头中大部分字段不是连续的,受不同PE内容的影响很大。比如,上表并没有列出数据目录表中加载配置和延迟导入表项的空间。如果一个PE中不存在以上特性,则这些空间中的[x].size域就是可用的。不连续的空间通常的用途是存放数据,对于连续的但字节数不多的空间,则可以存放代码。比如,以下常用的指令其字节码本身就不大:
00401000 > $ /EB 02 JMP SHORT exp1.00401004
00401002 |90 NOP
00401003 |90 NOP
00401004 > \90 NOP
00401005 . 33C0 XOR EAX,EAX
00401007 .^ 74 FB JE SHORT exp1.00401004
00401009 . 90 NOP
0040100A . 33C0 XOR EAX,EAX
0040100C . 75 01 JNZ SHORT exp1.0040100F
0040100E . 90 NOP
相对较大的连续空间则可以存放一段较长的指令字节码。这些空间主要包括:IMAGE_DOS_HEADER中的54个字节、标准头12个字节、扩展头14个字节、数据目录52个字节、每个节表项中的20个字节。
12.2.2 大小不固定的数据块
通过对一些大小不固定的数据块进行扩展,也可以获取足够的空间。这些大小不固定的数据块包括:
(1)DOS STUB
由于DOS STUB是为16位系统保留的,其中的任何一个字节都可以填充为任意值。
(2)PE扩展头IMAGE_OPTIONAL_HEADER32
在IMAGE_FILE_HEADER中有一个字段记录了PE扩展头的长度。该字段为:SizeOfOptionalHeader。注意,这个字段为DW类型,最多能扩展一个字的空间。
(3)数据目录项
数据目录表的项数由字段IMAGE_OPTIONAL_HEADER32.NumberOfRvaAndSizes来定义,通过修改该值也可以扩充或缩小文件头的尺寸。
(4)节表
在节表数据结构中有一个值SizeOfRawData,表示节在文件对齐后的尺寸。修改这个值也可以起到扩充节大小的作用。
注意 若修改了一个节的大小,其他节在文件的起始地址都要跟着修改。
理论上讲,只要是大小不固定的块,都有被扩展的可能。关键的问题是,PE加载器的机制是否允许修改,大家可以通过实验自行测试(2)(3)(4)部分的可行性。后面会有一个专门的实验来验证(1)的可行性。
如果可执行文件不存在输出表,那么当PE加载器将其加载到内存以后,PE的文件头部分数据就已经是无用的了。这也就意味着,PE文件头相关数据结构中的所有的字段,在运行期均是可随意填充任何值的。唯一遗憾的是PE加载器在加载完PE以后,把该段内存设置成了只读的R属性。
12.2.3 因对齐产生的补足空间
操作系统对PE文件的强制对齐特性,使得PE文件的节中存有大量为对齐而补足的0。这种机制同样影响到文件头部。由于默认对齐尺寸为200h大小的限制,大部分的系统文件(如记事本、kernel32.dll等)的文件头部只剩下很少的空间。
12.3 PE文件变形原则
前面对PE变形时与字段有关的空间进行了简单的分析,本节重点研究变形时要遵循的一些原则。在对PE文件进行变形时,改动PE数据结构中某些字段的值需要遵循一些原则,如果没有原则地随意变形,将会导致生成的目标PE文件无法被操作系统识别并加载。在对PE进行变形时需要特别注意这些原则。
12.3.1 关于数据目录表
数据目录表的个数必须大于等于2。如果PE文件的最后一个字节位于目录表之间,如介于第3项资源表定义之间,即[DD[2].VirtualAddress]<文件总长度<[DD[2].isize],则文件中无法定义资源表的大小,PE加载器默认资源表的大小为0。
一个完整的数据目录表在普通的PE文件中可读写的字段如下,以下截取了测试用的PE文件的数据目录表。从测试看,连续AA的部分可以是任意值。
00000080 AA AA AA AA AA AA AA AA 70 01 00 00 p...
00000090 3C 00 00 00 00 00 00 00 00 00 00 00 AA AA AA AA <...........
000000A0 AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA
000000B0 AA AA AA AA 00 00 00 00 00 00 00 00 AA AA AA AA ........
000000C0 AA AA AA AA AA AA AA AA AA AA AA AA 00 00 00 00 ....
000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............
000000E0 00 00 00 00 00 00 00 00 00 00 00 00 AA AA AA AA ............
000000F0 AA AA AA AA 00 00 00 00 00 00 00 00 AA AA AA AA ........
00000100 AA AA AA AA
12.3.2 关于节表
PE文件头中可以没有节的定义,但必须将文件头部的字段IMAGE_FILE_HEADER.NumberOfSections设置为1。
12.3.3 关于导入表
导入表是PE的核心。要想在已有的PE中静态引入动态链接库的函数,必须通过变形技术构造一个合理的导入表(这里的“合理”指的是结构上的合理),或者重构已有导入表。
在12.5.7小节将看到一个只有133字节的PE文件。在该文件中,PE头部的数据结构中的字段能减的都减了,能重叠的也都重叠了,但即使是在这么短的PE中,导入表的双桥结构还是存在的。
导入表的IMAGE_IMPORT_DESCRIPTOR结构是顺序排列的。前面我们讲过,“指向的数组最后以一个内容全0的结构作为结束”。其实,这个条件可以宽限到只判断IMAGE_IMPORT_DESCRIPTOR.Name1是否为0即可。如果一个PE文件的结尾刚好没有空间存储该字段对应的值,则系统会默认该字段是存在的,并且其值为0。
12.3.4 关于程序数据
数据可以存储在内存中的任何位置,可以位于文件头,也可以位于其他节中。
代码和数据一样,可以在内存的任何位置,但所在的节(无论是指定的节还是文件头部),其节的属性必须可读、可写、可执行。将代码段设置为可写属性主要是考虑到某些程序会将一些变量存储在代码段,且在程序中有为该变量赋值的代码。
如果所有的数据(程序变量、导入表、IAT等)都在文件头部,也就是说加载进内存的PE文件只有文件头存在,假设其大小为一个页面1000h,那么操作系统会因为IAT的缘故自动将该页面设置为ERW,即可读、可写、可执行(这和以前我们看到的文件头部只读是不一样的)。
12.3.5 关于对齐
节的对齐尺寸必须大于或等于文件的对齐尺寸。由于文件的对齐尺寸被定义为2的N次幂,所以通常会将文件对齐尺寸设置得更小,并使两个的值相等,以达到缩小PE文件的目的。如本章后面讲的两个例子中,文件对齐粒度用了10h,内存对齐粒度用了4h,即:
SectionAlignment=FileAlignment=10h
SectionAlignment=FileAlignment=4h
12.3.6 几个关注的字段
每当修改了程序的尺寸后,程序中相关的字节码的位置、字节码的长度会发生或多或少的变化,这种变化势必会影响一些记录这些位置和大小的字段,表12-2所列字段是在变形时必须要关注、指定或修改的。
表12-2 变形时需要关注、指定或修改的字段

表12-3所列是在变形时可当做固定标志的(即对大多数EXE文件来说经常不变的)字段。
表12-3 变形时可做固定标志的字段

表12-3中带有中括号[]的表达式表示由该地址处取出的值作为定位对应字段的偏移。下面通过讲解两个实际例子的操作过程,学习将PE尺寸变小的方法。
12.4 将PE变小的实例HelloWorldPE
本节要分析的源程序与第1章的HelloWorld.asm有一点区别,即将字符串定义为“HelloWorldPE”。为了能与最终手工打造修改后的PE程序有所区别,这里将源代码及最终生成的PE的字节码分别列出来。
12.4.1 源程序HelloWorld的字节码(2560字节)
要手工打造的源代码见代码清单12-3。
代码清单12-3 手工修改用的HelloWorld源代码(chapter12\helloworld.asm)
;HelloWorld.asm 手工修改用的HelloWorld源代码
;使用 nmake 或下列命令进行编译和链接:
;ml -c -coff pebKernelBase.asm
;link -subsystem:windows pebKernelBase.obj
.386
.model flat,stdcall
option casemap:none ;区分大小写
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
;数据段
.data
szText db 'HelloWorldPE', 0
;代码段
.code
start:
invoke MessageBox, NULL, addr szText, NULL, MB_OK
invoke ExitProcess, NULL
end start
源代码比较简单,程序实现了弹出窗口的功能,弹出的窗口中显示字符串“HelloWorldPE”。

将HelloWorld.asm编译链接生成最终的EXE文件,使用FlexHex打开HelloWorld.exe,复制字节码并按照类别分为以下四部分:文件头部、代码段、导入表和数据段。各部分字节码如下。
(1)文件头部=文件头+节表+补齐(大小400h)


00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ......... ..
00000010 B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 .......@.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 B0 00 00 00 ...............
00000040 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 .....!.L!Th
00000050 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F is program canno
00000060 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 t be run in DOS
00000070 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 mode....$.......
00000080 5D 5C 6D C1 19 3D 03 92 19 3D 03 92 19 3D 03 92 ]\m.=..=..=.
00000090 97 22 10 92 1E 3D 03 92 E5 1D 11 92 18 3D 03 92 "..=....=.
000000A0 52 69 63 68 19 3D 03 92 00 00 00 00 00 00 00 00 Rich.=.........
000000B0 50 45 00 00 4C 01 03 00 E5 9F 11 4C 00 00 00 00 PE..L....L....
000000C0 00 00 00 00 E0 00 0F 01 0B 01 05 0C 00 02 00 00 ...............
000000D0 00 04 00 00 00 00 00 00 00 10 00 00 00 10 00 00 ................
000000E0 00 20 00 00 00 00 40 00 00 10 00 00 00 02 00 00 . ....@.........
000000F0 04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ................
00000100 00 40 00 00 00 04 00 00 00 00 00 00 02 00 00 00 .@..............
00000110 00 00 10 00 00 10 00 00 00 00 10 00 00 10 00 00 ................
00000120 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 ................
00000130 10 20 00 00 3C 00 00 00 00 00 00 00 00 00 00 00 . ..<...........
00000140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000180 00 00 00 00 00 00 00 00 00 20 00 00 10 00 00 00 ......... ......
00000190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000001A0 00 00 00 00 00 00 00 00 2E 74 65 78 74 00 00 00 .........text...
000001B0 24 00 00 00 00 10 00 00 00 02 00 00 00 04 00 00 $...............
000001C0 00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60 ............ ..`
000001D0 2E 72 64 61 74 61 00 00 92 00 00 00 00 20 00 00 .rdata...... ..
000001E0 00 02 00 00 00 06 00 00 00 00 00 00 00 00 00 00 ................
000001F0 00 00 00 00 40 00 00 40 2E 64 61 74 61 00 00 00 ....@..@.data...
00000200 0D 00 00 00 00 30 00 00 00 02 00 00 00 08 00 00 .....0..........
00000210 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 C0 ............@..
000003F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(2)代码段=代码+补齐(大小200h)

00000400 6A 00 6A 00 68 00 30 40 00 6A 00 E8 08 00 00 00 j.j.h.0@.j.....
00000410 6A 00 E8 07 00 00 00 CC FF 25 08 20 40 00 FF 25 j..... %. @. %
00000420 00 20 40 00 00 00 00 00 00 00 00 00 00 00 00 00 . @.............
000005F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(3)导入表=导入表及相关结构+补齐 ( 大小200h)

00000600 76 20 00 00 00 00 00 00 5C 20 00 00 00 00 00 00 v ......\ ......
00000610 54 20 00 00 00 00 00 00 00 00 00 00 6A 20 00 00 T ..........j ..
00000620 08 20 00 00 4C 20 00 00 00 00 00 00 00 00 00 00 . ..L ..........
00000630 84 20 00 00 00 20 00 00 00 00 00 00 00 00 00 00 ... ..........
00000640 00 00 00 00 00 00 00 00 00 00 00 00 76 20 00 00 ............v ..
00000650 00 00 00 00 5C 20 00 00 00 00 00 00 9D 01 4D 65 ....\ .......Me
00000660 73 73 61 67 65 42 6F 78 41 00 75 73 65 72 33 32 ssageBoxA.user32
00000670 2E 64 6C 6C 00 00 80 00 45 78 69 74 50 72 6F 63 .dll.. .ExitProc
00000680 65 73 73 00 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C ess.kernel32.dll
00000690 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000007F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(4)数据段=数据+补齐 (大小200h)

00000800 48 65 6C 6C 6F 57 6F 72 6C 64 50 45 00 00 00 00 HelloWorldPE....
000009F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
以上列出了完整的HelloWorld.exe的字节码,主要是为了和最终打造生成的较小的PE文件进行比对。下面先跳过手工打造过程,看最终生成的目标PE文件。
12.4.2 目标PE文件的字节码(432字节)
最终打造的目标PE见随书文件chapter12\HelloWorld_7.exe,其所有的字节码长度为432字节,可以在Windows XP SP3环境运行,在OD中调试时其内存空间分配如图12-3所示。


图12-3 OD中HelloWorld_7.exe的内存分配
从图中可以看出,HelloWorld_7文件中所有的数据被加载进内存后,均被安排到了文件头的位置,且文件头部数据的访问属性被设置为RWE,即可读、可写、可执行。以下是该PE文件的完整的字节码:27行 * 16 = 432字节
00000000 4D 5A 48 65 6C 6C 6F 57 6F 72 6C 64 50 45 00 00 MZHelloWorldPE..
00000010 4C 01 01 00 D6 53 0F 4C 00 00 00 00 00 00 00 00 L...S.L........
00000020 E0 00 0F 01 0B 01 05 0C B0 01 00 00 00 00 00 00 ..............
00000030 00 00 00 00 9C 00 00 00 00 00 00 00 0C 00 00 00 ...............
00000040 00 00 40 00 10 00 00 00 10 00 00 00 04 00 00 00 ..@.............
00000050 00 00 00 00 04 00 00 00 00 00 00 00 00 10 00 00 ................
00000060 30 01 00 00 00 00 00 00 02 00 00 00 00 00 10 00 0...............
00000070 00 10 00 00 00 00 10 00 00 10 00 00 00 00 00 00 ................
00000080 10 00 00 00 AA AA AA AA AA AA AA AA 40 01 00 00 ....@...
00000090 3C 00 00 00 00 00 00 00 00 00 00 00 6A 00 6A 00 <...........j.j.
000000A0 68 02 00 40 00 6A 00 E8 10 00 00 00 6A 00 E8 0F h..@.j.....j..
000000B0 00 00 00 CC 00 00 00 00 00 00 00 00 FF 25 38 01 ........... %8.
000000C0 40 00 FF 25 30 01 40 00 AA AA AA AA 00 00 00 00 @. %0.@.....
000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000E0 00 00 00 00 00 00 00 00 00 00 00 00 AA AA AA AA ............
000000F0 AA AA AA AA 00 00 00 00 00 00 00 00 AA AA AA AA ........
00000100 AA AA AA AA 2E 48 65 6C 6C 6F 50 45 B0 01 00 00 .HelloPE...
00000110 00 00 00 00 B0 01 00 00 00 00 00 00 AA AA AA AA ...........
00000120 AA AA AA AA AA AA AA AA E0 00 00 E0 AA AA AA AA ..
00000130 92 01 00 00 00 00 00 00 78 01 00 00 00 00 00 00 .......x.......
00000140 70 01 00 00 AA AA AA AA AA AA AA AA 86 01 00 00 p......
00000150 38 01 00 00 68 01 00 00 AA AA AA AA AA AA AA AA 8...h...
00000160 A0 01 00 00 30 01 00 00 92 01 00 00 00 00 00 00 ...0..........
00000170 78 01 00 00 00 00 00 00 9D 01 4D 65 73 73 61 67 x........Messag
00000180 65 42 6F 78 41 00 75 73 65 72 33 32 2E 64 6C 6C eBoxA.user32.dll
00000190 00 00 80 00 45 78 69 74 50 72 6F 63 65 73 73 00 .. .ExitProcess.
000001A0 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 00 00 00 00 kernel32.dll....
字节码中存在连续AA字节的部分都是可以再次利用的空间。
12.5节就从该文件头部开始,详细介绍打造目标PE的全过程。
----------------------------------------------------------
笔记:
把上面的字节码转成exe过程,打开FFlexHEX,定位到00000000起始位置,【编辑】=》【插入即时数据】

数据格式这选择16进制字 格式,然后复制上面的432个字节数据,

点确定:


然后保存,

打开helloworld_7.exe测试

12.5 打造目标PE的步骤
12.4节为我们展示了打造前后PE文件的字节码对比,通过对比可以发现,打造后的目标PE文件尽管变得更小,却依然具备打造前的PE的所有功能。
本节将详细介绍此次打造的全过程。希望读者能够全面理解和把握PE文件头部数据结构中各字段的作用,同时,也让读者了解改变某些字段的值对整个PE文件所产生的影响。
12.5.1 对文件头的处理
根据前面介绍的结构覆盖技术和数据转移技术压缩文件头,主要操作包括:把NT头提前,覆盖DOS头部分,只保留最重要的e_lfanew字段。
因为数据段的起始地址BaseOfData是一个可以修改的字段,所以让BaseOfData刚好落在e_lfanew这里,然后将这一部分更改为指向PE头的0ch,如下所示:

修改后:

00000000 4D 5A 90 00 02 00 00 00 01 00 00 00 50 45 00 00 MZ.........PE..
00000010 4C 01 03 00 FA F2 04 4C 00 00 00 00 00 00 00 00 L....L........
00000020 E0 00 0F 01 0B 01 05 0C 00 02 00 00 00 04 00 00 ...............
00000030 00 00 00 00 00 10 00 00 00 10 00 00 0C 00 00 00 ................
00000040 00 00 40 00 00 10 00 00 00 02 00 00 04 00 00 00 ..@.............
将IMAGE_NT_HEADERS提到前面来并不影响程序的运行。除了BaseOfData字段需要改成指向PE头的指针外,其他都无需改动。
从偏移02h开始一直到0Ch的数据没有什么用处。于是把数据段中的数据放到了这里。不幸的是,原来要显示的字符串“HelloWorldPE”长度好像超出了这个范围;幸运的是,字符串里的“PE”刚好和PE文件的标志重叠了。如下所示:

每个节表占40个字节

00000000 4D 5A 48 65 6C 6C 6F 57 6F 72 6C 64 50 45 00 00 MZHelloWorldPE..
00000010 4C 01 03 00 FA F2 04 4C 00 00 00 00 00 00 00 00 L....L........
删除节.data的内容,即从800h处开始的内容全部删除,然后将.rdata节表后的文件头数据的所有内容清零,将节数量从原来的3更改为2。

12.5.2 对代码段的处理
首先来看HelloWorld.exe代码段字节码反汇编的结果。
1.程序代码段反汇编代码
使用OD打开HelloWorld.exe,复制反汇编代码段内容如下:
00400130 >/$ 6A 00 PUSH 0 ; /Style = MB_OK|MB_APPLMODAL
00400132 |. 6A 00 PUSH 0 ; |Title = NULL
00400134 |. 68 02004000 PUSH HelloWor.00400002 ; |Text = "HelloWorldPE"
00400139 |. 6A 00 PUSH 0 ; |hOwner = NULL
0040013B |. E8 08000000 CALL <JMP.&user32.MessageBoxA> ; \MessageBoxA
00400140 |. 6A 00 PUSH 0 ; /ExitCode = 0
00400142 \. E8 07000000 CALL <JMP.&kernel32.ExitProcess> ; \ExitProcess
00400147 CC INT3
00400148 $- FF25 68014000 JMP DWORD PTR DS:[<&user32.MessageBoxA>]
0040014E .- FF25 60014000 JMP DWORD PTR DS:[<&kernel32.ExitProcess>
将以上代码的字节码整理出来,然后将这些字节码移动到数据目录表中。
2.将代码嵌入数据目录表
将代码移动到PE文件头部的数据目录表中,见加黑部分。在覆盖时需要注意不要将有用的部分覆盖。
第3项为资源表,第4项为异常表,第5项为安全表,目录都没有用到,因此把代码插入到资源表的位置0ch+90h = 9ch

00000080 AA AA AA AA AA AA AA AA 40 01 00 00 ....@...
00000090 3C 00 00 00 00 00 00 00 00 00 00 00 6A 00 6A 00 <...........j.j.
000000A0 68 02 00 40 00 6A 00 E8 10 00 00 00 6A 00 E8 0F h..@.j.....j..
000000B0 00 00 00 CC 00 00 00 00 00 00 00 00 FF 25 38 01 ........... %8.
000000C0 40 00 FF 25 30 01 40 00 AA AA AA AA 00 00 00 00 @. %0.@.....
000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000E0 00 00 00 00 00 00 00 00 00 00 00 00 AA AA AA AA ............
000000F0 AA AA AA AA 00 00 00 00 00 00 00 00 AA AA AA AA ........
00000100 AA AA AA AA
以上显示的字节码中,短跳转代码指令E8中涉及的偏移部分已经做了修改。由于独立代码部分长度刚好填完数据目录表项03、04和05,免去了按照较短的空闲长度重新构造代码的麻烦。下面来看对导入表部分数据的处理。
12.5.3 对导入表的处理
按照第4章介绍的导入表重组的方法,将导入表更改为如下字节码:

00000130 92 01 00 00 00 00 00 00 78 01 00 00 00 00 00 00 .......x.......
00000140 70 01 00 00 AA AA AA AA AA AA AA AA 86 01 00 00 p......
00000150 38 01 00 00 68 01 00 00 AA AA AA AA AA AA AA AA 8...h...
00000160 A0 01 00 00 30 01 00 00 92 01 00 00 00 00 00 00 ...0..........
00000170 78 01 00 00 00 00 00 00 9D 01 4D 65 73 73 61 67 x........Messag
00000180 65 42 6F 78 41 00 75 73 65 72 33 32 2E 64 6C 6C eBoxA.user32.dll
00000190 00 00 80 00 45 78 69 74 50 72 6F 63 65 73 73 00 .. .ExitProcess.
000001A0 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 00 00 00 00 kernel32.dll....
可以看到,从0130h开始的16个字节为IAT的内容,与之相关的由字段original FirstThunk指向的数据结构则放到了导入表的最后一个全0的IMAGE_IMPORT_DESCRIPTOR结构中。因为前面说过,只要保证该结构的name1(框起来的部分)为0,即可满足导入表结构数组以全0结束的条件。从0140h开始部分即为导入表结构数组。
12.5.4 对部分字段值的修正
相关数据基本安排就绪,接下来的工作就是修正文件头部因数据迁移而导致的字段的值的变更。主要包括以下几个部分。
1.定义节.HelloPE
由于.HelloPE段中存放了常量、数据和代码,所以该段必须可读、可写、可执行。下面是节表中对.HelloPE节表项中的各字段的赋值:
❑ 标志位:0E00000E0h
❑ 节的名字:自定义
❑ 字符串为:.HelloPE
❑ 节区的实际尺寸:01b0h
❑ 节区起始RVA:从头开始,即0000h
❑ 文件对齐后的长度:01b0h
❑ 节位于文件的偏移:从头开始,即0000h
注意 节区的实际尺寸可以在0800h范围内随意更改,不受任何影响,这里选择文件长度01b0h。
.HelloPE节表项结构的相关数据如下:

00000100 2E 48 65 6C 6C 6F 50 45 B0 01 00 00 .HelloPE...
00000110 00 00 00 00 B0 01 00 00 00 00 00 00 AA AA AA AA ...........
00000120 AA AA AA AA AA AA AA AA E0 00 00 E0 ..
2.基地址、执行入口和代码段大小
装入的基地址不变,依然是00400000h;而执行入口则更改为009Ch,即文件偏移009Ch处。由于可执行文件很小(小于200h),所以这里的文件偏移地址即为RVA,无需转换。代码段大小即整个文件的大小000001b0h,相关数据如下:

00000020 B0 01 00 00 00 00 00 00 .......
00000030 00 00 00 00 9C 00 00 00 00 00 00 00 0C 00 00 00 ...............
00000040 00 00 40 00 10 00 00 00 10 00 00 00 04 00 00 00 ..@.............
3.对齐尺寸
为了让文件变得更小,文件的对齐尺寸和内存的对齐尺寸均设置为00000010h,即16个字节。相关数据如下:
00000040 00 00 40 00 10 00 00 00 10 00 00 00 04 00 00 00 ..@.............
4.文件头大小与PE内存映像大小
所有头+节表的大小为00000130h,而PE在内存中的映像大小为00001000h,相关数据如下:

00000050 00 00 00 00 04 00 00 00 00 00 00 00 00 10 00 00 ...............
00000060 30 01 00 00 00 00 00 00 02 00 00 00 00 00 10 00 0...............
00000070 00 10 00 00 00 00 10 00 00 10 00 00 00 00 00 00 ................
00000080 10 00 00 00 ....
5.数据目录表中导入表字段
导入表的起始RVA=00000140h,长度为3Ch。相关数据如下:

00000080 AA AA AA AA AA AA AA AA 40 01 00 00 ....@...
00000090 3C 00 00 00 00 00 00 00 00 00 00 00 6A 00 6A 00 <...........j.j.
最后修改,把000001B0开始的位置后面的数据全部清空,然后整个数据与HelloWorld_7.exe文件数据对比。修正后保存,双击运行

12.5.5 修改后的文件结构
手动修改以后的PE文件结构如图12-4所示。

图12-4 手动修改后的PE结构
如图所示,源PE中数据段的数据存储在目标PE的DOS MZ头和PE标识之间,源PE的程序代码存储在目标PE的数据目录表中;文件头部定义了一个节表项,导入表和IAT表安排在目标PE的尾部。
12.5.6 修改后的文件分析
接下来将使用工具PEInfo和PEComp分别对比两个文件,得到的结果如下。
1. PEInfo运行结果对比
下面来看PEInfo对目标PE的输出:
文件名:D:\masm32\source\chapter10\HelloWorld_7.exe
---------------------------------------------------
运行平台: 0x014c
节的数量: 1
文件属性: 0x010f
建议装入基地址: 0x00400000
文件执行入口(RVA地址): 0x009c
---------------------------------------------------------------------------------
节名称 未对齐前长度 内存中的偏移(对齐后的) 文件中对齐后的长度 文件中的偏移 节的属性
---------------------------------------------------------------------------------
.HelloPE 000001b0 00000000 000001b0 00000000 e00000e0
------------------------------------------------------
导入表所处的节:.HelloPE?
------------------------------------------------------
导入库:user32.dll
-----------------------------
OriginalFirstThunk 00000170
TimeDateStamp aaaaaaaa
ForwarderChain aaaaaaaa
FirstThunk 00000138
-----------------------------
00000413 MessageBoxA
导入库:kernel32.dll
-----------------------------
OriginalFirstThunk 00000168
TimeDateStamp aaaaaaaa
ForwarderChain aaaaaaaa
FirstThunk 00000130
-----------------------------
00000128 ExitProcess
与源PE相比,目标PE中的节少了,但导入表还是很完整的。模块的基地址没有发生变化,程序代码由于搬迁到数据目录表中,所以入口地址发生了变化。
使用PEComp工具打开两个PE文件,运行结果如图12-5所示。

图12-5 手工打造的PE程序与源程序对比
从图中可以看出,源PE与目标PE文件头部不相同的地方很多。造成这种结果的最主要的原因是在手工打造时使用了数据转移技术。
2. 使用PEComp工具对比结果
12.5.7 目标文件更小的实例分析
下面看一个能显示指定信息对话框的更小的PE文件miniPE程序,其大小总共为133字节。该文件的字节码如下。
1.字节码
00000000 4D 5A FF FF 50 45 00 00 4C 01 01 00 75 73 65 72 MZ PE..L...user
00000010 33 32 2E 64 6C 6C 00 FF 40 00 0F 01 0B 01 FF FF 32.dll. @.....
00000020 4D 65 73 73 61 67 65 42 6F 78 41 00 44 00 00 00 MessageBoxA.D...
00000030 FF 15 7C 00 40 00 C3 FF 00 00 40 00 04 00 00 00 .|.@. ..@.....
00000040 04 00 00 00 B8 20 00 40 00 EB 03 FF 04 00 6A 00 .... .@.. ..j.
00000050 50 50 EB 20 89 00 00 00 85 00 00 00 00 00 00 00 PP ..........
00000060 02 00 FF 00 7C 00 00 00 0C 00 00 00 7C 00 00 00 .. .|.......|...
00000070 0C 00 00 00 6A 00 EB B8 02 00 00 00 1E 00 00 00 ....j.........
00000080 00 00 00 00 5C ....\
2.源程序
生成以上字节码的源代码见代码清单12-4。为了去除微软编译器的提示错误,避免在链接时追加任何其他内容,以及汇编指令调用时对invoke指令的分解,这次使用了Borland公司的Tasm和Tlink作为这个源文件的编译器和链接器。具体方法可以参照源文件头部的注释。
代码清单12-4 miniPE程序(chapter12\minipe.asm)
;---------------------------------
; miniPE程序(133字节)
;
; 该程序使用Borland公司的编译器和链接器
; Tasm minipe.asm
; Tlink /3 /t minipe.obj, minipe.exe
; 2011.2.19
;--------------------------------
.386
IBase equ 400000h
HEADER SEGMENT
ASSUME CS:HEADER,FS:NOTHING,ES:HEADER,DS:HEADER
CodeBase:
;*************** PE DOS 头 *************************
DosSignature dw 5a4dh
dw 0ffffh
;*************** PE 标准头 *************************
WinSignature dd 4550h
Machine dw 014ch
NumberOfSections dw 1
;TimeDateStamp dd 0
;PointerToSymbolTable dd 0
;NumberOfSymbols dd 0
user32 db "user32.dll",0
db 0ffh
SizeOfOptionalHeader dw OptHeaderSize
Characteristics dw 010fh
;************************* PE 可选头 *************************
Magic dw 10bh
LinkerVersion dw 0ffffh
;SizeOfCode dd 0
;SizeOfInitializedData dd 0
;SizeOfUninitializedData dd 0
MessageBoxA db "MessageBoxA",0
AddressOfEntryPoint dd start
;---------------------
; 程序在这里最终使用了
; 跳转指令jmp 00400018
; 执行MessageBoxA函数
;---------------------
next3:
;BaseOfCode dd 0
;BaseOfData dd 0
dw 15ffh
dd IBase+IAT1
ret
db 0ffh
ImageBase dd IBase
SectionAlignment dd 4
FileAlignment dd 4
;------------------
; 程序执行入口:
;------------------
start:
;OperatingSystemVersion dd 0ffffffffh
;ImageVersion dd 0ffffffffh
mov eax,offset IBase+MessageBoxA
jmp short next1
db 0ffh
dw 4
next1:
push 0
push eax
push eax
jmp short next2
;SubsystemVersion dd 0ffff0004h
;Win32VersionValue dd 0ffffffffh
SizeOfImage dd IMAGE_SIZE
SizeOfHeaders dd PE_HEADER_SIZE
OptHeaderSize=$-Magic
IAT:
CheckSum dd 0
Subsystem dw 2
DllCharacteristics dw 0ffh
SizeOfStackReserve dd IAT1
SizeOfStackCommit dd user32
SizeOfHeapReserve dd IAT1
SizeOfHeapCommit dd user32
next2:
;LoaderFlags dd 0ffffffffh
push 0
jmp short next3
NumberOfRvaAndSizes dd 2h
;************************* 数据目录表 *************************
IAT1:
IDE_Export dd MessageBoxA-2,0
IDE_Import db IAT-CodeBase
;************************* 数据目录表结束*************************
;************************* PE文件头结束 *************************
;************************* 整个代码结束 *************************
PE_HEADER_SIZE=$
IMAGE_SIZE=PE_HEADER_SIZE+4
HEADER ENDS
END
从代码的注释可以看出,该PE使用的数据结构包括:
❑ IMAGE_DOS_HEADER❑ IMAGE_FILE_HEADER
❑ IMAGE_OPTIONAL_HEADER32
❑ IMAGE_IMPORT_DESCRIPTOR [0]
❑ IMAGE_IMPORT_DESCRIPTOR [1].VirtualAddress
其中数据目录只用了两个,且最后一个还没有用全,因为节表在程序里没有定义。对该代码的详细分析见图12-6和图12-7。

图12-6 133字节的PE程序分析一

图12-7 133字节的PE程序分析二
如图所示,为了便于分析,源程序中每行被按照功能划分为6列,它们依次是:
第1列 标号,用于标识源程序中的一些特殊位置。
第2列 指令,即汇编源代码。
第3列 结构字段名,定义此处的结构和字段。
第4列 字段的值,为每个字段赋值。
第5列 用分号做的注释,标注该行的含义。
第6列 对应的字节码。
12.6 小结
本章介绍了PE的变形技术。所谓变形就是通过技术手段使PE文件的大小发生变化,或缩小或扩大;无论怎么变,都能保证PE文件能被Windows PE加载器加载,且能正常运行。本章首先介绍了四种变形技术、PE数据结构和PE文件中可以被二次利用的空间,以及变形时需要遵循的原则;最后,通过对HelloWorldPE的变形过程进行分析,帮助读者全面理解和把握PE数据结构中相关字段的作用。
本章在全书中具有承前启后的作用,既是对前面所学知识的一个简单回顾和复习,又能为下一步利用这些技术实施静态文件补丁和应用做好知识上的储备。
本文解决在使用zlib库时遇到的多个未解析外部符号错误,如_inflateInit2_和_crc32等,并提供修改zconf.h文件的具体步骤。
4836

被折叠的 条评论
为什么被折叠?



