TracerPid 检测的原理
Linux 下进程被 ptrace 附加时,/proc/self/status 里的 TracerPid 字段会变成调试器的 PID,未被调试时是 0。SO 反调试的标准做法就是周期性读这个文件:
// 典型检测代码(SO 里的实现)
int check_debugger() {
char buf[512];
int fd = open("/proc/self/status", O_RDONLY);
read(fd, buf, sizeof(buf));
close(fd);
char* p = strstr(buf, "TracerPid:");
int pid = atoi(p + 10);
if (pid != 0) {
// 被调试:闪退、死循环、或悄悄标记上报
kill(getpid(), SIGKILL);
}
return pid;
}高级一点的壳不用 open/read(容易被 hook),改用系统调用 svc 直接发起,或者 openat + pread。绕过前先在 IDA 里确认它用的哪个 API。
姿势一:hook libc 的 open/read(对付常规实现)
var open = Module.findExportByName('libc.so', 'open');
Interceptor.attach(open, {
onEnter: function(args) {
var path = args[0].readCString();
if (path === '/proc/self/status' || path === '/proc/self/stat') {
this.fake = true;
}
},
onLeave: function(retval) {
// 不能简单阻止 open(它后面还要读别的),放行但标记
}
});
var read = Module.findExportByName('libc.so', 'read');
Interceptor.attach(read, {
onEnter: function(args) { this.buf = args[1]; this.len = args[2]; },
onLeave: function(retval) {
if (this.buf && retval.toInt32() > 0) {
try {
var content = this.buf.readCString(retval.toInt32());
if (content && content.indexOf('TracerPid') >= 0) {
var fixed = content.replace(/TracerPid:\s*\d+/, 'TracerPid:\t0');
this.buf.writeUtf8String(fixed.padEnd(retval.toInt32(), '\0'));
}
} catch(e) {}
}
}
});姿势二:hook svc 系统调用(对付 syscall 直调)
壳用汇编 svc #0 直接发系统调用时,libc 层的 hook 全部失效。这时要在更底层动手——hook 不了 svc 本身,但可以 hook 它的"前后":
// 思路:找到 SO 里发起 svc 的指令地址,用 Interceptor 替换
// 先用 IDA 定位 svc 指令偏移,然后:
var soBase = Module.findBaseAddress('libtarget.so');
var svcAddr = soBase.add(0x1234); // svc 指令地址,以实际分析为准
// 把 openat(__NR_openat) 的返回 fd 替换为一个假文件的 fd
// 更实用的做法:提前 open 一个自己伪造的 status 文件,把 fd 换进去更通用的方案是 Frida 的 Stalker 或直接 patch:把检测函数的返回值 patch 成 0。在 IDA 里定位检测函数后:
var checkAddr = Module.findBaseAddress('libtarget.so').add(0x5678);
Interceptor.replace(checkAddr, new NativeCallback(function() {
return 0; // 永远返回"未被调试"
}, 'int', []));Interceptor.replace 是最干净的姿势——检测函数整个被替换,不执行原始逻辑,连时序特征都不会留下。
姿势三:内核/镜像级欺骗(对付强检测)
商业壳(某盾、某几安全)会做多重校验:TracerPid + 进程名 + frida 端口 + 内存特征扫描。应用层 hook 防不胜防时的终极方案:
- frida-server 改名改端口:
./fs-server -l 0.0.0.0:8888,二进制文件名改掉,端口非默认 - 用 patched 内核或 magisk 模块让
/proc对所有进程都报 TracerPid: 0 - 换注入方式:不用 frida 的 ptrace 注入,改用 LD_PRELOAD 或 xposed 内联 hook,检测面完全不同
精髓:检测的"回应方式"比检测本身更重要
逆向反调试时,新手只关注"怎么绕过检测",老手先看"检测到之后干什么":
- 直接闪退:最好对付,绕过检测点就行
- 静默标记:检测到调试后不发作,悄悄在后续请求的参数里带上"被调试"标记上报风控——这种最阴,你以为绕过了,账号其实已经被标记。所以绕过后要验证:对比调试态和非调试态的请求参数有没有差异
- 延迟发作:检测到后几分钟才闪退,让你误判是别的问题。对付这种用二分法:逐个禁用 hook 点,定位触发源
总结
- TracerPid 检测的本质是读
/proc/self/status,绕过就是伪造读取结果 - libc 层 hook 对付常规实现,svc 直调用 Interceptor.replace 换函数
- 强检测上内核级方案:改名改端口、patched 内核、换注入方式
- 先看检测的回应方式再动手,静默标记型最危险
交流微信:run1255