从 Hook 原理分析 Inline Hook 的实现(以 WeiXin 为例)
网上很多 Hook 教程都是直接告诉你找偏移、调用 Hook 模块即可真正讲清楚 Hook 为什么这样实现、每一步在干什么的却比较少。今天就从Inline Hook 的底层原理来分析整个流程。声明本文仅用于技术分析与学习交流。为了保护分析对象文中涉及的软件名称、偏移地址及部分代码均进行了修改不能直接用于任何实际软件。一个 Hook 调用长什么样很多 Hook 框架最终都是类似下面这样一个接口Hook( wechatwin_基址 偏移, 6, {96,86,232,0,0,0,0,97}, 接收消息, true );五个参数分别代表参数含义Hook地址需要Hook的位置模块基址偏移备份长度覆盖原指令长度调用数据写入的新代码模板回调地址我们自己的处理函数是否前置是否先执行自己的代码真正的 Hook其实就是围绕这五个参数完成的。Hook 的整体流程整个 Inline Hook 可以概括为下面几步原函数 │ │ ▼ 修改前5字节 │ ▼ jmp --- 新申请内存(JMP区) │ ▼ 恢复被覆盖指令 │ ▼ 执行自己的回调函数 │ ▼ 恢复现场 │ ▼ jmp 回原函数剩余代码整个流程可以理解成截胡 - 做自己的事情 - 再回去继续执行原来的代码。第一步备份原始指令首先要保存即将覆盖掉的机器码。ReadProcessMemory( -1, Hook地址, 原字节集, 备份长度, 长度 );例如准备覆盖 6 个字节55 8B EC 83 EC 10这些指令稍后还要继续执行因此必须提前备份。否则程序执行流程就断掉了。如下图图1Hook 前OD 中看到的是程序原始指令。图1第二步申请一块新的可执行内存JPM_地址 VirtualAlloc( 0, 1024, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE );例如申请得到00AF0000为什么要申请因为我们的 Hook 代码不能直接写到原来的函数里面。需要一块新的空间用来恢复原始指令调用自己的函数跳回原函数很多人把这里叫Trampoline或者跳板第三步恢复原始指令第一件事就是把刚才备份的数据复制过去。WriteProcessMemory( -1, JPM_地址, 原字节集, 备份长度, 0 );此时00AF0000 55 8B EC 83 EC 10这就是原来被覆盖掉的代码。第四步写入我们的 Hook 模板例如{96,86,232,0,0,0,0,97}很多人第一次看到这串数据都会懵。其实拆开以后非常简单。60 pushad 56 push esi E8 ???????? 61 popad其中E8表示call xxxx后面的四个00 00 00 00只是预留位置。稍后会计算真正的 CALL 地址。第五步计算回调函数地址代码里寻找字节集( 调用数据, {232} )实际上就是找到E8的位置。例如96 86 232 00 00 00 00 97那么E8位于第3个字节。随后回调偏移 备份长度假设3 6 9然后开始计算目标地址 - 当前地址 - 5也就是CALL偏移 回调函数 - CALL指令地址 - 5最终把算出的四字节偏移写回E8 XX XX XX XX这样执行到这里时就会调用我们的函数。如下图图2第六步写入跳回原函数的 JMP自己的代码执行完以后程序不能停在这里。还需要jmp跳回Hook地址 备份长度继续执行后面的代码。于是WriteProcessMemory( JPM地址备份长度写入长度, {E9}偏移, 5, 0 );此时跳板区域已经完整了。流程变成恢复原指令 ↓ 调用自己的函数 ↓ jmp ↓ 原函数剩余代码如下图图3第七步修改原函数真正开始 Hook。把原来的代码改成E9 XXXXXXXX对应代码WriteProcessMemory( Hook地址, {E9}偏移, 5, 0 );于是程序执行到这里时不会再继续执行原函数。而是jmp ↓ 我们申请的新内存OD 中就能看到原来的代码已经变成jmp 00AF0000如下图图4第八步NOP 填充因为jmp固定占5 字节如果原来覆盖的是6 7 8 9...那么多出来的字节必须NOP填充。例如90 90 90对应代码WriteProcessMemory( Hook地址5, 填充Dm, 长度, 0 );否则CPU 会把残留的半条指令继续执行。程序极有可能直接崩溃。如下图图5最终执行流程整个 Inline Hook 最终执行顺序如下程序运行 ↓ 执行 Hook 点 ↓ jmp 到跳板 ↓ 恢复被覆盖的原始指令 ↓ 执行自己的回调函数 ↓ jmp 回原函数 ↓ 继续执行剩余代码可以看到所谓的 Inline Hook本质上并不复杂它只是利用了一条JMP指令改变程序执行流再借助一块新申请的可执行内存Trampoline恢复现场、执行自定义逻辑最后跳回原函数继续运行。很多 Hook 框架如 MinHook、Detours 等虽然封装了大量细节但核心思想都与本文分析的流程一致保存原指令 → 跳转到跳板 → 执行自定义代码 → 返回原执行流。理解了这一过程再去阅读各种 Hook 框架源码会容易得多。123123