zer0daysec 发表于 2024-3-25 19:22:16

[原创] PE 文件结构剖析之导出目录表

本帖最后由 zer0daysec 于 2024-3-26 08:43 编辑

这一次我们将讨论导出目录表,这部分在 PE 结构中是非常重要的,回忆一下,在 NT 头中的可选头最后一个字段便是描述目录表总体信息,一般为 16 个成员,但最后一个成员全为 0,那么实际 "有效" 的只有 15 个有效成员
有专门宏定义来定义它们:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0x0   // 1)
#define IMAGE_DIRECTORY_ENTRY_IMPORT          0x1   // 2)
#define IMAGE_DIRECTORY_ENTRY_RESOURCE      0x2   // 3)
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       0x3   // 4)
#define IMAGE_DIRECTORY_ENTRY_SECURITY      0x4   // 5)
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       0x5   // 6)
#define IMAGE_DIRECTORY_ENTRY_DEBUG         0x6   // 7)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    0x7   // 8)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       0x8   // 9)
#define IMAGE_DIRECTORY_ENTRY_TLS             0x9   // 10)
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG   0xA   // 11)
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT    0xB   // 12)
#define IMAGE_DIRECTORY_ENTRY_IAT             0xC   // 13)
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT    0xD   // 14)
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR0xE   // 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.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)
        {
                continue;
        }

        BOOL bHaveName = FALSE;
        // 查看当前函数有没有名称
        for (DWORD j = 0; j < pExportTableVA->NumberOfNames; ++j)
        {
                // 如果序号表中保存了当前地址的下标,说明是名称导出
                // i 表示当前地址下标,j 是名称表和序号表的下标
                if (i == pwOrdTable)
                {
                        bHaveName = TRUE;
                        std::cout << "0x" << std::setfill('0') << std::setw(4) << i + pExportTableVA->Base
                                << "\t 0x" << std::setw(8) << pdwFunctionsTable << "\t"
                                << (char*)(dwFileAddr + Rva2Foa(pNtHeader, pdwNamesTable)) << std::endl;
                }
        }
        // 这是一个序号导出的函数
        if (!bHaveName)
        {
                std::cout << "0x" << std::setfill('0') << std::setw(4) << i + pExportTableVA->Base
                        << "\t 0x" << std::setw(8) << pdwFunctionsTable << "\t"
                        << "" << 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));
        if (strcmp(functionName, "AllocConsole") == 0)
        {
                std::cout << "AllocConsole Address in kernel32.dll: " << kernel32.dll 载入基地址 + pdwFunctionsTable] << std::endl;
        }
}
最后以一张图理清楚其中的关系:



页: [1]
查看完整版本: [原创] PE 文件结构剖析之导出目录表