19、软件补丁与修改:字节级Patch、内存补丁、API Hook原理、Detours与MinHook

说到软件补丁和修改,这其实是我在逆向工程中花时间最多的一块。你想想看,我们分析完一个程序,知道了它的逻辑漏洞或者想要扩展功能,最后总要落实到“改”这个动作上。怎么改?改哪里?改完怎么让它生效?这就是今天要聊的核心。

我个人习惯把补丁技术分成三个层次:字节级、内存级、API级。每个层次都有它的适用场景,也有各自的坑。咱们一个一个来。

19.1 字节级Patch:最直接,也最危险

字节级Patch,说白了就是直接改二进制文件。你拿一个十六进制编辑器,找到目标地址,把机器码改了,保存。完事。

我在早期做软件破解时,经常用这种方式。比如一个软件判断注册码是否正确,if条件跳转是75 0A(jnz),我直接改成90 90(nop nop),或者改成74 0A(jz)。这样不管注册码对不对,程序都走成功分支。

核心思路:找到关键跳转或关键函数调用,用NOP填充或修改条件码。

举个例子,假设有一段汇编:

00401000  |.  85C0          test eax, eax
00401002  |.  75 0A         jnz short 0040100E   ; 如果eax不为0,跳转到成功
00401004  |.  68 00304000   push 00403000        ; "注册失败"
00401009  |.  E8 02000000   call MessageBoxA
0040100E  |>  68 10304000   push 00403010        ; "注册成功"

我把75 0A改成EB 0A(jmp),不管eax是什么,都直接跳到成功。这就是最经典的字节级Patch。

注意:字节级Patch是永久性的。你改了文件,它就变了。如果改错了,程序直接崩溃,而且没有后悔药。我曾经有一次改一个DLL,把偏移算错了,结果整个程序启动就蓝屏——嗯,那是内核驱动,教训深刻。

19.2 内存补丁:运行时修改,不碰文件

内存补丁跟字节级Patch的区别在于:不改文件,只改运行时的内存。程序启动后,我们用调试器或者注入的DLL,把目标地址的内存内容改了。程序重启后,一切恢复原样。

为什么需要这个?我遇到过很多场景:

  • 程序有完整性校验(CRC校验自身文件),你改了文件它就报错
  • 你只是想临时测试某个修改效果
  • 目标程序是加密的,运行时才解密,你只能改内存

实现方式很简单,Windows下用WriteProcessMemory

// 内存补丁示例:把目标地址的5个字节改成NOP
BYTE patch[] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
DWORD oldProtect;
VirtualProtectEx(hProcess, (LPVOID)targetAddr, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
WriteProcessMemory(hProcess, (LPVOID)targetAddr, patch, 5, NULL);
VirtualProtectEx(hProcess, (LPVOID)targetAddr, 5, oldProtect, &oldProtect);

这里有个关键点:内存页保护。目标地址所在的页面通常是PAGE_EXECUTE_READ,你不能直接写。必须先改成PAGE_EXECUTE_READWRITE,写完再恢复。我见过不少新手忘了这一步,WriteProcessMemory返回成功但实际没写进去——因为权限不够。

我的习惯:做内存补丁时,先调用VirtualQueryEx查一下目标地址的页属性,记下来,改完再恢复。这样最安全。

19.3 API Hook原理:拦截函数调用

API Hook比前面两个层次更高。你不是改某个指令,而是拦截整个函数调用。比如程序调用CreateFile,你让它先走你的代码,你可以修改参数、记录日志、甚至直接返回假结果。

原理其实不复杂。函数调用是通过地址跳转的。你只要把函数入口处的指令改成jmp yourFunc,程序一调用就跳到你的代码里。你的代码处理完,再跳回原函数继续执行。

典型的Hook流程:

  1. 保存原函数入口的前5~10个字节(用于后续调用原函数)
  2. 在原函数入口写入jmp hookFunc(5字节)
  3. 在hookFunc中执行你的逻辑
  4. 如果需要调用原函数,执行保存的字节,再跳回原函数+偏移

我画了一张图,帮你理解这个流程:

API Hook 原理流程图 应用程序 调用CreateFile 原 CreateFile 入口被改为 jmp HookFunc (你的代码) 修改参数 / 记录日志 / 拦截 调用原函数(执行保存的字节) 原函数继续执行 返回结果 应用收到结果

你看,流程其实很清晰。但实现起来有几个坑:

  • 指令长度问题:x86指令是变长的。你保存前5个字节,但可能第5个字节是一条指令的中间部分。我建议至少保存8个字节,并且用反汇编引擎确认指令边界。
  • 多线程安全:你在改指令的时候,另一个线程可能正好在执行这个函数。轻则崩溃,重则数据损坏。我一般用SuspendThread暂停所有线程,改完再恢复。
  • 64位与32位的区别:x64下jmp需要绝对地址,不能直接用相对跳转。因为x64的jmp相对偏移只有32位,而地址是64位的。

我曾经踩过的坑:有一次Hook ntdll的某个函数,保存了5个字节,结果原函数入口刚好是一条mov rdi, rdi(2字节)加一条push rbp(1字节)加一条mov rbp, rsp(3字节)。我保存5字节,把mov rbp, rsp截断了。调用原函数时直接崩溃。后来我改用capstone反汇编引擎,先反汇编再决定保存多少字节。

19.4 Detours:微软的官方方案

Detours是微软研究院出的一个Hook库,也是我早期用得最多的。它解决了上面说的很多问题:指令边界、多线程安全、x64支持。

Detours的核心API就几个:

// 安装Hook
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourAttach(&(PVOID&)pOriginalFunc, pHookFunc);
DetourTransactionCommit();

// 卸载Hook
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)pOriginalFunc, pHookFunc);
DetourTransactionCommit();

Detours的原理是:它会在原函数前面插入一个无条件跳转到你的Hook函数,同时把原函数的前几条指令移到一块叫“trampoline”的内存区域。这样你调用原函数时,实际上是执行trampoline里的代码,然后跳回原函数+偏移。

Detours的优点:

  • 成熟稳定,微软自己也在用
  • 支持x86、x64、ARM
  • 事务机制,可以批量安装/卸载

缺点:

  • 商业使用需要授权(虽然个人用免费)
  • 比较重,如果只是简单Hook,有点杀鸡用牛刀

19.5 MinHook:轻量级替代方案

MinHook是我现在个人项目里最常用的。它开源、轻量、支持x86/x64,而且代码只有几个文件。

用法跟Detours类似,但更简洁:

// 初始化
MH_Initialize();

// 创建Hook
MH_CreateHook(&CreateFileA, &HookCreateFileA, (void**)&pOriginalCreateFileA);

// 启用Hook
MH_EnableHook(&CreateFileA);

// 禁用
MH_DisableHook(&CreateFileA);

// 清理
MH_Uninitialize();

MinHook的原理跟Detours类似,也是用trampoline。但它有一个很巧妙的设计:它会在第一次启用Hook时才真正修改内存。这样你可以在初始化阶段创建很多Hook,但只有需要时才激活,性能开销很小。

我对比过几个主流Hook库:

库名 大小 x64支持 多线程安全 许可证 我的评价
Detours ~500KB 商业需授权 功能全,但重
MinHook ~30KB BSD开源 轻量,推荐
EasyHook ~200KB LGPL 支持远程注入
mhook ~20KB 部分 MIT 极简,但文档少

我的建议:如果是商业项目,用Detours(花钱买安心)。如果是个人工具或逆向分析,MinHook足够了。我自己的逆向工具箱里,MinHook是标配。

19.6 实战:用MinHook拦截MessageBoxA

说了这么多理论,咱们来点实际的。下面是一个完整的MinHook示例,拦截MessageBoxA,把弹窗内容改成我们自己的:

#include <windows.h>
#include "MinHook.h"

// 定义函数指针类型
typedef int (WINAPI *MESSAGEBOXA)(HWND, LPCSTR, LPCSTR, UINT);

// 保存原函数指针
MESSAGEBOXA pOriginalMessageBoxA = NULL;

// Hook函数
int WINAPI HookMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
    // 修改文本内容
    lpText = "你被Hook了!——来自蓝海资料掘金营";
    lpCaption = "Hook Demo";
    
    // 调用原函数
    return pOriginalMessageBoxA(hWnd, lpText, lpCaption, uType);
}

int main() {
    // 初始化MinHook
    if (MH_Initialize() != MH_OK) {
        return 1;
    }
    
    // 创建Hook
    if (MH_CreateHook(&MessageBoxA, &HookMessageBoxA, 
                      (void**)&pOriginalMessageBoxA) != MH_OK) {
        return 1;
    }
    
    // 启用Hook
    if (MH_EnableHook(&MessageBoxA) != MH_OK) {
        return 1;
    }
    
    // 测试:弹窗会被拦截
    MessageBoxA(NULL, "原始文本", "原始标题", MB_OK);
    
    // 禁用Hook
    MH_DisableHook(&MessageBoxA);
    
    // 清理
    MH_Uninitialize();
    
    return 0;
}

这段代码编译后运行,你会发现弹窗显示的是“你被Hook了!”,而不是“原始文本”。这就是API Hook的威力。

核心要点回顾:

  • 字节级Patch:改文件,永久生效,适合简单破解
  • 内存补丁:改运行时内存,临时生效,适合有校验的程序
  • API Hook:拦截函数调用,最灵活,适合功能扩展和监控
  • Detours:微软官方方案,稳定但重
  • MinHook:轻量开源,个人推荐

嗯,关于补丁和Hook,今天就聊这么多。这些东西我用了十几年,每次遇到新场景还是会有新发现。你如果刚开始接触,建议先从MinHook入手,写几个小例子试试。别怕崩溃——我当年写Hook,一天崩个十几次是常事。崩多了,你就知道哪里容易出问题了。


公众号:蓝海资料掘金营,微信deep3321