第12节-PE文件格式与Windows装载
第 12 节:PE 文件格式与 Windows 装载
所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 6 周
课程主题:可执行文件与程序装载
本节目标:理解 Windows 下 PE 可执行文件的基本结构,掌握 DOS Header、PE Header、Section Table、.text、.rdata、.data、.rsrc、Import Table、IAT、DLL 加载等概念,并能用 PE-bear、Detect It Easy、Ghidra、x64dbg 观察 PE 文件。
1. 本节课你要学会什么
学完这一节,你应该能回答下面几个问题:
- PE 是什么?
- Windows 为什么能识别一个
.exe或.dll? - DOS Header 和
MZ魔数是什么? - PE Header 和 Optional Header 中保存了什么?
- Section Table 是什么?
.text、.rdata、.data、.rsrc分别通常放什么?- Import Table 和 IAT 是什么?
- DLL 是如何被加载和调用的?
- PE 和 ELF 有哪些相似点和不同点?
- 为什么 PE 文件格式对 Windows 逆向、漏洞分析、恶意代码分析非常重要?
本节课的主线是:
1 | |
上一节我们学习了 Linux ELF,这一节学习 Windows PE。两者都是可执行文件格式,但设计细节不同。
2. PE 是什么
PE 全称是:
1 | |
中文常译为:
1 | |
Windows 下常见 PE 文件包括:
.exe.dll.sys.ocx.scr- 某些
.cpl
也就是说,PE 不只包括普通可执行程序。
例如:
1 | |
它们都可能是 PE 格式。
3. PE 为什么重要
PE 是 Windows 逆向工程的基础。
3.1 对 Windows Loader
Windows Loader 需要根据 PE 文件知道:
- 这是不是合法 PE 文件
- 入口点在哪里
- 需要映射哪些节到内存
- 每个节的权限是什么
- 依赖哪些 DLL
- 导入哪些函数
- 是否有资源
- 是否需要重定位
- 是否启用了安全缓解机制
3.2 对逆向工程
逆向 PE 程序时,你会关注:
- ImageBase
- Entry Point
- Section
- Import Table
- IAT
- Export Table
- 字符串
- 资源
- TLS Callback
- 是否加壳
- 是否动态解析 API
3.3 对恶意代码分析
Windows 恶意代码大多是 PE 文件。
分析时常看:
- 导入了哪些 API
- 是否有可疑节名
- 是否节权限异常
- 是否包含资源 payload
- 是否加壳
- 是否动态加载 DLL
- 是否修改 IAT
- 是否有反调试 API
- 是否使用网络、文件、注册表、进程相关 API
4. 准备一个示例程序
可以准备一个简单 Windows C 程序 pe_demo.c:
1 | |
如果你使用 MinGW,可以编译:
1 | |
如果在 Windows 上使用 Visual Studio 或 Developer Command Prompt,可以用对应编译器生成 exe。
如果暂时不能编译,也可以用任意简单 exe,例如自己写的 hello.exe 或系统自带小程序做观察。
5. PE 文件的整体结构
PE 文件大致结构可以简化为:
1 | |
不同编译器、链接器和加壳器生成的 PE 会有所不同。
但核心结构类似。
6. DOS Header 和 MZ 魔数
PE 文件最开头是 DOS Header。
它的前两个字节通常是:
1 | |
对应 ASCII:
1 | |
所以很多工具会说:
1 | |
MZ 是 PE 文件的重要魔数。
你可以用十六进制工具查看 exe 文件开头。
例如:
1 | |
这说明它可能是 PE 文件。
DOS Header 中有一个很重要的字段:
1 | |
它保存 PE Header 在文件中的偏移。
Windows Loader 会通过它找到真正的 PE 头。
7. DOS Stub 是什么
DOS Header 后面通常有一小段 DOS Stub。
很多 PE 文件中可以看到字符串:
1 | |
这是历史兼容遗留。
现代 Windows 程序一般不依赖 DOS Stub 的业务逻辑。
逆向时看到它不用紧张。
它只是 PE 文件结构中的常见部分。
8. PE Signature
通过 DOS Header 的 e_lfanew 找到 PE Header 后,会看到 PE 签名:
1 | |
对应:
1 | |
这是 PE Header 的标识。
所以一个典型 PE 文件会同时包含:
1 | |
MZ 在文件开头,PE\0\0 在 e_lfanew 指向的位置。
9. COFF File Header
PE Signature 后面是 COFF File Header。
它包含一些基本文件信息,例如:
- Machine:目标架构
- NumberOfSections:节数量
- TimeDateStamp:时间戳
- PointerToSymbolTable:符号表指针,现代 PE 中常不重要
- SizeOfOptionalHeader:Optional Header 大小
- Characteristics:文件属性
常见 Machine:
| 值 | 含义 |
|---|---|
| 0x014c | x86 32 位 |
| 0x8664 | x86-64 / AMD64 |
逆向时,Machine 可以告诉你这是 32 位还是 64 位程序。
这会影响:
- 寄存器
- 调用约定
- 栈布局
- 调试器选择
- 反汇编模式
10. Optional Header 并不“可选”
PE 中有一个结构叫:
1 | |
名字叫 Optional,但对可执行文件来说非常重要,基本不是可有可无。
它包含装载所需的关键信息,例如:
- AddressOfEntryPoint
- ImageBase
- SectionAlignment
- FileAlignment
- SizeOfImage
- SizeOfHeaders
- Subsystem
- DLLCharacteristics
- DataDirectory
这些字段决定 Windows 如何把 PE 映射到内存。
11. AddressOfEntryPoint
AddressOfEntryPoint 是程序入口点的 RVA。
RVA 是:
1 | |
即相对虚拟地址。
它不是文件偏移,也不是绝对运行地址。
如果:
1 | |
那么默认入口虚拟地址大致是:
1 | |
注意:
入口点通常不是 main。
Windows 程序也会先执行运行时启动代码,然后再调用 main / WinMain。
12. ImageBase
ImageBase 是 PE 希望被加载到的首选基址。
例如 64 位程序常见:
1 | |
DLL 也有自己的 ImageBase。
如果启用了 ASLR,实际加载地址可能不是首选 ImageBase。
Windows Loader 会选择合适地址加载,并根据需要做重定位。
逆向时,你需要区分:
1 | |
这三个概念非常重要。
13. RVA、VA 和 File Offset
PE 分析中常见三个地址概念。
13.1 RVA
RVA 是相对虚拟地址。
1 | |
例如:
1 | |
13.2 VA
VA 是虚拟地址,即程序加载到内存后的地址。
调试器中看到的地址通常是 VA。
例如 x64dbg 中:
1 | |
13.3 File Offset
File Offset 是文件偏移。
它表示数据在磁盘 PE 文件中的位置。
例如:
1 | |
RVA 和 File Offset 需要通过 Section Table 转换。
逆向工具会帮你做很多转换,但你需要知道它们不是同一个概念。
14. Section Table 是什么
Section Table 描述 PE 文件中各个节的信息。
常见字段:
- Name
- VirtualAddress
- VirtualSize
- PointerToRawData
- SizeOfRawData
- Characteristics
其中:
| 字段 | 含义 |
|---|---|
| Name | 节名 |
| VirtualAddress | 加载到内存后的 RVA |
| VirtualSize | 内存中大小 |
| PointerToRawData | 文件中偏移 |
| SizeOfRawData | 文件中大小 |
| Characteristics | 权限和属性 |
常见节:
1 | |
不同编译器可能使用不同节名。
加壳程序也可能出现奇怪节名。
15. .text 节
.text 通常存放程序代码。
包括:
- 函数机器指令
- 运行时启动代码
- 编译器生成的辅助代码
它通常具有权限:
1 | |
一般不应该可写。
如果你看到某个 PE 的 .text 节同时可写和可执行,需要提高警惕。
这不一定恶意,但值得分析。
16. .rdata 节
.rdata 通常存放只读数据。
常见内容:
- 字符串常量
- 导入相关只读结构
- RTTI 信息
- const 数据
- 某些跳转表
例如:
1 | |
字符串 hello 和 title 可能在 .rdata。
逆向时字符串窗口中的很多字符串来自 .rdata。
17. .data 节
.data 通常存放已初始化的全局变量和静态变量。
例如:
1 | |
这些数据需要在文件中保存初始值。
.data 通常权限是:
1 | |
通常不可执行。
18. .rsrc 节
.rsrc 是资源节。
Windows 程序可以把资源放在 PE 文件中,例如:
- 图标
- 对话框
- 菜单
- 字符串表
- 版本信息
- 位图
- 嵌入文件
恶意代码有时也会把 payload 或配置数据藏在资源节里。
所以恶意代码分析中,.rsrc 很重要。
可以用工具查看资源:
- Resource Hacker
- PE-bear
- Detect It Easy
- CFF Explorer
- Ghidra
19. .reloc 节
.reloc 是重定位节。
如果 PE 不能加载到首选 ImageBase,Windows Loader 需要修正一些地址。
.reloc 提供重定位信息。
ASLR 依赖程序能够被重定位。
如果 PE 没有重定位信息,地址随机化可能受限。
逆向和漏洞利用中,重定位和 ASLR 关系密切。
20. Data Directory
Optional Header 中有 Data Directory。
它指向 PE 中一些重要表结构。
常见包括:
- Export Table
- Import Table
- Resource Table
- Exception Table
- Base Relocation Table
- TLS Table
- Load Config Table
- IAT
你不需要一开始全部掌握,但要知道:
PE 中很多高级结构不是靠节名固定定位,而是通过 Data Directory 指向。
例如 Import Table 的位置由 Data Directory 告诉 Loader。
21. Import Table 是什么
Import Table 保存程序依赖的 DLL 和函数信息。
例如一个程序可能导入:
1 | |
这些导入告诉 Windows Loader:
1 | |
逆向分析中,Import Table 是非常重要的行为线索。
如果程序导入:
1 | |
你可以初步推测它可能具有进程、内存、注册表、网络相关行为。
22. IAT 是什么
IAT 全称:
1 | |
中文常译为:
1 | |
程序调用外部 DLL 函数时,需要知道函数实际地址。
Windows Loader 加载程序时,会把导入函数的实际地址填入 IAT。
程序后续调用外部 API 时,可能通过 IAT 间接调用。
简化理解:
1 | |
IAT 对逆向和 Hook 都很重要。
23. IAT 为什么重要
IAT 重要原因:
- 能反映程序使用了哪些外部 API
- 程序调用外部 API 时可能通过 IAT
- 安全软件和分析工具可以监控 IAT
- 恶意代码可能修改 IAT 实现 Hook
- 加壳程序可能重建 IAT
- 脱壳分析常涉及 IAT 修复
例如:
1 | |
这个 IAT 项中保存的是 user32.dll!MessageBoxA 的实际地址。
24. Export Table 是什么
Export Table 保存一个 PE 文件向外提供的函数或符号。
DLL 常有导出表。
例如:
1 | |
EXE 也可以有导出表,但 DLL 更常见。
逆向 DLL 时,Export Table 可以告诉你:
- 这个 DLL 提供哪些函数
- 函数名是什么
- 函数序号是什么
- 函数 RVA 是多少
25. DLL 加载过程的直觉
Windows 程序启动时,Loader 会:
- 映射 EXE 到进程地址空间
- 读取 Import Table
- 找到依赖的 DLL
- 把 DLL 映射到进程地址空间
- 解析导入函数地址
- 填写 IAT
- 处理重定位
- 初始化 TLS 等结构
- 调用入口点
所以一个 Windows 进程通常不只有 EXE 本身,还会加载很多 DLL。
在 Process Explorer 或 x64dbg 中可以看到模块列表。
26. 静态导入和动态加载
26.1 静态导入
程序在 Import Table 中明确列出依赖 DLL 和函数。
例如:
1 | |
这种容易被 PE 工具看到。
26.2 动态加载
程序运行时调用:
1 | |
动态加载 DLL 并解析函数地址。
例如:
1 | |
这样 VirtualAlloc 可能不会直接出现在 Import Table 中。
恶意代码常用动态加载隐藏敏感 API。
27. TLS Callback 是什么
TLS 全称:
1 | |
PE 中可以有 TLS Callback。
TLS Callback 的特点是:
1 | |
这对逆向很重要。
因为你以为程序从 Entry Point 开始,实际上在入口点之前可能已经执行了 TLS Callback。
恶意代码和壳有时会利用 TLS Callback 做:
- 反调试
- 解密代码
- 初始化隐藏逻辑
- 检查环境
所以 PE 分析时,如果行为很早发生,要检查 TLS Table。
28. PE 节权限和异常节名
PE Section 有权限属性。
常见权限包括:
- 可读
- 可写
- 可执行
正常情况下:
1 | |
如果看到:
1 | |
就要进一步分析。
加壳或恶意样本中可能出现:
1 | |
这些不一定都恶意,但可能表示压缩、加壳或保护。
29. PE 与 ELF 的对照
| 主题 | ELF | PE |
|---|---|---|
| 常见系统 | Linux / Unix-like | Windows |
| 魔数 | 7f 45 4c 46 |
MZ + PE\0\0 |
| 可执行文件 | 无固定扩展名 | 常见 .exe |
| 共享库 | .so |
.dll |
| 代码节 | .text |
.text |
| 只读数据 | .rodata |
.rdata |
| 数据节 | .data / .bss |
.data |
| 装载视角 | Program Header | PE Header + Section Table |
| 动态导入 | PLT/GOT、dynsym | Import Table、IAT |
| 重定位 | relocation sections | .reloc |
| 资源 | 较少内建资源机制 | .rsrc 常见 |
两者思想相似:
1 | |
但具体结构和工具不同。
30. PE 和逆向工程
逆向 PE 程序时,可以按这个顺序观察:
- 用 Detect It Easy 判断文件类型、架构、是否加壳
- 用 PE-bear 查看 Header、Section、Import Table
- 用 strings 或工具查看字符串
- 用 Ghidra / IDA 查看函数和反编译
- 用 x64dbg 动态调试入口点和关键 API
- 用 Process Monitor 观察文件、注册表、进程行为
- 用 Wireshark 观察网络行为
PE 分析不只是看静态结构,还要结合运行行为。
31. PE 和漏洞分析
Windows 漏洞分析中,PE 信息可以回答:
- 程序是 32 位还是 64 位
- 是否启用 ASLR
- 是否启用 DEP
- 是否启用 CFG
- 是否有 SafeSEH / SEHOP
- 程序基址是多少
- 哪些模块被加载
- IAT 中有哪些 API
- 哪些节可写/可执行
- 崩溃地址属于哪个模块
例如 x64dbg 中看到崩溃地址:
1 | |
你需要判断:
1 | |
这和模块加载和 PE 基址有关。
32. PE 和恶意代码分析
恶意代码分析中,PE 是最常见对象之一。
静态分析时常看:
- 文件哈希
- 编译时间戳
- 入口点
- 节名和节权限
- 导入表
- 字符串
- 资源节
- 是否加壳
- TLS Callback
动态分析时常看:
- 文件行为
- 注册表行为
- 网络行为
- 进程树
- API 调用
- 内存分配和权限修改
- 是否释放或注入 payload
PE 结构提供静态线索,动态工具验证真实行为。
33. Windows 实验:用 Detect It Easy 识别 PE
准备一个 exe,例如 pe_demo.exe。
用 Detect It Easy 打开。
观察:
- 文件类型是否为 PE
- 32 位还是 64 位
- 编译器或链接器信息
- 是否可能加壳
- 入口点
- 节信息
提交时记录:
1 | |
34. Windows 实验:用 PE-bear 查看结构
用 PE-bear 打开 pe_demo.exe。
重点查看:
- DOS Header
- NT Headers
- File Header
- Optional Header
- Section Headers
- Import Table
- IAT
- Resource
记录:
MZ是否存在PE\0\0是否存在- ImageBase
- AddressOfEntryPoint
- Section 数量
.text、.rdata、.data、.rsrc是否存在- 导入了哪些 DLL
- 导入了哪些函数
35. Windows 实验:用 Ghidra 查看导入和字符串
用 Ghidra 打开 pe_demo.exe。
步骤:
- Import File
- 运行自动分析
- 查看 Symbol Tree
- 查看 Imports
- 查看 Defined Strings
- 找
hello pe、pe_demo_output.txt - 查看字符串 XREF
- 找到调用
CreateFileA、WriteFile、MessageBoxA的位置 - 查看 Decompiler 伪代码
思考:
- 导入函数是否能帮助理解行为?
- 字符串是否能定位关键逻辑?
- 伪代码是否和原始 C 程序接近?
36. Windows 实验:用 x64dbg 观察入口点和 API
用 x64dbg 打开 pe_demo.exe。
练习:
- 运行到系统断点或入口点
- 查看模块基址
- 查看入口点附近代码
- 在
CreateFileA或MessageBoxA下断点 - 运行程序
- 命中断点后查看参数寄存器
- 查看返回值
RAX - 查看栈和内存
重点观察:
RIP当前属于哪个模块- 主程序基址是多少
- API 参数在哪里
- 文件名字符串在哪里
- API 调用是否成功
37. Windows 实验:Process Monitor 观察 PE 行为
用 Process Monitor:
- 清空事件
- 设置过滤器:Process Name is
pe_demo.exe - 运行程序
- 观察文件事件
- 如果有 MessageBox,确认程序行为
重点看:
CreateFileWriteFileCloseFile- Image Load 事件
记录:
1 | |
38. 本节重点总结
你需要记住这些核心结论:
- PE 是 Windows 常见可执行文件格式,常见于
.exe、.dll、.sys。 - PE 文件开头通常有
MZ,并通过e_lfanew找到PE\0\0签名。 - Optional Header 中有入口点、ImageBase、Data Directory 等装载关键信息。
- RVA、VA、File Offset 是 PE 分析中的三个重要地址概念。
- Section Table 描述
.text、.rdata、.data、.rsrc等节的位置、大小和权限。 - Import Table 描述程序需要哪些 DLL 和函数。
- IAT 保存导入函数运行时实际地址,是逆向、Hook、脱壳分析的重要结构。
- DLL 加载和导入解析由 Windows Loader 完成。
- TLS Callback 可能在入口点之前执行,恶意代码和壳可能利用它。
- PE 分析需要结合静态结构和动态行为工具。
39. 本节课后作业
作业 1:PE 基本识别
选择一个简单 exe,用 Detect It Easy 或 PE-bear 查看。
提交内容:
1 | |
作业 2:Section Table 分析
用 PE-bear 查看 Section。
提交内容:
1 | |
作业 3:Import Table 和 IAT 分析
查看 Import Table。
提交内容:
1 | |
作业 4:字符串和 XREF 分析
用 Ghidra 打开 exe。
提交内容:
1 | |
作业 5:动态调试 API 参数
用 x64dbg:
- 在一个导入 API 上下断点,例如
CreateFileA/W、MessageBoxA、WriteFile - 运行程序
- 查看参数寄存器和返回值
提交内容:
1 | |
作业 6:Process Monitor 行为验证
运行程序并用 Process Monitor 观察。
提交内容:
1 | |
40. 自测题
题 1
PE 的全称是什么?常见哪些文件扩展名使用 PE 格式?
题 2
MZ 和 PE\0\0 分别是什么?
题 3
e_lfanew 的作用是什么?
题 4
RVA、VA、File Offset 有什么区别?
题 5
.text、.rdata、.data、.rsrc 通常分别存放什么?
题 6
Import Table 和 IAT 有什么区别?
题 7
为什么 TLS Callback 对逆向分析很重要?
题 8
为什么恶意代码可能动态解析 API?
题 9
PE 和 ELF 有哪些相似点?
41. 自测题参考答案
答 1
PE 全称是 Portable Executable。Windows 下常见 .exe、.dll、.sys、.scr、.ocx 等文件可能使用 PE 格式。
答 2
MZ 是 DOS Header 开头的魔数,表示这是典型 PE 文件开头;PE\0\0 是 PE Signature,位于 e_lfanew 指向的位置,用于标识真正的 PE Header。
答 3
e_lfanew 位于 DOS Header 中,用于指向 PE Header 在文件中的偏移。Windows Loader 通过它找到 PE\0\0 和后续 PE 结构。
答 4
RVA 是相对虚拟地址,相对于 ImageBase;VA 是程序加载到内存后的虚拟地址;File Offset 是数据在磁盘文件中的偏移。三者不是同一个概念,需要通过 ImageBase 和 Section Table 转换。
答 5
.text 通常存放代码;.rdata 存放只读数据、字符串和部分导入相关信息;.data 存放已初始化可写全局数据;.rsrc 存放图标、菜单、版本信息、字符串表、嵌入文件等资源。
答 6
Import Table 描述程序需要导入哪些 DLL 和函数;IAT 保存这些导入函数在运行时解析后的实际地址,程序调用外部 API 时可能通过 IAT 间接调用。
答 7
因为 TLS Callback 可能在程序入口点之前执行。恶意代码和加壳程序可能利用它进行反调试、解密、环境检查或隐藏初始化逻辑。
答 8
动态解析 API 可以减少静态导入表中的敏感函数暴露,也能在运行时按需加载 DLL 和函数。恶意代码常用这种方式隐藏行为或增加分析难度。
答 9
PE 和 ELF 都是可执行文件格式,都描述程序如何被加载到内存、入口点在哪里、有哪些代码和数据区域、依赖哪些外部库、如何处理重定位和动态链接等。
42. 下一节预告
下一节课会讲:
1 | |
你会学习:
- 进程和线程的区别
- 一个进程内多个线程共享什么
- 每个线程私有什么
- 线程栈
- Linux pthread
- Windows Thread
- 为什么恶意代码和系统程序经常创建线程