模块 8 - PE 可执行文件格式
恶意软件开发课程 - PE 可执行文件格式
模块 8 - PE 可执行文件格式#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
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 格式可以:
- 隐藏 Payload 在 PE 文件的特定节中
- 解析 IAT(导入地址表)来隐藏 API 调用
- 修改 PE 头来绕过检测
- 实现 DLL 注入和代码注入
- 理解程序如何被加载到内存
PE 结构#
下图显示了 PE 可执行文件的简化结构。图像中显示的每个头都定义为一个数据结构,其中保存了有关 PE 文件的信息。每个数据结构将在本模块中详细解释。

📚 知识扩展:PE结构全景图
PE文件就像一本书的结构:
plaintext📖 一本书的结构 🖥️ PE文件的结构 ├── 封面 ├── DOS Header (MZ) ├── 目录 ├── DOS Stub ├── 前言 ├── NT Header (PE) │ ├── 作者信息 │ ├── File Header │ └── 版本信息 │ └── Optional Header ├── 章节 ├── Section Headers │ ├── 第1章:正文 │ ├── .text (代码) │ ├── 第2章:图片 │ ├── .data (数据) │ └── 第3章:附录 │ ├── .rdata (只读数据) └── 索引 └── Import/Export TablesPE结构概览(从上到下):
组件 大小 作用 重要性 DOS Header 64字节 识别PE文件(MZ签名) ⭐⭐⭐ DOS Stub 可变 DOS兼容性消息 ⭐ NT Header 可变 PE文件核心信息 ⭐⭐⭐⭐⭐ Section Headers 40字节×N 描述各个节 ⭐⭐⭐⭐ Sections 可变 实际的代码和数据 ⭐⭐⭐⭐⭐
DOS 头 (IMAGE_DOS_HEADER)#
PE 文件的第一个头总是以两个字节 0x4D 和 0x5A 作为前缀,通常称为 MZ。这些字节代表 DOS 头签名,用于确认正在解析或检查的文件是有效的 PE 文件。DOS 头是一个数据结构,定义如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // 魔术数字 (Magic number)
WORD e_cblp; // 文件最后一页的字节数
WORD e_cp; // 文件中的页数
WORD e_crlc; // 重定位
WORD e_cparhdr; // 头的段落大小
WORD e_minalloc; // 需要的最小额外段落
WORD e_maxalloc; // 需要的最大额外段落
WORD e_ss; // 初始(相对)SS 值
WORD e_sp; // 初始 SP 值
WORD e_csum; // 校验和
WORD e_ip; // 初始 IP 值
WORD e_cs; // 初始(相对)CS 值
WORD e_lfarlc; // 重定位表的文件地址
WORD e_ovno; // 覆盖号
WORD e_res[4]; // 保留字
WORD e_oemid; // OEM 标识符(用于 e_oeminfo)
WORD e_oeminfo; // OEM 信息;e_oemid 特定
WORD e_res2[10]; // 保留字
LONG e_lfanew; // NT 头的偏移量
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;c结构体中最重要的成员是 e_magic 和 e_lfanew。
e_magic 是 2 字节,固定值为 0x5A4D 或 MZ。
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 签名:
plaintext用十六进制编辑器打开任何 .exe 文件: 偏移量 十六进制 ASCII 0000 4D 5A 90 00 03 00 00 00 MZ...... ^^^^^ 这就是 MZ!两个关键成员:
1. e_magic(魔术数字)
cWORD e_magic = 0x5A4D; // 'MZ' 的十六进制 // 验证是否为 PE 文件: if (dosHeader->e_magic != 0x5A4D) { printf("这不是有效的 PE 文件!\n"); }2. e_lfanew(指向 NT Header 的指针)
c// 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#include <Windows.h> void ParseDOSHeader(PVOID fileBase) { // 获取 DOS Header PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)fileBase; // 验证 MZ 签名 if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE) { // 0x5A4D printf("❌ 不是有效的 PE 文件!\n"); return; } printf("✅ 找到 MZ 签名\n"); printf("NT Header 偏移量:0x%08X\n", dosHeader->e_lfanew); // 获取 NT Header PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS) ((ULONG_PTR)fileBase + dosHeader->e_lfanew); // 继续解析... }
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:
plaintext十六进制编辑器中的 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....
NT 头 (IMAGE_NT_HEADERS)#
NT 头至关重要,因为它包含了其他两个重要的镜像头:FileHeader 和 OptionalHeader,其中包含大量关于 PE 文件的信息。与 DOS 头类似,NT 头包含用于验证它的签名成员。通常,签名元素等于”PE”字符串,由 0x50 和 0x45 字节表示。但由于签名的数据类型是 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;c64 位版本:
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_HEADER32 和 IMAGE_OPTIONAL_HEADER64。
📚 知识扩展:NT Header 详解
NT = New Technology
- Windows NT 是微软 1993 年推出的新架构
- NT Header 是 PE 格式的核心
PE 签名验证:
c// 验证 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'32位 vs 64位区别:
特性 32位 64位 NT Header 类型 IMAGE_NT_HEADERS32IMAGE_NT_HEADERS64Optional Header IMAGE_OPTIONAL_HEADER32IMAGE_OPTIONAL_HEADER64File Header 相同 相同 Magic 值 0x10B 0x20B 指针大小 4字节 8字节 如何判断是32位还是64位?
cPIMAGE_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"); }
文件头 (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(机器类型):
c#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; }Characteristics(特征标志):
c#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"); }NumberOfSections(节数量):
cprintf("PE 文件包含 %d 个节\n", fileHeader->NumberOfSections); // 例如:PE 文件包含 5 个节 // .text, .data, .rdata, .rsrc, .relocTimeDateStamp(编译时间):
c// Unix 时间戳(自1970年1月1日以来的秒数) time_t compileTime = fileHeader->TimeDateStamp; printf("编译时间:%s", ctime(&compileTime)); // 例如:编译时间:Wed Dec 18 01:00:00 2024
可选头 (IMAGE_OPTIONAL_HEADER)#
可选头很重要,虽然它被称为”可选”,但对于 PE 文件的执行来说是必不可少的。之所以称为可选,是因为某些文件类型没有它。
可选头有两个版本,一个用于 32 位系统,一个用于 64 位系统。两个版本的数据结构中的成员几乎相同,主要区别在于某些成员的大小。64 位版本使用 ULONGLONG,32 位版本使用 DWORD。此外,32 位版本有一些在 64 位版本中找不到的成员。
32 位版本:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;c64 位版本:
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;c可选头包含大量可以使用的信息。下面是一些常用的结构成员:
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(入口点):
c// 程序开始执行的地方 DWORD entryPoint = optionalHeader->AddressOfEntryPoint; // 例如:0x00001000 // 计算入口点的实际地址: PVOID entryPointAddr = (PVOID)((ULONG_PTR)imageBase + entryPoint); // 在恶意软件中的应用: // 1. 修改入口点以执行恶意代码 // 2. 在原始入口点之前插入代码ImageBase(首选基地址):
c// 32位程序常见 ImageBase: // EXE: 0x00400000 // DLL: 0x10000000 // 64位程序常见 ImageBase: // EXE: 0x0000000140000000 // DLL: 0x0000000180000000 // 由于 ASLR,实际加载地址通常不同: PVOID actualBase = GetModuleHandle(NULL); // 实际基地址 ULONGLONG preferredBase = optionalHeader->ImageBase; // 首选基地址 if ((ULONG_PTR)actualBase != preferredBase) { printf("ASLR 生效:基地址已随机化\n"); printf("首选:0x%p\n", (void*)preferredBase); printf("实际:0x%p\n", actualBase); }SizeOfImage(镜像大小):
c// PE 文件加载到内存后的总大小 DWORD imageSize = optionalHeader->SizeOfImage; // 用途:分配足够的内存来加载 PE PVOID buffer = VirtualAlloc(NULL, imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);DllCharacteristics(DLL特征标志):
c// 检查是否启用了 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
数据目录 (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 Directory 和 Import Address Table。
💡 初学者提示:Data Directory 数组
Data Directory 就像一本书的目录:
- 告诉你在哪里找到特定的信息
- 每个目录项指向一个特定的表
访问 Data Directory 示例:
cPIMAGE_NT_HEADERS ntHeader = ...; PIMAGE_DATA_DIRECTORY dataDir = ntHeader->OptionalHeader.DataDirectory; // 获取导入表 PIMAGE_DATA_DIRECTORY importDir = &dataDir[IMAGE_DIRECTORY_ENTRY_IMPORT]; if (importDir->VirtualAddress != 0) { printf("导入表地址:0x%08X\n", importDir->VirtualAddress); printf("导入表大小:%d 字节\n", importDir->Size); } // 获取导出表 PIMAGE_DATA_DIRECTORY exportDir = &dataDir[IMAGE_DIRECTORY_ENTRY_EXPORT]; if (exportDir->VirtualAddress != 0) { printf("这个文件导出了函数\n"); } // 获取资源目录 PIMAGE_DATA_DIRECTORY resourceDir = &dataDir[IMAGE_DIRECTORY_ENTRY_RESOURCE]; if (resourceDir->VirtualAddress != 0) { printf("包含资源(图标、字符串等)\n"); }重要性排序(对恶意软件开发):
- ⭐⭐⭐⭐⭐ IAT (Import Address Table) - API 调用
- ⭐⭐⭐⭐⭐ Export Directory - 导出的函数
- ⭐⭐⭐⭐ Base Relocation - ASLR 支持
- ⭐⭐⭐ Resource Directory - 隐藏资源
- ⭐⭐ TLS Directory - 线程本地存储
导出目录 (Export Directory)#
PE 的导出目录是一个数据结构,包含有关从可执行文件导出的函数和变量的信息。它包含导出的函数和变量的地址,可以被其他可执行文件用来访问函数和数据。导出目录通常存在于导出函数的 DLL 中(例如 kernel32.dll 导出 CreateFileA)。
导入地址表 (Import Address Table)#
导入地址表是 PE 中的一个数据结构,包含有关从其他可执行文件导入的函数的地址的信息。这些地址用于访问其他可执行文件中的函数和数据(例如 Application.exe 从 kernel32.dll 导入 CreateFileA)。
📚 知识扩展:Export Directory vs Import Address Table
理解导入和导出:
plaintext程序A.exe kernel32.dll ├── 导入表 (IAT) ├── 导出表 │ ├── CreateFileW ───→ ├── CreateFileW │ ├── WriteFile ───→ ├── WriteFile │ └── CloseHandle ───→ └── CloseHandleExport Directory(导出目录)- DLL的”菜单”:
c// kernel32.dll 的导出目录包含: - CreateFileW @ 地址 0x7FF900001000 - WriteFile @ 地址 0x7FF900001100 - ReadFile @ 地址 0x7FF900001200 - CloseHandle @ 地址 0x7FF900001300Import Address Table(导入地址表)- 程序的”订单单”:
c// myapp.exe 的 IAT 包含: - CreateFileW → 指向 kernel32.dll 中的 CreateFileW - WriteFile → 指向 kernel32.dll 中的 WriteFile在恶意软件中的应用:
1. API Hashing(API 哈希):
plaintext不在 IAT 中导入 API,而是: 1. 遍历 kernel32.dll 的导出表 2. 计算函数名的哈希值 3. 匹配目标哈希找到函数 4. 动态调用 优点:IAT 看起来很干净,难以检测2. IAT Hooking(IAT 钩子):
c// 修改 IAT 中的地址,劫持 API 调用 IAT[CreateFileW] = (PVOID)MyMaliciousCreateFile; // 现在程序调用 CreateFileW 时,实际调用了我们的函数
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)。PhysicalAddress或VirtualSize- 节在内存中时的大小。VirtualAddress- 节在内存中起始位置的偏移量。
📚 知识扩展:PE Sections 完全指南
常见节的用途表:
节名 内容 权限 恶意软件用途 .text可执行代码 RX (读+执行) 隐藏 Shellcode .data已初始化变量 RW (读+写) 隐藏加密的Payload .rdata只读数据、字符串 R (只读) 隐藏配置信息 .rsrc资源(图标、对话框) R (只读) 隐藏大型Payload .reloc重定位信息 R (只读) 可以删除/修改 遍历所有节的代码:
c#include <Windows.h> #include <stdio.h> void EnumerateSections(PVOID imageBase) { // 获取 DOS 和 NT 头 PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)imageBase; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS) ((ULONG_PTR)imageBase + dosHeader->e_lfanew); // 获取第一个节头 PIMAGE_SECTION_HEADER sectionHeader = IMAGE_FIRST_SECTION(ntHeader); // 遍历所有节 printf("节名称\t\t虚拟地址\t大小\t\t权限\n"); printf("======================================================\n"); for (int i = 0; i < ntHeader->FileHeader.NumberOfSections; i++) { // 节名称(最多8字符) char sectionName[9] = {0}; memcpy(sectionName, sectionHeader[i].Name, 8); // 虚拟地址 DWORD virtualAddr = sectionHeader[i].VirtualAddress; // 虚拟大小 DWORD virtualSize = sectionHeader[i].Misc.VirtualSize; // 特征(权限) DWORD characteristics = sectionHeader[i].Characteristics; // 解析权限 char permissions[4] = "---"; if (characteristics & IMAGE_SCN_MEM_READ) permissions[0] = 'R'; if (characteristics & IMAGE_SCN_MEM_WRITE) permissions[1] = 'W'; if (characteristics & IMAGE_SCN_MEM_EXECUTE) permissions[2] = 'X'; printf("%-12s\t0x%08X\t0x%08X\t%s\n", sectionName, virtualAddr, virtualSize, permissions); } }输出示例:
plaintext节名称 虚拟地址 大小 权限 ====================================================== .text 0x00001000 0x00010000 R-X .rdata 0x00020000 0x00005000 R-- .data 0x00030000 0x00002000 RW- .rsrc 0x00040000 0x00001000 R-- .reloc 0x00050000 0x00001000 R--在恶意软件中隐藏 Payload:
方法1:添加新节
c// 在 .rsrc 节中隐藏加密的 Shellcode 1. 创建 .rsrc 节 2. 将加密的 Payload 放入 3. 设置权限为 R(只读)- 看起来正常 4. 运行时解密到新分配的内存方法2:使用 .text 节
c// 将 Shellcode 混淆后放在 .text 节末尾 // 看起来像正常代码的一部分 unsigned char payload[] = {0x90, 0x90, ...}; // 已混淆 // 运行时解密 DecryptPayload(payload, sizeof(payload));方法3:使用 .data 节
c// .data 节的数据段 __declspec(allocate(".data")) unsigned char embeddedPayload[4096] = { 0x48, 0x83, 0xEC, ... // 加密的 Shellcode };
其他参考资料#
如果需要对某些部分进行进一步说明,强烈推荐 0xRick’s Blog ↗ 上的以下博客文章。
- PE Overview - https://0xrick.github.io/win-internals/pe2/ ↗
- DOS Header, DOS Stub and Rich Header - https://0xrick.github.io/win-internals/pe3/ ↗
- NT Headers - https://0xrick.github.io/win-internals/pe4/ ↗
- Data Directories, Section Headers and Sections - https://0xrick.github.io/win-internals/pe5/ ↗
- PE Imports (Import Directory Table, ILT, IAT) - https://0xrick.github.io/win-internals/pe6/ ↗
结论#
第一次遇到 PE 头时,理解它们可能具有挑战性。幸运的是,基础模块都不需要对 PE 结构有深入的了解。但是,要使恶意软件执行更复杂的技术,需要更好的理解,因为某些代码需要解析 PE 文件的头和节。这可能会在中级和高级模块中看到。
🎯 本模块学习要点
必须掌握的概念:
- ✅ PE 文件的基本结构(DOS Header → NT Header → Sections)
- ✅ MZ 和 PE 签名的作用
- ✅ Optional Header 中的关键字段
- ✅ 常见 PE 节的用途(.text、.data、.rdata等)
- ✅ Import Address Table(IAT)的作用
- ✅ Export Directory 的作用
- ✅ Data Directory 数组的用途
PE 结构速记:
plaintextPE 文件 = DOS Header (MZ) + DOS Stub + NT Header (PE) ├─ File Header └─ Optional Header └─ Data Directory[16] + Section Headers + Sections (.text, .data, .rdata, .rsrc...)关键数据结构:
结构 关键成员 用途 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节信息 实践建议:
- 使用 PE-Bear 打开程序,观察 PE 结构
- 用十六进制编辑器查看 MZ 和 PE 签名
- 编写代码遍历 PE 节
- 尝试修改 IAT 或添加新节
工具推荐:
- PE-Bear:可视化 PE 结构
- CFF Explorer:编辑 PE 文件
- PE-sieve:检测恶意 PE 修改
- HxD:十六进制编辑器
下一步: 学习 DLL(动态链接库)的使用