本文为 Maldev Academy 中的 Module59 小节的笔记,通过 Detours 库实现 API Hook。(完整学习内容请自行前往跳转链接查看)。课程中对 API Hook 基本原理的讲解、通过 MinHook 库实现 API Hook、自定义代码实现 API Hook,在这里就不展开详解了,课程文章原文写的很清楚明白。本篇着重讲解下 Detours 库实现 API Hook 的基本原理,以及如何加载使用 Detours 库。
Detours Hooking Library 是微软研究院开发的一款软件库,用于在 Windows 中拦截并重定向函数调用。该库可以将特定函数的调用重定向到用户自定义的替代函数,从而在调用过程中执行额外逻辑或修改原函数的行为。Detours 通常用于 C/C++ 程序,同时支持 32 位和 64 位应用程序。
先介绍几个后面会经常提到的概念:
- Source Function(源函数):原始代码中调用目标函数的地方。
- Detour Function(绕行函数):用户自定义的替代函数,被插入以拦截并替代目标函数执行,从而实现自定义逻辑。
- Trampoline Function(跳转函数):Detours 自动生成的函数,用来保存目标函数入口的原始指令,并在 Hook 后接力执行目标函数剩余代码。
- Target Function(目标函数):需要被 Hook 的真实函数本体,即将被写入跳转指令的那个函数入口。
当程序在没Hook状态下要执行 目标函数 时,控制权直接跳转到目标函数(Target Function)。

当程序在Hook状态下要执行 目标函数 时,控制权直接跳转到用户提供的 绕行函数(Detours)。绕行函数 可以执行任何合适的拦截预处理。绕行函数 可以直接将控制权返回给源函数(1->5);也可以调用 跳转函数,该函数会在不拦截的情况下调用 目标函数,目标函数执行完毕后,会将控制权返回给 绕行函数,绕行函数执行适当的后处理,然后将控制权返回给 源函数(1->2->3->4->5)。下图显示了有拦截情况下函数调用的逻辑控制流。

Detours 库通过重写目标函数在进程内的二进制映像来拦截目标函数。Detours 实际上会重写两个函数:目标函数本身、匹配的跳转函数,以及一个函数指针(即目标指针)。跳转函数(Trampoline) 由 Detours 自动分配,是 Detours Hook 机制自身必须创建的组件,不管你要不要保存它,不管你写不写变量,只要 DetourAttach 发生就会创建 跳转函数。
在插入 Detour 之前,跳转函数 中仅包含一条跳转到目标函数的指令。

在插入 Detour 之后,Detours 会将 目标函数 的前几条指令(至少5个字节,足以执行一条无条件跳转指令)替换为 无条件跳转(jmp) 到用户提供的 绕行函数。目标函数中被移除的指令则保留在一个 跳转函数 中。该 跳转函数 包含 从目标函数中移除的指令(前几个字节) 以及一个 无条件跳转(jmp) 到 目标函数剩余部分 的跳转指令。

|
|
完整示例:

1、使用 Detours 库
要使用 Detours 库的函数,需要先下载并编译 Detours 仓库,以生成编译所需的静态库文件(.lib)。此外,还需要包含头文件 detours.h,具体步骤在 Detours Wiki 的 Using Detours 章节中有详细说明。
(1)Clone the Detours repository
|
|
(2)Build with nmake
如需仅构建 detours 库,请切换到 detours/src 目录。
如需构建 detours 库及所有示例,请切换到 detours 目录并运行 nmake 命令。

我们需要打开 x86 Native Tools Command Prompt for VS 和 x64 Native Tools Command Prompt for VS,分别用于编译 x86 和 x64 的静态库文件。

切换到 detours/src 目录并运行 nmake 命令。


(3)现在应该会生成一个名为 lib.<ARCH> 的目录,其中包含 Detours 的静态库文件;这里的 <ARCH> 表示你所编译的目标架构。构建过程中还会生成一个 include 目录,里面包含该库所需的头文件。
C:\Users\Admin\Desktop\Detours>dir lib.X64
detours.lib
etours.pdb
C:\Users\Admin\Desktop\Detours>dir lib.X86
detours.lib
detours.pdb

(4)32-bit vs 64-bit Detours Library
本模块的共享代码中包含预处理代码,用于根据当前使用的机器架构自动选择要链接的 Detours .lib 文件版本。为此使用了 _M_X64 和 _M_IX86 这两个宏。它们由编译器定义,用来指示目标机器是运行 64 位还是 32 位版本的 Windows。
预处理代码如下所示:
|
|
(5)copy libs and header to project

(6)Add libs and header to project
copy and rename libs and header file to project.

in project’s “Header Files”, add “detours.h” for easier code viewing.

2、Transactions (事务)
Detours 库会将目标函数(即要被 hook 的函数)的前几条指令替换为一条无条件跳转指令,使其跳转到用户提供的 detour 函数,也就是实际要执行的替代函数。unconditional jump 这个术语也常被称为 trampoline(跳板)。
该库通过 事务(transactions) 的机制,对目标函数执行 安装和卸载 hook 的操作。事务允许将多个函数 hook 作为一个整体进行管理和应用,这在需要批量修改程序行为时非常有用。同时,这种机制也使用户能够在需要时轻松撤销所有变更。
在使用事务时,可以开启一个新的事务,向其中添加多个函数 hook,然后提交事务(commit)。当事务被提交后,所有已添加的 hook 会一次性应用到程序中;卸载 hook 时,流程也类似。
3、Detours API 函数
在使用任何 Hook 技术时,第一步始终是获取要 Hook 的 WinAPI 函数的地址。只有拿到函数地址,才能确定无条件跳转指令应当写入到哪里。本模块中,将使用 MessageBoxA 作为示例的被 Hook 目标函数。
下面是 Detours 库提供的 API 函数:
- DetourTransactionBegin —— 开始一个新的事务,用于附加或移除 detour。这是进行 hook 或 unhook 时必须首先调用的函数。
- DetourUpdateThread —— 更新当前事务。Detours 使用此函数将某个线程“加入(Enlist)”到当前事务中。
- DetourAttach —— 在当前事务中,为目标函数安装 hook。但该修改不会生效,直到调用
DetourTransactionCommit。 - DetourDetach —— 在当前事务中,从目标函数移除 hook。同样不会立即生效,直到调用
DetourTransactionCommit。 - DetourTransactionCommit —— 提交当前事务,使附加或移除 detour 的操作正式生效。
上述函数都会返回一个 LONG 类型的值,用于判断函数执行结果。如果执行成功,Detours API 会返回 NO_ERROR,即数值 0;如果失败,则会返回非零值。该非零返回值可作为错误代码,用于调试和定位问题。
4、替换被 Hook 的 API
下一步是创建一个用于替换被 Hook API 的函数(Detour Function)。绕行函数应当与原始函数具有相同的数据类型,并可选择性地接受相同的参数。这样做的目的是能够检查或修改传入原函数的参数值。
例如,下面的函数可以作为 MessageBoxA 的 detour 函数,它允许开发者查看原始参数的内容。
|
|
需要注意的是,绕行函数(Detour Function)可以比原函数接收更少的参数,但不能接收比原函数更多的参数,否则会访问无效地址,从而引发访问违规(access violation)异常。
当被 Hook 的函数被调用、Hook 生效时,程序会执行自定义的绕行函数(Detour Function)。然而,为了让程序的执行流程能够继续,绕行函数(Detour Function)必须返回一个原本该由目标函数返回的有效值。
一种简单做法是:在绕行函数内部再次调用原始函数来获取返回值并返回。但这样会产生问题,因为此时原始函数已经被 Hook 了——再次调用它时,会再次进入绕行函数本身,导致无限递归循环。这不是 Detours 库的 bug,而是所有 Hook 技术都需要面对的通用问题。
为了更好地理解这一点,下面的代码片段展示了绕行函数 MyMessageBoxA 在其内部调用 MessageBoxA。这种写法会导致无限循环。程序会一直卡在执行 MyMessageBoxA,原因是:
MyMessageBoxA 调用了 MessageBoxA,而 MessageBoxA 已经被 Hook,会再次跳转到 MyMessageBoxA,从而形成无限递归调用。
|
|
解决方法1:全局原始函数指针
Detours 库可以通过在 Hook 之前保存原始函数的指针来解决这个问题。该指针可以存储在一个全局变量中,并在 detour 函数内部调用该指针,而不是再次调用已经被 Hook 的目标函数本身。这样便可避免无限循环问题。
|
|
解决方法2:使用不同的API
另一种更通用的解决方案是:调用另一个未被 Hook、但具有相同功能的函数。例如 MessageBoxA 与 MessageBoxW,VirtualAlloc 与 VirtualAllocEx。通过调用未被 Hook 的对应 API,可以避免再次调用已被 Hook 的目标函数,从而避免递归和无限循环问题。
|
|
5、Detours Hook 过程
如前所述,Detours 库是通过事务(transaction)机制运行的。因此,要 Hook 一个 API 函数,需要执行以下步骤:创建一个事务、向事务提交操作(Hook 或 Unhook)、然后提交事务使修改生效。
|
|
6、Detours UnHook 过程
下面的代码片段展示了与前一节相同的流程,只不过此处执行的是 unhook 操作。
|
|
7、Main 函数
前面展示的 Hook 与 Unhook 例程并未包含 main 函数。下面给出的 main 函数示例,仅用于调用未 Hook 和已 Hook 的 MessageBoxA,以展示两种执行效果。
|
|
注意:程序编译建议在Release模式下测试完成,Debug模式可能不进行Hook。
8、Trampoline 函数
这里讲述下 Trampoline Function 在 Detours 中的初始化、变化、角色关系。
基本角色再确认:
| 名称 | 含义 |
|---|---|
| Target Function | 你要 Hook 的函数(如 MessageBoxA) |
| Detour Function | 你的 Hook 函数(如 MyMessageBoxA) |
| Trampoline Function | Detours 创建的 “目标函数前几条指令 + 跳转到剩余部分” 的函数 |
| Target Pointer | 你定义的函数指针变量(如 g_pMessageBoxA),用于指向 target 或 trampoline |

Trampoline 初始化与演化:四个阶段
(1)Hook 前(尚未调用 DetourAttach)
此时一切都是普通状态。
Target Function (MessageBoxA):
+------------------------+
| 原始指令 A0 |
| 原始指令 A1 |
| 原始指令 A2 |
| ... |
+------------------------+
Trampoline:
(不存在,还没有被创建)
Target Pointer:
g_pMessageBoxA → MessageBoxA 入口
g_pMessageBoxA ─────┐
↓
+------------------+
MessageBoxA: | A0; A1; A2; ... |
+------------------+
(2)Detours 分配 Trampoline(空壳阶段)
调用 DetourAttach 后,首先做的就是为 trampoline 分配一段可执行的内存。此时 trampoline 初始内容只有:
jmp MessageBoxA
即:
Target Function (MessageBoxA)(尚未被改动):
+------------------------+
| 原始指令 A0 |
| 原始指令 A1 |
| 原始指令 A2 |
| ... |
+------------------------+
Trampoline(已创建,但没复制指令):
+------------------------+
| JMP MessageBoxA | ← 仅初始跳回目标函数
+------------------------+
Target Pointer(尚未被修改):
g_pMessageBoxA → MessageBoxA 入口
g_pMessageBoxA ─────┐
↓
+------------------+
MessageBoxA: | A0; A1; A2; ... |
+------------------+
(3)Detours 构建完整 Trampoline(复制目标函数前几条指令)
Detours 现在会对 MessageBoxA 的前 N 个字节反汇编,直到 ≥ 5 bytes(因为要覆盖最少 5 字节写入 JMP 指令)。
举例:
MessageBoxA:
A0: 55 push rbp
A1: 48 89 E5 mov rbp, rsp
A2: 48 83 EC 20 sub rsp, 0x20
A3: ...
假设前三条(A0,A1,A2)共 8 字节 → 够 5 字节。
Detours 就会把这些复制到 trampoline:
Target Function (MessageBoxA)(尚未被改动):
+------------------------+
| 原始指令 A0 |
| 原始指令 A1 |
| 原始指令 A2 |
| ... |
+------------------------+
Trampoline(复制指令完成):
+------------------------------+
| 55 | ← 原 A0
| 48 89 E5 | ← 原 A1
| 48 83 EC 20 | ← 原 A2
| JMP MessageBoxA + 8 | ← 跳回剩余指令
+------------------------------+
Target Pointer(尚未被修改):
g_pMessageBoxA → MessageBoxA 入口
g_pMessageBoxA ─────┐
↓
+------------------+
MessageBoxA: | A0; A1; A2; ... |
+------------------+
(4)重写目标函数入口(正式装载 Detour)
Detours 将目标函数前 N 字节重写。
Target Function (MessageBoxA)(重写完成):
+------------------------------+
| JMP MyMessageBoxA |
| (原A0,A1,A2 已被覆盖) |
+------------------------------+
| A3:(MessageBoxA+8)(原函数剩余) |
| ... |
+------------------------------+
Trampoline(复制指令完成):
+------------------------------+
| 55 | ← 原 A0
| 48 89 E5 | ← 原 A1
| 48 83 EC 20 | ← 原 A2
| JMP MessageBoxA + 8 | ← 跳回剩余指令
+------------------------------+
Target Pointer(尚未被修改):
g_pMessageBoxA → MessageBoxA 入口
g_pMessageBoxA ─────┐
↓
+--------------------+
MessageBoxA: | JMP MyMessageBoxA | ← 注意:已经不是原来的入口了
+--------------------+
(5)DetourTransactionCommit 完成(真正 attach 完成)
Target Function (MessageBoxA)(重写完成):
+------------------------------+
| JMP MyMessageBoxA |
| (原A0,A1,A2 已被覆盖) |
+------------------------------+
| A3:(MessageBoxA+8)(原函数剩余) |
| ... |
+------------------------------+
Trampoline(复制指令完成):
+------------------------------+
| 55 | ← 原 A0
| 48 89 E5 | ← 原 A1
| 48 83 EC 20 | ← 原 A2
| JMP MessageBoxA + 8 | ← 跳回剩余指令
+------------------------------+
Target Pointer(指针修改指向到 Trampoline):
g_pMessageBoxA → Trampoline
g_pMessageBoxA ─────┐
↓
+---------------------+
Trampoline: | 55 | ← 原 A0
| 48 89 E5 | ← 原 A1
| 48 83 EC 20 | ← 原 A2
| JMP MessageBoxA + 8 | ← 跳回剩余指令
+---------------------+
至此,Hook完成。UnHook的话,原理类似,反着恢复即可。
还需要提一下是,在 Module61 自定义代码实现 API Hook文章中,trampoline 一词指代的是 trampoline(跳板) 代码片段,并非 Detours 库中的 trampoline function 这一特定功能的函数。