zer0daysec 发表于 2024-4-2 10:40:23

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

本帖最后由 zer0daysec 于 2024-4-2 13:44 编辑

本篇文章讨论导入目录表
导入目录表为 PE 文件从其它第三库中导入 API,供本程序使用,根据导入目录表能大概猜测出程序的大致行为,导入目录表在 PE 文件结构中也是相当重要,应用程序调用了系统中的某些函数,那么这些信息就会体现在导入目录表中。

导入目录表结构体:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
      DWORD   Characteristics;
      DWORD   OriginalFirstThunk;// 1) 指向导入名称表 (INT) 的 RVA
    };
    DWORD   TimeDateStamp;         // 2) 时间标识
    DWORD   ForwarderChain;          // 3) 转发链,如果不转发则此值为 0
    DWORD   Name;                  // 4) 指向导入映像文件的名字
    DWORD   FirstThunk;            // 5) 指向导入地址表 (IAT) 的 RVA
} IMAGE_IMPORT_DESCRIPTOR;
这个结构起到引导作用,引导系统找到真正保存有导入信息的其它两个结构,这两个结构为 IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME,有多少个导入映像,就有多少个 IMAGE_IMPORT_DESCRIPTOR 结构,最后以一个空的 IMAGE_IMPORT_DESCRIPTOR 结构结束。

OriginalFirstThunk 字段指向 INT 的 RVA,INT 是一个 IMAGE_THUNK_DATA 结构数组,数组中的每个 IMAGE_THUNK_DATA 结构会再指向 IMAGE_IMPORT_BY_NAME 结构,结尾为一个全 0 的空 IMAGE_THUNK_DATA 结构结束;

ForwarderChain 字段为导入表转发器 forwarders 的索引值,一个映像文件可以输出一个没有在本文件内定义的符号,并且这个符号可以是从另一个映像文件引入的,这样的符号称为转发符号,当此值为 -1 时,说明此文件转发已结束,当为 0 时,证明此映像文件未启用此机制。

Name 字段为导入映像名称;

FirstThunk 字段为导入地址表 IAT 的 RVA;

这个结构体只需要关注两个字段,分别为第一个和最后一个,第一个保存的是导入名称表 (INT) 的 RVA,第二个保存的是导入地址表 (IAT) 的 RVA,这两个都指向为 IMAGE_THUNK_DATA 结构数组,IMAGE_THUNK_DATA 结构如下:

typedef struct _IMAGE_THUNK_DATA32 {
    union {
      PBYTEForwarderString;               // 1) 转发字符串的 RVA
      PDWORD Function;                        // 2) 被导入函数的地址
      DWORD Ordinal;                        // 3) 被导入函数的序号
      PIMAGE_IMPORT_BY_NAMEAddressOfData;   // 4) 导入名称表 RVA
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
此结构里只有 u1 成员,且为联合体结构,ForwarderString 字段负责与导入表转发器 forwarders 协同工作,当导入表的 ForwarderChain 不为 0 时,此值有效,并指向包含有转发函数与导出这个函数的映像文件名的字符串 RVA。

Function 字段为导入表导入函数的实际内存地址,此字段仅在映像被加载,且此结构为 IAT 的前提下有效。

Ordinal 字段为导入表导入函数的导出序号,当 IMAGE_THUNK_DATA 的最高位为 1 时,此值有效。

AddressOfData 字段为指向 IMAGE_IMPORT_BY_NAME 结构,当以上 3 个值都未生效时,此值有效。

微软规定当 IMAGE_THUNK_DATA 最高位为 1 时,就采用序号导入方式,而且此时这个值的低 31 位将被看作是一个函数序号。

在导出目录表一节当中知道函数导出的方式有两种,一种为仅以序号导出,另一个是以名称一块导出,同样,当以仅序号导入时,IMAGE_THUNK_DATA 最高位为 1,当以名称导入时,是不是最高位也为 1?一个 32 位的数最高为 1,这个数最小为 0x80000000,在 windows 内存中,0x80000000 以上的空间被称为系统空间,仅系统可用,因此程序中的有效访问地址总是小于 0x80000000,这也就有效解决了序号导入方式与名称导入方式可能会发生碰撞问题,这设计的妙啊。

IMAGE_SNAP_BY_ORDINAL 这个宏可以判断此项是否为序号,参数为 Ordinal:

#ifdef _WIN64
#define IMAGE_ORDINAL_FLAG            IMAGE_ORDINAL_FLAG64
#define IMAGE_ORDINAL(Ordinal)          IMAGE_ORDINAL64(Ordinal)
typedef IMAGE_THUNK_DATA64            IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA64             PIMAGE_THUNK_DATA;
#define IMAGE_SNAP_BY_ORDINAL(Ordinal)IMAGE_SNAP_BY_ORDINAL64(Ordinal)
typedef IMAGE_TLS_DIRECTORY64         IMAGE_TLS_DIRECTORY;
typedef PIMAGE_TLS_DIRECTORY64          PIMAGE_TLS_DIRECTORY;
#else
#define IMAGE_ORDINAL_FLAG            IMAGE_ORDINAL_FLAG32
#define IMAGE_ORDINAL(Ordinal)          IMAGE_ORDINAL32(Ordinal)
typedef IMAGE_THUNK_DATA32            IMAGE_THUNK_DATA;
typedef PIMAGE_THUNK_DATA32             PIMAGE_THUNK_DATA;
#define IMAGE_SNAP_BY_ORDINAL(Ordinal)IMAGE_SNAP_BY_ORDINAL32(Ordinal)
typedef IMAGE_TLS_DIRECTORY32         IMAGE_TLS_DIRECTORY;
typedef PIMAGE_TLS_DIRECTORY32          PIMAGE_TLS_DIRECTORY;
#endif

#define IMAGE_ORDINAL_FLAG64 0x8000000000000000
#define IMAGE_ORDINAL_FLAG32 0x80000000
#define IMAGE_SNAP_BY_ORDINAL64(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG64) != 0)
#define IMAGE_SNAP_BY_ORDINAL32(Ordinal) ((Ordinal & IMAGE_ORDINAL_FLAG32) != 0)
在这里再说下 Function 字段,在 PE 文件未被系统加载之前,INT 和 IAT 都是使用 AddressOfData 字段指向 IMAGE_IMPORT_BY_NAME 结构,当加载后,操作系统先会遍历 INT 中的内容,并逐一取出已导入函数的内存地址,然后将这些动态获取的地址逐一填入到 IAT 中,此时操作系统使用的是 Function 字段,IMAGE_IMPORT_BY_NAME 结构:

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;    // 1) 需导入的函数序号
    BYTE    Name; // 2) 需导入的函数名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint 字段保存的是序号,Name 字段保存的是一个不定长的字符串,以下图为描述整个导入目录表结构的情况:



代码实现:

// 13.解析导入表目录
std::cout << "\nImport Table" << std::endl;

DWORD dwImportTableRVA = OptionalHeader.
        DataDirectory.VirtualAddress;

PIMAGE_IMPORT_DESCRIPTOR pImportTableVA = (PIMAGE_IMPORT_DESCRIPTOR)(dwFileAddr +
        Rva2Foa(pNtHeader, dwImportTableRVA));

// 13.1 开始遍历所有导入的 Dll
while (pImportTableVA->Name != NULL)
{
        // 13.2 解析被导入的模块名称
        std::cout << "\nName: " << (char*)(dwFileAddr +
                Rva2Foa(pNtHeader, pImportTableVA->Name)) << std::endl;
       
        // INT: 不管是在内存中还是在文件中,通常保存的是函数的名称
        // IAT: 在文件中通常保存的是函数的名称,在内存中会被修复成函数的地址
        PIMAGE_THUNK_DATA pINTable = (PIMAGE_THUNK_DATA)(dwFileAddr +
                Rva2Foa(pNtHeader, pImportTableVA->OriginalFirstThunk));
       
        std::cout << "Ordials\tName" << std::endl;
       
        // INT 表以一个全 0 的字段结尾
        while (pINTable->u1.Function)
        {
                // 函数有两种导入方式,以序号方式导入和以名称方式导入
                // 需要判断函数是否有名字
                if (IMAGE_SNAP_BY_ORDINAL(pINTable->u1.Ordinal))
                {
                        // 当最高位为 1 时,说明这是一个以序号导出的函数
                        // 这个字段的低 2 个字节保存的是当前函数的序号
                        std::cout << "0x" << std::setfill('0') << std::setw(4)
                                << (pINTable->u1.Ordinal & 0xFFFF) << "\t" << "" << std::endl;
                }
                else
                {
                        // 当这个函数是以名称方式导入时,当前字段保存的是一个 RVA
                        // 指向 IMAGE_IMPORT_BY_NAME 结构体
                        // 第一个成员保存的是导入函数的序号
                        // 第二个成员保存的是一个不定长的字符串
                        PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(dwFileAddr +
                                Rva2Foa(pNtHeader, pINTable->u1.AddressOfData));
                        std::cout << "0x" << std::setfill('0') << std::setw(4) << pName->Hint
                                << "\t" << pName->Name << std::endl;
                }
                // 遍历下一个函数
                pINTable++;
        }
        // 遍历下一个模块
        pImportTableVA++;
}
运行结果:


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