- 公开视频 -> 链接点击跳转公开课程
- 博客首页 -> 链接点击跳转博客主页
目录
[什么是 IMAGE_EXPORT_DIRECTORY?](#什么是 IMAGE_EXPORT_DIRECTORY?)
[PE 文件与 Export Table 的关系](#PE 文件与 Export Table 的关系)
[PE 文件的整体视角](#PE 文件的整体视角)
[Export Table 在 PE 中的定位](#Export Table 在 PE 中的定位)
[IMAGE_EXPORT_DIRECTORY 结构](#IMAGE_EXPORT_DIRECTORY 结构)
[MajorVersion 和 MinorVersion](#MajorVersion 和 MinorVersion)
[Export Table 的三个主要数组的关系](#Export Table 的三个主要数组的关系)
[API Hook](#API Hook)
[恶意 Shellcode 的注入](#恶意 Shellcode 的注入)
什么是 IMAGE_EXPORT_DIRECTORY?
概述
- 在 Windows PE 文件结构中,IMAGE_EXPORT_DIRECTORY 是用于描述模块 (DLL 或 EXE) 所导出(函数或变量)信息的核心结构。
- 通过 Export Directory,其他模块可以访问当前模块公开的符号,如动态函数链接调用的函数地址。
PE 文件与 Export Table 的关系
PE 文件的整体视角
PE 文件是一种模块化执行文件格式。它包含以下部分:
- Headers 部分:包括 DOS Header、PE Header,用来描述文件结构和入口点等。
- Data Directories 部分:包含导出表(Export Table)、导入表(Import Table)、重定位表(Relocation Table)等指针。
Export Table 在 PE 中的定位
- Export Table 是 Data Directories 的一项,位于 PE 可选头部中的 IMAGE_DATA_DIRECTORY 中。
- 它的作用是描述该模块导出的所有符号(函数或变量),并支持函数按名称或序号的调用。
- 常用于动态链接库 (DLL) 的导出 API。
IMAGE_EXPORT_DIRECTORY 结构
数据结构定义
在 Windows SDK 的 winnt.h
文件中,IMAGEEXPORTDIRECTORY 的结构定义如下:
cpp
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 保留字段(总为0)
DWORD TimeDateStamp; // 时间戳(编译器生成时间)
WORD MajorVersion; // 主版本号(通常为0)
WORD MinorVersion; // 次版本号(通常为0)
DWORD Name; // DLL名称的RVA(如"kernel32.dll")
DWORD Base; // 导出函数序号的基值(实际序号=索引+Base)
DWORD NumberOfFunctions; // 导出函数总数(最大的导出序号 - 最小的导出序号 + 1)
DWORD NumberOfNames; // 按名称导出的函数数量(可能小于总数)
DWORD AddressOfFunctions; // 函数地址数组的RVA(每个元素为函数RVA)
DWORD AddressOfNames; // 函数名称数组的RVA(每个元素为名称RVA)
DWORD AddressOfNameOrdinals; // 函数序号数组的RVA(每个元素为索引)
} IMAGE_EXPORT_DIRECTORY;
字段详解
Characteristics
- 保留字段,通常为 0。
TimeDateStamp
- 用于表示导出表生成的时间戳。
- 用于验证导出表的有效性。
MajorVersion 和 MinorVersion
- 保留字段,表示模块的主次版本号。实际很少使用。
Name
- 模块名称(DLL 文件名)的 RVA,例如 "kernel32.dll"。
Base
- 起始导出序号值。默认是 1,但可以自定义为其它值。
NumberOfFunctions
- 导出函数地址表中条目数量。
- 即模块实际可以导出的符号总量。
NumberOfNames
- 导出名称表中的名称数量。
- 注意: 这可能小于 NumberOfFunctions,因为不是所有函数都有名称(某些只通过序号导出)。
AddressOfFunctions
- 一个指向 RVA 的数组,每个条目是导出函数地址。
AddressOfNames
- 一个指向 RVA 的数组,每个条目是导出函数名称字符串的 RVA。
AddressOfNameOrdinals
- 一个指向数组的 RVA,每个条目对应导出名称表中名称的序号(以 Base 为基数)。
Export Table 的三个主要数组的关系
AddressOfFunctions(函数地址表)
- 储存实际导出函数的入口地址(RVA)。
- 大小等于 NumberOfFunctions * sizeof(DWORD)。
AddressOfNames(名称指针表)
- 储存名称的 RVA,仅包含按名称导出的符号。
- 大小等于 NumberOfNames * sizeof(DWORD)。
AddressOfNameOrdinals(序号表)
- 每个名称对应的函数序号。
- 这些序号用来索引函数地址表中的指定条目。
- 大小等于 NumberOfNames * sizeof(WORD)。
导出表的工作流程
- PE 加载器读取 Data Directory,定位到 IMAGE_EXPORT_DIRECTORY 的 RVA。
- 通过 AddressOfFunctions、AddressOfNames 和 AddressOfNameOrdinals 解析导出符号。
- 如果按照函数名导出,从名称表和序号表定位到函数地址。
- 如果按照函数序号导出,直接用序号索引函数地址表。
- 动态生成外部模块可以调用的函数地址。
导出表在攻击中的应用
API Hook
攻击者通过修改导出表(尤其是导出地址表,即 Export Address Table, EAT)中函数地址,将正常的函数调用重定向到恶意函数,进而拦截数据或执行恶意代码。
- 功能篡改:
- 攻击者拦截导出的关键函数(如 OpenFile、ReadFile),窃取用户数据或操作文件。
- 隐藏恶意行为:
- 将监控函数(如 NtQuerySystemInformation)指向伪造的函数,隐藏自身的进程和对象。
恶意 Shellcode 的注入
- 添加自定义导出函数,伪装成合法的 DLL。
- 使用恶意 Shellcode 替代导出函数实现远程控制或蠕虫传播。
导出表在防御中的实际应用
完整性验证
导出表是 PE 文件结构的核心,可以作为完整性校验的依据:
- 验证导出地址是否指向合法的函数地址。
- 验证所有导出函数的序号和名称是否匹配。
- 针对 DLL 劫持攻击的防御:通过比对导出表内容和原始文件的导出表,检测是否被篡改。
- 防止 Hook 或 API 替换攻击:验证导出表指向的内存区域是否合法。
内存防护机制
Windows 的现代内存防护技术可以用来保护导出表免受修改。
- DEP(数据执行保护):
- 防止攻击者直接修改导出表的函数地址指向恶意代码。
- ASLR(地址空间布局随机化):
- 随机化模块加载地址,增加攻击者定位导出表的难度。