百趣云 百趣云的博客

SO 反调试对抗:TracerPid 检测的原理与三种绕过姿势

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 防不胜防时的终极方案:

  1. frida-server 改名改端口./fs-server -l 0.0.0.0:8888,二进制文件名改掉,端口非默认
  2. 用 patched 内核或 magisk 模块/proc 对所有进程都报 TracerPid: 0
  3. 换注入方式:不用 frida 的 ptrace 注入,改用 LD_PRELOAD 或 xposed 内联 hook,检测面完全不同

精髓:检测的"回应方式"比检测本身更重要

逆向反调试时,新手只关注"怎么绕过检测",老手先看"检测到之后干什么":

  • 直接闪退:最好对付,绕过检测点就行
  • 静默标记:检测到调试后不发作,悄悄在后续请求的参数里带上"被调试"标记上报风控——这种最阴,你以为绕过了,账号其实已经被标记。所以绕过后要验证:对比调试态和非调试态的请求参数有没有差异
  • 延迟发作:检测到后几分钟才闪退,让你误判是别的问题。对付这种用二分法:逐个禁用 hook 点,定位触发源

总结

  1. TracerPid 检测的本质是读 /proc/self/status,绕过就是伪造读取结果
  2. libc 层 hook 对付常规实现,svc 直调用 Interceptor.replace 换函数
  3. 强检测上内核级方案:改名改端口、patched 内核、换注入方式
  4. 先看检测的回应方式再动手,静默标记型最危险

交流微信:run1255

By 百趣云 阅读量:3 On