PE 文件 数据目录

文章目录

数据目录就像PE文件的"索引"或"总目录",它告诉你所有重要表格都藏在哪里。


第1课:为什么需要数据目录?

PE文件里有很多关键的数据结构:导出函数表、导入函数表、资源(图标、对话框等)、重定位表、调试信息等等。这些表的位置和大小不是固定的,它们由编译器/链接器在生成PE文件时决定。

如果没有一个统一的"总目录",操作系统加载器或分析工具就不知道该去哪里找这些表。它不能靠猜,也不能用固定的偏移,因为每次编译后的布局都可能不同。

所以,PE文件在头部预留了一个数组,这个数组的每个槽位专门登记一种特定表格的位置(RVA)和大小 。这就是数据目录(Data Directory)


第2课:数据目录在PE文件里的位置

要理解数据目录的位置,我们需要先认识PE文件的宏观结构。一个PE文件大致由以下几部分按顺序组成:

部分 通俗叫法 作用
DOS头 DOS Header / MZ头 让文件在DOS下能显示"This program cannot be run in DOS mode"
PE签名 PE Signature 标识这是一个PE文件 (PE\0\0)
COFF文件头 File Header 记录机器类型、节数量等基础信息
可选头 Optional Header 虽然叫"可选",但在EXE/DLL里是必须的。记录入口点、基址、对齐值、数据目录等极其重要的信息
节表 Section Table 描述每个节(如.text, .data)在文件和内存中的位置、大小、属性
节数据 Sections 真正的代码和数据

数据目录就是可选头(Optional Header)的最后一部分。 可选头是一个很大的结构体,它的末尾定义了一个数组,专门存放各目录项的位置和大小。在C语言定义中,它长这样:

c 复制代码
typedef struct _IMAGE_OPTIONAL_HEADER {
    // ... 前面有很多字段,如入口点、基址、节对齐等 ...
    DWORD NumberOfRvaAndSizes;  // 数据目录项的个数
    IMAGE_DATA_DIRECTORY DataDirectory[IMPORTANT_NUMBER];
} IMAGE_OPTIONAL_HEADER;

NumberOfRvaAndSizes 这个字段告诉你后面跟了多少个数据目录条目。虽然标准定义了16个常见的目录槽位,但这个数字理论上可以让操作系统知道实际登记了几个。通常这个值是16,但即使在文件里只定义了少数几个,加载器也会根据这个值来读取。


第3课:一个数据目录条目的结构

每个数据目录条目都是一个非常简单、固定大小的结构体,总共只有8个字节:

字段名 字节数 含义
VirtualAddress 4 该目录项所指向的数据结构的起始 RVA (相对虚拟地址)
Size 4 该数据结构的大小(字节数)

如果某一个目录项没有被使用,它的 VirtualAddressSize 都会被设置为 0。例如,一个没有导出任何函数的EXE文件,其导出表对应的数据目录项就是全零。

这里的 VirtualAddressRVA,也就是相对于文件加载到内存后的基址的偏移。在解析文件时,我们通常需要把RVA转换成文件内的实际偏移(FOA),才能读到真正的数据。关于RVA和文件偏移的转换,我们在后面第6课详细说。


第4课:标准数据目录条目索引表(16项)

Windows PE格式预定义了16个数据目录槽位,每个槽位都有固定的用途。下面是最完整的表格,每一个都值得记住索引号:

索引 目录名称 含义 通俗解释
0 Export Table 导出表 我这个DLL提供了哪些函数给别人用?
1 Import Table 导入表 我要用到其他DLL的哪些函数?
2 Resource Table 资源表 图标、菜单、对话框、字符串等资源都在这里
3 Exception Table 异常表 用于异常处理(如x64的展开数据)
4 Certificate Table 安全证书表 数字签名、安全证书(这个比较特殊,是直接文件偏移而不是RVA)
5 Base Relocation Table 基址重定位表 就是我们上次详细讨论的那个重定位表!
6 Debug 调试信息 存放调试符号信息(如CodeView, PDB路径)
7 Architecture 架构特定数据 极少用,通常为0
8 Global Ptr 全局指针 用于某些体系结构,通常为0
9 TLS Table 线程局部存储表 每个线程有自己的变量副本,这里放初始化数据
10 Load Config Table 加载配置表 存放安全配置,如SEH防护、Guard CF等
11 Bound Import 绑定导入表 提高DLL加载速度的预绑定信息
12 IAT 导入地址表 导入函数地址的实际存放处,运行时被填充
13 Delay Import Descriptor 延迟导入表 直到函数第一次被调用时才加载DLL
14 COM Descriptor CLR运行时头 .NET程序的数据,指向CLR头
15 Reserved 保留,必须为0 未使用

注意:有些目录项看起来很相似,比如导入表(索引1)和IAT(索引12),它们的区别在于:导入表描述的是"我要从哪个DLL导入什么函数",而IAT是运行时"这些导入函数的入口地址存放在哪块内存"。两者配合工作。


第5课:几个最重要的数据目录项深入说明

我们挑选其中与程序加载和逆向最密切的几个,稍微展开。

5.1 导出表(索引0)

DLL通过导出表告诉外界:"我可以提供这些函数"。导出表里记录了函数名、序号和函数的RVA。当你调用 GetProcAddress 时,系统就是在查询这个表。

5.2 导入表(索引1)

EXE或DLL通过导入表声明:"我需要从这些DLL里调用这些函数"。它是一个结构数组,每个结构描述一个DLL,以及要从该DLL导入的一系列函数。加载器在加载程序时,会根据导入表去加载依赖的DLL,并找到函数的实际地址,填入IAT。

5.3 资源表(索引2)

程序里嵌入的图标、位图、对话框模板、版本信息等都放在资源表。资源表是一个树形结构,按类型、名称、语言分层组织。

5.4 基址重定位表(索引5)

这就是上一讲花大量篇幅分析的表。它记录了当程序没有加载到预设基址时,哪些位置需要加上偏移修正。它的数据目录项给出整个重定位表的RVA和大小,操作系统通过这个条目找到它。

5.5 IAT(索引12)

IAT (Import Address Table) 是导入地址表。虽然在导入表里指定了要调用哪些函数,但函数的真实地址直到DLL加载后才知道。加载器把解析得到的函数地址填到 IAT 中。程序代码通过间接调用 IAT 中的地址来调用导入函数。IAT 也是一个数据目录项,因为有些保护或优化工具会直接引用它。

5.6 延迟导入表(索引13)

为了加快程序启动速度,可以把某些DLL标记为"延迟加载"。这样只有在该DLL的函数被实际调用时,加载器才去加载DLL并填入IAT。这个表的结构与导入表类似,但由延迟加载辅助函数处理。


第6课:如何从数据目录的RVA找到磁盘文件中的实际数据?

数据目录项给的 VirtualAddress 是内存加载后的RVA,并非文件内的偏移。但当我们静态分析一个PE文件(比如用十六进制编辑器打开查看)时,需要把RVA转换成文件偏移(File Offset 或 FOA)。转换的核心依据是节表(Section Table)。

节表里面记录了每个节在文件中的起始偏移(PointerToRawData)和在内存中的起始RVA(VirtualAddress),以及节的大小。转换的大致思路如下:

  1. 确定目标 RVA 属于哪个节。
    找到满足 节内存起始RVA <= 目标RVA < 节内存起始RVA + 节内存对齐后的大小 的那个节。
  2. 计算该 RVA 在节内的偏移:节内偏移 = 目标RVA - 节内存起始RVA
  3. 文件偏移 = 节的PointerToRawData + 节内偏移

如果RVA不在任何节内(比如落在PE头或者节间隙中),或者节的 PointerToRawData 为0,则表示该数据在文件中可能不存在,或者仅存在于内存展开时。

这就是为什么只靠数据目录的RVA还不能直接读文件,需要配合节表。


第7课:一个具体的例子

假设我们想找到一个DLL的重定位表。步骤如下:

  1. 在PE文件头的末尾,找到可选头(Optional Header),读取其最后一个部分的数据目录数组。
  2. 定位到索引5的元素(因为基址重定位表是第6个,索引从0开始)。
  3. 读到 VirtualAddress = 0x0002B000Size = 0x000000A0
  4. 通过节表转换:发现 RVA 0x2B000 落在 .reloc 节中,该节的内存起始RVA为 0x2B000,文件偏移为 0x1A800
  5. 因为 RVA 正好等于节的起始RVA,所以节内偏移为0,文件偏移也为 0x1A800
  6. 从文件偏移 0x1A800 开始,读取 0xA0 字节,就得到了整个重定位表的原始二进制数据。
  7. 然后按照上一讲的块结构去解析。

可以看出,数据目录就是一个指向宝藏的"藏宝图",它不直接包含数据,而是给出确切的位置和大小。


第8课:数据目录的总览表格

为了让你快速回忆,我整理一个关于数据目录本身的核心总结表:

问题 答案
在哪里? 在PE可选头(Optional Header)的末尾部分
由什么组成? NumberOfRvaAndSizesIMAGE_DATA_DIRECTORY 结构,每个8字节
每个结构包含什么? VirtualAddress (4字节RVA) + Size (4字节)
默认多少个? 通常为16个,涵盖各种标准表
未使用的条目如何表示? VirtualAddress 和 Size 均为 0
目录表本身有大小吗? 是的,通常占用 16 × 8 = 128 字节,在可选头之内
如何从RVA找到文件位置? 利用节表将RVA转换成文件偏移(FOA)
最重要的目录项是哪几个? 导出表(0)、导入表(1)、资源(2)、重定位(5)、IAT(12)等

有了数据目录这张总清单,我们才能系统地访问PE文件内的一切关键信息。

相关推荐
CLX05051 小时前
CSS如何制作响应式图片集布局_利用object-fit填充空间
jvm·数据库·python
Achou.Wang1 小时前
Go语言并发编程中的死锁防范与破解之道
服务器·开发语言·golang
Full Stack Developme1 小时前
SQL发展历史
数据库·sql
灵晔君2 小时前
【Linux】进程(三)——进程切换、O (1) 调度、环境变量、命令行参数
linux·运维·服务器
2303_821287382 小时前
SQL如何进行分组后字符串拼接_使用GROUP_CONCAT或STRING_AGG
jvm·数据库·python
weixin_459753942 小时前
CSS文本渲染在不同操作系统差异_使用font-smoothing平滑化
jvm·数据库·python
林熙蕾LXL2 小时前
进程间通信
linux
zcn1262 小时前
关于非相关子查询改写经验
数据库·sql·sql优化改写
追梦开发者2 小时前
MongoDB 踩坑实录②:数据建模和索引没搞对,查询慢了整整 10 倍
数据库·mongodb·database