MinHook - 极简 x86/x64 API 挂钩库






4.96/5 (150投票s)
为 x64/x86 环境提供 Microsoft Detours 功能的基础部分。
- 下载源代码 (v1.3.3) - 49.1 KB
- 下载预编译的 DLL (v1.3.3) - 19.0 KB
- 下载预编译的静态库 (v1.3.3) - 1.8 MB
- 下载示例 (v1.3.2) - 120.6 KB
背景
正如您这些对 Windows API 挂钩感兴趣的人所知道的,Microsoft Research 有一个名为 Detours 的优秀库。它确实很有用,但它的免费版(称为“Express”)不支持 x64 环境。虽然它的商业版(称为“Professional”)支持 x64,但对我来说太贵了,负担不起。它要花费大约 10,000 美元!
因此,我决定从头开始编写我自己的库,或者说“穷人的 Detours”。但我并没有将我的库设计成 Detours 的完美克隆。它只包含 API 挂钩功能,因为这正是我想要的。
截至 2016 年 4 月,此库已用于一些项目:7+ Taskbar Tweaker、Better Explorer、ConEmu、DxWnd、Mumble、NonVisual Desktop Access、Open Broadcaster Software、QTTabBar、x360ce,以及一些游戏的模组等等。很高兴发现这个项目对人们有所帮助。
使用库
看看下面的示例代码。就这些。它挂钩 `MessageBoxW()` 函数并修改其文本。此示例包含在源代码和二进制存档中。请在 x64 和 x86 模式下进行尝试。
对于熟悉 NuGet 的用户,现在可以获得NuGet 包。
#include <Windows.h>
#include "MinHook.h"
#if defined _M_X64
#pragma comment(lib, "libMinHook.x64.lib")
#elif defined _M_IX86
#pragma comment(lib, "libMinHook.x86.lib")
#endif
typedef int (WINAPI *MESSAGEBOXW)(HWND, LPCWSTR, LPCWSTR, UINT);
// Pointer for calling original MessageBoxW.
MESSAGEBOXW fpMessageBoxW = NULL;
// Detour function which overrides MessageBoxW.
int WINAPI DetourMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
{
return fpMessageBoxW(hWnd, L"Hooked!", lpCaption, uType);
}
int main()
{
// Initialize MinHook.
if (MH_Initialize() != MH_OK)
{
return 1;
}
// Create a hook for MessageBoxW, in disabled state.
if (MH_CreateHook(&MessageBoxW, &DetourMessageBoxW,
reinterpret_cast<LPVOID*>(&fpMessageBoxW)) != MH_OK)
{
return 1;
}
// or you can use the new helper function like this.
//if (MH_CreateHookApiEx(
// L"user32", "MessageBoxW", &DetourMessageBoxW, &fpMessageBoxW) != MH_OK)
//{
// return 1;
//}
// Enable the hook for MessageBoxW.
if (MH_EnableHook(&MessageBoxW) != MH_OK)
{
return 1;
}
// Expected to tell "Hooked!".
MessageBoxW(NULL, L"Not hooked...", L"MinHook Sample", MB_OK);
// Disable the hook for MessageBoxW.
if (MH_DisableHook(&MessageBoxW) != MH_OK)
{
return 1;
}
// Expected to tell "Not hooked...".
MessageBoxW(NULL, L"Not hooked...", L"MinHook Sample", MB_OK);
// Uninitialize MinHook.
if (MH_Uninitialize() != MH_OK)
{
return 1;
}
return 0;
}
如果您是 C++ 用户,可以为 `MH_CreateHook()` 和 `MH_CreateHookApi()` 编写一个小包装器。它允许您删除那些烦人的 `reinterpret_cast`,例如
template <typename T>
inline MH_STATUS MH_CreateHookEx(LPVOID pTarget, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHook(pTarget, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}
template <typename T>
inline MH_STATUS MH_CreateHookApiEx(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, T** ppOriginal)
{
return MH_CreateHookApi(
pszModule, pszProcName, pDetour, reinterpret_cast<LPVOID*>(ppOriginal));
}
...
// Create a hook for MessageBoxW, in disabled state.
if (MH_CreateHookApiEx(L"user32", "MessageBoxW", &DetourMessageBoxW, &fpMessageBoxW) != MH_OK)
{
return 1;
}
工作原理
此软件的基本概念与 **Microsoft Detours** 和 Daniel Pistelli 的 Hook-Engine 相同。它用 x86 的 JMP
(无条件跳转)指令替换目标函数的入口点,跳转到钩子函数。这是一种安全、稳定且经过验证的方法。
覆盖目标函数
在 x64/x86 指令集中,`JMP` 指令有几种形式。我决定始终使用 32 位相对 `JMP`,占用 5 个字节。这是实际中可以使用到的最短形式。在这种情况下,越短越好。
在 x86 模式下,32 位相对 `JMP` 可以覆盖整个地址空间。由于溢出的位数在相对地址计算中会被忽略,所以在 x86 模式下,函数地址无关紧要。
; x86 mode (assumed that the target function is at 0x40000000)
; 32bit relative JMPs of 5 bytes cover whole address space
0x40000000: E9 FBFFFFBF JMP 0x0 (EIP+0xBFFFFFFB)
0x40000000: E9 FAFFFFBF JMP 0xFFFFFFFF (EIP+0xBFFFFFFA)
; Shorter forms are useless in this case
; 8bit JMPs of 2 bytes cover -126 ~ +129 bytes
0x40000000: EB 80 JMP 0x3FFFFF82 (EIP-0x80)
0x40000000: EB 7F JMP 0x40000081 (EIP+0x7F)
; 16bit JMPs of 4 bytes cover -32764 ~ +32771 bytes
0x40000000: 66E9 0080 JMP 0x3FFF8004 (EIP-0x8000)
0x40000000: 66E9 FF7F JMP 0x40008003 (EIP+0x7FFF)
然而,在 x64 模式下,这是一个问题。与整个地址空间相比,它只能覆盖非常窄的范围。因此,我引入了一个名为“中继函数”的新函数,它只是一个 64 位跳转到钩子函数,并放置在目标函数附近。幸运的是,`VirtualAlloc()` API 函数可以接受分配的地址,并且很容易在目标函数附近查找未分配的区域。
; x64 mode (assumed that the target function is at 0x140000000)
; 32bit relative JMPs of 5 bytes cover about -2GB ~ +2GB
0x140000000: E9 00000080 JMP 0xC0000005 (RIP-0x80000000)
0x140000000: E9 FFFFFF7F JMP 0x1C0000004 (RIP+0x7FFFFFFF)
; Target function (Jump to the Relay Function)
0x140000000: E9 FBFF0700 JMP 0x140080000 (RIP+0x7FFFB)
; Relay function (Jump to the Detour Function)
0x140080000: FF25 FAFF0000 JMP [0x140090000 (RIP+0xFFFA)]
0x140090000: xxxxxxxxxxxxxxxx ; 64bit address of the Detour Function
构建跳板函数
目标函数被覆盖为钩子函数。那么,我们如何调用原始的目标函数呢?在许多情况下,我们必须从钩子函数内部调用原始函数。MinHook 有一个名为“跳板函数”的函数,用于调用原始函数(Daniel Pistelli 称之为“桥接函数”)。这是原始函数入口点的一段克隆代码,后面跟着一个无条件跳转,用于恢复到原始函数。真实世界的例子在这里。这些是 `MinHook` 实际创建的。
我们应该反汇编原始函数,以了解指令边界和需要复制的指令。我采用了 **Vyacheslav Patkov** 的 **Hacker Disassembler Engine** (HDE) 作为反汇编器。它小巧、轻便,非常适合我的目的。我反汇编了 Windows XP、Vista 和 7 上的数千个 API 函数进行检查,并为它们构建了跳板函数。
; Original "USER32.dll!MessageBoxW" in x64 mode
0x770E11E4: 4883EC 38 SUB RSP, 0x38
0x770E11E8: 4533DB XOR R11D, R11D
; Trampoline
0x77064BD0: 4883EC 38 SUB RSP, 0x38
0x77064BD4: 4533DB XOR R11D, R11D
0x77064BD7: FF25 5BE8FEFF JMP QWORD NEAR [0x77053438 (RIP-0x117A5)]
; Address Table
0x77053438: EB110E7700000000 ; Address of the Target Function +7 (for resuming)
; Original "USER32.dll!MessageBoxW" in x86 mode
0x7687FECF: 8BFF MOV EDI, EDI
0x7687FED1: 55 PUSH EBP
0x7687FED2: 8BEC MOV EBP, ESP
; Trampoline
0x0014BE10: 8BFF MOV EDI, EDI
0x0014BE12: 55 PUSH EBP
0x0014BE13: 8BEC MOV EBP, ESP
0x0014BE15: E9 BA407376 JMP 0x7687FED4
如果原始函数包含分支指令怎么办?当然,它们应该被修改为指向与原始函数相同的地址。
; Original "kernel32.dll!IsProcessorFeaturePresent" in x64 mode
0x771BD130: 83F9 03 CMP ECX, 0x3
0x771BD133: 7414 JE 0x771BD149
; Trampoline
; (Became a little complex, because 64 bit version of JE doesn't exist)
0x77069860: 83F9 03 CMP ECX, 0x3
0x77069863: 74 02 JE 0x77069867
0x77069865: EB 06 JMP 0x7706986D
0x77069867: FF25 1BE1FEFF JMP QWORD NEAR [0x77057988 (RIP-0x11EE5)]
0x7706986D: FF25 1DE1FEFF JMP QWORD NEAR [0x77057990 (RIP-0x11EE3)]
; Address Table
0x77057988: 49D11B7700000000 ; Where the original JE points.
0x77057990: 35D11B7700000000 ; Address of the Target Function +5 (for resuming)
; Original "gdi32.DLL!GdiFlush" in x86 mode
0x76479FF4: E8 DDFFFFFF CALL 0x76479FD6
; Trampoline
0x00147D64: E8 6D223376 CALL 0x76479FD6
0x00147D69: E9 8B223376 JMP 0x76479FF9
; Original "kernel32.dll!CloseProfileUserMapping" in x86 mode
0x763B7918: 33C0 XOR EAX, EAX
0x763B791A: 40 INC EAX
0x763B791B: C3 RET
0x763B791C: 90 NOP
; Trampoline (Additional jump is not required, because this is a perfect function)
0x0014585C: 33C0 XOR EAX, EAX
0x0014585E: 40 INC EAX
0x0014585F: C3 RET
在 x64 模式下,RIP 相对寻址模式也是一个问题。它们的相对地址应该被修改为指向相同的地址。
; Original "kernel32.dll!GetConsoleInputWaitHandle" in x64 mode
0x771B27F0: 488B05 11790C00 MOV RAX, [0x7727A108 (RIP+0xC7911)]
; Trampoline
0x77067EB8: 488B05 49222100 MOV RAX, [0x7727A108 (RIP+0x212249)]
0x77067EBF: FF25 4BE3FEFF JMP QWORD NEAR [0x77056210 (RIP-0x11CB5)]
; Address Table
0x77056210: F7271B7700000000 ; Address of the Target Function +7 (for resuming)
; Original "user32.dll!TileWindows" in x64 mode
0x770E023C: 4883EC 38 SUB RSP, 0x38
0x770E0240: 488D05 71FCFFFF LEA RAX, [0x770DFEB8 (RIP-0x38F)]
; Trampoline
0x77064A80: 4883EC 38 SUB RSP, 0x38
0x77064A84: 488D05 2DB40700 LEA RAX, [0x770DFEB8 (RIP+0x7B42D)]
0x77064A8B: FF25 CFE8FEFF JMP QWORD NEAR [0x77053360 (RIP-0x11731)]
; Address Table
0x77053360: 47020E7700000000 ; Address of the Target Function +11 (for resuming)
结论
尽管这个库小巧而简单,但我认为它足够实用。请尽情使用!
历史
- 2017 年 1 月 8 日:将源代码更新到 v1.3.3。
- 添加了一个辅助函数 MH_CreateHookApiEx。(感谢 asm256)
- 支持 Visual Studio 2017 RC。
- 2015 年 11 月 1 日:将源代码更新到 v1.3.2。
- 支持 Visual Studio 2015。
- 支持 MinGW。
- 2015 年 5 月 18 日:将源代码更新到 v1.3.2-beta2。
- 修复了一些细微的 Bug。(感谢 RaMMicHaeL)
- 添加了一个辅助函数 MH_StatusToString。(感谢 Jan Klass)
- 2015 年 5 月 12 日:将源代码更新到 v1.3.2-beta。
- 修复了 x64 模式下可能存在的线程死锁。(感谢 Aleh Kazakevich)
- 进一步减小了代码体积。
- 支持 Visual Studio 2015 RC。(实验性)
- 2015 年 3 月 19 日:将源代码更新到 v1.3.1。
- 与 v1.3.1-beta 相比没有重大改动。
- 2015 年 3 月 11 日:将源代码更新到 v1.3.1-beta。
- 添加了 MH_CreateHookApi 函数。
- 修复了某些工具报告的虚假内存泄漏。
- 修复了兼容性问题。
- 2014 年 9 月 13 日:将源代码更新到 v1.3。
- 与 v1.3-beta3 相比没有重大改动。
- 2014 年 7 月 31 日:将源代码更新到 v1.3-beta3。
- 修复了一些小 Bug。
- 改进了内存管理。
- 2014 年 7 月 21 日:将源代码更新到 v1.3-beta2。
- 将参数更改为 Windows 友好类型。(void* 改为 LPVOID)
- 修复了一些小 Bug。
- 重新组织了源代码文件。
- 进一步减小了代码体积。
- 2014 年 7 月 17 日:将源代码更新到 v1.3-beta。
- 用纯 C 重写,以减小代码体积和内存占用。(由 Andrey Unis 建议)
- 简化了整体代码库,使其更易读、更易维护。
- 将许可证从 3 子句 BSD 许可证更改为 2 子句 BSD 许可证。
- 2014 年 6 月 21 日:将源代码更新到 v1.2.2。
- 修复了使用 Express 版本进行编译的问题。
- 减小了静态库的大小。
- 2014 年 6 月 18 日:将源代码更新到 v1.2.1c。
- 将日文注释翻译成英文。
- 将源代码文件从 Shift-JIS 转换为 UTF-8。
- 2014 年 6 月 16 日:将源代码和二进制文件更新到 v1.2.1。
- 进行了大量 Bug 修复和改进。(感谢 jarredholman 和 RaMMicHaeL)
- 修复了使用 VC2008、2010、2012 和 2013 进行编译的问题。
- 2009 年 11 月 26 日:更新了源代码、二进制文件和示例代码
- 修复了一个严重 Bug。(感谢 xliqz)
- 随着 Bug 的修复,更改了接口。
- 2009 年 11 月 23 日:更新了源代码和二进制文件
- 修复了小 Bug(内部类型不匹配等)
- 将 `.LIB` 文件与 `.DLL` 文件分离。
- 添加了示例可执行文件。
- 2009 年 11 月 22 日:首次发布