- UID
- 1625
- 积分
- 34
- 精华
- 0
- 沃币
- 3 枚
- 注册时间
- 2024-3-8
|
本帖最后由 zer0daysec 于 2024-3-26 08:43 编辑
这一次我们将讨论导出目录表,这部分在 PE 结构中是非常重要的,回忆一下,在 NT 头中的可选头最后一个字段便是描述目录表总体信息,一般为 16 个成员,但最后一个成员全为 0,那么实际 "有效" 的只有 15 个有效成员
有专门宏定义来定义它们:
- #define IMAGE_DIRECTORY_ENTRY_EXPORT 0x0 //[0x78] 1)
- #define IMAGE_DIRECTORY_ENTRY_IMPORT 0x1 //[0x80] 2)
- #define IMAGE_DIRECTORY_ENTRY_RESOURCE 0x2 //[0x88] 3)
- #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 0x3 //[0x90] 4)
- #define IMAGE_DIRECTORY_ENTRY_SECURITY 0x4 //[0x98] 5)
- #define IMAGE_DIRECTORY_ENTRY_BASERELOC 0x5 //[0xA0] 6)
- #define IMAGE_DIRECTORY_ENTRY_DEBUG 0x6 //[0xA8] 7)
- #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 0x7 //[0xB0] 8)
- #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 0x8 //[0xB8] 9)
- #define IMAGE_DIRECTORY_ENTRY_TLS 0x9 //[0xC0] 10)
- #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 0xA //[0xC8] 11)
- #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 0xB //[0xD0] 12)
- #define IMAGE_DIRECTORY_ENTRY_IAT 0xC //[0xD8] 13)
- #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 0xD //[0xE0] 14)
- #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 0xE //[0xE8] 15)
复制代码
本篇主要讲解的是第一个
导出目录表一般出现在 Dll 文件中,由于前面采用的示例未有导出目录表,为了做演示,便随便拿系统中某个 Dll (kernel32.dll) 来做解析。
导出目录表是 PE 文件为其它程序提供 API 的一种函数示例导出方式,除此之外,还可导出自身一些变量以及类,供第三方程序使用,导出目录表结构
- typedef struct _IMAGE_EXPORT_DIRECTORY {
- DWORD Characteristics; // 1) 保留,恒为0x00000000
- DWORD TimeDateStamp; // 2) 时间戳
- WORD MajorVersion; // 3) 主版本号,一般不赋值
- WORD MinorVersion; // 4) 子版本号,一般不赋值
- DWORD Name; // 5) 模块名称
- DWORD Base; // 6) 索引基数
- DWORD NumberOfFunctions; // 7) 导出地址表中的成员个数
- DWORD NumberOfNames; // 8) 导出名称表中的成员个数
- DWORD AddressOfFunctions; // 9) 导出地址表(EAT)
- DWORD AddressOfNames; // 10) 导出名称表(ENT)
- DWORD AddressOfNameOrdinals; // 11) 指向导出序列号数组
- } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
复制代码
Characteristics 字段恒为 0,保留;
TimeDateStamp 字段为导出表创建的时间 (GMT);
Name 字段为存储模块名 (ASCII 字符) 的 RVA;
Base 字段为导出 API 函数索引值的基数,函数索引值 = 导出函数索引值 - 基数,一般情况下为 0;
NumberOfFunctions 字段为导出地址表 (EAT) 中成员数量;
NumberOfNames 字段为导出名称表 (ENT) 中成员数量,ENT 的数量 <= EAT 的数量;
AddressOfFunctions 字段为导出地址表;
AddressOfNames 字段为导出名称表;
AddressOfNameOrdinals 字段为导出序列号数组;
有三张表非常重要,分别为导出地址表 EAT、导出名称表 ENT、导出序号表 EOT,三者的关系:
函数导出有两种方式,一种仅以序号导出,另一种是同时以序号和名称导出。需要注意的是,EAT 的数量会大于等于 ENT 的数量。
- LIBRARY "Dll1"
- EXPORTS
- fun1 @1 NONAME
- fun2 @2 NONAME
- fun3 @3 NONAME
- LIBRARY "Dll1"
- EXPORTS
- fun1 @1
- fun2 @2
- fun3 @3 NONAME
复制代码
kernel32.dll 中导出目录表的 RVA 为 0x92CA0,大小为 0xDC60,位于 .rdata 区段中,转换成文件偏移为 0x78CA0
在 010Editor 里也验证了这个结果
代码实现:
- // 12.解析导出目录表
- DWORD dwExportTableRVA = OptionalHeader.
- DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
- PIMAGE_EXPORT_DIRECTORY pExportTableVA = (PIMAGE_EXPORT_DIRECTORY)(dwFileAddr +
- Rva2Foa(pNtHeader, dwExportTableRVA));
- // 12.1 解析模块的名称
- std::cout << "\nName: " << (char*)(dwFileAddr +
- Rva2Foa(pNtHeader, pExportTableVA->Name)) << std::endl;
- // 12.2 分别获取序号表、名称表、地址表的 VA
- WORD* pwOrdTable = (WORD*)(dwFileAddr +
- Rva2Foa(pNtHeader, pExportTableVA->AddressOfNameOrdinals));
- DWORD* pdwNamesTable = (DWORD*)(dwFileAddr +
- Rva2Foa(pNtHeader, pExportTableVA->AddressOfNames));
- DWORD* pdwFunctionsTable = (DWORD*)(dwFileAddr +
- Rva2Foa(pNtHeader, pExportTableVA->AddressOfFunctions));
- std::cout << "Ordinal\tAddress\t\tNames" << std::endl;
- // 函数地址表中的个数就是函数的个数
- for (DWORD i = 0; i < pExportTableVA->NumberOfFunctions; ++i)
- {
- // 判断当前函数地址是否有效
- if (NULL == pwOrdTable[i])
- {
- continue;
- }
- BOOL bHaveName = FALSE;
- // 查看当前函数有没有名称
- for (DWORD j = 0; j < pExportTableVA->NumberOfNames; ++j)
- {
- // 如果序号表中保存了当前地址的下标,说明是名称导出
- // i 表示当前地址下标,j 是名称表和序号表的下标
- if (i == pwOrdTable[j])
- {
- bHaveName = TRUE;
- std::cout << "0x" << std::setfill('0') << std::setw(4) << i + pExportTableVA->Base
- << "\t 0x" << std::setw(8) << pdwFunctionsTable[i] << "\t"
- << (char*)(dwFileAddr + Rva2Foa(pNtHeader, pdwNamesTable[j])) << std::endl;
- }
- }
- // 这是一个序号导出的函数
- if (!bHaveName)
- {
- std::cout << "0x" << std::setfill('0') << std::setw(4) << i + pExportTableVA->Base
- << "\t 0x" << std::setw(8) << pdwFunctionsTable[i] << "\t"
- << "[None]" << std::endl;
- }
- }
复制代码
运行结果:
GetProcAddress 函数是个典型例子,向它提供模块名的句柄和函数名就能获取到函数地址,自己可以去实现一个这样一个函数。
测试 (GetProcAddress(GetModuleHandle("kernel32.dll"), "AllocConsole");):
- // AllocConsole 0x23b00
- for (DWORD i = 0; i < pExportTableVA->NumberOfNames; ++i)
- {
- char* functionName = (char*)(dwFileAddr + Rva2Foa(pNtHeader, pdwNamesTable[i]));
- if (strcmp(functionName, "AllocConsole") == 0)
- {
- std::cout << "AllocConsole Address in kernel32.dll: " << kernel32.dll 载入基地址 + pdwFunctionsTable[pwOrdTable[i]] << std::endl;
- }
- }
复制代码
最后以一张图理清楚其中的关系:
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|