PTRACE_TRACEME 与反调试
文章目录
- PTRACE_TRACEME 与反调试
- TRACEME 的底层机制
- 内核级标记
- 后续 - 触发中断
- 使用 ptrace 单步调试的代码模式:
- 反调试:抢占调试器插槽
- 绕过 TRACEME 反调试
- 对抗手段:
PTRACE_TRACEME 与反调试
设置 PTRACE_TRACEME
是 Linux 的经典反调试手段,因为系统限制调试是独占的,一个进程只能有一个 debugger。我们看下面两种 Linux 系统调用模式,其中 PTRACE_TRACEME
适用于让父进程调试自己,PTRACE_ATTACH
适用于调试器附加到其他进程。
// TRACEME demo
pid_t child_pid;
child_pid = fork();
if (child_pid == 0) {
// 子进程:声明自己被跟踪,允许父进程跟踪
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) < 0) { /* err */ }
printf("Child: I'm being traced!\n");
// 子进程执行一些操作
execl("/bin/ls", "ls", NULL); // 替换为 ls 命令
} else if (child_pid > 0) {
// 父进程:等待子进程停止
int status;
waitpid(child_pid, &status, 0);
if (WIFSTOPPED(status)) printf("Parent: Child is stopped by ptrace.\n");
// 父进程继续跟踪子进程
ptrace(PTRACE_CONT, child_pid, NULL, NULL);
// 等待子进程结束
waitpid(child_pid, &status, 0);
printf("Parent: Child process exited.\n");
}
// output
Child: I'm being traced!
Parent: Child is stopped by ptrace.
trace-me trace-me.c
Parent: Child process exited.
// PTRACE_ATTACH demo
pid_t target_pid;
printf("Enter the PID of the process to attach: ");
scanf("%d", &target_pid);
// 附加到目标进程,目标进程会受到 SIGSTOP 停止
if (ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) < 0) { /* err */ }
printf("Attached to process %d\n", target_pid);
// 等待目标进程停止
int status;
waitpid(target_pid, &status, 0);
if (WIFSTOPPED(status)) printf("Process %d is stopped.\n", target_pid);
// 继续运行目标进程
if (ptrace(PTRACE_CONT, target_pid, NULL, NULL) < 0) { /* err */ }
printf("Process %d continued.\n", target_pid);
// 分离目标进程
if (ptrace(PTRACE_DETACH, target_pid, NULL, NULL) < 0) { /* err */ }
printf("Detached from process %d\n", target_pid);
}
TRACEME 的底层机制
内核级标记
当进程调用 ptrace(PTRACE_TRACEME, 0, 0, 0)
时,内核会在该进程的 task_struct
中设置 PT_PTRACED
标志位。此标志表示该进程已主动声明 “允许被父进程跟踪”,同时隐式触发以下规则:
- 一个进程同一时间只能被一个调试器跟踪(无论是通过
PTRACE_TRACEME
还是PTRACE_ATTACH
)。 - 若后续有其他调试器尝试附加(如 gdb 的 attach 命令),内核会拒绝并返回 EPERM 错误。
后续 - 触发中断
设置标记不代表子进程立刻就会被中断,必须收到停止信号时才会被父进程捕获。通常的做法是调用 exec()
触发一个 SIGTRAP
。当前进程被标记为 PT_PTRACED
后,在调用 exec
系列函数时,内部的 load_elf_binary
函数会给自己发送 SIGTRAP
信号触发信号处理。
通用的 do_signal
函数在处理 PT_PTRACED
进程时会触发特殊逻辑。它会停止 current_thread,给父进程发送 SIGCHLD
信号,然后调用 schedule
切换时间片。此时子进程被父进程通过 waitpid
捕获。父进程之后可以使用 PTRACE_CONT
, PTRACE_SINGLESTEP
调试子进程。
使用 ptrace 单步调试的代码模式:
ptrace 请求 | 作用 |
---|---|
PTRACE_TRACEME | 设置当前进程为可被跟踪状态,并在 execve() 后触发 SIGTRAP |
PTRACE_GETREGS | 获取寄存器信息 |
PTRACE_PEEKDATA | 读取进程内存 |
PTRACE_POKEDATA | 修改进程内存 |
PTRACE_SETREGS | 修改寄存器信息 |
PTRACE_SINGLESTEP | 单步执行一条指令 |
PTRACE_SYSCALL | 在系统调用进入和退出时暂停 |
具体操作流程
- 父进程 fork 出一个子进程
- 子进程调用
PTRACE_TRACEME
并执行execve()
- 父进程 wait 子进程
SIGTRAP
- 父进程循环调用
ptrace(PTRACE_SYSCALL, ...)
或PTRACE_SINGLESTEP
进行调试**PTRACE_GETREGS
获取rip
(指令地址)、rax
(返回值)、orig_rax
(系统调用号)PTRACE_PEEKDATA
读取内存内容( get/set 内存的数据和指令)PTRACE_POKEDATA
修改指令执行行为,配合单步 rip 可以解析当前汇编,执行到特定地址时修改寄存器。
SYSCALL
分别在 syscall 的 enter 和 exit 时触发,配合 orig_rax, rax 可以判断在哪个 syscall
反调试:抢占调试器插槽
子进程在启动时立即调用 PTRACE_TRACEME
,相当于提前占用了唯一的调试器插槽。即使父进程不进行任何实际跟踪操作,该插槽已被占用,导致外部调试器无法通过常规手段附加。
例如 XCTF 的 LoopCrypto 首先调用 TRACEME,然后检查 /proc/pid/status
的 TracerPid 字段等于零,来保证成功设置 TRACEME
(感觉应该类似判断返回值小于零?)
绕过 TRACEME 反调试
利用时间差:在子进程调用 PTRACE_TRACEME 前,父进程或外部程序能快速附加调试器,仍可拦截子进程。
静默跟踪:父进程可通过 PTRACE_SEIZE(非侵入式跟踪)或直接忽略跟踪事件,避免触发信号暂停,使子进程看似未被调试。
对抗手段:
- 代码修补:修改二进制,移除 ptrace 调用或跳过相关代码。
- 内核模块:通过自定义内核模块强制清除 PT_PTRACED 标志。
- Frida 等工具:使用 frida-server 或 LD_PRELOAD 注入,在进程内存中动态禁用反调试代码。例如在二进制加载后运行前直接 hook 掉 init array 中的反调试函数。(XCTF LoopCrypto)