本节为轩辕的编程宇宙公众号《从零开始学逆向》的“IAT Hook”小节的学习笔记。详细课程内容请自行 跳转 加入查看。
在之前的 Maldev 开发课程中,我们已经深入学习过对 PE 文件的结构解析以及导入表(Import Table)的相关机制。这里的 IAT Hook 本质上是一种基于“间接地址寻址”原理的执行流劫持技术。
在 PE 文件被加载至内存并完成重定位后,程序对所有外部 API 的调用(如 MessageBoxW)都会通过 IAT(Import Address Table)进行间接跳转。
IAT Hook 的核心逻辑在于:通过同步遍历 INT(Import Name Table) 与 IAT(Import Address Table),精准定位目标函数在内存中的地址槽位(Slot);随后利用 VirtualProtect 暂时突破内存页的只读保护,将槽位中原始的 API 入口地址篡改为自定义的劫持函数地址。
为了文章的易读性,我把之前写过的”PE文件头解析“文章的 INT Table 及 IAT Table 部分搬了过来。如果还是不理解的话,建议跳转详细学习”PE文件头解析“。
本文代码在 VS 2026 - x86 Debug 模式下编译测试完成。
在解析 PE 文件的逻辑流程中,每一个被引入的 DLL(如 kernel32.dll、user32.dll)都必须拥有自己独立的 INT 和 IAT。
如果引入了 5 个 DLL,那么在逻辑解析时,就会看到 5 个不同的 INT 表和 5 个不同的 IAT 表。

IMAGE_IMPORT_DESCRIPTOR 结构在 Winnt.h 头文件 中定义如下,但微软并未正式记录该结构:
1
2
3
4
5
6
7
8
9
10
|
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // 桥1 => 指向 INT 表
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // Dll 名称
DWORD FirstThunk; // 桥2 => 指向 IAT 表
} IMAGE_IMPORT_DESCRIPTOR;
|
IMAGE_IMPORT_DESCRIPTOR 结构体是 PE 文件导入表的核心入口。每一个被程序引用的 DLL 都会对应一个 IMAGE_IMPORT_DESCRIPTOR 结构。
- OriginalFirstThunk (桥1):指向 INT (Import Name Table) 的 RVA(相对虚拟地址)。它是查找函数名的依据。
- Name:指向一个以
NULL 结尾的字符串,包含该 DLL 的名称(如 kernel32.dll)。
- FirstThunk (桥2):指向 IAT (Import Address Table) 的 RVA。它是程序运行时实际跳转执行的地址所在地。
INT 是一个结构体数组,其对应的底层定义是 IMAGE_THUNK_DATA。它是一份只读的“索引清单”。
1
2
3
4
5
6
7
8
|
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // 转发字符串的 RVA
DWORD Function; // 在 INT 中,此字段指向名字结构 (AddressOfData)
DWORD Ordinal; // 被导入函数的序号
DWORD AddressOfData; // 被导入函数的名称RVA,指向 IMAGE_IMPORT_BY_NAME 结构
} u1;
} IMAGE_THUNK_DATA32;
|
1
2
3
4
|
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; // 被导入函数的序号
CHAR Name[1]; // 被导入函数的函数名称字符串(如 "ExitProcess")
} IMAGE_IMPORT_BY_NAME;
|
IAT 在代码层面的定义与 INT 完全一样(也是 IMAGE_THUNK_DATA 数组),但其字段在不同阶段代表不同的含义。
1
2
3
4
5
6
7
8
9
|
// 结构体与 INT 相同,但在内存中加载后,Function 字段意义改变
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // 转发字符串的 RVA
DWORD Function; // 重点:加载后,这里存储的是函数的【真实内存地址】
DWORD Ordinal; // 被导入函数的序号
DWORD AddressOfData; // 被导入函数的名称 RVA,指向 IMAGE_IMPORT_BY_NAME 结构
} u1;
} IMAGE_THUNK_DATA32;
|
-
桥2的终点:FirstThunk 指向这个数组的开头。
-
文件状态(静止):其内容通常和 INT 一样,也指向 IMAGE_IMPORT_BY_NAME。
-
内存状态(运行):Windows 加载器会根据 INT 找到函数名,查询地址后,直接覆写这个 u1.Function 字段。此时它不再是 RVA 偏移,而是一个硬编码的 VA(虚拟地址),如 0x77E11234。
静态阶段(磁盘上的 PE 文件)。程序还没运行、存储在硬盘中时。
IMAGE_IMPORT_DESCRIPTOR 里的 OriginalFirstThunk(桥1)指向 INT。
IMAGE_IMPORT_DESCRIPTOR 里的 FirstThunk(桥2)指向 IAT。
- 此时,INT 和 IAT 里的内容是完全一样的:它们都存储着一系列 RVA(相对虚拟地址),这些地址都指向同一个目的地——
IMAGE_IMPORT_BY_NAME 结构体(即函数名字符串,如 “ExitProcess”)。
动态阶段(Windows 加载器的工作)。当双击运行程序时,Windows 加载器(Loader)会将 PE 文件映射到内存,并开始“整体联动”。
- 定位 DLL:加载器读取
IMAGE_IMPORT_DESCRIPTOR 的 Name 字段(如 “kernel32.dll”),并在系统中找到该 DLL,将其加载到内存中。
- 寻找函数(通过桥1):加载器查看
OriginalFirstThunk 指向的 INT。它读取里面的 IMAGE_THUNK_DATA,找到函数名称字符串,如 “ExitProcess”。
- 获取真实地址:加载器在已加载的
kernel32.dll 导出表中搜索 “ExitProcess”,计算出它在当前内存中的绝对地址(例如 0x77E11234)。
- 填充结果(通过桥2):加载器将这个绝对地址写回
FirstThunk 指向的 IAT 对应位置。
最终状态。
- 桥1 (OriginalFirstThunk -> INT):依然指向函数名 “ExitProcess”。它就像备份清单,永远记录着最初想要什么。
- 桥2 (FirstThunk -> IAT):现在指向的是真实的函数地址。
Windows 加载器通过以下逻辑确保所有依赖环境就绪。
外层循环:遍历 DLL 列表
- 动作:加载器从
DataDirectory[1] 指向的地址开始,挨个读取 IMAGE_IMPORT_DESCRIPTOR。
- 联动:读取
Name 字段,加载目标 DLL(如 user32.dll)到进程空间。
- 终止:直到遇到一个全 0 的描述符结构体,外层循环结束。
内层循环:遍历函数名与填充地址
-
初始化双指针:
-
查名(INT 侧):加载器读取指针 A 处的 RVA,定位到 IMAGE_IMPORT_BY_NAME 结构,获取函数名字字符串(如 “MessageBoxW”)。
-
解析地址:在 DLL 的导出表中查找该名字对应的真实内存虚拟地址 (VA)。
-
填位(IAT 侧):加载器将得到的 VA 地址直接覆盖掉指针 B 所在的槽位。
-
同步步进:指针 A 和 指针 B 同时向后移动 4 字节(32位)或 8 字节(64位)。
-
全量填充:即便程序的代码只用了一个函数,加载器也会把该 DLL 在导入表中列出的所有函数全部填完,以备不时之需。直到遇到 0x00000000 结束该 DLL 的内层循环。
获取每个 DLL 对应的 INT Table 和 IAT Table:
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
|
// Maldev 50 小节中,使用的是 ReadFile 将文件读入 pBuff。这属于物理文件读取,而不是内存映射加载。
// 需通过如下两行获取当前加载进内存后的 PE 文件地址,才能获取每个 DLL 对应的 INT Table 和 IAT Table
HMODULE hModule = GetModuleHandleA(NULL); // NULL 时表示当前 PE 文件
PBYTE pBase = (PBYTE)hModule; // 也可直接使用 hModule
// 获取 DOS Header
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
// 获取 Nt Header
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
// 获取 Nt Header 下的 Optional Header
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
// 第一步:获取导入表描述符数组的首地址
PIMAGE_IMPORT_DESCRIPTOR pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// 第二步:遍历所有的 DLL (因为每个 DLL 都有自己的 INT 和 IAT)
while (pImgImpDesc->Name != NULL) {
// 获取当前 DLL 的名称
PCHAR pDllName = (PCHAR)(pBase + pImgImpDesc->Name);
printf("[+] DLL Name: %s \n", pDllName);
// 第三步:获取该 DLL 对应的 INT 表 (桥1)
// 物理路径:pBase + OriginalFirstThunk
PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(pBase + pImgImpDesc->OriginalFirstThunk);
// 第四步:获取该 DLL 对应的 IAT 表 (桥2)
// 物理路径:pBase + FirstThunk
PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(pBase + pImgImpDesc->FirstThunk);
printf("\t[#] INT RVA: 0x%0.8X (Address: 0x%p)\n", pImgImpDesc->OriginalFirstThunk, pINT);
printf("\t[#] IAT RVA: 0x%0.8X (Address: 0x%p)\n", pImgImpDesc->FirstThunk, pIAT);
// 指向下一个 DLL 描述符
pImgImpDesc++;
}
|
运行效果:
[+] DLL Name: KERNEL32.dll
[#] INT RVA: 0x0001B250 (Address: 0x008CB250)
[#] IAT RVA: 0x0001B000 (Address: 0x008CB000)
[+] DLL Name: USER32.dll
[#] INT RVA: 0x0001B2E0 (Address: 0x008CB2E0)
[#] IAT RVA: 0x0001B090 (Address: 0x008CB090)
[+] DLL Name: VCRUNTIME140D.dll
[#] INT RVA: 0x0001B310 (Address: 0x008CB310)
[#] IAT RVA: 0x0001B0C0 (Address: 0x008CB0C0)
[+] DLL Name: ucrtbased.dll
[#] INT RVA: 0x0001B360 (Address: 0x008CB360)
[#] IAT RVA: 0x0001B110 (Address: 0x008CB110)
下面的代码为我本人对轩辕大佬实现代码的理解。加入了自己理解的一些注释,重命名了一些变量名、变量类型等操作,来方便自己的理解。下面代码仅供参考,如有标注错误或代码实现错误,均以参考的轩辕大佬实现的代码为正确实现。
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
// IATHook.cpp
#include <Windows.h>
#include <stdio.h>
int WINAPI HookMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType) {
MessageBoxA(hWnd, "IAT Hook Success!\n", "IAT Hook", MB_OK);
return 0;
}
BOOL InstallIATHook(const char *DllName, const char *Hooked_Func_Name, void *HandlerFunction) {
DWORD dwOldProtection = NULL;
// 获取当前加载进内存后的 PE 文件地址 pBase。
HMODULE hModule = GetModuleHandleA(NULL);
PBYTE pBase = (PBYTE)hModule; // 也可直接使用 hModule
// HMODULE 类型实际上是一个指向加载的模块(通常是 DLL 或 EXE)的基地址,GetModuleHandleA 参数 NULL 时,表示当前 PE 文件。
// PBYTE 类型则是一个指向字节的指针,它用于按字节访问内存。
// 获取 DOS Header
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
// 获取 Nt Header
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
// 获取 Nt Header 下的 Optional Header
IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
// 获取 导入表描述符 数组的首地址
PIMAGE_IMPORT_DESCRIPTOR pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
// 外层循环:遍历所有的 DLL (因为每个 DLL 都有自己的 INT 和 IAT)
while (pImgImpDesc->Name != NULL) {
// 获取当前 DLL 的名称(pImgImpDesc->Name 是 RVA)
PCHAR pDllName = (PCHAR)(pBase + pImgImpDesc->Name);
printf("[+] DLL Name: %s \n", pDllName);
// 判断是否与传入的 DLL 名称是否一致(不区分大小写)
if (_stricmp(DllName, pDllName) == 0) {
// 如果一致,获取该 DLL 对应的 INT 表 (桥1)、 IAT 表 (桥2)
PIMAGE_THUNK_DATA pINT = (PIMAGE_THUNK_DATA)(pBase + pImgImpDesc->OriginalFirstThunk);
PIMAGE_THUNK_DATA pIAT = (PIMAGE_THUNK_DATA)(pBase + pImgImpDesc->FirstThunk);
printf("\t[#] INT RVA: 0x%0.8X (Address: 0x%p)\n", pImgImpDesc->OriginalFirstThunk, pINT);
printf("\t[#] IAT RVA: 0x%0.8X (Address: 0x%p)\n", pImgImpDesc->FirstThunk, pIAT);
// 内层循环:遍历该 DLL 的 INT 表中的所有函数名。
while (pINT->u1.Function != NULL) {
// 获取当前 INT 节点指向的函数名结构体 IMAGE_IMPORT_BY_NAME
PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME)(pBase + pINT->u1.Function);
// 判断是否与传入的目标函数名一致(如 "MessageBoxW")(区分大小写)
if (strcmp(Hooked_Func_Name, pImportByName->Name) == 0) {
// 由于 INT (pInt) 和 IAT (pIat) 是并行排列的数组。
// 那么,当我们在 pInt 中找到匹配的名字时,当前的 pIat 指针正好指向存储该函数地址的槽位。
DWORD *TargetFunAddrPtr = (DWORD*)pIAT;
// 修改内存页访问保护属性
VirtualProtect(TargetFunAddrPtr, 4, PAGE_EXECUTE_READWRITE, &dwOldProtection);
// 将 IAT 槽位中原始的 API 函数入口地址覆盖为自定义的劫持函数地址 (HandlerFunction)。
*TargetFunAddrPtr = (DWORD)HandlerFunction;
// 还原内存页访问保护属性
VirtualProtect(TargetFunAddrPtr, 4, dwOldProtection, &dwOldProtection);
// Hook 完成
return TRUE;
}
// 同步步进 INT 和 IAT,保持索引对应关系
pINT++;
pIAT++;
}
}
// 移动到下一个 DLL 描述符
pImgImpDesc++;
}
// 未找到指定的 DLL 或函数
return FALSE;
}
int main(int argc, char *argv[]) {
InstallIATHook("User32.dll", "MessageBoxW", HookMessageBoxW);
MessageBoxW(NULL, L"Test Information.", L"Test", MB_OK);
return 0;
}
|
运行效果:
