76、iOS逆向(五):越狱检测与绕过、反调试(ptrace、sysctl)
说实话,iOS逆向做到一定程度,你一定会碰到一个绕不开的坎——应用自我保护。不管是越狱检测还是反调试,都是App开发者用来防你的手段。反过来,作为逆向工程师,我们得研究怎么绕过它们。
这一讲,我带你深入看看iOS上最常见的两种防护:越狱检测和反调试。我会从原理讲到实战,再给出绕过思路。嗯,都是我在真实项目中踩过的坑。
一、越狱检测:App怎么知道手机被越狱了?
越狱检测的原理其实不复杂。说白了,就是App在运行时检查一些「越狱后才会出现」的特征。我见过的大多数检测手段,无非下面这几类。
1. 文件路径检测
越狱后的iOS系统,会多出一些敏感路径。比如:
/Applications/Cydia.app/Library/MobileSubstrate/MobileSubstrate.dylib/usr/sbin/sshd/etc/apt
App会调用NSFileManager的fileExistsAtPath:来检查这些文件。只要有一个存在,就判定为越狱。
核心思路:检测「正常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检测函数
用Frida或Substrate 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二进制,把检测逻辑的跳转指令改成nop或ret。这招比较硬核,但很彻底。
注意:绕过越狱检测可能违反App的条款。我只建议在合法授权的逆向分析中使用,比如你自己开发的App,或者你拥有测试权限的应用。
三、反调试:ptrace与sysctl
反调试是另一道防线。App会想办法阻止你用lldb或gdb附加到进程。iOS上最常见的反调试手段就是ptrace和sysctl。
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文件,直接找到ptrace或sysctl的调用点,把BL指令改成NOP。这样App根本不会执行反调试代码。
我个人的习惯:先用Frida动态分析,确认反调试的触发时机和位置。然后再决定是用Hook还是静态修改。动态分析快,但静态修改更彻底。
五、知识体系总览
下面这张图,我把越狱检测与绕过、反调试与绕过的核心逻辑画了出来。你看一眼就能明白整体脉络。
六、实战中的避坑指南
讲几个我实际踩过的坑,你遇到了可以少走弯路。
我曾经在绕过一个App的ptrace反调试时,只Hook了ptrace函数,结果App还是崩溃了。后来发现,它用了sysctl做二次检测。所以,一定要同时处理ptrace和sysctl,甚至还要检查有没有其他反调试手段。
另外,有些App会在多个线程中反复调用反调试函数。你Hook一次可能不够,得确保所有线程都被拦截。Frida的Interceptor.attach默认是全局的,这点倒不用担心。
还有一个细节:越狱检测的时机。很多App在application:didFinishLaunchingWithOptions:里做检测,但也有一些会在viewDidLoad或网络请求回调里做。我建议你从App启动开始就Hook住所有敏感函数,不要等到崩溃了再排查。
我的建议:写一个Frida脚本,一次性Hook住fileExistsAtPath:、access、ptrace、sysctl、fork、system等十几个常用检测点。这样不管App用哪种组合拳,你都能第一时间发现。
好了,这一讲的内容就到这里。越狱检测和反调试是iOS逆向中非常实用的技能,掌握了它们,你就能应对大多数App的防护了。