5、PE文件结构解析:DOS头、NT头、节表、导入表与导出表、资源节
说到PE文件,我脑子里第一反应就是——这玩意儿是Windows可执行文件的灵魂。你平时双击的exe、dll、sys,底层全是PE结构。搞逆向,不懂PE结构,就像开车不看仪表盘,能跑但迟早出事。
我个人习惯,拿到一个陌生样本,第一件事就是拿十六进制编辑器打开,先瞄一眼开头两个字节:4D 5A。看到这个,心里就踏实了——这是个合法的PE文件。嗯,咱们就从这里开始拆解。
5.1 DOS头——老古董但必须存在
PE文件的开头,永远是DOS头。结构体叫IMAGE_DOS_HEADER,大小64字节。为什么Windows还在用这个?说白了是为了兼容性。当年DOS时代留下的遗产,现在成了PE文件的身份证。
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // 签名,必须是 0x5A4D ("MZ")
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标识符
WORD e_oeminfo; // OEM信息
WORD e_res2[10]; // 保留字
LONG e_lfanew; // NT头偏移,关键字段!
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
这里最关键的字段是e_lfanew,它指向真正的NT头。偏移量在0x3C位置,4字节。我曾经遇到过一个恶意样本,故意把e_lfanew改成一个错误的值,导致分析工具直接崩溃。嗯,这种小把戏,你只要手动校验一下就能识破。
5.2 NT头——PE文件的核心身份证
NT头才是PE文件的真正起点。结构体是IMAGE_NT_HEADERS,包含三部分:签名、文件头、可选头。
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // "PE\0\0" (0x00004550)
IMAGE_FILE_HEADER FileHeader; // 文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 可选头
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
文件头(FileHeader)里有个字段我特别关注——NumberOfSections。它告诉你有多少个节。节的数量直接决定了文件的结构复杂度。我见过最多有40多个节的加壳程序,分析起来那叫一个酸爽。
可选头(OptionalHeader)虽然叫"可选",但实际是必选的。这里面藏着程序入口点AddressOfEntryPoint、镜像基址ImageBase、节对齐SectionAlignment等关键信息。你想想看,没有入口点,程序从哪开始跑?
5.3 节表——文件的地图册
节表紧跟在NT头后面。每个节表项IMAGE_SECTION_HEADER大小40字节,描述了文件中的一个节。常见的节有:
| 节名 | 用途 | 常见特征 |
|---|---|---|
| .text | 代码段 | 可执行,不可写 |
| .data | 已初始化数据 | 可读写,不可执行 |
| .rdata | 只读数据 | 导入表、导出表常驻于此 |
| .rsrc | 资源节 | 图标、字符串、对话框等 |
| .reloc | 重定位表 | DLL必备 |
每个节表项包含:节名、虚拟大小、虚拟地址、原始数据大小、原始数据偏移、标志位。标志位决定了这个节能不能执行、能不能写。我曾经分析过一个木马,它把恶意代码藏在.rsrc节里,然后动态修改节属性加上可执行标志,再跳过去执行。这种手法现在很常见,但第一次遇到时确实让我折腾了好一阵子。
5.4 导入表与导出表——程序的"外交部门"
导入表告诉系统:这个程序需要调用哪些外部函数。比如你的exe调用了MessageBoxW,导入表里就会记录"user32.dll"和"MessageBoxW"。
导入表的结构是IMAGE_IMPORT_DESCRIPTOR数组,以全零结构体结尾。每个描述符指向一个DLL的导入信息。我习惯用dumpbin /imports命令快速查看导入表,比手动解析快得多。
// 导入表结构示意
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT (Import Name Table)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // DLL名称的RVA
DWORD FirstThunk; // IAT (Import Address Table)
} IMAGE_IMPORT_DESCRIPTOR;
导出表则相反,它告诉外部程序:这个DLL提供了哪些函数。比如kernel32.dll导出了CreateFile、ReadFile等。导出表结构是IMAGE_EXPORT_DIRECTORY,包含函数数量、函数地址数组、函数名称数组等。
搞逆向时,我经常通过导出表来定位API。比如你想hook某个函数,先找到它的导出序号,再定位到函数地址。嗯,这里有个坑:有些DLL的导出函数没有名字,只有序号,你得通过序号来调用。
5.5 资源节——藏东西的好地方
资源节(.rsrc)是个树形结构,根节点是IMAGE_RESOURCE_DIRECTORY。它包含三层:类型、ID、语言。比如一个图标文件,路径可能是:
- 类型层:RT_ICON (3)
- ID层:IDI_MAIN (101)
- 语言层:LANG_CHINESE (0x0804)
资源节里可以藏很多东西:图标、光标、位图、字符串表、版本信息、对话框模板,甚至自定义数据。我见过一个样本,把加密的payload藏在资源节里,运行时读取并解密执行。这种手法叫"资源节注入",非常经典。
解析资源节,我推荐用Resource Hacker或者PE-bear。手动解析的话,你得一层层遍历目录结构,找到叶子节点,再读取数据。说实话,手动解析挺费劲的,但能让你真正理解它的布局。
5.6 知识体系总览
下面这张图,是我梳理的PE文件结构核心脉络。你对照着看,心里就有谱了。
这张图把PE文件的骨架画清楚了。从上到下依次是:DOS头 → NT头 → 节表 → 各个节 → 导入导出表。你每次分析一个exe,都可以按这个顺序来梳理。
说实话,PE结构看着复杂,但拆开来看就那几块。我刚开始学的时候,也是对着文档啃了好几天,后来动手分析了几十个样本,慢慢就熟了。嗯,别急,多练几次,你也能一眼看穿PE文件的底细。