PE文件头解析

本文为 Maldev Academy 中的 Module50 - Parsing PE Headers 小节的笔记内容,通过代码实现解析 PE 文件头。(完整学习内容请自行前往跳转链接查看)。

虽然之前也接触过 PE 文件头之类的内容,但并没有直接写代码解析获取各部分的内容,通过这节的内容刚好学习下。在网上可以找到现成的 PE 文件结构图。

img

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

010-Editor 示例图:

image-20251124163630050

PE文件头解析:

image-20251124011455053

提前需要先理解一个概念 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即可。

1
2
3
4
5
// Pointer to the structure 
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pPE;		
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE){
	return -1;
}

pPE 变量为在内存中加载的要解析的 PE 原始文件的字节数据。通过CreateFileA、GetFileSize、HeapAlloc获取即可。

二、NT Header (IMAGE_NT_HEADERS)

NT Header包括三部分:SignatureFile HeaderOptional Header

DOS 头结构中的 e_lfanew 成员是一个指向 IMAGE_NT_HEADERS 结构的 RVA 偏移。为了访问 NT Header,只需要把 PE 文件在内存中的基址与这个偏移(e_lfanew)相加即可。下面的代码片段演示了这一点。

1
2
3
4
5
// Pointer to the structure
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pPE + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) {
	return -1;
}

1、Signature

一个常量签名值(4D 5A - MZ)。

2、File Header

由于 File Header 是 IMAGE_NT_HEADERS 结构体的一个成员,因此可以通过下面这行代码来访问:

1
IMAGE_FILE_HEADER		ImgFileHdr	= pImgNtHdrs->FileHeader;

File Header 的成员包括: Machine、NumberOfSections、TimeDateStamp、PointerToSymbolTable、NumberOfSymbols、SizeOfOptionalHeader、Characteristics。

3、Optional Header

由于 Optional Header 是 IMAGE_NT_HEADERS 结构的一个成员,因此可以通过下面的方式访问:

1
2
3
4
5
6
7
IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
if (ImgOptHdr.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) {
	return -1;
}
// IMAGE_NT_OPTIONAL_HDR_MAGIC's value depends on whether the application is 32 or 64-bit.
// IMAGE_NT_OPTIONAL_HDR32_MAGIC - 32-bit
// IMAGE_NT_OPTIONAL_HDR64_MAGIC - 64-bit

我们还可以通过下面的方式获取 Optional Header:

1
PIMAGE_OPTIONAL_HEADER pImgOptHdr = (PIMAGE_OPTIONAL_HEADER)((ULONG_PTR)pImgNtHdrs + sizeof(DWORD) + sizeof(IMAGE_FILE_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 结构如下所示:

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

PE 文件中一些预定义的数据目录包括:

  • IMAGE_DIRECTORY_ENTRY_EXPORT —— 包含 PE 文件导出的函数与数据的信息
  • IMAGE_DIRECTORY_ENTRY_IMPORT —— 包含从其它模块导入的函数与数据的信息
  • IMAGE_DIRECTORY_ENTRY_RESOURCE —— 包含 PE 文件中的资源(例如图标、字符串、位图等)的信息
  • IMAGE_DIRECTORY_ENTRY_EXCEPTION —— 包含 PE 文件中异常处理表的信息

可以通过下面的代码访问 data directories:

1
IMAGE_DATA_DIRECTORY DataDir = ImgOptHdr.DataDirectory[#INDEX IN THE ARRAY#];

例如,获取 Export Directory 的 Data Directory 可以这样写:

1
IMAGE_DATA_DIRECTORY ExpDataDir = ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
(1)Export Directory/Export Table

Export Table 是一个名为 IMAGE_EXPORT_DIRECTORY 的结构,定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

IMAGE_EXPORT_DIRECTORY 结构用于存储 PE 文件导出的函数和数据的信息。这些信息存放在 Data Directory 数组中,索引为 IMAGE_DIRECTORY_ENTRY_EXPORT。从 IMAGE_OPTIONAL_HEADER 中获取该结构可以这样写:

1
PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
(2)Import Directory/Import Table

Import Table 是一个名为 IMAGE_IMPORT_DIRECTORY 的结构,定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct _IMAGE_IMPORT_DIRECTORY {
	union {
		uint32_t Characteristics;
		uint32_t OriginalFirstThunk;
	};
	uint32_t TimeDateStamp;
	uint32_t ForwarderChain;
	uint32_t NameRva;
	uint32_t ThunkTableRva;
}IMAGE_IMPORT_DIRECTORY, * PIMAGE_IMPORT_DIRECTORY;

要从 IMAGE_OPTIONAL_HEADER 结构中获取它,可以这样做:

1
(PVOID)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress)
(3)Import Address Table

IMAGE_IMPORT_DESCRIPTOR 结构同样没有在微软官方文档中进行说明,不过它在 Winnt.h 头文件 中是这样定义的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk;
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;

要从 IMAGE_OPTIONAL_HEADER 结构中获取 Import Address Table(导入地址表),可以这样写:

1
IMAGE_IMPORT_DESCRIPTOR* pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress);
(4)TLS DIRECTORY

IMAGE_TLS_DIRECTORY —— 该结构用于存储 PE 文件中关于线程本地存储(Thread-Local Storage,TLS)数据的信息。此时需要了解的重点是如何从 IMAGE_OPTIONAL_HEADER 中获取这个结构;至于它的详细内容,会在后续模块中需要使用时再进行说明。

1
PIMAGE_TLS_DIRECTORY pImgTlsDir  = (PIMAGE_TLS_DIRECTORY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);
(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 中获取该结构;至于详细的内容,会在后续模块中需要使用时再进行说明。

1
2
3
4
5
6
7
8
9
Exception Directory (DataDirectory[3])

RUNTIME_FUNCTION[] (located usually in .pdata)
 each entry
┌──────────────────────────────┐
 BeginAddress                 
 EndAddress                   
 UnwindInfoAddress ────────────────→ UNWIND_INFO  (in .xdata)
└──────────────────────────────┘

获取方式:

1
2
(PVOID)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress);
PIMAGE_RUNTIME_FUNCTION_ENTRY pImgRunFuncEntry = (PIMAGE_RUNTIME_FUNCTION_ENTRY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress);
(6)Base Relocation Table

IMAGE_BASE_RELOCATION —— 该结构用于存储 PE 文件中的基址重定位(Base Relocation)信息。基址重定位用于在 PE 文件被加载到与其链接时不同的内存地址时,对文件中导入的函数和变量的地址进行修正。此时需要了解的是如何从 IMAGE_OPTIONAL_HEADER 中获取该结构;至于详细内容,会在后续模块中需要使用时再进行说明。

1
PIMAGE_BASE_RELOCATION pImgBaseReloc = (PIMAGE_BASE_RELOCATION)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

三、Section Headers (IMAGE_SECTION_HEADERS)

需要注意几个重要的 PE 段,比如 .text.data.reloc.rsrc 等。此外,根据编译器及其设置的不同,可能还会存在更多的 PE 段。每个段都有一个 IMAGE_SECTION_HEADER 结构用来描述该段的信息。IMAGE_SECTION_HEADER 结构定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

IMAGE_SECTION_HEADER 结构会以数组的形式存放在 PE 文件的头部中。要访问数组的第一个元素,需要越过 IMAGE_NT_HEADERS,因为各个 section 就紧跟在 NT Header 后面。下面的代码片段展示了如何获取 IMAGE_SECTION_HEADER 结构,其中 pImgNtHdrs 是指向 IMAGE_NT_HEADERS 的指针。

1
PIMAGE_SECTION_HEADER pImgSectionHdr = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + sizeof(IMAGE_NT_HEADERS));

遍历数组:

遍历这个数组需要知道数组的大小,可以通过 IMAGE_FILE_HEADER.NumberOfSections 成员获取。数组中后续的每一个元素,都位于当前元素偏移 sizeof(IMAGE_SECTION_HEADER) 的位置。

1
2
3
4
5
6
7
8
PIMAGE_SECTION_HEADER pImgSectionHdr = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + sizeof(IMAGE_NT_HEADERS));
// pImgSectionHdr is a pointer to section 0

for (size_t i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++) {
	// pImgSectionHdr is a pointer to section 1
	pImgSectionHdr = (PIMAGE_SECTION_HEADER)((PBYTE)pImgSectionHdr + (DWORD)sizeof(IMAGE_SECTION_HEADER));
	// pImgSectionHdr is a pointer to section 2
}

至此,便解析出 PE 文件中的大部分关键内容了。

updatedupdated2025-12-092025-12-09