PE文件结构浅解析

1.序言

时光荏苒,再次重拾起逆向程序这一块其实我是很开心的,当初之所以选择了别的方向,主要还是担心就业问题,既然能够有机会从事这方面的工作,我得赶紧把之前学习过的知识补救回来,那么就从PE结构开始吧,也怪当初自己学习的时候没有做笔记的习惯,导致现在比较麻烦些。

2.PE文件结构介绍

首先PE结构是windows可执行应用程序的一种格式,如果我们的程序不遵顼PE文件格式,那么windows将无法识别我们的程序,我们在实际编写程序的时候,其实并不需要关注PE文件结构,因为编译器会为我们做好这些,但我们写程序并不能止步于此,懂PE文件结构,懂调试的人甚至能把你整个代码逻辑挖出来,代码基本上很难做到完美无缺陷,一旦被找到漏洞,那么别人的可操作空间就非常高了。因此,我们要知其然且知其所以然,做软件安全方面的工作,要能会攻击,知道别人怎么攻击你的软件,这时候我们才能做出防御,就好比绝地求生这款游戏,游戏开发的很精彩,但由于开发人员对于安全防护上面的不足,导致这款游戏外挂横行,几乎被毁了。

3.PE文件结构浅解析

扯了这么多,要开始学习PE文件结构了,这里用到16进制文件查看器PETools,为了更加清晰,这里所有的数值都使用16进制

0x00000000 ~ 0x0000003F 是为DOS头,即 IMAGE_DOS_HEADER

结构体属性名 地址(RAW 长度 数据 描述
e_magic: 0x00000000 02h 0x5A4D DOS可执行文件标记”MZ”头,定义为”5A4Dh”,定值.
e_cblp: 0x00000002 02h 0x0050 Bytes on last page of file.
e_cp: 0x00000004 02h 0x0002 Pages in file.
e_crlc: 0x00000006 02h 0x0000 Relocations.
e_cparhdr: 0x00000008 02h 0x0004 Size of header in paragraphs.
e_minalloc: 0x0000000a 02h 0x000F Minimum extra paragraphs needed.
e_maxalloc: 0x0000000c 02h 0xFFFF Maximum extra paragraphs needed.
e_sp: 0x00000010 02h 0x00B8 Initial SP value.
e_csum: 0x00000012 02h 0x0000 Checksum.
e_ss: 0x0000000e 02h 0x0000 Initial (relative) SS value.
e_ip: 0x00000014 02h 0x0000 Initial IP value.DOS代码入口IP.
e_cs: 0x00000016 02h 0x0000 Initial (relative) CS value.DOS代码的入口CS.
e_lfarlc: 0x00000018 02h 0x0040 File address of relocation table.
e_res[4]: 0x0000001c 08h 0x0000000000000000 Reserved words.
e_ovno: 0x0000001a 02h 0x001A Overlay number.
e_oemid: 0x00000024 02h 0x0000 OEM identifier (for e_oeminfo).
e_oeminfo: 0x00000026 02h 0x0000 OEM information; e_oemid specific.
e_res2[10]: 0x00000028 14h 0x0000000000000000000
000000000000000000000
Reserved words.长度为”20”.
e_lfanew: 0x0000003c 04h 0x00000200 偏移量:0x3C File address of new exe header.PE文件头地址.

这里有很多DOS遗留下来的文件格式,我们这里只关心2个数据,e_magic固定值为5A4D,e_lfanew是PE文件头的位置偏移,这里我其实可以将他理解成指针,指向PE文件头。

从 0x00000040 ~ 0x000001FF 这段空间留空(前提是e_lfanew指向的位置为0X00000200),作为DOS代码的存放空间,基本没用处(其实这段空间里面的汇编二进制指令能够在DOS下面输出一段话,this program must be run under win32),因为在windows下用不到,从0X00000200开始,才是我们真正的PE文件结构了(这里按0X00000200算,即e_lfanew 所指向的位置偏移)

IMAGE_NT_HEADERS32 是个结构体,结构如下:

结构体属性名 地址(RAW 长度 描述
Signature 0x00000200 04h PE文件标志(“PE”,0,0),长度为”4”.
FileHeader 0x00000204 14h IMAGE_FILE_HEADER结构,长度为”20”.
OptionalHeader 0x000002f7 e0h IMAGE_OPTIONAL_HEADER32结构,默认长度为”e0h”(224).

其中 Signature 为PE标志,内容为00004550h,FileHeader和OptionalHeader都是结构体指针。

IMAGE_FILE_HEADER (映像文件头)结构如下:

结构体属性名 地址(RAW 长度 数据 描述
Machine 0x00000204 02h 0x014C 运行平台(可执行文件的CPU类型,常值为:”014Ch”)
NumberOfSections 0x00000206 02h 0x0008 区段/节/块(Section)的数目.
TimeDateStamp 0x00000208 04h 0x5245FF44 文件创建时的时间戳(日历时间为:2013.09.28 05:57:24).
PointerOfSymbolTable 0x0000020c 04h 0x00000000 COFF符号指针,指向符号表(用于调试).
NumberOfSymbols 0x00000210 04h 0x00000000 符号表(即上一字段)中的符号个数(用于调试).
SizeOfOptionHeader 0x00000214 02h 0x00E0 可选首部长度(可选头大小).在OBJ中,该字段为0;在可执行文件中,是指IMAGE_OPTIONAL_HEADER结构的长度.
Characteristics 0x00000216 02h 0x010E 文件属性/特性(文件信息标志)

关键数据 NumberOfSections 区块表数目后面会用到。

描述
0001h IMAGE_FILE_RELOCS_STRIPPED
0002h IMAGE_FILE_EXECUTABLE_IMAGE
0004h IMAGE_FILE_LINE_NUMS_STRIPPED
0008h IMAGE_FILE_LOCAL_SYMS_STRIPPED
0010h IMAGE_FILE_AGGRESIVE_WS_TRIM
0020h IMAGE_FILE_LARGE_ADDRESS_AWARE
0080h IMAGE_FILE_BYTES_REVERSED_LO
0100h IMAGE_FILE_32BIT_MACHINE
0200h IMAGE_FILE_DEBUG_STRIPPED
0400h IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP
0800h IMAGE_FILE_NET_RUN_FROM_SWAP
1000h IMAGE_FILE_SYSTEM
2000h IMAGE_FILE_DLL
4000h IMAGE_FILE_UP_SYSTEM_ONLY
8000h IMAGE_FILE_BYTES_REVERSED_HI

文件属性 特征值对照表:

IMAGE_OPTIONAL_HEADER32 (可选映像头)结构如下:

结构体属性名 地址(RAW 长度 偏移 数据 描述
Magic 0x00000218 02h e_lfanew+0x18 0x010B 标志字(幻数),常值为”010Bh”.用来说明文件是ROM映像,还是普通可执行的映像
MajorLinkerVersion 0x0000021a 01h e_lfanew+0x1A 0x05 链接器主(首)版本号.
MinorLinkerVersion 0x0000021b 01h e_lfanew+0x1B 0x00 链接器次(副)版本号.
SizeOfCode: 0x0000021c 04h e_lfanew+0x1C 0x00015000 代码段(块)大小,所有Code Section总共的大小(只入不舍),这个值是向上对齐某一个值的整数倍.
SizeOfInitializedData: 0x00000220 04h e_lfanew+0x20 0x00009000 已初始化数据块大小.即在编译时所构成的块的大小(不包括代码段),但这个数据并不太准确.
SizeOfUninitializedData: 0x00000224 04h e_lfanew+0x24 0x00000000 未初始化数据块大小.装载程序要在虚拟地址空间中为这些数据约定空间.未初始化数据通常在.bbs块中.
AddressOfEntryPoint(RVA): 0x00000228 04h e_lfanew+0x28 0x00001000 程序开始执行的入口地址/入口点EP(RVA).这是一个”相对虚拟地址”.
BaseOfCode: 0x0000022c 04h e_lfanew+0x2C 0x00001000 代码段(块)起始地址.
BaseOfData: 0x00000230 04h e_lfanew+0x30 0x00016000 数据段(块)起始地址.
ImageBase: 0x00000234 04h e_lfanew+0x34 0x00400000 基址,程序默认装入的基地址.
SectionAlignment: 0x00000238 04h e_lfanew+0x38 0x00001000 内存中的节(块”Section”)的对齐值,常为:0x1000或0x04.
FileAlignment: 0x0000023c 04h e_lfanew+0x3C 0x00000200 文件中的节(块”Section”)的对齐值,常为:0x1000或0x200或0x04.
MajorOperatingSystemVersion 0x00000240 02h e_lfanew+0x40 0x0004 操作系统主(首)版本号.
MinorOperatingSystemVersion 0x00000242 02h e_lfanew+0x42 0x0000 操作系统次(副)版本号.
MajorImageVersion: 0x00000244 02h e_lfanew+0x44 0x0000 该可执行文件的主(首)版本号,由程序员自定义.
MinorImageVersion: 0x00000246 02h e_lfanew+0x46 0x0000 该可执行文件的次(副)版本号,由程序员自定义.
MajorSubsystemVersion: 0x00000248 02h e_lfanew+0x48 0x0004 所需子系统主(首)版本号.
MinorSubsystemVersion: 0x0000024a 02h e_lfanew+0x4A 0x0000 所需子系统次(副)版本号.
Win32VersionValue: 0x0000024c 04h e_lfanew+0x4C 0x00000000 保留.总是”00000000”.
SizeOfImage: 0x00000250 04h e_lfanew+0x50 0x00026000 映像大小(映像装入内存后的总尺寸/内存中整个PE映像的尺寸).
SizeOfHeaders: 0x00000254 04h e_lfanew+0x54 0x00000600 首部及块表(首部+块表)的大小.
CheckSum: 0x00000258 04h e_lfanew+0x58 0x00000000 CRC校验和.
Subsystem: 0x0000025c 02h e_lfanew+0x5C 0x0002 子系统:Windows 图形用户界面/图形接口子系统(Image runs in the Windows GUI subsystem.).
DllCharacteristics: 0x0000025e 02h e_lfanew+0x5E 0x0000 DLLMain()函数何时被调用.当文件为DLL程序时使用,默认值为”0”.
SizeOfStackReserve: 0x00000260 04h e_lfanew+0x60 0x00100000 初始化时为线程保留的栈大小.
SizeOfStackCommit: 0x00000264 04h e_lfanew+0x64 0x00002000 初始化时线程实际使用的栈大小.这个值总比”SizeOfStackReserve”要小一些.
SizeOfHeapReserve: 0x00000268 04h e_lfanew+0x68 0x00100000 初始化时为进程保留的堆大小.
SizeOfHeapCommit: 0x0000026c 04h e_lfanew+0x6C 0x00001000 初始化时进程实际使用的堆大小.这个值总比”SizeOfHeapReserve”要小一些.
LoaderFlags: 0x00000270 04h e_lfanew+0x70 0x00000000 设置自动调用断点或调试器.与调试有关,默认值为”0”.
NumberOfRvaAndSizes: 0x00000274 04h e_lfanew+0x74 0x00000010 数据目录结构的数量(项数).值总为”00000010h”(16项).
DataDirectory: 0x00000278 ~ 0x000002f7 80h 数据目录表(16项,每个成员占8字节).

关键数据:

ImageBase 基地址

BaseOfCode,代码段起始偏移地址,使用OD 的时候会看到从0x00401000,实际就是这么来的

BaseOfData 数据段起始偏移地址 SizeOfCode代码块大小

AddressOfEntryPoint 程序入口地址,这个和代码起始地址其实有有区别的,你可以从代码段的中间开始执行

SizeOfImage 映像大小,内存中。

DllCharacteristics 是否是DLL

SizeOfStackReserve SizeOfStackCommit SizeOfHeapReserve SizeOfHeapCommit 这些都是堆栈数据

NumberOfRvaAndSizes 数据目录项大小

DataDirectory 数据目录项的索引地址 里面簇拥你有输入表、输出表等数据的RVA地址。

RVA解释,基地址+RVA其实就是相当于PE文件结构在内存中的地址了,当然,这里的内存是Windows虚拟出来的。

这里RVA 和FOV的转换相对复杂,暂时没那么多时间给我去复习这个了,不过找到一个参考资料的地址:我是链接

IMAGE_DATA_DIRECTORY (数据目录表)结构如下:

成员 地址(RAW) 偏移量 数据:RVA 大小 说明 结构
Export Table 0x00000278 e_lfanew+0x078 0x00022000 0x00000242 导出表 IMAGE_DIRECTORY_ENTRY_EXPORT
Import Table 0x00000280 e_lfanew+0x080 0x00021000 0x00000C2C 导入表 IMAGE_DIRECTORY_ENTRY_IMPORT
Resources Table 0x00000288 e_lfanew+0x088 0x00023000 0x00001000 资源 IMAGE_DIRECTORY_ENTRY_RESOURCE
Exception Table 0x00000290 e_lfanew+0x090 0x00000000 0x00000000 异常 IMAGE_DIRECTORY_ENTRY_EXCEPTION
Security Table 0x00000298 e_lfanew+0x098 0x00000000 0x00000000 安全证书 IMAGE_DIRECTORY_ENTRY_SECURITY
Debug 0x000002a8 e_lfanew+0x0A8 0x00000000 0x00000000 调试信息 IMAGE_DIRECTORY_ENTRY_DEBUG
Base relocation Table 0x000002a0 e_lfanew+0x0A0 0x00024000 0x000019A4 重定位表 IMAGE_DIRECTORY_ENTRY_BASERELOC
Architecture(Copyrught) 0x000002b0 e_lfanew+0x0B0 0x00000000 0x00000000 版权所有 IMAGE_DIRECTORY_ENTRY_ARCHITECTURE(IMAGE_DIRECTORY_ENTRY_COPYRIGHT)
Global Ptr 0x000002b8 e_lfanew+0x0B8 0x00000000 0x00000000 全局指针 IMAGE_DIRECTORY_ENTRY_GLOBALPTR
Tread local storage(TLS) 0x000002c0 e_lfanew+0x0C0 0x00020000 0x00000018 TLS 表 IMAGE_DIRECTORY_ENTRY_TLS
Load configuration 0x000002c8 e_lfanew+0x0C8 0x00000000 0x00000000 加载配置 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
Bound Import 0x000002d0 e_lfanew+0x0D0 0x00000000 0x00000000 绑定导入 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
Import Address Table(IAT) 0x000002d8 e_lfanew+0x0D8 0x00000000 0x00000000 IAT 表 IMAGE_DIRECTORY_ENTRY_IAT
Delay Import 0x000002e0 e_lfanew+0x0E0 0x00000000 0x00000000 延迟导入 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
COM descriptor 0x000002e8 e_lfanew+0x0E8 0x00000000 0x00000000 COM IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
保留: 0x000002f0 e_lfanew+0x0F0 0x00000000 0x00000000 保留 NULL

关键的数据是导出表和导入表,这里的RVA是比较麻烦些,如果要直接在文件中读出,需要转换为FOV,之前看过一本逆向相关的书,里面有提到TLS里面可以做一些手脚达到检测调试工具的意图,应该也算挺重要的一个表,IAT 和延迟导入表也都比较重要,重定位表等等,这里的数据都是非常有用的,每个都可以是一个调试点。

在目录表的后面,紧跟着的是 区块表

IMAGE_SECTION_HEADER (块表|区段|节表)结构说明:

成员 地址(RAW) 数据:RVA 说明
Name: 0x000002f8 [.text] [名称,长度:8位(16字节)的ASCII码.]
VirtualAddress: 0x00000304 00001000 [V(VO),内存中偏移(该块的RVA).]
VirtualSize: 0x00000300 00015000 [V(VS),内存中大小(对齐前的长度).]
SizeOfRawData: 0x00000308 00014400 [R(RS),文件中大小(对齐后的长度).]
PointerToRawData: 0x0000030c 00000600 [R(RO),文件中偏移.]
PointerToRelocation: 0x00000310 00000000 [在OBJ文件中使用,重定位的偏移.]
PointerToLinenumbers: 0x00000314 00000000 [行号表的偏移,提供调试.]
NumberOfRelocations: 0x00000316 0000 [在OBJ文件中使用,重定位项数目.]
NumberOfLinenumbers: 0x00000318 0000 [行号表中行号的数目.]
Characteristics: 0x0000031c 60000020 [标志(块属性):20000000h 40000000h 00000020h ]

这里有8块这样的数据,至于为什么是8块,是由上面所提到的区块表数量NumberOfSections属性决定的。

计算FOV的话没这个恐怕是不行了,通过读取这个内存中的地址和在文件中的地址来计算FOV,关于文件对其方式,我这里也暂时不深究,等开始研究FOV和RVA 转换的时候自然就明白了。

标志(属性块) 常用特征值对照表:

属性 描述
00000020h IMAGE_SCN_CNT_CODE Section contains code.(包含可执行代码)
00000040h IMAGE_SCN_CNT_INITIALIZED_DATA Section contains initialized data.(该块包含已初始化的数据)
00000080h IMAGE_SCN_CNT_UNINITIALIZED_DATA Section contains uninitialized data.(该块包含未初始化的数据)
00000200h IMAGE_SCN_LNK_INFO Section contains comments or some other type of information.
00000800h IMAGE_SCN_LNK_REMOVE Section contents will not become part of image.
00001000h IMAGE_SCN_LNK_COMDAT Section contents comdat.
00004000h IMAGE_SCN_NO_DEFER_SPEC_EXC Reset speculative exceptions handling bits in the TLB entries for this section
00008000h IMAGE_SCN_GPREL Section content can be accessed relative to GP.
00500000h IMAGE_SCN_ALIGN_16BYTES Default alignment if no others are specified.
01000000h IMAGE_SCN_LNK_NRELOC_OVFL Section contains extended relocations.
02000000h IMAGE_SCN_MEM_DISCARDABLE Section can be discarded.
04000000h IMAGE_SCN_MEM_NOT_CACHED Section is not cachable.
08000000h IMAGE_SCN_MEM_NOT_PAGED Section is not pageable.
10000000h IMAGE_SCN_MEM_SHARED Section is shareable(该块为共享块).
20000000h IMAGE_SCN_MEM_EXECUTE Section is executable.(该块可执行)
40000000h IMAGE_SCN_MEM_READ Section is readable.(该块可读)
80000000h IMAGE_SCN_MEM_WRITE Section is writeable.(该块可写)

各种常见块(Sections) 的描述:

代码块名称 解释
.text 代码块
.data 初始化的数据.
.idata 导入表.
.rsrc 资源数据.
.reloc 基地址重定位表.
.edata 输出表.
.tls thread local storage,线程局部存储器.
.rdata 存放调试目录和说明字符串.

再后面就是各个数据段的具体内容了,可以对PE文件结构进行修改达到再运行程序之前弹出对话框,又或是修改导入表,让程序加载你的DLL文件,总之,掌握了PE文件结构,你简直可以对别人的程序为所欲为。。关键还是在于当今软件加密技术的发展,SE保护壳,滴水的保护壳,还有VMP 等保护技术,着实让人头疼不已,但若是要再加密解密这一块有所建树,PE文件结构这块知识是跑不了,后续内核程序结构当中,关于内存注入,关于驱动程序的编写,关于加载程序之后,抹去在内存中程序的PE结构等知识,也是需要扎实的PE文件结构基础来分
析的。

下面附上一张PE文件结构图: