
前置知识
导入表
当我们的可执行程序需要调用其他 DLL 文件中的函数时,就需要用到导入表。例如,程序需要用到 CreateProcess 函数,那么它就必须依赖 kernel32.dll 文件,并将其中 CreateProcess 函数的信息“导入”到自己的地盘里,之后才能顺利调用。
为了统一管理这些“外来”函数,系统引入了导入表这一结构。简单来说,程序里每调用一个导入函数,系统就会去导入表里查找该函数的“登记信息”,获取其地址,然后完成调用。

因此,程序加载器在调用任何导入函数之前,首要任务就是找到导入表的位置。当可执行文件被映射到内存时,一切都是从 Dos Header 开始的。这个头部里有一个关键的 e_lfanew 字段,它指向 PE 文件头的偏移量。在 PE 文件头中,有一个可选头结构,里面存放着数据目录项,而导入表的信息就保存在这里。所以,在内存中找到导入表的完整路径是:Dos Header -> Nt Header -> Option Header -> Import Table。

借助《加密与解密》一书中的经典图示,我们可以更直观地了解导入表的结构。

图中涉及的变量很多,我们先聚焦三个核心成员:OriginalFirstThunk、FirstThunk 以及 Name。
Name:指向所导入的DLL库的名称字符串。
OriginalFirstThunk:指向输入名称表(INT),里面存储着导入函数的信息(比如函数名或序号)。
FirstThunk:指向输入地址表(IAT)。初始化时,OriginalFirstThunk 和 FirstThunk 指向的是同一块区域(都是函数信息)。
输入名称表(INT)的具体结构可以参考下图,我们主要关注 Ordinal 和 AddressOfData 两个字段。
Ordinal:以序号的形式标识导入函数。
AddressOfData:以函数名的形式标识导入函数。

那么,INT 和 IAT 的本质区别是什么呢?关键在于,程序加载器会从 INT 中读取函数名(或序号),然后在内存中定位该函数在对应DLL中的实际地址,并将这个真实地址填写到 IAT 中。所以,加载完成后,IAT 里存放的就是可以直接跳转的函数地址了。下图清晰地展示了这个过程:

导入地址表钩取技术
所谓导入地址表钩取技术(IAT Hook),原理非常直接:通过修改输入地址表(IAT)中某个函数的地址值,当程序再次尝试调用该函数时,就会“误入歧途”,跳转到我们预设的自定义地址上。
钩取之前,程序的调用流程是正常的:

成功实施钩取之后,调用流程就被我们“劫持”了:

总结一下 IAT Hook 的核心步骤:
- 确定目标:明确需要钩取的导入函数。
- 定位战场:获取目标进程的输入地址表(IAT)的地址。
- 实施替换:在IAT中搜索目标函数的地址,并将其替换为我们自定义函数的地址。
- 完成调用:在我们的自定义函数中处理逻辑后,必须重新调用原始的被钩取函数,以确保程序功能完整。
确定需要钩取的导入函数
首先,我们需要侦察目标程序都导入了哪些函数。使用 PE 分析工具(如 PE-bear, CFF Explorer 等)可以清楚地看到,目标程序导入了 kernel32.dll,并且其中包含了 CreateProcessW 函数。

那么,我们就以钩取 CreateProcessW 函数为例进行实战。
获取导入地址表的地址
按照之前讲解的路径 DOS Header -> Nt Header -> Option Header -> Import Table 进行解析,即可定位到 IAT。这本质上是对 Windows PE文件结构的一次遍历。
关键代码如下:
...
//获取当前进程的基地址
hMod = GetModuleHandle(NULL);
pBase = (PBYTE)hMod;
//进程的基地址是从DOS头开始的
pImageDosHeader = (PIMAGE_DOS_HEADER)hMod;
//通过e_lfanew变量获取NT头的偏移,然后加上基地址及NT头的位置
pImageNtHeaders = (PIMAGE_NT_HEADERS)(pBase + pImageDosHeader->e_lfanew);
//数据目录项下标为1的项是导入表
pImageImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pImageNtHeaders->OptionalHeader.DataDirectory[1].VirtualAddress + pBase)
...
获取导入函数地址并修改
找到导入表的地址后,我们需要遍历其中所有的导入描述符。首先提取 Name 字段,判断是否为我们要钩取的DLL(例如 kernel32.dll)。匹配成功后,继续遍历该描述符对应的 IAT 数组,寻找目标函数(如 CreateProcessW)的地址。找到后,将其替换为我们自定义函数的地址。

代码如下,这里涉及了关键的指针操作和内存权限修改:
...
//遍历导入表项
for (; pImageImportDescriptor->Name; pImageImportDescriptor++)
{
//获取导入库的名称
szLibName = (LPCSTR)(pImageImportDescriptor->Name + pBase);
//比较导入库的名称,判断是否为kernel32.dll
if (!_stricmp(szLibName, szDllName))
{
//获取IAT
PIMAGE_THUNK_DATA pImageThunkData = (PIMAGE_THUNK_DATA)(pImageImportDescriptor->FirstThunk + pBase);
//获取导入函数地址
for (; pImageThunkData->u1.Function; pImageThunkData++)
{
//判断函数地址是否是需要钩取的函数地址,这里需要注意的是64位与32位地址的区别
if (pImageThunkData->u1.Function == (ULONGLONG)pfnOrg)
{
//修改IAT的权限为可写
VirtualProtect(&pImageThunkData->u1.Function, 4, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//将原始的地址修改为自定义函数地址
pImageThunkData->u1.Function = (ULONGLONG)pfnNew;
//将权限恢复
VirtualProtect(&pImageThunkData->u1.Function, 4, dwOldProtect, &dwOldProtect);
return TRUE;
}
}
}
...
在自定义函数中重新调用被钩取的函数
这一步至关重要。我们需要定义一个与原函数返回值类型、调用约定和参数列表完全一致的自定义函数。这样,我们就能接收到原始调用传来的所有参数,可以任意检查、修改它们,然后再将(可能被篡改的)参数传递给真正的原始函数。

示例代码如下,这里我们篡改了 CreateProcessW 的第一个参数(应用程序名),强制其启动计算器:
...
LPCWSTR applicationName = L"C:\\Windows\\System32\\calc.exe";
return ((LPFN_CreateProcessW)g_pOrgFunc)(applicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);
...
完整代码示例可以在 GitHub 查看:https://github.com/h0pe-ay/HookTechnology/blob/main/Hook-IAT/iat.cpp
调试技巧
在开发这类底层钩子时,调试是关键。我最初使用 x64dbg,但后来发现 WinDbg 配合源码调试更加强大。这里记录几个核心的调试步骤和命令。
符号表与源码加载
在 WinDbg 的设置中,可以指定默认的源码目录和符号文件(.pdb)路径。符号文件是由 Visual Studio 编译生成的,包含了调试信息。
注意:默认的符号路径 srv*c:\Symbols*https://msdl.microsoft.com/download/symbols 会从微软服务器下载系统符号,如果只想调试自己的代码,可以删掉它以避免不必要的下载等待。

源码文件也可以在侧边栏通过 Open source file 选项直接打开。
DLL加载调试
IAT Hook 通常需要先将我们的 Hook DLL 注入到目标进程。为了调试注入和钩取过程,我们需要在 DLL 被加载时中断。
- 使用
sxe ld:xxx.dll 命令,可以在加载 xxx.dll 时触发断点。
- 使用
sxe ud:xxx.dll 命令,则在卸载 xxx.dll 时触发断点。

关闭编译器优化
为了方便单步跟踪和查看变量值,建议在编译用于调试的 DLL 时关闭编译器优化。否则,自定义函数中的一些变量可能会被优化掉,导致调试困难。
在 Visual Studio 的项目属性中,找到“C/C++” -> “优化”,将“优化”选项设置为“已禁用(/Od)”即可。

总结与思考
IAT Hook 是一种经典的Windows安全与逆向技术,其原理清晰,实现相对直接。它通过篡改程序内存中“函数地址簿”(IAT)的方式,实现了对特定API调用的监控和拦截。掌握这项技术,不仅能深化对Windows PE文件结构和程序加载机制的理解,也是迈向更高级Hook技术(如Inline Hook、SSDT Hook等)的重要一步。
当然,现代的安全软件(如反病毒、EDR)对这类内存篡改行为非常敏感。在实际应用中,还需要考虑如何绕过内存保护、如何隐蔽自身等技术细节,这又是另一个广阔的领域了。
参考链接
对底层安全和Hook技术感兴趣的开发者,欢迎在云栈社区交流讨论,共同探索操作系统的奥秘。