Skip to content

反调试机制加固与抗 Frida/ScyllaHide 绕过检测 ​

防御目标与威胁面 ​

在 05-anti-debug-bypass.md 中,攻击者通常采用三类手段瓦解传统单点反调试逻辑:

  1. API Hook 伪造返回值:利用 Frida Interceptor.replace / attach 劫持 IsDebuggerPresent、CheckRemoteDebuggerPresent、NtQueryInformationProcess(篡改 ProcessDebugPort/ProcessDebugObjectHandle/ProcessDebugFlags)以及阻断 NtSetInformationThread(ThreadHideFromDebugger)。
  2. NOP Patch 与插件隐藏:对 IsDebuggerPresent 等调用点直接写入 0x90 NOP,或利用 ScyllaHide 清零 PEB.BeingDebugged、NtGlobalFlag 并伪造异常链。
  3. 模拟器与无痕调试:通过 bochscpu 等全系统模拟或无硬件断点方式规避 DR0-DR7 与 rdtsc 阈值检查。

因此,现代防逆向体系不应依赖孤立的 if (IsDebuggerPresent()) exit() 分支,而应将系统 API 完整性核验、Frida 运行时特征探测与密钥耦合执行结合。

核心防御架构 ​

绕过手法加固与检测对策关键实现约束
Frida Hook ntdll/kernel32 导出启动及周期校验 ntdll!NtQueryInformationProcess、NtSetInformationThread 入口 stub验证前 8 字节是否符合 4C 8B D1 B8 ?? ?? 00 00,若为 E9/FF 25/68 则确认被 Hook
阻断 NtSetInformationThread(0x11)先调用 NtSetInformationThread(ThreadHideFromDebugger),再用 NtQueryInformationThread(0x11) 回读验证若 Frida 篡改了入参为 0xFFFFFFFF,回读结果不为 TRUE 即可揭露拦截
Frida 注入进程执行脚本扫描进程线程名、模块名、命名管道与内存字符串特征(frida-agent、gum-js-loop、gmain、linjector)同步检查 ProcessSignaturePolicy(CIG)阻断未签名 frida-agent.dll 加载
NOP 抹除反调试调用点用自身代码段校验和参与后续业务解密密钥派生(Integrity-Coupled Key Derivation)任何 0x90 补丁会改变代码哈希,导致后续数据解密静默失败而非直接暴露判定点
ScyllaHide 清零 PEB 与伪造 rdtsc结合独立内核驱动检测调试对象,或使用外部硬件/服务端时间锚定比对延迟不将 PEB.BeingDebugged 作为唯一依据,多信号交叉关联

加固实现与检测代码 ​

以下 C++ 代码实现两项关键校验:一是核查 ntdll 关键反调试/查询函数入口是否被 Frida/ScyllaHide 篡改(检测非 mov r10, rcx; mov eax, ssn 模式);二是检测 ThreadHideFromDebugger 设置回读一致性及内存中残留的 Frida 线程/特征字符串:

cpp
// anti_debug_hardening.cpp: ntdll 入口完整性校验 + ThreadHide 回读 + Frida 探测
#include <windows.h>
#include <winternl.h>
#include <cstdint>
#include <cstring>
#include <vector>

typedef NTSTATUS(NTAPI* pNtSetInformationThread)(HANDLE, ULONG, PVOID, ULONG);
typedef NTSTATUS(NTAPI* pNtQueryInformationThread)(HANDLE, ULONG, PVOID, ULONG, PULONG);

bool IsNtdllStubIntact(const char* funcName) {
    HMODULE hNt = GetModuleHandleW(L"ntdll.dll");
    if (!hNt) return false;
    const auto* p = reinterpret_cast<const uint8_t*>(GetProcAddress(hNt, funcName));
    if (!p) return false;
    // x64 ntdll 标准 syscall stub 开头: 4C 8B D1 B8 <ssn:4B>
#ifdef _WIN64
    return (p[0] == 0x4C && p[1] == 0x8B && p[2] == 0xD1 && p[3] == 0xB8);
#else
    return (p[0] == 0xB8); // x86 mov eax, ssn
#endif
}

bool VerifyThreadHideNotIntercepted() {
    HMODULE hNt = GetModuleHandleW(L"ntdll.dll");
    auto NtSIT = reinterpret_cast<pNtSetInformationThread>(GetProcAddress(hNt, "NtSetInformationThread"));
    auto NtQIT = reinterpret_cast<pNtQueryInformationThread>(GetProcAddress(hNt, "NtQueryInformationThread"));
    if (!NtSIT || !NtQIT) return false;

    constexpr ULONG ThreadHideFromDebugger = 0x11;
    // 若 Frida 将 ThreadHideFromDebugger 篡改为 0xFFFFFFFF,设置调用会返回错误或回读仍为 FALSE
    NTSTATUS st = NtSIT(GetCurrentThread(), ThreadHideFromDebugger, nullptr, 0);
    if (st != 0) return false;

    BOOLEAN isHidden = FALSE;
    ULONG retLen = 0;
    st = NtQIT(GetCurrentThread(), ThreadHideFromDebugger, &isHidden, sizeof(isHidden), &retLen);
    return (st == 0 && isHidden == TRUE);
}

bool DetectFridaMemoryArtifacts() {
    const char* markers[] = {"frida-agent", "gum-js-loop", "LIBFRIDA"};
    SYSTEM_INFO si = {};
    GetSystemInfo(&si);
    uintptr_t cur = reinterpret_cast<uintptr_t>(si.lpMinimumApplicationAddress);
    uintptr_t maxA = reinterpret_cast<uintptr_t>(si.lpMaximumApplicationAddress);

    MEMORY_BASIC_INFORMATION mbi = {};
    while (cur < maxA && VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi))) {
        if (mbi.State == MEM_COMMIT && (mbi.Protect & (PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READ))) {
            if (mbi.Type == MEM_PRIVATE && mbi.RegionSize <= 4 * 1024 * 1024) {
                const char* buf = reinterpret_cast<const char*>(mbi.BaseAddress);
                for (const char* marker : markers) {
                    size_t mlen = std::strlen(marker);
                    for (SIZE_T i = 0; i + mlen <= mbi.RegionSize; ++i) {
                        if (std::memcmp(buf + i, marker, mlen) == 0) return true;
                    }
                }
            }
        }
        cur += mbi.RegionSize;
    }
    return false;
}

以下 Python 脚本用于离线检查保护代码区段哈希与 ntdll stub 状态向量,验证无异常 Hook 或 NOP 覆盖:

python
"""verify_antidebug_integrity.py: 检查 ntdll stub 签名及关键反调试函数完整性"""
from __future__ import annotations
import hashlib


def check_ntdll_stub_x64(stub_bytes: bytes) -> bool:
    return len(stub_bytes) >= 8 and stub_bytes[:4] == b"\x4C\x8B\xD1\xB8"


def derive_coupled_key(code_slice: bytes, master_secret: bytes) -> bytes:
    """将反调试/校验函数字节哈希耦合进业务密钥,防止 NOP Patch 跳过检查"""
    code_digest = hashlib.sha256(code_slice).digest()
    return hashlib.sha256(master_secret + code_digest).digest()


if __name__ == "__main__":
    clean_stub = b"\x4C\x8B\xD1\xB8\x19\x00\x00\x00"
    hooked_stub = b"\xE9\x44\x33\x22\x11\x90\x90\x90"
    assert check_ntdll_stub_x64(clean_stub) is True
    assert check_ntdll_stub_x64(hooked_stub) is False

    orig_fn = b"\x48\x83\xEC\x28\xFF\x15\x10\x00\x00\x00\x85\xC0\x74\x05"
    nopped_fn = b"\x48\x83\xEC\x28\x90\x90\x90\x90\x90\x90\x85\xC0\x74\x05"
    secret = b"session-root-key"
    assert derive_coupled_key(orig_fn, secret) != derive_coupled_key(nopped_fn, secret)
    print("Anti-debug hardening self-check passed.")

防御工作流 ​

text
1. 启用 CIG 阻断未签名 Agent:进程初始化激活 ProcessSignaturePolicy,阻止未签名的 frida-agent.dll 载入
2. 校验系统导出完整性:检查 ntdll!NtQueryInformationProcess、NtSetInformationThread 与 kernel32!IsDebuggerPresent 入口字节是否含跳转补丁
3. 执行 ThreadHide 闭环核验:调用 NtSetInformationThread(0x11) 后立即通过 NtQueryInformationThread(0x11) 验证回读状态
4. 消除显式布尔分支:将反调试代码段哈希混入后续业务密钥派生过程,杜绝单点 NOP 绕过
5. 部署运行时异常特征扫描:定期检查进程线程名与私有内存区是否出现 Frida/Gum 运行时标识

验证与判定标准 ​

  • Frida Hook 检出:附加 Frida 并拦截 NtQueryInformationProcess 或 NtSetInformationThread 后,IsNtdllStubIntact 必须返回 false 或 VerifyThreadHideNotIntercepted 必须返回 false。
  • NOP 补丁失效:将任一反调试调用替换为 0x90 字节序列后,derive_coupled_key 输出的密钥必须发生变化,导致后续加密资源解密失败。
  • 未签名 Agent 阻断:在启用 CIG 后,尝试注入未签名 frida-agent 模块必须被操作系统拦截失败。

MCP 工具映射 ​

防御步骤MCP 工具说明
检查样本防护标志与导入 APItriage_pe、ghidra_summary_imports梳理 IsDebuggerPresent、NtQueryInformationProcess 等调用位置
分析反调试函数耦合面ghidra_headless_analyze识别反调试判断分支是否易被单点 NOP 跳过
生成内存特征检测规则search_pattern、make_yara_stub生成针对 frida-agent、gum-js-loop 的检测特征
检索红蓝成对文档kb_router对照 05-anti-debug-bypass.md 全部绕过手段

Apache-2.0 · For authorized learning and defensive research only · Disclaimer