关于 anti-debugging-techniques
用于分析和绕过 Linux 与 Windows 二进制程序反调试措施的安全技能,整理检测手法、绕过方法及相关分析工具。
主要功能
- 01Linux ptrace 与进程检查
- 02Windows API 和 PEB 检查
- 03计时与信号检测
- 04TLS 回调与 VEH 技术
- 05绕过方法和工具参考
适用场景
- 01分析在调试器下退出的二进制程序
- 02研究恶意软件的反调试检查
- 03处理 CTF 反调试挑战
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill anti-debugging-techniques安装后在 Claude Code 中输入/anti-debugging-techniques确认已加载。
附带文件ANTI_DEBUG_MATRIX.md
详细介绍
技能:反调试技术——检测与绕过手册
AI 加载说明:涵盖 Linux 和 Windows 上的专家级反调试技术,包括 ptrace、PEB 标志、NtQueryInformationProcess、时序攻击、基于信号的检测、TLS 回调、VEH 技巧及相应的绕过方法。基础模型经常会忽略用户态与内核态检测之间的区别,以及各自正确的补丁策略。
0. 相关路由
- 当二进制文件还使用控制流平坦化、VM 保护或字符串加密时,参阅 代码混淆与反混淆
- 当反调试逻辑位于自定义 VM 分发器中时,参阅 VM 与字节码逆向
- 想要以符号方式完全跳过反调试检查时,参阅 符号执行工具
高级参考
在需要以下内容时,还应加载 ANTI_DEBUG_MATRIX.md:
- 技术 × 操作系统 × 检测方法 × 绕过方法的完整交叉参考矩阵
- 各项技术的可靠性评级及误报说明
- 工具兼容性表(GDB、x64dbg、WinDbg、Frida、ScyllaHide)
快速绕过选项
| 检测类别 | 首选绕过方法 | 备用方案 |
|---|---|---|
| 基于 ptrace(Linux) | 使用 LD_PRELOAD 钩取 ptrace() → 返回 0 | 使用内核模块隐藏跟踪器 |
| PEB.BeingDebugged(Windows) | 修补 fs:[0x30]+0x2 处的 PEB 字节 | 使用 ScyllaHide 自动修补 |
| 时序检查(rdtsc) | 在 rdtsc 之后设置条件断点,修正寄存器 | 使用 Frida 钩取 rdtsc 返回值 |
| IsDebuggerPresent | 将调用替换为 NOP / 钩取并返回 0 | 使用 x64dbg 内置隐藏功能 |
| INT 2D / UD2 异常 | 设置 VEH 以妥善处理 | 使用 TitanHide 驱动 |
1. LINUX 反调试技术
1.1 ptrace(PTRACE_TRACEME)
经典的自行附加方式:进程调用 ptrace(PTRACE_TRACEME, 0, 0, 0)。如果调试器已附加,该调用就会失败(返回 -1)。
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) {
exit(1); // debugger detected
}
绕过方法:
| 方法 | 方式 |
|---|---|
LD_PRELOAD 垫片 | 编译共享库:long ptrace(int r, ...) { return 0; },并设置 LD_PRELOAD |
| 二进制补丁 | 将 ptrace 调用替换为 NOP,或修补返回值检查 |
| GDB 捕获 | catch syscall ptrace → 在返回时将 $rax 修改为 0 |
| 内核模块 | 钩取 sys_ptrace 以允许多个跟踪器 |
1.2 /proc/self/status——TracerPid
FILE *f = fopen("/proc/self/status", "r");
// parse TracerPid: if non-zero → debugger attached
绕过方法:在 /proc/self 上挂载 FUSE 文件系统,或使用 LD_PRELOAD 钩取 fopen/fread,将 TracerPid 过滤为 0。
1.3 时序检查(rdtsc / clock_gettime)
测量两处之间经过的时间;调试器单步执行会造成明显延迟。
rdtsc
mov ebx, eax ; save low 32 bits
; ... protected code ...
rdtsc
sub eax, ebx
cmp eax, 0x1000 ; threshold
ja debugger_detected
绕过方法:在第二个 rdtsc 之后设置硬件断点,将 eax 修改为通过比较。也可以使用 Frida 替换计时函数。
1.4 基于信号的检测(SIGTRAP)
volatile int caught = 0;
void handler(int sig) { caught = 1; }
signal(SIGTRAP, handler);
raise(SIGTRAP);
if (!caught) exit(1); // debugger swallowed the signal
调试器附加后,SIGTRAP 会被调试器消费,而不会传递给处理程序。绕过方法:在 GDB 中使用 handle SIGTRAP nostop pass 转发信号。
1.5 /proc/self/maps 与 LD_PRELOAD 检测
检查是否存在注入的库,或具有调试器/插桩特征的内存区域。
FILE *f = fopen("/proc/self/maps", "r");
while (fgets(buf, sizeof(buf), f)) {
if (strstr(buf, "frida") || strstr(buf, "LD_PRELOAD"))
exit(1);
}
绕过方法:钩取 fopen("/proc/self/maps") 以返回经过筛选的版本,或重命名 Frida 的 Agent 库。
1.6 环境变量检查
某些保护机制会检查 LD_PRELOAD、LINES、COLUMNS(由 GDB 终端设置)或调试器专用环境变量。
绕过方法:启动前取消设置可疑环境变量,或钩取 getenv()。
2. WINDOWS 反调试技术
2.1 IsDebuggerPresent / CheckRemoteDebuggerPresent
if (IsDebuggerPresent()) ExitProcess(1);
BOOL debugged = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &debugged);
if (debugged) ExitProcess(1);
绕过方法:钩取 kernel32!IsDebuggerPresent 并使其返回 0,或直接修补 PEB。
2.2 PEB 标志
| 字段 | 偏移量(x64) | 调试时的值 | 正常值 |
|---|---|---|---|
BeingDebugged | PEB+0x02 | 1 | 0 |
NtGlobalFlag | PEB+0xBC | 0x70 (FLG_HEAP_*) | 0 |
ProcessHeap.Flags | Heap+0x40 | 0x40000062 | 0x00000002 |
ProcessHeap.ForceFlags | Heap+0x44 | 0x40000060 | 0 |
mov rax, gs:[0x60] ; PEB
movzx eax, byte [rax+0x02] ; BeingDebugged
test eax, eax
jnz debugger_detected
绕过方法:将这四个字段全部清零。ScyllaHide 会自动完成此操作。
2.3 NtQueryInformationProcess
| 信息类 | 值 | 调试时的返回值 |
|---|---|---|
ProcessDebugPort | 0x07 | 非零端口 |
ProcessDebugObjectHandle | 0x1E | 有效句柄 |
ProcessDebugFlags | 0x1F | 0(反转!) |
绕过方法:挂钩 ntdll!NtQueryInformationProcess,根据各信息类返回正常值。
2.4 硬件断点检测
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &ctx);
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3)
ExitProcess(1);
绕过方法:挂钩 GetThreadContext,将 DR0–DR3 清零;或者预先使用 NtSetInformationThread(ThreadHideFromDebugger)(讽刺的是,这本身就是反调试技术)。
2.5 INT 2D / INT 3 / UD2 异常技巧
INT 2D 是内核调试服务中断。没有调试器时,它会引发 STATUS_BREAKPOINT;有调试器时,行为会有所不同(跳过字节)。
xor eax, eax
int 2dh
nop ; debugger may skip this byte
; ... divergent execution path ...
绕过方法:在 VEH 中处理,或修补中断指令。
2.6 TLS 回调
TLS 回调会在 main() / WinMain() 之前执行。放在这里的反调试检查会在调试器首次中断前运行。
绕过方法:在 x64dbg 中设置 “Break on TLS Callbacks” 选项。在 WinDbg 中,使用 sxe ld 在模块加载时中断。
2.7 NtSetInformationThread(ThreadHideFromDebugger)
NtSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
调用后,线程会对调试器不可见——断点和单步执行会悄无声息地失效。
绕过方法:挂钩 NtSetInformationThread,在 ThreadInfoClass == 0x11 时将其替换为 NOP。
2.8 基于 VEH 的检测
注册一个向量化异常处理程序,检查 EXCEPTION_RECORD 是否存在调试器特有行为(单步标志、具有调试器语义的保护页违规)。
绕过方法:理解 VEH 逻辑,并确保异常链的行为与未调试时完全相同。
3. 高级多层技术
3.1 自调试(fork + ptrace)
进程会派生一个子进程,由子进程通过 ptrace 附加到父进程。如果外部调试器已附加,子进程的 ptrace 操作就会失败。
pid_t child = fork();
if (child == 0) {
if (ptrace(PTRACE_ATTACH, getppid(), 0, 0) == -1)
kill(getppid(), SIGKILL);
else
ptrace(PTRACE_DETACH, getppid(), 0, 0);
_exit(0);
}
wait(NULL);
绕过方法:修补 fork() 的返回值,或终止/分离看门狗子进程。
3.2 多进程调试检测
父进程和子进程协同检查彼此的调试状态,形成相互监视模式。
绕过方法:同时附加到两个进程(使用 GDB 的 follow-fork-mode,或启动两个调试器实例)。
3.3 多检查点计时检测
在多个函数中分散计时检查,并比较累计偏差。单独修补会失败,因为总偏差仍会超过阈值。
绕过方法:使用 Frida Interceptor.replace 替换所有计时源(rdtsc、clock_gettime、QueryPerformanceCounter),使其返回受控值。
3.4 Nanomite / INT3 修补
将原始条件跳转替换为 INT3(0xCC)。父调试器进程会处理每个 INT3,评估条件,并相应设置子进程的 EIP。
绕过方法:通过跟踪所有 INT3 处理程序还原原始跳转表,然后修补二进制文件。
4. 对策工具
| 工具 | 平台 | 功能 |
|---|---|---|
| ScyllaHide | Windows (x64dbg/IDA/OllyDbg) | 自动修补 PEB、挂钩 NtQuery*、隐藏线程、修正计时 |
| TitanHide | Windows(内核驱动程序) | 在内核级别隐藏,绕过所有用户模式检查 |
| Frida | 跨平台 | 基于脚本挂钩任意函数、伪造计时结果 |
| LD_PRELOAD shims | Linux | 在加载时替换 ptrace、getenv、fopen |
| GDB scripts | Linux | catch syscall、条件断点、寄存器修正 |
| Qiling | 跨平台 | 全系统仿真,绕过所有硬件检查 |
5. 系统化绕过方法
Step 1: Static analysis — identify anti-debug calls
└─ Search for: ptrace, IsDebuggerPresent, NtQuery, rdtsc,
GetTickCount, SIGTRAP, INT 2D, TLS directory entries
Step 2: Classify each check
├─ API-based → hook or patch the call
├─ Flag-based → patch PEB/proc fields
├─ Timing-based → spoof time source
├─ Exception-based → forward/handle exception correctly
└─ Multi-process → handle both processes
Step 3: Apply bypass (order matters)
1. Load ScyllaHide / set LD_PRELOAD (covers 80% of checks)
2. Handle TLS callbacks (break before main)
3. Patch remaining custom checks (Frida or binary patch)
4. Verify: run with breakpoints, confirm no premature exit
Step 4: Validate bypass completeness
└─ Set BP on ExitProcess/exit/_exit — if hit unexpectedly,
a check was missed → trace back from exit call
6. 决策树
Binary exits/crashes under debugger?
│
├─ Crashes immediately before main?
│ └─ TLS callback anti-debug
│ └─ Enable TLS callback breaking in debugger
│
├─ Crashes at startup?
│ ├─ Linux: check for ptrace(TRACEME)
│ │ └─ LD_PRELOAD hook or NOP patch
│ └─ Windows: check IsDebuggerPresent / PEB
│ └─ ScyllaHide or manual PEB patch
│
├─ Crashes after some execution?
│ ├─ Consistent crash point → API-based check
│ │ ├─ NtQueryInformationProcess → hook return values
│ │ ├─ /proc/self/status → filter TracerPid
│ │ └─ Hardware BP detection → hook GetThreadContext
│ │
│ ├─ Variable crash point → timing-based check
│ │ └─ Hook rdtsc / QueryPerformanceCounter
│ │
│ └─ Crash on breakpoint hit → exception-based check
│ ├─ INT 2D / INT 3 trick → handle in VEH
│ └─ SIGTRAP handler → GDB: handle SIGTRAP pass
│
├─ Debugger loses control silently?
│ └─ ThreadHideFromDebugger
│ └─ Hook NtSetInformationThread
│
├─ Child process detects and kills parent?
│ └─ Self-debugging (fork+ptrace)
│ └─ Patch fork() or handle both processes
│
└─ All basic bypasses applied but still detected?
└─ Multi-layer / custom checks
├─ Use Frida for comprehensive API hooking
├─ Full emulation with Qiling
└─ Trace all calls to exit/abort to find remaining checks
7. CTF 与现实场景模式
常见的 CTF 反调试模式
| 模式 | 出现频率 | 快速绕过方法 |
|---|---|---|
单次 ptrace(TRACEME) | 非常常见 | 使用 LD_PRELOAD 一行命令 |
IsDebuggerPresent + NtGlobalFlag | 常见 | ScyllaHide |
| 循环中的 rdtsc 计时 | 中等 | 修补比较阈值 |
| signal(SIGTRAP) + raise | 中等 | GDB 信号转发 |
| fork + ptrace 看门狗 | 少见但棘手 | 终止子进程或修补 fork |
| Nanomite INT3 替换 | 少见(高级) | 重建跳转表 |
实际保护方案
| 保护器 | 主要反调试机制 | 推荐工具 |
|---|---|---|
| VMProtect | PEB + 计时 + 驱动级检测 | TitanHide + ScyllaHide |
| Themida | 多层 PEB + SEH + 计时 | ScyllaHide + 手动修补 |
| Enigma Protector | IsDebuggerPresent + CRC 检查 | x64dbg + ScyllaHide |
| UPX(自定义) | 通常没有(仅加壳) | 标准脱壳 |
| 自定义(恶意软件) | 差异很大 | 使用 Frida + Qiling 进行分析 |
8. 快速参考——绕过速查表
Linux 一行命令
# LD_PRELOAD anti-ptrace
echo 'long ptrace(int r, ...) { return 0; }' > /tmp/ap.c
gcc -shared -o /tmp/ap.so /tmp/ap.c
LD_PRELOAD=/tmp/ap.so ./target
# GDB: catch and bypass ptrace
(gdb) catch syscall ptrace
(gdb) commands
> set $rax = 0
> continue
> end
Frida 反调试绕过(跨平台)
// Hook IsDebuggerPresent (Windows)
Interceptor.replace(
Module.getExportByName('kernel32.dll', 'IsDebuggerPresent'),
new NativeCallback(() => 0, 'int', [])
);
// Hook ptrace (Linux)
Interceptor.replace(
Module.getExportByName(null, 'ptrace'),
new NativeCallback(() => 0, 'long', ['int', 'int', 'pointer', 'pointer'])
);
// Timing spoof
Interceptor.attach(Module.getExportByName(null, 'clock_gettime'), {
onLeave(retval) {
// manipulate timespec to hide debugger delay
}
});
x64dbg ScyllaHide 快速设置
- Plugins → ScyllaHide → Options
- 勾选:PEB BeingDebugged、NtGlobalFlag、HeapFlags
- 勾选:NtQueryInformationProcess(所有类)
- 勾选:NtSetInformationThread(HideFromDebugger)
- 勾选:GetTickCount、QueryPerformanceCounter
- 应用 → 重新启动调试会话