本文为 Maldev Academy 中的 Module50 - Parsing PE Headers 小节的笔记内容,通过代码实现解析 PE 文件头。(完整学习内容请自行前往跳转链接查看)。
虽然之前也接触过 PE 文件头之类的内容,但并没有直接写代码解析获取各部分的内容,通过这节的内容刚好学习下。在网上可以找到现成的 PE 文件结构图。

包括课程原文章也有示意图。但后面写代码时参考起来总感觉用起来不是那么顺手。便参考 010-Editor 工具载入PE文件后下面的字段解析,画了张图。
010-Editor 示例图:

PE文件头解析:

提前需要先理解一个概念 Relative Virtual Addresses (RVAs)。
相对虚拟地址(RVA)是用于引用 PE 文件内位置的地址。它们用于指定 PE 文件中各种数据结构和部分的位置,例如代码、数据和资源。
RVA 是一个32位值,指定数据结构或节从PE文件开头的偏移(offset)。它被称为“相对”地址,因为它指定的是从文件开头的偏移,而不是内存中的绝对地址。这允许同一个文件在内存中的不同地址加载,而无需对文件中的RVA进行任何更改。
RVA 在 PE 文件格式中被广泛使用,以指定文件中各种数据结构和节的位置。例如,PE 头包含几个 RVA,指定代码和数据节、导入和导出表以及其他重要数据结构的位置。
要将相对虚拟地址(RVA)转换为虚拟地址(VA),只需要操作系统将模块的基地址(模块加载在内存中的位置)加上 RVA 即可。这使得操作系统能够访问模块内指定位置的数据,而不管模块在内存中的加载位置。
剩下的就好说了,直接使用对应的代码开始解析 PE 文件的各个段即可。
一、DOS Header
由于 DOS Header 位于 PE 文件的最开始,检索 DOS Header 只需将pPE变量类型转换为PIMAGE_DOS_HEADER即可。
|
|
pPE 变量为在内存中加载的要解析的 PE 原始文件的字节数据。通过CreateFileA、GetFileSize、HeapAlloc获取即可。
二、NT Header (IMAGE_NT_HEADERS)
NT Header包括三部分:Signature、File Header、Optional Header。
DOS 头结构中的 e_lfanew 成员是一个指向 IMAGE_NT_HEADERS 结构的 RVA 偏移。为了访问 NT Header,只需要把 PE 文件在内存中的基址与这个偏移(e_lfanew)相加即可。下面的代码片段演示了这一点。
|
|
1、Signature
一个常量签名值(4D 5A - MZ)。
2、File Header
由于 File Header 是 IMAGE_NT_HEADERS 结构体的一个成员,因此可以通过下面这行代码来访问:
|
|
File Header 的成员包括: Machine、NumberOfSections、TimeDateStamp、PointerToSymbolTable、NumberOfSymbols、SizeOfOptionalHeader、Characteristics。
3、Optional Header
由于 Optional Header 是 IMAGE_NT_HEADERS 结构的一个成员,因此可以通过下面的方式访问:
|
|
我们还可以通过下面的方式获取 Optional Header:
|
|
Optional Header 的重要成员包括: Magic、MajorLinkerVersion、MinorLinkerVersion、SizeOfCode、SizeOfInitializedData、SizeOfUninitializedData、AddressOfEntryPoint、BaseOfCode、BaseOfData、ImageBase、MajorOperatingSystemVersion、MinorOperatingSystemVersion、MajorImageVersion、MinorImageVersion、DataDirectory。
3-1、DataDirectory:
Data Directory 可以从 Optional Header 的最后一个成员获取。这个成员是一个 IMAGE_DATA_DIRECTORY 数组,也就是说数组中的每一个元素都是一个 IMAGE_DATA_DIRECTORY 结构,用于引用某一个特殊的数据目录。IMAGE_DATA_DIRECTORY 结构如下所示:
|
|
PE 文件中一些预定义的数据目录包括:
IMAGE_DIRECTORY_ENTRY_EXPORT—— 包含 PE 文件导出的函数与数据的信息IMAGE_DIRECTORY_ENTRY_IMPORT—— 包含从其它模块导入的函数与数据的信息IMAGE_DIRECTORY_ENTRY_RESOURCE—— 包含 PE 文件中的资源(例如图标、字符串、位图等)的信息IMAGE_DIRECTORY_ENTRY_EXCEPTION—— 包含 PE 文件中异常处理表的信息
可以通过下面的代码访问 data directories:
|
|
例如,获取 Export Directory 的 Data Directory 可以这样写:
|
|
(1)Export Directory/Export Table
Export Table 是一个名为 IMAGE_EXPORT_DIRECTORY 的结构,定义如下:
|
|
IMAGE_EXPORT_DIRECTORY 结构用于存储 PE 文件导出的函数和数据的信息。这些信息存放在 Data Directory 数组中,索引为 IMAGE_DIRECTORY_ENTRY_EXPORT。从 IMAGE_OPTIONAL_HEADER 中获取该结构可以这样写:
|
|
(2)Import Directory/Import Table
Import Table 是一个名为 IMAGE_IMPORT_DIRECTORY 的结构,定义如下:
|
|
要从 IMAGE_OPTIONAL_HEADER 结构中获取它,可以这样做:
|
|
(3)Import Address Table
IMAGE_IMPORT_DESCRIPTOR 结构同样没有在微软官方文档中进行说明,不过它在 Winnt.h 头文件 中是这样定义的:
|
|
要从 IMAGE_OPTIONAL_HEADER 结构中获取 Import Address Table(导入地址表),可以这样写:
|
|
(4)TLS DIRECTORY
IMAGE_TLS_DIRECTORY —— 该结构用于存储 PE 文件中关于线程本地存储(Thread-Local Storage,TLS)数据的信息。此时需要了解的重点是如何从 IMAGE_OPTIONAL_HEADER 中获取这个结构;至于它的详细内容,会在后续模块中需要使用时再进行说明。
|
|
(5)EXCEPTION DIRECTORY
IMAGE_DIRECTORY_ENTRY_EXCEPTION —— 包含 PE 文件中异常处理表(Exception Handling Tables)的信息。
IMAGE_RUNTIME_FUNCTION_ENTRY —— 该结构用于存储 PE 文件中某个运行时函数的信息。运行时函数(runtime function)指由 Windows 操作系统的异常处理机制(SEH/C++ Exception)调用,用于执行某个异常的异常处理代码的函数。此时需要了解的是如何从 IMAGE_OPTIONAL_HEADER 中获取该结构;至于详细的内容,会在后续模块中需要使用时再进行说明。
|
|
获取方式:
|
|
(6)Base Relocation Table
IMAGE_BASE_RELOCATION —— 该结构用于存储 PE 文件中的基址重定位(Base Relocation)信息。基址重定位用于在 PE 文件被加载到与其链接时不同的内存地址时,对文件中导入的函数和变量的地址进行修正。此时需要了解的是如何从 IMAGE_OPTIONAL_HEADER 中获取该结构;至于详细内容,会在后续模块中需要使用时再进行说明。
|
|
三、Section Headers (IMAGE_SECTION_HEADERS)
需要注意几个重要的 PE 段,比如 .text、.data、.reloc、.rsrc 等。此外,根据编译器及其设置的不同,可能还会存在更多的 PE 段。每个段都有一个 IMAGE_SECTION_HEADER 结构用来描述该段的信息。IMAGE_SECTION_HEADER 结构定义如下:
|
|
IMAGE_SECTION_HEADER 结构会以数组的形式存放在 PE 文件的头部中。要访问数组的第一个元素,需要越过 IMAGE_NT_HEADERS,因为各个 section 就紧跟在 NT Header 后面。下面的代码片段展示了如何获取 IMAGE_SECTION_HEADER 结构,其中 pImgNtHdrs 是指向 IMAGE_NT_HEADERS 的指针。
|
|
遍历数组:
遍历这个数组需要知道数组的大小,可以通过 IMAGE_FILE_HEADER.NumberOfSections 成员获取。数组中后续的每一个元素,都位于当前元素偏移 sizeof(IMAGE_SECTION_HEADER) 的位置。
|
|
至此,便解析出 PE 文件中的大部分关键内容了。