76、iOS逆向(五):越狱检测与绕过、反调试(ptrace、sysctl)

说实话,iOS逆向做到一定程度,你一定会碰到一个绕不开的坎——应用自我保护。不管是越狱检测还是反调试,都是App开发者用来防你的手段。反过来,作为逆向工程师,我们得研究怎么绕过它们。

这一讲,我带你深入看看iOS上最常见的两种防护:越狱检测反调试。我会从原理讲到实战,再给出绕过思路。嗯,都是我在真实项目中踩过的坑。

一、越狱检测:App怎么知道手机被越狱了?

越狱检测的原理其实不复杂。说白了,就是App在运行时检查一些「越狱后才会出现」的特征。我见过的大多数检测手段,无非下面这几类。

1. 文件路径检测

越狱后的iOS系统,会多出一些敏感路径。比如:

  • /Applications/Cydia.app
  • /Library/MobileSubstrate/MobileSubstrate.dylib
  • /usr/sbin/sshd
  • /etc/apt

App会调用NSFileManagerfileExistsAtPath:来检查这些文件。只要有一个存在,就判定为越狱。

核心思路:检测「正常iOS设备上不应该存在的文件」。

2. 沙箱完整性检测

越狱后,沙箱限制被打破。App可以尝试写入系统目录(比如/private/),如果能写成功,说明越狱了。

3. 动态库加载检测

越狱设备通常会注入动态库(比如Substrate插件)。App可以通过_dyld_get_image_name遍历所有已加载的dylib,看看有没有可疑的库。

4. 系统调用检测

有些App会调用fork()system()。越狱环境下这些调用通常能成功,而普通iOS App会被沙箱拦截。

我的经验:很多App会组合使用多种检测方式,而不是只靠一种。我曾经遇到一个金融类App,它同时检查了5个路径、2个系统调用,还检测了MobileSubstrate。单一绕过往往不够。

二、绕过越狱检测:思路与工具

绕过越狱检测,说白了就是「骗过App,让它以为手机没越狱」。我个人常用的方法有下面几种。

1. Hook检测函数

FridaSubstrate Hook住fileExistsAtPath:access()等函数,让它们对敏感路径返回NO-1

// Frida脚本示例:绕过文件路径检测
Interceptor.attach(ObjC.classes.NSFileManager['- fileExistsAtPath:'].implementation, {
  onEnter: function(args) {
    var path = ObjC.Object(args[2]).toString();
    if (path.indexOf('Cydia') > -1 || path.indexOf('Substrate') > -1) {
      console.log('[+] 拦截越狱路径: ' + path);
      args[2] = ObjC.classes.NSString.stringWithString_('/tmp/fake_path');
    }
  }
});

2. 使用越狱隐藏工具

比如Liberty Lite(Cydia插件)或Shadow,它们能全局拦截越狱检测API。不过要注意,有些App会检测这些工具本身。

3. 手动修改二进制

如果你不想依赖运行时Hook,可以直接修改App的Mach-O二进制,把检测逻辑的跳转指令改成nopret。这招比较硬核,但很彻底。

注意:绕过越狱检测可能违反App的条款。我只建议在合法授权的逆向分析中使用,比如你自己开发的App,或者你拥有测试权限的应用。

三、反调试:ptrace与sysctl

反调试是另一道防线。App会想办法阻止你用lldbgdb附加到进程。iOS上最常见的反调试手段就是ptracesysctl

1. ptrace反调试

ptrace是一个系统调用,可以用来控制进程的调试行为。其中PT_DENY_ATTACH(值为31)这个请求码,就是用来拒绝调试器附加的。

// 典型的ptrace反调试代码
#include <sys/ptrace.h>

void disable_debugger() {
    ptrace(PT_DENY_ATTACH, 0, 0, 0);
}

App在启动时调用这个函数,之后任何调试器都无法附加。如果你尝试用lldb attach,会直接收到Segmentation fault: 11

原理:ptrace(PT_DENY_ATTACH)会设置一个内核标志,阻止其他进程调用ptrace来跟踪当前进程。

2. sysctl反调试

sysctl是另一个系统调用,可以用来查询进程信息。App可以通过sysctl检查当前进程是否有调试器附加(比如检查p_flag中的P_TRACED标志)。

// sysctl检测调试器
#include <sys/sysctl.h>

int is_debugger_attached() {
    int name[4];
    struct kinfo_proc info;
    size_t info_size = sizeof(info);
    
    name[0] = CTL_KERN;
    name[1] = KERN_PROC;
    name[2] = KERN_PROC_PID;
    name[3] = getpid();
    
    if (sysctl(name, 4, &info, &info_size, NULL, 0) == -1) {
        return 0;
    }
    
    return (info.kp_proc.p_flag & P_TRACED) != 0;
}

如果检测到调试器,App可以主动退出、崩溃,或者进入死循环。我见过最狠的,是直接调用exit(-1)

四、绕过反调试:实战技巧

绕过反调试,核心思路就是在App执行反调试代码之前,先把它干掉。或者,直接修改它的行为。

1. 绕过ptrace

最简单的方法是用Frida Hook住ptrace函数,当参数request等于PT_DENY_ATTACH时,直接返回0。

// Frida绕过ptrace反调试
var ptrace = Module.findExportByName(null, 'ptrace');
Interceptor.attach(ptrace, {
  onEnter: function(args) {
    var request = args[0].toInt32();
    if (request === 31) { // PT_DENY_ATTACH
      console.log('[+] 拦截ptrace(PT_DENY_ATTACH)');
      args[0] = ptr(0); // 修改request为0,让ptrace什么都不做
    }
  }
});

2. 绕过sysctl

同样,Hook住sysctl函数。当检测到App在查询KERN_PROC_PID时,把返回的p_flag中的P_TRACED位清零。

// Frida绕过sysctl反调试
var sysctl = Module.findExportByName(null, 'sysctl');
Interceptor.attach(sysctl, {
  onLeave: function(retval) {
    // 这里需要更精细的逻辑,判断是否在查询调试标志
    // 实际项目中,我会检查调用栈,确认是反调试检测
    console.log('[+] sysctl返回,可能已绕过调试检测');
  }
});

3. 静态修改二进制

如果你能拿到Mach-O文件,直接找到ptracesysctl的调用点,把BL指令改成NOP。这样App根本不会执行反调试代码。

我个人的习惯:先用Frida动态分析,确认反调试的触发时机和位置。然后再决定是用Hook还是静态修改。动态分析快,但静态修改更彻底。

五、知识体系总览

下面这张图,我把越狱检测与绕过、反调试与绕过的核心逻辑画了出来。你看一眼就能明白整体脉络。

iOS逆向:越狱检测与反调试知识体系 越狱检测 文件路径检测(Cydia、Substrate等) 沙箱完整性检测(写入系统目录) 动态库加载检测(_dyld_get_image_name) 系统调用检测(fork、system) 反调试 ptrace(PT_DENY_ATTACH) sysctl(KERN_PROC_PID) + P_TRACED 主动退出 / 崩溃 / 死循环 绕过方法:Frida Hook / 静态修改二进制 / 越狱隐藏工具

六、实战中的避坑指南

讲几个我实际踩过的坑,你遇到了可以少走弯路。

我曾经在绕过一个App的ptrace反调试时,只Hook了ptrace函数,结果App还是崩溃了。后来发现,它用了sysctl做二次检测。所以,一定要同时处理ptrace和sysctl,甚至还要检查有没有其他反调试手段。

另外,有些App会在多个线程中反复调用反调试函数。你Hook一次可能不够,得确保所有线程都被拦截。Frida的Interceptor.attach默认是全局的,这点倒不用担心。

还有一个细节:越狱检测的时机。很多App在application:didFinishLaunchingWithOptions:里做检测,但也有一些会在viewDidLoad或网络请求回调里做。我建议你从App启动开始就Hook住所有敏感函数,不要等到崩溃了再排查。

我的建议:写一个Frida脚本,一次性Hook住fileExistsAtPath:accessptracesysctlforksystem等十几个常用检测点。这样不管App用哪种组合拳,你都能第一时间发现。

好了,这一讲的内容就到这里。越狱检测和反调试是iOS逆向中非常实用的技能,掌握了它们,你就能应对大多数App的防护了。

公众号:蓝海资料掘金营,微信deep3321