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流程:
- 保存原函数入口的前5~10个字节(用于后续调用原函数)
- 在原函数入口写入
jmp hookFunc(5字节) - 在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