第37章:Windows API Hook(二):Inline Hook原理与实现(5字节跳转)

上一章我们聊了IAT Hook,那是修改导入表来劫持API调用。说实话,IAT Hook虽然稳定,但局限性也很明显——它只能钩住通过导入表调用的函数。如果目标程序用LoadLibrary + GetProcAddress动态获取函数地址,IAT Hook就抓瞎了。

这时候,Inline Hook就派上用场了。它直接在函数代码开头动手脚,不管你用哪种方式调用,统统拦截。我个人习惯把Inline Hook叫做“硬核钩子”,因为它真的在改代码。

Inline Hook的核心思想

说白了,Inline Hook的原理就一句话:把目标函数的前几个字节改成一条JMP指令,跳转到我们的Hook函数

你想想看,CPU执行代码是一行一行来的。如果我们在函数入口处放一条JMP,CPU就会乖乖跳到我们指定的地址去执行。等我们的Hook函数处理完了,再跳回原来的函数继续执行。

嗯,这里要注意:被覆盖的原始字节必须保存下来,不然Hook函数执行完后没法恢复现场。

为什么是5字节?

这个问题我刚开始学的时候也困惑过。为什么偏偏是5字节?

答案很简单:x86架构下,一条JMP指令正好占5字节

JMP指令的机器码格式是:

E9 xx xx xx xx
  • E9:JMP的操作码(1字节)
  • xx xx xx xx:相对偏移地址(4字节)

这个偏移量怎么算?公式是:

偏移 = 目标地址 - 当前指令地址 - 5

为什么要减5?因为JMP指令执行时,EIP已经指向了下一条指令。所以偏移量要减去当前指令的长度。

核心要点:5字节跳转是最经典的Inline Hook方案。它简单、高效,覆盖了绝大多数Windows API函数的开头。

Inline Hook的完整流程

我在项目中实现过很多次Inline Hook,总结下来就是6步:

  1. 获取目标函数地址:用GetProcAddress拿到API的真实地址
  2. 保存原始字节:把函数开头的5个字节读出来存好
  3. 写入JMP指令:计算偏移,写入E9 + 偏移量
  4. 执行Hook函数:当API被调用时,先执行我们的代码
  5. 恢复原始字节:在Hook函数中,把保存的5字节写回去
  6. 调用原始API:执行真正的API函数
  7. 重新Hook:等原始API返回后,再把JMP写回去

我曾经踩过一个坑:多线程环境下,恢复和重新Hook之间有个时间窗口。如果另一个线程在这期间调用了API,就会直接执行原始函数,绕过我们的Hook。后来我用了InterlockedExchange之类的原子操作才解决。

代码实现

来看一个完整的实现。我以Hook MessageBoxA为例:

// 保存原始字节
BYTE g_originalBytes[5] = {0};

// Hook函数
int WINAPI HookedMessageBoxA(
    HWND hWnd, LPCSTR lpText, 
    LPCSTR lpCaption, UINT uType)
{
    // 1. 恢复原始字节
    DWORD oldProtect;
    VirtualProtect(g_pMessageBoxA, 5, 
                   PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy(g_pMessageBoxA, g_originalBytes, 5);
    VirtualProtect(g_pMessageBoxA, 5, 
                   oldProtect, &oldProtect);
    
    // 2. 调用原始API
    int result = MessageBoxA(hWnd, 
        "已被Hook拦截!", lpCaption, uType);
    
    // 3. 重新Hook
    InstallHook();
    
    return result;
}

// 安装Hook
BOOL InstallHook()
{
    // 获取目标函数地址
    HMODULE hMod = GetModuleHandleA("user32.dll");
    g_pMessageBoxA = (PBYTE)GetProcAddress(hMod, "MessageBoxA");
    
    // 保存原始字节
    memcpy(g_originalBytes, g_pMessageBoxA, 5);
    
    // 计算JMP偏移
    // JMP指令地址 = g_pMessageBoxA
    // 目标地址 = HookedMessageBoxA
    DWORD offset = (DWORD)HookedMessageBoxA - 
                   (DWORD)g_pMessageBoxA - 5;
    
    // 写入JMP指令
    DWORD oldProtect;
    VirtualProtect(g_pMessageBoxA, 5, 
                   PAGE_EXECUTE_READWRITE, &oldProtect);
    
    g_pMessageBoxA[0] = 0xE9;  // JMP操作码
    memcpy(&g_pMessageBoxA[1], &offset, 4);
    
    VirtualProtect(g_pMessageBoxA, 5, 
                   oldProtect, &oldProtect);
    
    return TRUE;
}

小技巧:VirtualProtect是必须的。因为代码段默认是只读的,不改内存属性直接写会触发访问违例。

Hook链与多钩子问题

实际项目中,往往不止一个程序在Hook同一个API。这就引出了Hook链的问题。

假设A程序先Hook了MessageBoxA,B程序后Hook。B在安装Hook时,保存的是A写入的JMP指令。当B的Hook函数恢复原始字节时,恢复的是A的JMP,而不是真正的API开头。

这就乱套了。

我建议的解决方案是:用Trampoline(跳板)函数。在分配的内存中重建被覆盖的指令,然后加上JMP跳回原始函数+5的位置。这样每个Hook都有自己的跳板,互不干扰。

方案 优点 缺点
直接恢复法 实现简单 多Hook冲突
Trampoline法 支持多Hook 实现复杂
Detours库 微软官方方案 商业使用需授权

x64架构下的差异

如果你在x64下做Inline Hook,要注意:5字节JMP不够用了

为什么?因为x64的地址是64位的,而JMP指令的偏移量只有32位。如果目标地址和当前地址相差超过2GB,5字节JMP就跳不过去。

解决方案有两种:

  • 用14字节的JMP:mov rax, 目标地址 + jmp rax
  • 用中间跳板:在附近地址放一个64位跳板

我个人更推荐第二种,因为14字节覆盖范围太大,容易破坏其他指令。

实战中的避坑指南

做Inline Hook这么多年,我总结了几条血泪教训:

警告:

  • 不要Hook正在执行的函数:如果目标函数当前正在被调用,修改它的代码会导致崩溃。建议用SuspendThread暂停所有线程。
  • 注意指令边界:有些函数开头不是5字节对齐的指令。比如开头是一条3字节指令加一条2字节指令,你只覆盖前5字节会破坏第二条指令。这时候要用Trampoline。
  • SEH异常处理:写入内存时可能触发异常,记得用__try/__except包起来。
  • 不要Hook短函数:如果函数总长度不到5字节,就没法Hook。这种情况很少见,但确实存在。

Inline Hook的应用场景

说实话,Inline Hook的用途非常广泛:

  • API监控:记录程序调用了哪些API,传了什么参数
  • 功能扩展:在API调用前后插入自定义逻辑
  • 安全防护:拦截恶意API调用
  • 游戏外挂:嗯,这个我就不多说了

我曾经在一个安全项目中,用Inline Hook拦截了NtCreateThreadEx,成功阻止了恶意代码的远程线程注入。那次经历让我深刻体会到:Hook点选对了,事半功倍

核心流程图

下面这张图展示了Inline Hook的完整调用流程:

Inline Hook 调用流程图 应用程序调用API JMP跳转到Hook函数 执行Hook逻辑(参数修改等) 恢复原始5字节 + 调用原始API 返回结果 注:虚线表示返回路径,实线表示调用路径

这张图清晰地展示了:应用程序调用API时,首先命中我们写入的JMP指令,然后跳转到Hook函数。Hook函数处理完业务逻辑后,恢复原始字节并调用真正的API,最后将结果返回给应用程序。

总结

Inline Hook是逆向工程中非常核心的技术。5字节跳转虽然简单,但背后涉及内存管理、线程安全、指令对齐等很多细节。我建议你从最简单的MessageBoxA开始练手,逐步增加复杂度。

记住一句话:Hook容易,写好难。真正的高手,是在各种边界情况下都能稳定运行的Hook。

练习建议:写一个程序,Hook CreateFileA,记录所有文件打开操作。这是很多安全软件的基础功能。


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