Files
Seep-Reverse-Lab/MANUAL/ANTI-DEBUG.md
T

7.4 KiB
Raw Blame History

MANUAL — 反调试对抗与强反调试绕过 (Anti-Debug Bypass SOP)

本文件是当 Agent 在 G0/G1 阶段检测到目标存在反调试或强对抗机制时的标准化处置手册。 覆盖 Windows 用户态全量反调试技法、通用补丁模板与代理 DLL 框架接入指引。

读取顺序:先通过 seep_auto_triage 识别壳类型 → 与本文档对比签名 → 选择对应打桩策略。


一、 快速诊断表:常见反调试信号 → 处置路由

检测到的信号 最可能机制 首选处置策略
IsDebuggerPresent 出现在导入表 PEB.BeingDebugged NOP / 硬编码返回 0
NtQueryInformationProcess + 0x7 ProcessDebugPort Syscall Stub 替换
CheckRemoteDebuggerPresent 同上 Hook 返回 FALSE
GetTickCount / QueryPerformanceCounter 频繁调用 时序反调试 钩子固定返回值
CreateToolhelp32Snapshot + 进程名比对 调试器进程名检测 Hook 过滤快照列表
OutputDebugString 后检测错误码 ODS 陷阱 无操作(正常运行不受影响)
NtSetInformationThread 0x11 ThreadHideFromDebugger Hook 丢弃该调用
注册表 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug AeDebug 检测 值打补丁 / Hook RegQueryValueEx
断点扫描 / 自校验 CRC 内存完整性自检 修正 CRC 或 NOP 校验逻辑
驱动 \Device\DebuggerDetect / ObRegisterCallbacks 内核级反调试 → 参考第五节:内核层对抗

二、 用户态 PEB 系列反调试 —— 最常见(通用补丁)

2.1 IsDebuggerPresent → 返回值强制为 0

原理:kernel32!IsDebuggerPresent 本质只读 PEB.BeingDebugged (offset 0x2)。

代理 DLL / Frida 修补(推荐):

// Frida 方案
Interceptor.attach(Module.getExportByName('kernel32.dll', 'IsDebuggerPresent'), {
  onLeave: function(retval) { retval.replace(0); }
});
// 内联汇编补丁(静态,适用于代理 DLL)
// 定位 IsDebuggerPresent 首地址,写入:
// XOR EAX, EAX
// RET
// 原始字节(x64)通常是: 65 48 8B 04 25 60 00 00 00 → 替换前 3 字节
BYTE patch[] = { 0x33, 0xC0, 0xC3 }; // xor eax,eax; ret
WriteProcessMemory(hProc, pIsDebuggerPresent, patch, sizeof(patch), nullptr);

2.2 NtQueryInformationProcess 信息类 7 (ProcessDebugPort) / 30 (ProcessDebugObjectHandle)

原理:成功连接调试器时 Port != 0。

// Frida 方案
const NtQIP = Module.getExportByName('ntdll.dll', 'NtQueryInformationProcess');
Interceptor.attach(NtQIP, {
  onEnter(args) { this.cls = args[1].toInt32(); this.out = args[2]; },
  onLeave(retval) {
    if (this.cls === 7 || this.cls === 30) {
      this.out.writePointer(ptr(0));  // 清零 DebugPort / DebugObject
    }
  }
});

2.3 NtSetInformationThread → ThreadHideFromDebugger (0x11)

调用此系统调用后,调试器事件将对该线程不可见,导致断点失效。

const NtSIT = Module.getExportByName('ntdll.dll', 'NtSetInformationThread');
Interceptor.attach(NtSIT, {
  onEnter(args) {
    if (args[1].toInt32() === 0x11) {
      args[1] = ptr(0);  // 替换为无害的 ThreadBasicInformation
    }
  }
});

三、 时序反调试 —— 高频坑(修复固定返回值)

调试状态下,GetTickCount / QueryPerformanceCounter 的推进速度远慢于正常运行,常被用来识别单步调试。

// Frida 方案:固定时钟
let baseTick = Date.now();
Interceptor.replace(Module.getExportByName('kernel32.dll', 'GetTickCount'),
  new NativeCallback(() => {
    baseTick += 15;  // 模拟正常 15ms 系统时钟步进
    return baseTick;
  }, 'uint32', [])
);

四、 进程名 / 窗口名 / 文件路径特征检测 —— 改名或 Hook 快照

目标程序扫描进程快照(CreateToolhelp32Snapshot + Process32Next)比对调试器进程名: x64dbg.exe、ollydbg.exe、ida.exe、ida64.exe、cheatengine.exe 等。

最简单处置:把调试器改名为 explorer2.exe、chrome_x.exe 等。

进阶 Hook 方案:

// 过滤 Process32Next 快照,抹去调试器进程条目
const blacklist = ['x64dbg.exe', 'ida64.exe', 'ollydbg.exe', 'wireshark.exe'];
const Process32Next = Module.getExportByName('kernel32.dll', 'Process32NextW');
Interceptor.attach(Process32Next, {
  onLeave(retval) {
    if (retval.toInt32() !== 0) {
      const entry = this.context.rdx;  // PROCESSENTRY32W 结构
      const exeName = entry.add(44).readUtf16String();
      if (blacklist.some(b => exeName && exeName.toLowerCase().includes(b))) {
        // 递归调用直到返回非黑名单进程(简化处理:置为无害进程名)
        entry.add(44).writeUtf16String('System');
      }
    }
  }
});

五、 内核级反调试对抗(Ring0 / 驱动层)

适用场景:游戏反作弊(ACE/EAC/BattlEye)、强保护商业软件、带 Ring0 驱动的许可校验模块。 注意:内核对抗超出 Agent 全自动范畴,以下为指导性 SOP,需结合 WinDbg + 内核调试器手工处理。

G0 阶段主动识别信号:

  • triage 输出中存在 .sys 驱动签名或服务注册(HKLM\SYSTEM\CurrentControlSet\Services)
  • 导入表出现 NtLoadDriver、ZwCreateSection 等内核接口
  • 运行时观察到 ObRegisterCallbacks / PsSetCreateProcessNotifyRoutine 回调注册

处置策略(按难度升序):

方案 适用条件 操作要点
Kernel Patch Guard 绕过 测试用 VM / Hyper-V 关闭 Driver Signature Enforcement,加载调试补丁驱动
VirtualBox + DKOM 沙箱分析 修改 EPROCESS.DebugPort = NULL 绕过内核调试器检测
全量内存 Dump + 用户态重放 驱动已运行 在内核加载完毕后 dump 进程内存,在用户态 Loader 中重放
Emulation (Unidbg/QEMU) 需脱离真实 OS 在模拟环境中重跑关键算法,完全规避内核对抗

六、 代理 DLL 注入框架模板(统一打桩方式)

适用于不能直接修改宿主 EXE(如有数字签名)的场景。

所有上述 Hook 均可集成在以下代理 DLL 框架中,加载即自动注入,宿主签名完好:

// anti_debug_patch.cpp — 代理 DLL 打桩框架(以 version.dll 为例)
#include <Windows.h>

// 实际 version.dll 导出函数转发(保留原功能)
#pragma comment(linker, "/export:GetFileVersionInfoA=C:\\Windows\\System32\\version.GetFileVersionInfoA")
// ... 其他导出

void ApplyAntiDebugPatches() {
    // 1. 清除 PEB.BeingDebugged
    __asm {
        mov eax, fs:[30h]  // PEB
        mov byte ptr [eax+2], 0  // BeingDebugged = 0
    }
    // 2. Hook NtQueryInformationProcess (如上 Frida 方案的 C++ 等效)
    // ...
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpvReserved) {
    if (reason == DLL_PROCESS_ATTACH) ApplyAntiDebugPatches();
    return TRUE;
}

七、 两击熔断纪律 —— 反调试对抗的止损规则

情形 触发条件 处置
用户态 Hook 无效(程序仍检测到调试器) 同构打桩 2 次失败 停止,升级为内核层分析
内核层方案部署失败 同构 2 次失败 停止,改用 Emulation(Unidbg/QEMU)路线
Emulation 行为无法覆盖 无法还原算法 输出"该目标超出当前工具链全自动范畴",转人工分析