反调试机制加固与抗 Frida/ScyllaHide 绕过检测
防御目标与威胁面
在 05-anti-debug-bypass.md 中,攻击者通常采用三类手段瓦解传统单点反调试逻辑:
- API Hook 伪造返回值:利用 Frida
Interceptor.replace/attach劫持IsDebuggerPresent、CheckRemoteDebuggerPresent、NtQueryInformationProcess(篡改ProcessDebugPort/ProcessDebugObjectHandle/ProcessDebugFlags)以及阻断NtSetInformationThread(ThreadHideFromDebugger)。 - NOP Patch 与插件隐藏:对
IsDebuggerPresent等调用点直接写入0x90NOP,或利用 ScyllaHide 清零PEB.BeingDebugged、NtGlobalFlag并伪造异常链。 - 模拟器与无痕调试:通过
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 工具 | 说明 |
|---|---|---|
| 检查样本防护标志与导入 API | triage_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 全部绕过手段 |