IAT:Import Address Table 导入地址表

间接寻址调用

  • 调用 DLL 函数时,汇编代码通过 间接寻址 实现,不直接 call 函数地址,而是通过一个中间地址跳转。

  • 示例:调用 MessageBox系统函数(属于 DLL)时,汇编代码为:

    1
    call dword ptr [004322d4]
    • 004322d4 中存储地址 X,程序通过跳转到 X 执行函数。
    • 004322d4 属于 .exe 的内存区域,而 X 可以指向 DLL 的内存区域(如 77d5050b)。

为什么需要间接寻址?

  • DLL 在加载时可能因重定位占用不同的内存地址。
  • 因此,调用 DLL 函数不能写死地址,而需要动态更新。
  • 程序先将 .exe 中某个位置(如 004322d4)写入代码,然后由操作系统在运行时填充实际的函数地址。

文件状态与加载状态的区别

  1. 未运行(文件状态)
    • 004322d4 指向的地址仅存储一个字符串,例如 MessageBox.USER32.dll
    • 需要根据 RVA(相对虚拟地址)转换为 FOA(文件偏移地址),再减去 ImageBase 才能找到位置。
  2. 运行中(内存状态)
    • .exe.dll 加载到内存时,操作系统修复了重定位表。
    • 004322d4 的内容被替换为 77d5050b(DLL 函数的绝对地址)。

IAT 的作用

  • IAT(Import Address Table,导入地址表)存储 .dll 和函数信息的中间地址。
  • 加载完成后,IAT 中的值从字符串变为实际的绝对地址。

总结

  • IAT 是由存放 DLL 函数信息的中间地址组成的表
  • 文件加载前,IAT 存储的内容是函数名的字符串地址。
  • 文件加载后,IAT 被系统动态更新为函数的绝对地址,用于实现动态链接调用。

image-20250102212552119

导入表

导出表和导入表的作用

  • 导出表
    • 提供给外部使用的函数清单,相当于饭店的菜单。
    • 用于告诉其他程序 DLL 中有哪些可用的函数。
  • 导入表
    • 程序使用的 DLL 函数清单,相当于客人点的菜品单。
    • 记录 .exe 所需使用的 DLL 和其中的函数。
  • 数据目录
    • 数据目录的第一项为导出表。
    • 数据目录的第二项为导入表。

导入表结构

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _IMAGE_IMPORT_DESCRIPTOR
{
union
{
DWORD Characteristics;
DWORD OriginalFirstThunk;
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // 指向 DLL 名的 RVA
DWORD FirstThunk; // 指向 IAT 表
} IMAGE_IMPORT_DESCRIPTOR;

定位导入表

  • VirtualAddress 指向多个导入表结构,每个结构对应一个 DLL。
  • 导入表以 sizeOf(IMAGE_IMPORT_DESCRIPTOR) 个全0结构结尾,表示结束。
  • Name 指向 DLL 名称的 RVA。
  • OriginalFirstThunk 指向 INT 表(Import Name Table)。
  • FirstThunk 指向 IAT 表(Import Address Table)。

INT 表和 IAT 表

  • INT 表:在文件加载前存储函数信息(如 RVA 指向的函数名)。
  • IAT 表:文件加载完成后,存储 DLL 函数的实际内存地址。
  • 虽然内容在加载前一致,但它们位于不同的内存区域。

IMAGE_IMPORT_DESCRIPTOR 的解析

  1. OriginalFirstThunk
    指向 INT 表(存储 IMAGE_THUNK_DATA 结构数组)。
  2. FirstThunk
    指向 IAT 表(存储 IMAGE_THUNK_DATA 结构数组)。找到这张表有两种方式,一种就是通过导入表这里找到,第二种就是通过数据目录表,倒数第三个项就是指向的 IAT 表。
  3. INT 表和 IAT 表的初始化
    • 加载前,两个表内容相同,均指向函数名或序号。
    • 加载后,系统调用 GetProcAddress 更新 IAT 表为函数的绝对地址。

IMAGE_THUNK_DATA 结构

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_THUNK_DATA32
{
union
{
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal; // 函数序号
PIMAGE_IMPORT_BY_NAME AddressOfData;// 指向 IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
  • 加载前

    • 存储一个 RVA,指向 IMAGE_IMPORT_BY_NAME
    • 如果是序号调用,则存储序号。

    image-20250102213937333

  • 加载后

    • 更新为函数的绝对地址。

    image-20250102214033756

IMAGE_IMPORT_BY_NAME 结构

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {						
WORD Hint; // 函数在导出表中的索引,可忽略
BYTE Name[1]; // 函数名称,以 '\0' 结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
  • Hint:导出表中函数地址表的索引,可为 0。
  • Name:变长数组,用于存储以 '\0' 结尾的函数名。

变长数组的使用
结构中定义为 BYTE Name[1],但在内存中动态分配实际长度以存储函数名,以优化内存使用。在这个结构中,BYTE Name[1] 使用的技巧是所谓的 “结构体变长数组”,这种技巧通常用于存储可变长度的数据。尽管声明为 BYTE Name[1],实际上这个字段可以容纳一个以 null 终止的字符串,而字符串的长度可以是任意的。

这是因为在PE文件的导入表中,函数名称并不是固定长度的,每个函数名称的长度都不同。因此,为了有效地存储这些不定长度的字符串,使用了变长数组的技巧。结构体只定义了一个字节的数组,但在实际使用时,根据函数名称的长度动态分配所需的内存,然后将函数名称存储在这个内存块中,以 null 终止字符串。

这种方法允许节省内存,因为不需要为每个结构分配固定大小的缓冲区以容纳字符串,而可以根据实际需要进行动态分配。在C/C++中,这种技巧在很多情况下用于处理可变长度的数据,例如字符串数组,以提高内存使用效率。在实际使用时,程序员通常会动态分配足够的内存以存储实际的字符串,然后将字符串内容复制到该内存中,以确保字符串正确存储和 null 终止。

导入表的加载过程

  1. 系统加载文件,将 .exe 和 DLL 放入内存。
  2. 修复重定位表,将所有相关地址调整到内存实际位置。
  3. 遍历 INT 表,根据函数名或序号调用 GetProcAddress,获取函数的实际地址并填入 IAT 表。

定位导入表的具体步骤

  1. 第一层循环
    遍历所有导入表(IMAGE_IMPORT_DESCRIPTOR),以全零结构为结束标志。
  2. 第二层循环
    遍历 OriginalFirstThunk 指向的 INT 表:
    • INT 表中的每个表项为 IMAGE_THUNK_DATA32 结构。
    • 表项值为 0 时,表示结束。
    • 表项内容可能是函数序号或函数名偏移(需进一步解析)。

image-20250102214318590

image-20250102214455509

Work:打印导入表

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include"PE.h"
bool StructIsNull(
IN LPVOID obj,
IN size_t size
) {
for (DWORD i = 0; i < size; i++)
{
if (*((PCHAR)obj + i) != 0)
{
return false;
}
}
return true;
}

void PrintImportTable(
IN LPVOID pe
) {
PIMAGE_DOS_HEADER pDosHeader = nullptr;
PIMAGE_FILE_HEADER pFileHeader = nullptr;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = nullptr;
PIMAGE_SECTION_HEADER* pSectionHeaderArr = nullptr;
AnlyzePE(pe, pDosHeader, pFileHeader, pOptionalHeader, pSectionHeaderArr);

PIMAGE_IMPORT_DESCRIPTOR importTable = (PIMAGE_IMPORT_DESCRIPTOR)((PCHAR)pe + RvaToFoa(pe, pOptionalHeader->DataDirectory[1].VirtualAddress));
while (!StructIsNull(importTable, sizeof(*importTable)))
{
const char* dllName = (PCHAR)pe + RvaToFoa(pe, importTable->Name);
printf(">>>>>>>>>> %s <<<<<<<<<<\n", dllName);
PDWORD thunk = (PDWORD)((PCHAR)pe + RvaToFoa(pe, importTable->OriginalFirstThunk));
while (*thunk)
{
if (*thunk >> 31 == 1)
{
printf("序号导出 = %d\n", *thunk & 0x7FFFFFFF);
}
else
{
PIMAGE_IMPORT_BY_NAME ibn = (PIMAGE_IMPORT_BY_NAME)((PCHAR)pe + RvaToFoa(pe, *thunk));
printf("名称导出 = %s\n", ibn->Name);
}
thunk++;
}
importTable = (PIMAGE_IMPORT_DESCRIPTOR)((PCHAR)importTable + 0x14);
}
free(pSectionHeaderArr);
}
int main()
{
//C:\Windows\System32\notepad.exe
LPVOID pe = ReadPE(R"(D:\source\repos\dllmain\Debug\MyDll.dll)");
if (pe)
{
PrintImportTable(pe);
free(pe);
}
return 0;
}

运行结果

image-20250102220242999