13、反反调试技术:绕过API检测、手动修复PEB、模拟执行与钩子绕过
各位好,我是老周。今天咱们聊点硬核的——反反调试。说白了,就是当调试器被对方发现后,我们怎么继续干活。
我刚开始搞逆向那会儿,碰到个加了VMP的样本,一开OD就退出。折腾了两天,最后发现只是检测了个IsDebuggerPresent。你说气不气人?从那以后,我就把反反调试当成了必修课。
这一章,我会把常见的反反调试手段拆开揉碎。咱们从API检测讲起,再到PEB手动修复,最后聊聊模拟执行和钩子绕过。嗯,内容不少,但都是实战干货。
核心思想:反反调试不是跟保护方案硬刚,而是找到它的逻辑漏洞,悄悄绕过去。
13.1 绕过API检测:从NtQueryInformationProcess说起
大多数保护方案都会调用NtQueryInformationProcess来查进程的调试端口。你想想看,只要这个API返回了非零的ProcessDebugPort,程序就知道自己被调试了。
怎么绕?我常用的方法有三种:
- Hook NtQueryInformationProcess——直接修改函数入口,让它永远返回0。
- 修改返回缓冲区——在调用返回前,把
DebugPort字段清零。 - 直接系统调用——绕过Win32 API,自己发syscall。
我个人最推荐第三种。为什么呢?因为很多保护只Hook了ntdll.dll里的导出函数,你直接发syscall,它根本不知道你在干嘛。
// 直接系统调用示例(x64)
// 绕过NtQueryInformationProcess的Hook
__asm {
mov rax, 0x22 // NtQueryInformationProcess的SSN
mov rcx, GetCurrentProcess()
mov rdx, 7 // ProcessDebugPort
mov r8, &debugPort
mov r9, 4
push 0
sub rsp, 8
syscall
add rsp, 16
}
小技巧:不同Windows版本的SSN不一样。我一般用syswhispers这类工具自动生成,省得自己手动查。
13.2 手动修复PEB:从根源上解决问题
PEB(进程环境块)里藏着好几个调试标记。最常见的两个:
BeingDebugged偏移0x02NtGlobalFlag偏移0x68
程序启动时,系统会把这两个字段设成1。保护代码只要读一下就知道自己被调试了。
修复方法其实很简单——在程序读取之前,手动把它们清零。我习惯在入口点附近下断点,然后直接修改内存。
// 手动修复PEB的BeingDebugged标志
// 在程序入口处执行
__asm {
mov eax, fs:[0x30] // 获取PEB地址
mov byte ptr [eax+2], 0 // 清零BeingDebugged
mov dword ptr [eax+0x68], 0 // 清零NtGlobalFlag
}
注意:有些保护会多次检查PEB。你不能只修一次就完事。我遇到过一种保护,它在每个关键函数调用前都检查一次BeingDebugged。这种情况,你得用硬件断点监控对PEB的读取。
说到硬件断点,这也是个绕坑的好办法。你可以在fs:[0x30]上设一个硬件读取断点,每次有人读PEB,调试器就会停下来。这时候你看看调用栈,就知道是谁在检查了。
13.3 模拟执行:让保护代码在沙箱里跑
有些保护太狠了——它不光检查PEB,还检查父进程、检查窗口名、检查驱动签名。你一个个绕,绕到天亮都绕不完。
这时候就该模拟执行上场了。说白了,就是让保护代码在一个虚拟环境里跑,它以为自己在真实系统里,其实啥都查不到。
我常用的工具是Unicorn引擎。它是个轻量级的CPU模拟器,支持ARM、x86、x64。你可以把一段代码加载进去,然后自己控制所有系统调用和内存访问。
// 使用Unicorn模拟执行一段代码
// 绕过所有系统级检测
uc_engine *uc;
uc_open(UC_ARCH_X86, UC_MODE_64, &uc);
// 映射内存
uc_mem_map(uc, 0x100000, 0x1000, UC_PROT_ALL);
uc_mem_write(uc, 0x100000, code, code_size);
// 设置栈
uc_reg_write(uc, UC_X86_REG_RSP, &stack_addr);
// Hook所有系统调用
uc_hook_add(uc, &hook, UC_HOOK_INTR, syscall_hook, NULL, 1, 0);
// 开始执行
uc_emu_start(uc, 0x100000, 0x100000 + code_size, 0, 0);
我在项目中遇到过一种保护,它用NtRaiseHardError来触发蓝屏检测。你想想看,在真实调试器里跑,一触发就蓝屏了。但在Unicorn里跑,我可以直接把这个调用Hook掉,返回一个假值。保护代码以为自己蓝屏了,其实啥事没有。
核心思路:模拟执行的核心是控制所有输入。保护代码查系统时间?你给它一个假时间。查进程列表?你给它一个假列表。它永远得不到真实信息。
13.4 钩子绕过:当保护代码Hook了你的API
有些保护不光检测自己,还会反过来Hook调试器的API。比如它Hook了ReadProcessMemory,你读内存的时候,它返回假数据。
怎么绕?我总结了几种方法:
| 钩子类型 | 绕过方法 | 适用场景 |
|---|---|---|
| Inline Hook | 手动恢复前5字节 | 简单保护 |
| IAT Hook | 直接调用ntdll原始地址 | 中等保护 |
| 内核级Hook | 使用硬件断点+手动syscall | 强保护 |
| Veh Hook | 注册自己的VEH处理函数 | 高级保护 |
嗯,这里我要特别说一下Inline Hook的绕过。保护代码会在API开头写一个jmp指令,跳转到它的检测代码。你只要把原来的指令恢复回去,就能正常调用了。
// 绕过Inline Hook:恢复原始指令
// 以NtOpenProcess为例
BYTE originalBytes[5] = {0x4C, 0x8B, 0xD1, 0xB8, 0x22}; // mov r10, rcx; mov eax, 0x22
// 写入原始指令
WriteProcessMemory(hProcess, ntOpenProcessAddr, originalBytes, 5, NULL);
// 现在调用NtOpenProcess就不会触发Hook了
NtOpenProcess(&hProc, PROCESS_ALL_ACCESS, &objAttr, &cid);
避坑指南:我曾经直接恢复指令后调用API,结果程序崩溃了。后来发现保护代码在Hook里做了引用计数,我恢复指令后,它计数不对,导致后续逻辑出错。正确的做法是:先保存Hook指令,调用完再恢复回去。或者干脆用直接系统调用,完全不经过Hook点。
13.5 实战:综合运用
说了这么多,咱们来一个综合案例。假设你遇到一个加了VMP和TMD双重保护的程序:
- 先用PEB修复处理BeingDebugged和NtGlobalFlag
- 再用直接系统调用绕过NtQueryInformationProcess的Hook
- 如果还有检测,把关键代码段放到Unicorn里模拟执行
- 最后用硬件断点监控对PEB和API的访问
我个人的经验是:不要一上来就上模拟执行,太慢了。先试试简单的PEB修复和API绕过,80%的保护到这步就跪了。剩下的20%,再用模拟执行和钩子绕过慢慢磨。
记住:反反调试不是技术竞赛,而是信息战。你只要比保护代码多知道一点信息,就能赢。
好了,这一章的内容就到这里。反反调试是个实践性很强的东西,光看没用,得动手。找个加了保护的样本练练手,你会发现这些技术其实没那么神秘。