PE文件头解析

本文为 Maldev Academy 中的 Module 50 小节的笔记内容,通过代码实现解析 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];
3-1-1、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);
3-1-2、INT & IAT Table

在解析 PE 文件的逻辑流程中,每一个被引入的 DLL(如 kernel32.dlluser32.dll)都必须拥有自己独立的 INTIAT

  • INT (Import Name Table):记录了该 DLL 中所有需要导入的函数名

  • IAT (Import Address Table):记录了该 DLL 中所有导入函数对应的内存地址

如果引入了 5 个 DLL,那么在逻辑解析时,就会看到 5 个不同的 INT 表和 5 个不同的 IAT 表。

虽然逻辑上是分开的,但在 PE 文件的二进制结构中,为了方便管理(比如修改内存属性),编译器通常会将所有 DLL 的 IAT 块连续地排在一起,形成一个巨大的“IAT 数据区”。

  • DataDirectory[1] (Import Directory):它指向的是 IMAGE_IMPORT_DESCRIPTOR 数组。每个描述符就像是一个“指针”,指明了总数据区中的哪一段属于当前的 DLL。
  • DataDirectory[12] (IAT Directory):它指向的就是这个连续的、总的 IAT 数据区的起点。

获取 Import Directory (索引 1):

1
2
3
4
// 对应 DataDirectory[1]
// 导入表描述符数组(包含所有 DLL 信息)从哪里开始。

PIMAGE_IMPORT_DESCRIPTOR pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

获取 Import Address Table Directory (索引 12):

1
2
3
4
// 对应 DataDirectory[12]
// 注意:这只是 IAT 数据的“物理起始地址”,不代表某个具体的 DLL

PVOID pImgIATDir = (PVOID)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress);

关于 INT Table 和 IAT Table 的分析。

image-20260121183623510

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;   // 桥1 => 指向 INT 表
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;                     // Dll 名称
    DWORD   FirstThunk;               // 桥2 => 指向 IAT 表
} IMAGE_IMPORT_DESCRIPTOR;

IMAGE_IMPORT_DESCRIPTOR 结构体是 PE 文件导入表的核心入口。每一个被程序引用的 DLL 都会对应一个 IMAGE_IMPORT_DESCRIPTOR 结构。

  • OriginalFirstThunk (桥1):指向 INT (Import Name Table) 的 RVA(相对虚拟地址)。它是查找函数名的依据。
  • Name:指向一个以 NULL 结尾的字符串,包含该 DLL 的名称(如 kernel32.dll)。
  • FirstThunk (桥2):指向 IAT (Import Address Table) 的 RVA。它是程序运行时实际跳转执行的地址所在地。

(1)INT Table(Import Name Table)

INT 是一个结构体数组,其对应的底层定义是 IMAGE_THUNK_DATA。它是一份只读的“索引清单”。

1
2
3
4
5
6
7
8
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // 转发字符串的 RVA
        DWORD Function;             // 在 INT 中,此字段指向名字结构 (AddressOfData)
        DWORD Ordinal;              // 被导入函数的序号
        DWORD AddressOfData;        // 被导入函数的名称RVA,指向 IMAGE_IMPORT_BY_NAME 结构
    } u1;
} IMAGE_THUNK_DATA32;
  • 桥1的终点:OriginalFirstThunk 指向的就是这个数组的开头。

  • 在磁盘文件里,它的 u1.AddressOfData 存储着一个 RVA,指向真正的函数名结构体 IMAGE_IMPORT_BY_NAME

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;          // 被导入函数的序号
    CHAR    Name[1];       // 被导入函数的函数名称字符串(如 "ExitProcess")
} IMAGE_IMPORT_BY_NAME;

(2)IAT Table(Import Address Table)

IAT 在代码层面的定义与 INT 完全一样(也是 IMAGE_THUNK_DATA 数组),但其字段在不同阶段代表不同的含义。

1
2
3
4
5
6
7
8
9
// 结构体与 INT 相同,但在内存中加载后,Function 字段意义改变
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // 转发字符串的 RVA
        DWORD Function;             // 重点:加载后,这里存储的是函数的【真实内存地址】
        DWORD Ordinal;              // 被导入函数的序号
        DWORD AddressOfData;        // 被导入函数的名称 RVA,指向 IMAGE_IMPORT_BY_NAME 结构
    } u1;
} IMAGE_THUNK_DATA32;
  • 桥2的终点:FirstThunk 指向这个数组的开头。

  • 文件状态(静止):其内容通常和 INT 一样,也指向 IMAGE_IMPORT_BY_NAME

  • 内存状态(运行):Windows 加载器会根据 INT 找到函数名,查询地址后,直接覆写这个 u1.Function 字段。此时它不再是 RVA 偏移,而是一个硬编码的 VA(虚拟地址),如 0x77E11234

(3)整体联动过程

静态阶段(磁盘上的 PE 文件)。程序还没运行、存储在硬盘中时。

  • IMAGE_IMPORT_DESCRIPTOR 里的 OriginalFirstThunk(桥1)指向 INT
  • IMAGE_IMPORT_DESCRIPTOR 里的 FirstThunk(桥2)指向 IAT
  • 此时,INT 和 IAT 里的内容是完全一样的:它们都存储着一系列 RVA(相对虚拟地址),这些地址都指向同一个目的地——IMAGE_IMPORT_BY_NAME 结构体(即函数名字符串,如 “ExitProcess”)。

动态阶段(Windows 加载器的工作)。当双击运行程序时,Windows 加载器(Loader)会将 PE 文件映射到内存,并开始“整体联动”。

  • 定位 DLL:加载器读取 IMAGE_IMPORT_DESCRIPTORName 字段(如 “kernel32.dll”),并在系统中找到该 DLL,将其加载到内存中。
  • 寻找函数(通过桥1):加载器查看 OriginalFirstThunk 指向的 INT。它读取里面的 IMAGE_THUNK_DATA,找到函数名称字符串,如 “ExitProcess”。
  • 获取真实地址:加载器在已加载的 kernel32.dll 导出表中搜索 “ExitProcess”,计算出它在当前内存中的绝对地址(例如 0x77E11234)。
  • 填充结果(通过桥2):加载器将这个绝对地址写回 FirstThunk 指向的 IAT 对应位置。

最终状态。

  • 桥1 (OriginalFirstThunk -> INT):依然指向函数名 “ExitProcess”。它就像备份清单,永远记录着最初想要什么。
  • 桥2 (FirstThunk -> IAT):现在指向的是真实的函数地址

(4)Windows 加载器全量初始化

Windows 加载器通过以下逻辑确保所有依赖环境就绪。

外层循环:遍历 DLL 列表

  • 动作:加载器从 DataDirectory[1] 指向的地址开始,挨个读取 IMAGE_IMPORT_DESCRIPTOR
  • 联动:读取 Name 字段,加载目标 DLL(如 user32.dll)到进程空间。
  • 终止:直到遇到一个全 0 的描述符结构体,外层循环结束。

内层循环:遍历函数名与填充地址

  • 初始化双指针:

    • 指针 A (INT):指向 OriginalFirstThunk 指向的 IMAGE_THUNK_DATA 数组。

    • 指针 B (IAT):指向 FirstThunk 指向的 IMAGE_THUNK_DATA 数组。

  • 查名(INT 侧):加载器读取指针 A 处的 RVA,定位到 IMAGE_IMPORT_BY_NAME 结构,获取函数名字字符串(如 “MessageBoxW”)。

  • 解析地址:在 DLL 的导出表中查找该名字对应的真实内存虚拟地址 (VA)

  • 填位(IAT 侧):加载器将得到的 VA 地址直接覆盖掉指针 B 所在的槽位。

  • 同步步进:指针 A 和 指针 B 同时向后移动 4 字节(32位)或 8 字节(64位)。

  • 全量填充:即便程序的代码只用了一个函数,加载器也会把该 DLL 在导入表中列出的所有函数全部填完,以备不时之需。直到遇到 0x00000000 结束该 DLL 的内层循环。

获取每个 DLL 对应的 INT Table 和 IAT Table:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Maldev 50 小节中,使用的是 ReadFile 将文件读入 pBuff。这属于物理文件读取,而不是内存映射加载。
// 需通过如下两行获取当前加载进内存后的 PE 文件地址,才能获取每个 DLL 对应的 INT Table 和 IAT Table
HMODULE hModule = GetModuleHandleA(NULL);  // NULL 时表示当前 PE 文件 
PBYTE pBase = (PBYTE)hModule;              // 也可直接使用 hModule
	
// 获取 DOS Header
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
// 获取 Nt Header
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
// 获取 Nt Header 下的 Optional Header
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;

// 第一步:获取导入表描述符数组的首地址
PIMAGE_IMPORT_DESCRIPTOR pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

// 第二步:遍历所有的 DLL (因为每个 DLL 都有自己的 INT 和 IAT)
while (pImgImpDesc->Name != NULL) {

    // 获取当前 DLL 的名称
    PCHAR pDllName = (PCHAR)(pBase + pImgImpDesc->Name);
    printf("[+] DLL Name: %s \n", pDllName);

    // 第三步:获取该 DLL 对应的 INT 表 (桥1)
    // 物理路径:pBase + OriginalFirstThunk
    PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(pBase + pImgImpDesc->OriginalFirstThunk);

    // 第四步:获取该 DLL 对应的 IAT 表 (桥2)
    // 物理路径:pBase + FirstThunk
    PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(pBase + pImgImpDesc->FirstThunk);

    printf("\t[#] INT RVA: 0x%0.8X (Address: 0x%p)\n", pImgImpDesc->OriginalFirstThunk, pINT);
    printf("\t[#] IAT RVA: 0x%0.8X (Address: 0x%p)\n", pImgImpDesc->FirstThunk, pIAT);

    // 指向下一个 DLL 描述符
    pImgImpDesc++;
}

运行效果:

[+] DLL Name: KERNEL32.dll
        [#] INT RVA: 0x0001B250 (Address: 0x008CB250)
        [#] IAT RVA: 0x0001B000 (Address: 0x008CB000)
[+] DLL Name: USER32.dll
        [#] INT RVA: 0x0001B2E0 (Address: 0x008CB2E0)
        [#] IAT RVA: 0x0001B090 (Address: 0x008CB090)
[+] DLL Name: VCRUNTIME140D.dll
        [#] INT RVA: 0x0001B310 (Address: 0x008CB310)
        [#] IAT RVA: 0x0001B0C0 (Address: 0x008CB0C0)
[+] DLL Name: ucrtbased.dll
        [#] INT RVA: 0x0001B360 (Address: 0x008CB360)
        [#] IAT RVA: 0x0001B110 (Address: 0x008CB110)
3-1-3、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);
3-1-4、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);
3-1-5、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 文件中的大部分关键内容了。

updatedupdated2026-01-282026-01-28