从 Hook 原理分析 Inline Hook 的实现(以 WeiXin 为例)
2026/7/23 10:08:50 网站建设 项目流程

网上很多 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

这些指令稍后还要继续执行,因此必须提前备份。

否则程序执行流程就断掉了。

如下图(图1):

Hook 前,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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询