0%
毅种循环

模块 8 - PE 可执行文件格式

恶意软件开发课程 - PE 可执行文件格式


模块 8 - PE 可执行文件格式#

[!IMPORTANT] 本知识库声明

本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示实践心得

  1. 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
  2. 版权归属:原始知识产权归原作者/官方所有。
  3. 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程
  4. 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。

PE 可执行文件格式#

简介#

PE (Portable Executable - 可移植可执行文件) 是 Windows 上可执行文件的文件格式。PE 文件扩展名的几个示例包括 .exe.dll.sys.scr。本模块讨论 PE 结构,这对于构建或逆向工程恶意软件非常重要。

请注意,本模块和后续模块通常会交替将可执行文件(例如 EXE、DLL)称为”镜像 (Images)”。

💡 初学者提示:什么是 PE 文件?

PE (Portable Executable) 就像一个标准化的”容器格式”:

  • 就像 ZIP 文件有固定的格式一样
  • PE 文件也有固定的结构
  • Windows 知道如何”打开”这个容器

常见的 PE 文件类型:

扩展名类型用途
.exe可执行程序双击运行的程序
.dll动态链接库被程序调用的函数库
.sys系统驱动内核模式驱动程序
.scr屏幕保护程序实际上就是 .exe

为什么要学习 PE 格式? 在恶意软件开发中,理解 PE 格式可以:

  1. 隐藏 Payload 在 PE 文件的特定节中
  2. 解析 IAT(导入地址表)来隐藏 API 调用
  3. 修改 PE 头来绕过检测
  4. 实现 DLL 注入和代码注入
  5. 理解程序如何被加载到内存

PE 结构#

下图显示了 PE 可执行文件的简化结构。图像中显示的每个头都定义为一个数据结构,其中保存了有关 PE 文件的信息。每个数据结构将在本模块中详细解释。

图片
图片

📚 知识扩展:PE结构全景图

PE文件就像一本书的结构:

📖 一本书的结构              🖥️ PE文件的结构
├── 封面                    ├── DOS Header (MZ)
├── 目录                    ├── DOS Stub  
├── 前言                    ├── NT Header (PE)
│   ├── 作者信息            │   ├── File Header
│   └── 版本信息            │   └── Optional Header
├── 章节                    ├── Section Headers
│   ├── 第1章:正文         │   ├── .text (代码)
│   ├── 第2章:图片         │   ├── .data (数据)
│   └── 第3章:附录         │   ├── .rdata (只读数据)
└── 索引                    └── Import/Export Tables
plaintext

PE结构概览(从上到下):

组件大小作用重要性
DOS Header64字节识别PE文件(MZ签名)⭐⭐⭐
DOS Stub可变DOS兼容性消息
NT Header可变PE文件核心信息⭐⭐⭐⭐⭐
Section Headers40字节×N描述各个节⭐⭐⭐⭐
Sections可变实际的代码和数据⭐⭐⭐⭐⭐

DOS 头 (IMAGE_DOS_HEADER)#

PE 文件的第一个头总是以两个字节 0x4D0x5A 作为前缀,通常称为 MZ。这些字节代表 DOS 头签名,用于确认正在解析或检查的文件是有效的 PE 文件。DOS 头是一个数据结构,定义如下:

结构体中最重要的成员是 e_magice_lfanew

e_magic 是 2 字节,固定值为 0x5A4DMZ

e_lfanew 是一个 4 字节值,保存 NT 头起始位置的偏移量。请注意,e_lfanew 始终位于偏移量 0x3C 处。

💡 初学者提示:理解 DOS Header

为什么叫 DOS Header?

  • DOS = Disk Operating System(磁盘操作系统)
  • Windows 之前的古老操作系统
  • PE 文件保留了 DOS 兼容性(历史遗留)

MZ 魔术数字的故事:

  • MZ = Mark Zbikowski(微软工程师名字的缩写)
  • 他设计了 DOS 可执行文件格式
  • 所有 PE 文件都以 MZ 开头

实际查看 MZ 签名:

用十六进制编辑器打开任何 .exe 文件:

偏移量  十六进制                      ASCII
0000    4D 5A 90 00 03 00 00 00    MZ......
        ^^^^^ 
        这就是 MZ!
plaintext

两个关键成员:

1. e_magic(魔术数字)

WORD e_magic = 0x5A4D;  // 'MZ' 的十六进制

// 验证是否为 PE 文件:
if (dosHeader->e_magic != 0x5A4D) {
    printf("这不是有效的 PE 文件!\n");
}
c

2. e_lfanew(指向 NT Header 的指针)

// e_lfanew 保存 NT Header 的偏移量
// 总是位于 DOS Header 的 0x3C 偏移处

LONG ntHeaderOffset = dosHeader->e_lfanew;
// 例如:0x000000F0(240字节处)

// 获取 NT Header 地址:
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)
    ((ULONG_PTR)baseAddress + dosHeader->e_lfanew);
c

实用代码示例:

DOS Stub#

在继续到 NT 头结构之前,有一个 DOS Stub,它是一个错误消息,如果程序在 DOS 模式或”磁盘操作模式”下加载,会打印”This program cannot be run in DOS mode”(此程序无法在 DOS 模式下运行)。值得注意的是,错误消息可以在编译时由程序员更改。这不是 PE 头,但了解它是有好处的。

💡 初学者提示:DOS Stub 的作用

DOS Stub 做什么?

  • 如果在纯 DOS 系统运行现代Windows程序
  • 显示错误消息:“This program cannot be run in DOS mode”
  • 然后退出

为什么需要它?

  • 向后兼容性(虽然现在几乎用不到)
  • 告诉用户这是 Windows 程序

可以修改吗?

  • ✅ 可以!有些恶意软件会修改这个消息
  • 甚至可以放入小的 DOS 程序

实际查看 DOS Stub:

十六进制编辑器中的 DOS Stub:

0E 1F BA 0E 00 B4 09 CD  ........
21 B8 01 4C CD 21 54 68  !..L.!Th
69 73 20 70 72 6F 67 72  is progr
61 6D 20 63 61 6E 6E 6F  am canno
74 20 62 65 20 72 75 6E  t be run
20 69 6E 20 44 4F 53 20   in DOS 
6D 6F 64 65 2E 0D 0D 0A  mode....
plaintext

NT 头 (IMAGE_NT_HEADERS)#

NT 头至关重要,因为它包含了其他两个重要的镜像头:FileHeaderOptionalHeader,其中包含大量关于 PE 文件的信息。与 DOS 头类似,NT 头包含用于验证它的签名成员。通常,签名元素等于”PE”字符串,由 0x500x45 字节表示。但由于签名的数据类型是 DWORD,签名将表示为 0x50450000,仍然是”PE”,只是用两个 null 字节填充。可以使用 DOS 头内的 e_lfanew 成员访问 NT 头。

NT 头结构根据机器架构而有所不同。

32 位版本:

typedef struct _IMAGE_NT_HEADERS {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
c

64 位版本:

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD                   Signature;
    IMAGE_FILE_HEADER       FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
c

唯一的区别是 OptionalHeader 数据结构,IMAGE_OPTIONAL_HEADER32IMAGE_OPTIONAL_HEADER64

📚 知识扩展:NT Header 详解

NT = New Technology

  • Windows NT 是微软 1993 年推出的新架构
  • NT Header 是 PE 格式的核心

PE 签名验证:

// 验证 PE 签名
PIMAGE_NT_HEADERS ntHeader = ...;

if (ntHeader->Signature != IMAGE_NT_SIGNATURE) {  // 0x00004550 ("PE\0\0")
    printf("❌ 不是有效的 NT Header!\n");
    return;
}

// 查看签名的实际字节:
// 小端序:50 45 00 00
// 读成字符:'P' 'E' '\0' '\0'
c

32位 vs 64位区别:

特性32位64位
NT Header 类型IMAGE_NT_HEADERS32IMAGE_NT_HEADERS64
Optional HeaderIMAGE_OPTIONAL_HEADER32IMAGE_OPTIONAL_HEADER64
File Header相同相同
Magic 值0x10B0x20B
指针大小4字节8字节

如何判断是32位还是64位?

PIMAGE_NT_HEADERS ntHeader = ...;

// 方法1:查看 Machine 字段
if (ntHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64) {
    printf("64位程序\n");
} else if (ntHeader->FileHeader.Machine == IMAGE_FILE_MACHINE_I386) {
    printf("32位程序\n");
}

// 方法2:查看 Magic 字段
if (ntHeader->OptionalHeader.Magic == 0x20B) {
    printf("64位 PE\n");
} else if (ntHeader->OptionalHeader.Magic == 0x10B) {
    printf("32位 PE\n");
}
c

文件头 (IMAGE_FILE_HEADER)#

继续下一个头,可以从先前的 NT 头数据结构访问它。

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;
  WORD  NumberOfSections;
  DWORD TimeDateStamp;
  DWORD PointerToSymbolTable;
  DWORD NumberOfSymbols;
  WORD  SizeOfOptionalHeader;
  WORD  Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
c

最重要的结构成员是:

  • NumberOfSections - PE 文件中节的数量(稍后讨论)。
  • Characteristics - 指定可执行文件的某些属性的标志,例如它是动态链接库 (DLL) 还是控制台应用程序。
  • SizeOfOptionalHeader - 以下可选头的大小

有关文件头的其他信息可以在官方文档页面上找到。

💡 初学者提示:File Header 关键成员

Machine(机器类型):

#define IMAGE_FILE_MACHINE_I386     0x014c  // x86
#define IMAGE_FILE_MACHINE_AMD64    0x8664  // x64
#define IMAGE_FILE_MACHINE_ARM      0x01c0  // ARM

// 使用示例:
switch (fileHeader->Machine) {
    case IMAGE_FILE_MACHINE_I386:
        printf("32位 (x86) 程序\n");
        break;
    case IMAGE_FILE_MACHINE_AMD64:
        printf("64位 (x64) 程序\n");
        break;
}
c

Characteristics(特征标志):

#define IMAGE_FILE_EXECUTABLE_IMAGE     0x0002  // 可执行
#define IMAGE_FILE_DLL                  0x2000  // DLL文件
#define IMAGE_FILE_SYSTEM               0x1000  // 系统文件

// 检查是否为 DLL:
if (fileHeader->Characteristics & IMAGE_FILE_DLL) {
    printf("这是一个 DLL 文件\n");
} else if (fileHeader->Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) {
    printf("这是一个 EXE 文件\n");
}
c

NumberOfSections(节数量):

printf("PE 文件包含 %d 个节\n", fileHeader->NumberOfSections);
// 例如:PE 文件包含 5 个节
// .text, .data, .rdata, .rsrc, .reloc
c

TimeDateStamp(编译时间):

// Unix 时间戳(自1970年1月1日以来的秒数)
time_t compileTime = fileHeader->TimeDateStamp;
printf("编译时间:%s", ctime(&compileTime));
// 例如:编译时间:Wed Dec 18 01:00:00 2024
c

可选头 (IMAGE_OPTIONAL_HEADER)#

可选头很重要,虽然它被称为”可选”,但对于 PE 文件的执行来说是必不可少的。之所以称为可选,是因为某些文件类型没有它。

可选头有两个版本,一个用于 32 位系统,一个用于 64 位系统。两个版本的数据结构中的成员几乎相同,主要区别在于某些成员的大小。64 位版本使用 ULONGLONG,32 位版本使用 DWORD。此外,32 位版本有一些在 64 位版本中找不到的成员。

32 位版本:

64 位版本:

可选头包含大量可以使用的信息。下面是一些常用的结构成员:

  • Magic - 描述镜像文件的状态(32 位或 64 位镜像)
  • MajorOperatingSystemVersion - 所需操作系统的主版本号(例如 11、10)
  • MinorOperatingSystemVersion - 所需操作系统的次版本号(例如 1511、1507、1607)
  • SizeOfCode - .text 节的大小(稍后讨论)
  • AddressOfEntryPoint - 文件入口点的偏移量(通常是 main 函数)
  • BaseOfCode - .text 节起始位置的偏移量
  • SizeOfImage - 镜像文件的大小(以字节为单位)
  • ImageBase - 它指定应用程序在执行时要加载到内存中的首选地址。但是,由于 Windows 的内存保护机制(如 ASLR - 地址空间布局随机化),很少看到镜像映射到其首选地址,因为 Windows PE 加载器会将文件映射到不同的地址。Windows PE 加载器完成的这种随机分配会导致在未来技术的实现中出现问题,因为某些被认为是常量的地址被更改了。然后,Windows PE 加载器将通过 PE 重定位来修复这些地址。
  • DataDirectory - 可选头中最重要的成员之一。这是一个 IMAGE_DATA_DIRECTORY 数组,其中包含 PE 文件中的目录(下面讨论)。

📚 知识扩展:Optional Header 重要成员详解

AddressOfEntryPoint(入口点):

// 程序开始执行的地方
DWORD entryPoint = optionalHeader->AddressOfEntryPoint;
// 例如:0x00001000

// 计算入口点的实际地址:
PVOID entryPointAddr = (PVOID)((ULONG_PTR)imageBase + entryPoint);

// 在恶意软件中的应用:
// 1. 修改入口点以执行恶意代码
// 2. 在原始入口点之前插入代码
c

ImageBase(首选基地址):

SizeOfImage(镜像大小):

// PE 文件加载到内存后的总大小
DWORD imageSize = optionalHeader->SizeOfImage;

// 用途:分配足够的内存来加载 PE
PVOID buffer = VirtualAlloc(NULL, imageSize, 
                            MEM_COMMIT | MEM_RESERVE, 
                            PAGE_EXECUTE_READWRITE);
c

DllCharacteristics(DLL特征标志):

// 检查是否启用了 ASLR
if (optionalHeader->DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) {
    printf("✅ ASLR 已启用\n");
}

// 检查是否启用了 DEP
if (optionalHeader->DllCharacteristics & IMAGE_DLLCHARACTERISTICS_NX_COMPAT) {
    printf("✅ DEP/NX 已启用\n");
}

// 常见标志:
#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE 0x0040  // ASLR
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT    0x0100  // DEP
#define IMAGE_DLLCHARACTERISTICS_NO_SEH       0x0400  // 无SEH
c
数据目录 (Data Directory)#

可以从可选头的最后一个成员访问数据目录。这是数据类型 IMAGE_DATA_DIRECTORY 的数组,具有以下数据结构:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
c

数据目录数组的大小为 IMAGE_NUMBEROF_DIRECTORY_ENTRIES,这是一个常量值 16。数组中的每个元素代表一个特定的数据目录,其中包含有关 PE 节或数据表(保存有关 PE 的特定信息的地方)的一些数据。

可以使用其在数组中的索引访问特定的数据目录。

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 导出目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // 导入目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // 资源目录
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // 异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   //安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // 基址重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // 调试目录
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // 架构特定数据
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // GP 的 RVA
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS 目录
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // 加载配置目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // 头中的绑定导入目录
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // 导入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // 延迟加载导入描述符
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM 运行时描述符
c

下面两节将简要提及两个重要的数据目录,Export DirectoryImport Address Table

💡 初学者提示:Data Directory 数组

Data Directory 就像一本书的目录

  • 告诉你在哪里找到特定的信息
  • 每个目录项指向一个特定的表

访问 Data Directory 示例:

重要性排序(对恶意软件开发):

  1. ⭐⭐⭐⭐⭐ IAT (Import Address Table) - API 调用
  2. ⭐⭐⭐⭐⭐ Export Directory - 导出的函数
  3. ⭐⭐⭐⭐ Base Relocation - ASLR 支持
  4. ⭐⭐⭐ Resource Directory - 隐藏资源
  5. ⭐⭐ TLS Directory - 线程本地存储
导出目录 (Export Directory)#

PE 的导出目录是一个数据结构,包含有关从可执行文件导出的函数和变量的信息。它包含导出的函数和变量的地址,可以被其他可执行文件用来访问函数和数据。导出目录通常存在于导出函数的 DLL 中(例如 kernel32.dll 导出 CreateFileA)。

导入地址表 (Import Address Table)#

导入地址表是 PE 中的一个数据结构,包含有关从其他可执行文件导入的函数的地址的信息。这些地址用于访问其他可执行文件中的函数和数据(例如 Application.exekernel32.dll 导入 CreateFileA)。

📚 知识扩展:Export Directory vs Import Address Table

理解导入和导出:

程序A.exe                 kernel32.dll
├── 导入表 (IAT)          ├── 导出表
│   ├── CreateFileW   ───→    ├── CreateFileW
│   ├── WriteFile     ───→    ├── WriteFile  
│   └── CloseHandle   ───→    └── CloseHandle
plaintext

Export Directory(导出目录)- DLL的”菜单”:

// kernel32.dll 的导出目录包含:
- CreateFileW      @ 地址 0x7FF900001000
- WriteFile        @ 地址 0x7FF900001100
- ReadFile         @ 地址 0x7FF900001200
- CloseHandle      @ 地址 0x7FF900001300
c

Import Address Table(导入地址表)- 程序的”订单单”:

// myapp.exe  的 IAT 包含:
- CreateFileW  → 指向 kernel32.dll 中的 CreateFileW
- WriteFile    → 指向 kernel32.dll 中的 WriteFile
c

在恶意软件中的应用:

1. API Hashing(API 哈希):

不在 IAT 中导入 API,而是:
1. 遍历 kernel32.dll 的导出表
2. 计算函数名的哈希值
3. 匹配目标哈希找到函数
4. 动态调用

优点:IAT 看起来很干净,难以检测
plaintext

2. IAT Hooking(IAT 钩子):

// 修改 IAT 中的地址,劫持 API 调用
IAT[CreateFileW] = (PVOID)MyMaliciousCreateFile;

// 现在程序调用 CreateFileW 时,实际调用了我们的函数
c

PE 节 (PE Sections)#

PE 节包含用于创建可执行程序的代码和数据。每个 PE 节都有一个唯一的名称,通常包含可执行代码、数据或资源信息。PE 节的数量并不恒定,因为不同的编译器可以根据配置添加、删除或合并节。某些节也可以稍后手动添加,因此它是动态的,IMAGE_FILE_HEADER.NumberOfSections 有助于确定该数量。

以下 PE 节是最重要的,几乎存在于每个 PE 中。

  • .text - 包含可执行代码,即编写的代码。
  • .data - 包含已初始化的数据,即代码中初始化的变量。
  • .rdata -包含只读数据。这些是以 const 为前缀的常量变量。
  • .idata - 包含导入表。这些是与使用代码调用的函数相关的信息表。Windows PE 加载器使用它来确定要加载到进程中的 DLL 文件,以及从每个 DLL 使用哪些函数。
  • .reloc - 包含有关如何修复内存地址的信息,以便程序可以无错误地加载到内存中。
  • .rsrc - 用于存储资源,如图标和位图

每个 PE 节都有一个 IMAGE_SECTION_HEADER 数据结构,其中包含有关它的有价值的信息。这些结构保存在 PE 文件的 NT 头之下,并相互堆叠,其中每个结构代表一个节。

回想一下,IMAGE_SECTION_HEADER 结构如下:

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;
c

查看元素,每一个都非常有价值和重要:

  • Name - 节的名称。(例如 .text、.data、.rdata)。
  • PhysicalAddressVirtualSize - 节在内存中时的大小。
  • VirtualAddress - 节在内存中起始位置的偏移量。

📚 知识扩展:PE Sections 完全指南

常见节的用途表:

节名内容权限恶意软件用途
.text可执行代码RX (读+执行)隐藏 Shellcode
.data已初始化变量RW (读+写)隐藏加密的Payload
.rdata只读数据、字符串R (只读)隐藏配置信息
.rsrc资源(图标、对话框)R (只读)隐藏大型Payload
.reloc重定位信息R (只读)可以删除/修改

遍历所有节的代码:

输出示例:

节名称        虚拟地址      大小          权限
======================================================
.text        0x00001000    0x00010000    R-X
.rdata       0x00020000    0x00005000    R--
.data        0x00030000    0x00002000    RW-
.rsrc        0x00040000    0x00001000    R--
.reloc       0x00050000    0x00001000    R--
plaintext

在恶意软件中隐藏 Payload:

方法1:添加新节

// 在 .rsrc 节中隐藏加密的 Shellcode
1. 创建 .rsrc 节
2. 将加密的 Payload 放入
3. 设置权限为 R(只读)- 看起来正常
4. 运行时解密到新分配的内存
c

方法2:使用 .text 节

// 将 Shellcode 混淆后放在 .text 节末尾
// 看起来像正常代码的一部分
unsigned char payload[] = {0x90, 0x90, ...};  // 已混淆

// 运行时解密
DecryptPayload(payload, sizeof(payload));
c

方法3:使用 .data 节

// .data 节的数据段
__declspec(allocate(".data"))
unsigned char embeddedPayload[4096] = {
    0x48, 0x83, 0xEC, ...  // 加密的 Shellcode
};
c

其他参考资料#

如果需要对某些部分进行进一步说明,强烈推荐 0xRick’s Blog 上的以下博客文章。

结论#

第一次遇到 PE 头时,理解它们可能具有挑战性。幸运的是,基础模块都不需要对 PE 结构有深入的了解。但是,要使恶意软件执行更复杂的技术,需要更好的理解,因为某些代码需要解析 PE 文件的头和节。这可能会在中级和高级模块中看到。

🎯 本模块学习要点

必须掌握的概念:

  1. ✅ PE 文件的基本结构(DOS Header → NT Header → Sections)
  2. ✅ MZ 和 PE 签名的作用
  3. ✅ Optional Header 中的关键字段
  4. ✅ 常见 PE 节的用途(.text、.data、.rdata等)
  5. ✅ Import Address Table(IAT)的作用
  6. ✅ Export Directory 的作用
  7. ✅ Data Directory 数组的用途

PE 结构速记:

PE 文件 =
  DOS Header (MZ)
  + DOS Stub
  + NT Header (PE)
    ├─ File Header
    └─ Optional Header
      └─ Data Directory[16]
  + Section Headers
  + Sections (.text, .data, .rdata, .rsrc...)
plaintext

关键数据结构:

结构关键成员用途
IMAGE_DOS_HEADERe_magic, e_lfanew验证PE,找到NT Header
IMAGE_NT_HEADERSSignature, FileHeader, OptionalHeaderPE核心信息
IMAGE_FILE_HEADERMachine, NumberOfSections, Characteristics架构和节数量
IMAGE_OPTIONAL_HEADERAddressOfEntry Point, ImageBase, DataDirectory入口点和数据目录
IMAGE_SECTIO N_HEADERName, VirtualAddress, Characteristics节信息

实践建议:

  1. 使用 PE-Bear 打开程序,观察 PE 结构
  2. 用十六进制编辑器查看 MZ 和 PE 签名
  3. 编写代码遍历 PE 节
  4. 尝试修改 IAT 或添加新节

工具推荐:

  • PE-Bear:可视化 PE 结构
  • CFF Explorer:编辑 PE 文件
  • PE-sieve:检测恶意 PE 修改
  • HxD:十六进制编辑器

下一步: 学习 DLL(动态链接库)的使用