65.9K
CodeProject 正在变化。 阅读更多。
Home

MinHook - 极简 x86/x64 API 挂钩库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (150投票s)

2009 年 11 月 22 日

BSD

5分钟阅读

viewsIcon

1151654

downloadIcon

50425

为 x64/x86 环境提供 Microsoft Detours 功能的基础部分。

 

v1.3.3 于 2017 年 1 月 8 日发布!
我们添加了对 Visual Studio 2017 的支持。欢迎访问我的 GitHub 仓库。您的评论和 Bug 报告深受好评。

背景

正如您这些对 Windows API 挂钩感兴趣的人所知道的,Microsoft Research 有一个名为 Detours 的优秀库。它确实很有用,但它的免费版(称为“Express”)不支持 x64 环境。虽然它的商业版(称为“Professional”)支持 x64,但对我来说太贵了,负担不起。它要花费大约 10,000 美元!

因此,我决定从头开始编写我自己的库,或者说“穷人的 Detours”。但我并没有将我的库设计成 Detours 的完美克隆。它只包含 API 挂钩功能,因为这正是我想要的。

截至 2016 年 4 月,此库已用于一些项目:7+ Taskbar TweakerBetter ExplorerConEmuDxWndMumbleNonVisual Desktop AccessOpen Broadcaster SoftwareQTTabBarx360ce,以及一些游戏的模组等等。很高兴发现这个项目对人们有所帮助。

使用库

看看下面的示例代码。就这些。它挂钩 `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。
    1. 添加了一个辅助函数 MH_CreateHookApiEx。(感谢 asm256)
    2. 支持 Visual Studio 2017 RC。
  • 2015 年 11 月 1 日:将源代码更新到 v1.3.2。
    1. 支持 Visual Studio 2015。
    2. 支持 MinGW。
  • 2015 年 5 月 18 日:将源代码更新到 v1.3.2-beta2。
    1. 修复了一些细微的 Bug。(感谢 RaMMicHaeL)
    2. 添加了一个辅助函数 MH_StatusToString。(感谢 Jan Klass)
  • 2015 年 5 月 12 日:将源代码更新到 v1.3.2-beta。
    1. 修复了 x64 模式下可能存在的线程死锁。(感谢 Aleh Kazakevich)
    2. 进一步减小了代码体积。
    3. 支持 Visual Studio 2015 RC。(实验性)
  • 2015 年 3 月 19 日:将源代码更新到 v1.3.1。
    1. 与 v1.3.1-beta 相比没有重大改动。
  • 2015 年 3 月 11 日:将源代码更新到 v1.3.1-beta。
    1. 添加了 MH_CreateHookApi 函数。
    2. 修复了某些工具报告的虚假内存泄漏。
    3. 修复了兼容性问题。
  • 2014 年 9 月 13 日:将源代码更新到 v1.3。
    1. 与 v1.3-beta3 相比没有重大改动。
  • 2014 年 7 月 31 日:将源代码更新到 v1.3-beta3。
    1. 修复了一些小 Bug。
    2. 改进了内存管理。
  • 2014 年 7 月 21 日:将源代码更新到 v1.3-beta2。
    1. 将参数更改为 Windows 友好类型。(void* 改为 LPVOID)
    2. 修复了一些小 Bug。
    3. 重新组织了源代码文件。
    4. 进一步减小了代码体积。
  • 2014 年 7 月 17 日:将源代码更新到 v1.3-beta。
    1. 用纯 C 重写,以减小代码体积和内存占用。(由 Andrey Unis 建议)
    2. 简化了整体代码库,使其更易读、更易维护。
    3. 将许可证从 3 子句 BSD 许可证更改为 2 子句 BSD 许可证。
  • 2014 年 6 月 21 日:将源代码更新到 v1.2.2。
    1. 修复了使用 Express 版本进行编译的问题。
    2. 减小了静态库的大小。
  • 2014 年 6 月 18 日:将源代码更新到 v1.2.1c。
    1. 将日文注释翻译成英文。
    2. 将源代码文件从 Shift-JIS 转换为 UTF-8。
  • 2014 年 6 月 16 日:将源代码和二进制文件更新到 v1.2.1。
    1. 进行了大量 Bug 修复和改进。(感谢 jarredholmanRaMMicHaeL
    2. 修复了使用 VC2008、2010、2012 和 2013 进行编译的问题。
  • 2009 年 11 月 26 日:更新了源代码、二进制文件和示例代码
    1. 修复了一个严重 Bug。(感谢 xliqz)
    2. 随着 Bug 的修复,更改了接口。
  • 2009 年 11 月 23 日:更新了源代码和二进制文件
    1. 修复了小 Bug(内部类型不匹配等)
    2. 将 `.LIB` 文件与 `.DLL` 文件分离。
    3. 添加了示例可执行文件。
  • 2009 年 11 月 22 日:首次发布
© . All rights reserved.