Skip to content

反调试检测与绕过 ​

场景 ​

目标 PE 有反调试保护(IsDebuggerPresent、NtGlobalFlag、PEB 标志),附加调试器后立即退出/行为异常。

输入信号 ​

  • x64dbg 附加后目标闪退或静默退出
  • 目标在无调试器时正常运行
  • 导入表有 IsDebuggerPresent、CheckRemoteDebuggerPresent、NtQueryInformationProcess
  • 代码中存在 int 3 或 rdtsc 时间差检测

反调试清单 ​

1. PEB->BeingDebugged (最常用) ​

cpp
// 目标检测方式:
BOOL IsDebugged_PEB() {
    // x64: PEB 在 GS:[0x60]
    // x86: PEB 在 FS:[0x30]
    PPEB peb = (PPEB)__readgsqword(0x60);
    return peb->BeingDebugged;  // +0x02
}

// 绕过: 直接清零
void Bypass_PEB() {
    uint8_t* peb = (uint8_t*)__readgsqword(0x60);
    *(uint8_t*)(peb + 0x02) = 0;  // BeingDebugged = 0
}

2. IsDebuggerPresent ​

cpp
// 目标: IsDebuggerPresent() → 内部读 PEB.BeingDebugged
// 绕过: Hook IsDebuggerPresent 返回 FALSE
// 或: 在调用前 NOP 掉

3. NtGlobalFlag ​

cpp
// 目标: PEB 中 +0xBC (x64: +0xBC, x86: +0x68)
// 调试器启动的进程被设置特定标志位:
// FLG_HEAP_ENABLE_TAIL_CHECK   = 0x10
// FLG_HEAP_ENABLE_FREE_CHECK   = 0x20
// FLG_HEAP_VALIDATE_PARAMETERS = 0x40
// 合计 = 0x70 (调试模式典型值)

// 绕过: 清零 NtGlobalFlag
void Bypass_NtGlobalFlag() {
    uint8_t* peb = (uint8_t*)__readgsqword(0x60);
    *(uint32_t*)(peb + 0xBC) = 0;   // x64 偏移
    // *(uint32_t*)(peb + 0x68) = 0; // x86 偏移
}

4. NtQueryInformationProcess ​

cpp
// 目标: 用 NtQueryInformationProcess(ProcessDebugPort) 检测调试端口
// 调试器附加后内核设置 DebugPort 非零

// 绕过 Frida 脚本:
var NtQueryInfo = Module.findExportByName("ntdll.dll",
    "NtQueryInformationProcess")
Interceptor.attach(NtQueryInfo, {
    onLeave: function(ret) {
        // ProcessDebugPort = 7
        if (this.ProcessInformationClass === 7) {
            Memory.writeUInt(this.ProcessInformation, 0)
        }
    }
})

5. 时间差检测 (rdtsc / QueryPerformanceCounter) ​

cpp
// 目标: 在两个检测点之间测时间, 如果过长→有调试器
// 绕过: 硬断不要停在检测区域内, 或 patch 阈值

6. 硬件断点检测 ​

cpp
// 目标: 读 DR0-DR7 调试寄存器, 非零→有硬件断点
// 绕过: Frida 不用硬件断点, or 在检测前清空 DR 寄存器
void ClearHWBp() {
    __asm {
        xor eax, eax
        mov dr0, eax
        mov dr1, eax
        mov dr2, eax
        mov dr3, eax
        mov dr7, eax
    }
}

VMP 自检专项(VMProtect) ​

VMProtect 的自检(SDK 或运行时注入)独立于虚拟化,检测手段与绕过:

检测手段说明绕过
VMProtectIsDebuggerPresent()SDK API,内部组合 PEB/异常链/时间检测Frida hook 返回 0;或 patch 调用点(见 05-crypto-unpack/02-vmp-virtualization-analysis)
int 2d / int 3 异常链检测未接调试器时异常处理链吞掉,接了则断下ScyllaHide 异常链伪造选项
RDTSC 时间差检测单步/断点延迟硬断不停检测区;patch 阈值
虚拟化工具检测检测调试器插件/模拟环境ScyllaHide 全选隐藏;或 bochscpu 全系统模拟执行

推荐前置方案:x64dbg 加载 ScyllaHide 插件(外部工具,需自行安装)勾选全部隐藏选项,再处理 VMP 样本;更隐蔽用 bochscpu(x64dbg 插件,Bochs 模拟器内执行,目标感知不到调试器,速度较慢)。

javascript
// Frida 早期替换 VMP SDK 自检(若导出存在)
var names = ["VMProtectIsDebuggerPresent", "VMProtectIsVirtualMachinePresent"]
names.forEach(function (n) {
    var p = Module.findExportByName(null, n)
    if (p) Interceptor.replace(p, new NativeCallback(function () {
        return 0
    }, 'int', []))
})

绕过实战脚本 ​

javascript
// Frida 通用反调试绕过
function bypassAntiDebug() {
    // 1. PEB BeingDebugged
    var peb = Process.findModuleByName("target.exe").base
    // ... 但 PEB 不在模块中, 在 TEB 指向的位置
    // 方法: hook IsDebuggerPresent

    var isDebug = Module.findExportByName("kernel32.dll", "IsDebuggerPresent")
    Interceptor.replace(isDebug, new NativeCallback(function() {
        return 0
    }, 'int', []))

    // 2. NtQueryInformationProcess
    var NtQIP = Module.findExportByName("ntdll.dll",
        "NtQueryInformationProcess")
    Interceptor.attach(NtQIP, {
        onEnter: function(args) {
            this.infoClass = args[1].toInt32()
        },
        onLeave: function(ret) {
            if (this.infoClass === 7) { // ProcessDebugPort
                Memory.writeUInt(this.context.rdx, 0)
            }
            if (this.infoClass === 30) { // ProcessDebugObjectHandle
                Memory.writeUInt(this.context.rdx, 0)
            }
            if (this.infoClass === 31) { // ProcessDebugFlags
                Memory.writeUInt(this.context.rdx, 1)
            }
        }
    })

    // 3. CheckRemoteDebuggerPresent
    var checkDbg = Module.findExportByName("kernel32.dll",
        "CheckRemoteDebuggerPresent")
    Interceptor.replace(checkDbg, new NativeCallback(function() {
        return 0
    }, 'int', []))

    // 4. NtSetInformationThread (HideThreadFromDebugger)
    var NtSIT = Module.findExportByName("ntdll.dll",
        "NtSetInformationThread")
    Interceptor.attach(NtSIT, {
        onEnter: function(args) {
            if (args[1].toInt32() === 0x11) { // ThreadHideFromDebugger
                // 阻止隐藏
                args[1] = ptr(0xFFFFFFFF)
            }
        }
    })
}

Patch 模式汇总 ​

cpp
// 常见反调试函数的 NOP Patch
struct AntiDebugPatch {
    const char* dll;
    const char* func;
    size_t patchLen;     // NOP 长度
} patches[] = {
    {"kernel32.dll", "IsDebuggerPresent", 5},
    {"kernel32.dll", "CheckRemoteDebuggerPresent", 5},
    {"ntdll.dll", "NtQueryInformationProcess", 5},
    {"kernel32.dll", "OutputDebugStringA", 5},   // 反反调试: 阻止字符串
};

// 批量 patch
for (auto& p : patches) {
    void* addr = GetProcAddress(GetModuleHandleA(p.dll), p.func);
    if (addr) WriteNOPSled(addr, p.patchLen);
}

攻击链 ​

目标闪退 → 导入表搜 IsDebuggerPresent/NtQueryInformationProcess
→ Ghidra 搜 GS:[0x60] / FS:[0x30] 引用 → 确认反调试类型
→ x64dbg: 在这些函数设断点 → 回溯调用者
→ Frida: 替换/Hook 反调试函数 → 或 Patch 检测代码为 NOP
→ 验证目标不再闪退

MCP 工具映射 ​

AI Agent 可调用以下 MCP 工具自动完成或加速上述攻击链步骤:

攻击链步骤MCP 工具说明
搜索 antidebug 行为函数ghidra_summary_call_focus搜索 antidebug 行为函数(behavior="antidebug")
在反调试 API 处自动生成断点make_x64dbg_breakpoint_script在反调试 API 处自动生成断点(presets="antidebug")
生成包含反调试绕过的动态分析包make_pe_crypto_unpack_plan生成包含反调试绕过的动态分析包

证据与验证闭环 ​

  • 记录样本 SHA256、架构、映像基址、RVA/VA/文件偏移换算及工具版本。
  • 静态结论绑定函数、Xref、导入、字符串和反编译片段;动态结论绑定断点、寄存器、栈、内存与调用时序。
  • 记录样本哈希、dump/patch 路径、原始字节、新字节和行为差异。
  • 将 x64dbg/Frida/Procmon/Ghidra 输出保存到 exports/windows/,按同一输入重跑,确认断点命中、内存变化和输出差异。

GPL-3.0 · 仅供授权环境下的学习与防御性研究使用