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改成一个错误的值,导致分析工具直接崩溃。嗯,这种小把戏,你只要手动校验一下就能识破。

小技巧: 用WinHex或010 Editor打开exe,跳到0x3C处读4字节,再跳到那个偏移,看到"PE\0\0"就对了。

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等关键信息。你想想看,没有入口点,程序从哪开始跑?

重点记忆: AddressOfEntryPoint 是RVA(相对虚拟地址),不是文件偏移。调试器加载后,实际入口 = ImageBase + AddressOfEntryPoint。

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;
注意: INT和IAT在文件加载前指向同一个结构——IMAGE_THUNK_DATA。加载后,IAT被系统填充为实际函数地址。所以,如果你在内存中看到IAT里全是地址,别惊讶,那是正常的。

导出表则相反,它告诉外部程序:这个DLL提供了哪些函数。比如kernel32.dll导出了CreateFileReadFile等。导出表结构是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头 e_magic = "MZ" NT头 Signature = "PE\0\0" 节表 N个节表项 .text 代码段 .data 数据段 .rdata 只读段 .rsrc 资源段 导入表 (IAT/INT) 与 导出表 (EAT) 程序与外部DLL的桥梁 逆向分析 = 解析PE结构 + 理解加载机制

这张图把PE文件的骨架画清楚了。从上到下依次是:DOS头 → NT头 → 节表 → 各个节 → 导入导出表。你每次分析一个exe,都可以按这个顺序来梳理。

说实话,PE结构看着复杂,但拆开来看就那几块。我刚开始学的时候,也是对着文档啃了好几天,后来动手分析了几十个样本,慢慢就熟了。嗯,别急,多练几次,你也能一眼看穿PE文件的底细。

核心总结: PE文件 = DOS头(定位NT头)+ NT头(签名+文件头+可选头)+ 节表(描述各节位置和属性)+ 各节内容(代码、数据、资源、导入导出表)。搞懂这个,逆向分析就成功了一半。
公众号:蓝海资料掘金营,微信deep3321