前言
PE 是Windows 平台上最主流的可执行文件格式。
可以在 winnt.h 中找到 PE 结构的定义。
EXE 与 DLL 其实使用的是一样的 PE 结构,只是在一个字段的内容不同,同时 DLL 拥有一些自己的扩展。
64位的 Windows 可执行程序使用的格式叫做 PE32+ , 没有新的结构加入。对很多字段进行了长度扩展。
当 PE 文件被装载入内存后,内存中的版本就叫 模块(Module)。映射文件的起始地址叫做模块句柄(hModule)。这个初始地址也叫基地址 ImageBase。
按照默认配置 VC++ 建立的 EXE 基地址 40 0000H , DLL 为 1000 0000H。
虚拟地址 VA = 基地址 ImageBase + 相对虚拟地址 RVA
PE 文件在磁盘中时,某个数据的位置相对文件开头的偏移量叫做 文件偏移地址 File Offset,或物理地址 RAW Offset。使用十六进制编辑器打开文件时显示的地址就是文件偏移地址。
如果编写解析 PE 的程序,读取其内部相关内容(输入表、输出表等),不能将区块名称做为参考。正确的方法是按照数据目录表中的字段进行定位。
INT 和 IAT 在磁盘中是一样的。在加载入内存后,INT 中的内容不变, IAT 中的每一项被改写为该函数的地址。
参考资料:
- 《加密与解密》
正文

MS-DOS 头
包括 MZ-header 和 DOS stub,后者就是一个 DOS 程序,通常用来显示这个程序不能在 DOS 模式下跑。
开头的两个字节必须是 5A4D,即 ASCII 值 “MZ”。是MS-DOS 创建者之一的名字缩写。文件开始偏移 3CH 处是 e_lfanew 是真正 PE 文件头的 RVA,大小 4 个字节。可以看到,我使用 PE View 打开的这个文件的 PE 文件头的偏移是 F8H。

PE 文件头
PE 文件头是 “IMAGE_NT_HEADERS” 的简称。位于 DOS stub 后面。可以通过前面的 e_lfanew 来找到 PE 文件头。其内部结构可参见这张我从 winnt.h 中截取的截图。

Signature
在有效的 PE 文件中,该字段值为 00004550H,即 ASCII码 PE\0\0 。
IMAGE_FILE_HEADER
映射文件头,包含一些 PE 文件的基本信息。指出 IMAGE OPTIONAL HEADER 的大小。

Machine
表示了目标 CPU 类型。 表格参考了 《加密与解密》。
| 机器 | 标志 |
|---|---|
| Intel i386 | 14CH |
| MIPS R3000 | 162H |
| MIPS R4000 | 166H |
| Alpha AXP | 184H |
| Power PC | 1F0H |
TimeDataStamp
文件创建时间,自 1970.1.1 GMT 以来的秒数。
PointerToSymbolTable、NumberOfSymbols
COFF 函数表在文件中的偏移位置以及其中符号数目。由于较为少见,不做赘述。
SizeOfOptionalHeader
就是 IMAGE_OPTIONAL_HEADER 的大小。32位通常值为 00E0H , 64 位通常为 00F0H。也有可能出现较大值。
Characteristics
文件属性。具体定义参照下图,同样来自 winnt.h。

IMAGE_OPTIONAL_HEADER
由于截图太长了就不放了,直接扔代码,这里以 64位 为例。
1 | typedef struct _IMAGE_OPTIONAL_HEADER64 { |
导入表(import table)
在运行前,磁盘上的PE文件是不知道他要调用的DLL中的函数的地址的。
导入可以这样划分:一般情况下我们编写程序调用DLL都是隐式调用,而如果我们在程序中使用 LoadLibrary 函数和 GetProcAddress 函数,这种方法叫显式调用。
要导入函数,我们可以在PE文件中看见其调入表。导入表的每个元素(IID, Image Import descriptor)都是一个 DLL 。 每个元素的结构体定义如下。IID数组以一个 NULL 结束(即全0填充)。
1 | typedef struct _IMAGE_IMPORT_DESCRIPTOR { |
挑上面的重要的说,Original First Thunk 指向的是 INT(Import Name Table)。First Thunk 指向的是 IAT(Import Address Table)。Name 则指向DLL 的名称字符串。
INT 和 IAT 中的元素都是 IMAGE THUNK DATA。其定义如下。在 32 位情况下是一个 双字。
1 | typedef struct _IMAGE_THUNK_DATA32 { |
对于这个数据结构,如果最高位是1,表示后面的位数代表一个函数序号,最高位是0,表示此时的双字的值代表一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。
1 | typedef struct _IMAGE_IMPORT_BY_NAME { |
实际分析一个 Import Table。

从图中可以看到每一个 IID 代表了一个 DLL。该数组最终以一个全部 为 0 的 IID 结尾。
然后我们根据每个 IID 中的 Original First Thunk 就可以找到 INT 的地址。INT内容如下图所示。

可以看到 INT 中的内容是根据不同的DLL进行分割,每个DLL的函数结束之后都会有一个 全 0 的 INT 元素标志结束。每一个 INT 元素代表了一个函数。
而 IAT 在装载入内存前其实和 INT 的内容是完全一致的,在装载入内存后,由装载器负责将对应函数地址装入 IAT 每个元素的对应位置。
RVA 与 FOA 的转换
文件偏移地址(File Offset Address,FOA)和内存无关,它是指某个位置距离文件头的偏移。
杂项
为什么我拿 Ollydbg 或 WinDbg 打开程序以后,没有装载到 IMAGE_BASE ?
很有可能是链接的时候开启了基地址随机化。以 VS 2019 为例,关闭地址随机化的方法为:项目属性、链接器、高级、随机基址。

把随机基址改成否就可以看到打开后被正常的装载到了 400000H 了。
思考题
File Alignment 和Section Alignment为什么不同?
文件对齐参考了磁盘扇区大小,因为磁盘扇区大小一般为 512 字节,故一般文件对齐为 200H。而节对齐考虑了内存对齐,通常为 4 K 对齐,即 1000 H。
程序会加载到内存的什么位置? 如何确保它从哪里开始执行?
AddressofEntryPoint+ImageBase。
ImageBase被占了,如何处理?
哪些代码或数据需要重定向?
为什么PE结构中有许多RVA?PE结构自身有哪些RVA?
16个数据表,IDT表,重定位表,AddressofEntryPoint, 节表
什么是RVA?谁把RVA变化为VA?
rva - SectionTables[i].VirtualAddress + SectionTables[i].PointerToRawData;
VRk = RVA - File_Offset
对于我们编写的程序而言,哪些地方(变量)需要重定位?
IAT和EAT的作用?
施工中