13、反反调试技术:绕过API检测、手动修复PEB、模拟执行与钩子绕过

各位好,我是老周。今天咱们聊点硬核的——反反调试。说白了,就是当调试器被对方发现后,我们怎么继续干活。

我刚开始搞逆向那会儿,碰到个加了VMP的样本,一开OD就退出。折腾了两天,最后发现只是检测了个IsDebuggerPresent。你说气不气人?从那以后,我就把反反调试当成了必修课。

这一章,我会把常见的反反调试手段拆开揉碎。咱们从API检测讲起,再到PEB手动修复,最后聊聊模拟执行和钩子绕过。嗯,内容不少,但都是实战干货。

核心思想:反反调试不是跟保护方案硬刚,而是找到它的逻辑漏洞,悄悄绕过去。

反反调试技术体系 反反调试 API检测绕过 PEB手动修复 模拟执行 钩子绕过 NtQueryInfo ZwSetInfoThread 直接系统调用 BeingDebugged NtGlobalFlag Heap Flags Unicorn引擎 指令级模拟 API转发 硬件断点 Inline Hook Trampoline

13.1 绕过API检测:从NtQueryInformationProcess说起

大多数保护方案都会调用NtQueryInformationProcess来查进程的调试端口。你想想看,只要这个API返回了非零的ProcessDebugPort,程序就知道自己被调试了。

怎么绕?我常用的方法有三种:

  1. Hook NtQueryInformationProcess——直接修改函数入口,让它永远返回0。
  2. 修改返回缓冲区——在调用返回前,把DebugPort字段清零。
  3. 直接系统调用——绕过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 偏移0x02
  • NtGlobalFlag 偏移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双重保护的程序:

  1. 先用PEB修复处理BeingDebugged和NtGlobalFlag
  2. 再用直接系统调用绕过NtQueryInformationProcess的Hook
  3. 如果还有检测,把关键代码段放到Unicorn里模拟执行
  4. 最后用硬件断点监控对PEB和API的访问

我个人的经验是:不要一上来就上模拟执行,太慢了。先试试简单的PEB修复和API绕过,80%的保护到这步就跪了。剩下的20%,再用模拟执行和钩子绕过慢慢磨。

记住:反反调试不是技术竞赛,而是信息战。你只要比保护代码多知道一点信息,就能赢。

好了,这一章的内容就到这里。反反调试是个实践性很强的东西,光看没用,得动手。找个加了保护的样本练练手,你会发现这些技术其实没那么神秘。


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