36、Windows API Hook(一):IAT Hook原理与实现

API Hook,说白了就是拦截某个函数的调用。你想想看,一个程序跑着跑着,突然调用了 MessageBoxW,我们能不能在它执行之前,先插一手?这就是 Hook 要做的事。

我最早接触 Hook 是在做游戏外挂分析的时候。那时候看到别人能拦截 Send 包,觉得特别神奇。后来自己动手写了一个 IAT Hook,才明白——其实原理没那么玄乎。

什么是 IAT?

IAT,全称 Import Address Table,导入地址表。每个 PE 文件里都有一张表,记录了它从别的 DLL 里导入的函数地址。

举个例子:你的程序调用了 MessageBoxW,这个函数在 user32.dll 里。程序启动时,加载器会把 MessageBoxW 的真实地址填到 IAT 里。之后程序调用它,实际上就是通过 IAT 跳转过去的。

嗯,这里要注意:IAT 是每个模块独立的。也就是说,notepad.exe 的 IAT 和 calc.exe 的 IAT 是两码事。

IAT Hook 的核心思想

IAT Hook 的思路很简单:

  1. 找到目标模块的 IAT
  2. 找到你要 Hook 的那个函数条目
  3. 把 IAT 里原来的函数地址,改成你的函数地址
  4. 你的函数执行完,再决定要不要调用原函数

说白了,就是偷梁换柱。程序以为它调的是 MessageBoxW,实际上跳到了你的 MyMessageBox 里。

关键点: IAT Hook 只能拦截通过 IAT 调用的函数。如果目标函数是通过 GetProcAddress 动态获取地址的,IAT Hook 就管不着了。这个坑我踩过,后面会讲。

动手实现一个 IAT Hook

我们拿 MessageBoxW 开刀。目标:拦截所有对 MessageBoxW 的调用,把弹窗内容改成我们自己的。

先看核心代码:

// 定义原函数类型
typedef int (WINAPI* pMessageBoxW)(
    HWND hWnd,
    LPCWSTR lpText,
    LPCWSTR lpCaption,
    UINT uType
);

// 保存原函数地址
pMessageBoxW OriginalMessageBoxW = NULL;

// 我们的 Hook 函数
int WINAPI HookedMessageBoxW(
    HWND hWnd,
    LPCWSTR lpText,
    LPCWSTR lpCaption,
    UINT uType
) {
    // 把弹窗内容改成我们自己的
    wchar_t* newText = L"你被 Hook 了!";
    wchar_t* newCaption = L"IAT Hook Demo";
    
    // 调用原函数(注意:这里用的是原函数地址)
    return OriginalMessageBoxW(hWnd, newText, newCaption, uType);
}

// 安装 Hook
BOOL InstallHook() {
    // 1. 获取当前模块的基址
    HMODULE hModule = GetModuleHandle(NULL);
    
    // 2. 解析 PE 头,找到 IAT
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(
        (BYTE*)hModule + pDosHeader->e_lfanew
    );
    
    // 3. 找到导入表
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(
        (BYTE*)hModule + 
        pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress
    );
    
    // 4. 遍历导入表,找到 user32.dll
    while (pImportDesc->Name) {
        char* dllName = (char*)((BYTE*)hModule + pImportDesc->Name);
        
        if (_stricmp(dllName, "user32.dll") == 0) {
            // 5. 遍历 IAT,找到 MessageBoxW
            PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(
                (BYTE*)hModule + pImportDesc->FirstThunk
            );
            
            while (pThunk->u1.Function) {
                // 检查是否是我们想要的函数
                if (pThunk->u1.Function == (ULONG_PTR)GetProcAddress(
                    GetModuleHandle(L"user32.dll"), "MessageBoxW")) {
                    
                    // 6. 保存原函数地址
                    OriginalMessageBoxW = (pMessageBoxW)pThunk->u1.Function;
                    
                    // 7. 修改内存保护属性
                    DWORD oldProtect;
                    VirtualProtect(&pThunk->u1.Function, sizeof(ULONG_PTR), 
                                  PAGE_READWRITE, &oldProtect);
                    
                    // 8. 写入 Hook 函数地址
                    pThunk->u1.Function = (ULONG_PTR)HookedMessageBoxW;
                    
                    // 9. 恢复保护属性
                    VirtualProtect(&pThunk->u1.Function, sizeof(ULONG_PTR), 
                                  oldProtect, &oldProtect);
                    
                    return TRUE;
                }
                pThunk++;
            }
        }
        pImportDesc++;
    }
    
    return FALSE;
}

代码看着长,其实逻辑很清晰。我拆开讲:

  • 第一步:拿到当前模块的基址。用 GetModuleHandle(NULL) 就行。
  • 第二步:解析 PE 头。DOS 头、NT 头,一路找下去。
  • 第三步:定位导入表。在数据目录里找 IMAGE_DIRECTORY_ENTRY_IMPORT
  • 第四步:遍历每个导入的 DLL。找到 user32.dll
  • 第五步:遍历该 DLL 的 IAT。找到 MessageBoxW 的条目。
  • 第六步:保存原地址,写入新地址。
小技巧: 修改 IAT 之前,一定要用 VirtualProtect 改内存保护属性。IAT 默认是只读的,直接写会崩。我刚开始写的时候忘了这步,程序直接挂了,查了半天才反应过来。

IAT Hook 的流程图

下面这张图展示了 IAT Hook 的完整流程:

IAT Hook 核心流程 程序调用 MessageBoxW 查找 IAT 中的 MessageBoxW 条目 IAT 中地址 是否被修改? 跳转到原函数 (MessageBoxW 真实地址) 跳转到 Hook 函数 (HookedMessageBoxW 地址) 可选:调用原函数 正常流程 Hook 流程 可选调用原函数

卸载 Hook

Hook 装上去,还得能卸下来。不然程序退出时可能会出问题。

BOOL UninstallHook() {
    // 找到之前保存的 IAT 条目
    // 把地址写回原函数地址
    // 注意:要确保原函数地址没有被释放
    
    // 代码逻辑和 InstallHook 类似
    // 只是最后一步改成:
    // pThunk->u1.Function = (ULONG_PTR)OriginalMessageBoxW;
    
    return TRUE;
}
注意: 卸载 Hook 时,一定要确保原函数地址仍然有效。如果原 DLL 已经被卸载了,写回原地址会导致程序崩溃。我遇到过这种情况:Hook 了 kernel32.dll 里的函数,结果程序卸载了某个组件,把 kernel32 也卸了...嗯,那场面,惨不忍睹。

IAT Hook 的局限性

IAT Hook 虽然好用,但不是万能的。我总结了几点:

局限性 说明 怎么办?
只能 Hook 导入函数 如果程序用 GetProcAddress 动态获取地址,IAT 里没有记录 用 Inline Hook 或 Detours
每个模块独立 Hook 了 notepad.exe 的 IAT,不影响 calc.exe 需要 Hook 所有目标模块
容易被检测 安全软件会检查 IAT 是否被篡改 用更隐蔽的 Hook 方式
64 位兼容性 32 位和 64 位的 PE 结构略有不同 注意区分 IMAGE_NT_HEADERS32IMAGE_NT_HEADERS64

实战中的坑

我当年做第一个 IAT Hook 时,遇到了一个特别诡异的问题:Hook 装上去之后,程序直接崩溃,连错误信息都没有。

查了半天,发现是 VirtualProtect 的参数写错了。我把 PAGE_READWRITE 写成了 PAGE_EXECUTE_READWRITE,虽然也能用,但某些模块的 IAT 在只读段里,改不了。

还有一个坑:如果你 Hook 的是 kernel32.dllntdll.dll 里的函数,要特别小心。这些 DLL 在进程里是共享的,你改了 IAT,可能会影响到其他线程。我建议新手先从 user32.dllwin32u.dll 练手。

调试技巧: 用 WinDbg 或 x64dbg 查看 IAT 内容。命令是 !dh 模块基址,然后找 IMPORT 目录。或者直接用 dumpbin /imports exe文件 查看静态 IAT。

总结

IAT Hook 是 API Hook 的入门技术。它简单、直接,适合用来理解 Hook 的基本原理。但说实话,在实际的逆向工程中,IAT Hook 用得并不多——因为局限性太大了。

我个人习惯把 IAT Hook 用在快速验证场景。比如分析一个恶意软件,我想看看它调用了哪些 API,但又不想跑起来,就可以先 Hook 住 IAT,记录所有调用。等摸清了它的行为,再决定下一步怎么走。

下一讲,我们会深入 Inline Hook。那才是真正的大杀器。不过先把 IAT Hook 吃透,后面的路就好走了。


公众号:蓝海资料掘金营,微信deep3321