一、iOS完整性校验与绕过:FairPlay DRM解密、代码签名校验、越狱检测与绕过
大家好,我是老张。今天我们来聊聊iOS逆向里一个绕不开的话题——完整性校验与绕过。说实话,这玩意儿我刚开始接触的时候也头大,但搞懂了核心逻辑,你会发现其实就那么几招。
iOS的完整性校验,说白了就是系统在问三个问题:
- 你是谁?——代码签名校验
- 你从哪来?——FairPlay DRM验证
- 你环境安全吗?——越狱检测
这三个问题任何一个没答好,App要么闪退,要么直接罢工。我当年在做一个金融类App的渗透测试时,就遇到过App检测到越狱环境后直接清空本地数据的情况——嗯,那场面,客户脸都绿了。
核心观点:完整性校验的本质是信任链的验证。从硬件到系统,从系统到App,每一层都在问上一级「你可靠吗?」。逆向工程要做的,就是在这个链条上找到可以插入的缝隙。
1.1 FairPlay DRM解密——撕开App的第一层保护
FairPlay是苹果的DRM方案。你从App Store下载的App,二进制文件是加密的。解密的过程发生在运行时,由内核配合FairPlay硬件密钥完成。
我个人习惯用dumpdecrypted来做这件事。原理很简单:在App加载到内存后,从内存中把已解密的镜像dump出来。
// 编译dumpdecrypted
$ git clone https://github.com/stefanesser/dumpdecrypted
$ cd dumpdecrypted
$ make
// 使用
$ DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /path/to/App.app/App
小提示:如果你遇到dump出来的文件还是加密的,检查一下是不是用了最新的iOS版本。我记得iOS 13之后,苹果改了某些内存保护机制,老版本的dumpdecrypted会失效。这时候可以试试frida-ios-dump,它走的是另一种路子。
1.2 代码签名校验——App的身份证检查
iOS要求所有可执行代码必须有有效的签名。这个签名从编译时就绑定了,包含Team ID、Bundle ID、权限信息等。
App自己也会做签名校验。常见做法是:
- 读取
_mh_execute_header的cputype和cpusubtype - 验证
LC_CODE_SIGNATURE加载命令 - 检查签名哈希是否匹配
我曾经遇到一个App,它会在启动时计算自身二进制文件的SHA256,然后跟硬编码在代码里的值比对。一旦发现被修改,直接弹窗「应用已损坏」然后退出。
绕过方法其实不复杂:
- 静态patch:找到校验函数,nop掉比较逻辑
- 动态hook:用Frida hook住校验函数,返回正确值
- 重签名:修改二进制后,用
ldid或codesign重新签名
// Frida hook代码签名校验示例
Interceptor.attach(Module.findExportByName(null, "memset"), {
onLeave: function(retval) {
// 这里可以检测并修改签名校验结果
console.log("memset called, possible signature check");
}
});
注意:重签名需要有效的证书。个人开发者证书只能签自己的App,企业证书风险高,容易被吊销。我建议在越狱设备上直接用ldid -S伪造签名,省事。
1.3 越狱检测——App在查你的底细
越狱检测分三类:文件检测、API检测、环境检测。我们一个个来看。
1.3.1 文件检测
这是最基础的检测方式。App会检查一些越狱后才会存在的文件或目录:
// 常见的越狱检测路径
/Applications/Cydia.app
/Library/MobileSubstrate/MobileSubstrate.dylib
/bin/bash
/usr/sbin/sshd
/etc/apt
/private/var/lib/apt/
检测代码通常长这样:
- (BOOL)isJailbroken {
NSArray *paths = @[@"/Applications/Cydia.app",
@"/Library/MobileSubstrate/MobileSubstrate.dylib",
@"/bin/bash",
@"/usr/sbin/sshd"];
for (NSString *path in paths) {
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
return YES;
}
}
return NO;
}
1.3.2 API检测
App会调用一些敏感API,看返回结果是否异常。比如:
fork()、system()——越狱设备上这些函数不会被沙盒限制dlopen()——尝试加载动态库看是否成功stat()——检查某些路径的权限
我见过最狠的一个App,它会尝试fork()一个子进程,如果成功就认为越狱了。因为iOS沙盒里普通App是fork不了的。
1.3.3 环境检测
这类检测更隐蔽。App会检查:
- 是否有SSH服务在运行
- 是否安装了Substrate或Cydia Substrate
- 动态库注入痕迹
- 甚至检查
/etc/fstab是否被修改
1.4 绕过越狱检测——Frida和Shadow的实战
好了,前面说了那么多检测手段,现在聊聊怎么绕过。我个人最常用的两个工具是Frida和Shadow。
1.4.1 用Frida绕过
Frida的强项在于动态插桩。我们可以hook住检测函数,直接修改返回值。
// Frida脚本:绕过文件检测
var NSFileManager = ObjC.classes.NSFileManager;
var fileExistsAtPath = NSFileManager["- fileExistsAtPath:"];
Interceptor.attach(fileExistsAtPath.implementation, {
onLeave: function(retval) {
var path = ObjC.Object(this.args[0]);
if (path.toString().indexOf("Cydia") > -1 ||
path.toString().indexOf("bash") > -1) {
console.log("[+] Blocked jailbreak check: " + path);
retval.replace(0); // 返回NO
}
}
});
实战技巧:不要只hook一个函数。很多App会同时用多种检测方式,你只绕过一种,它还有其他检查。我建议用Frida的Stalker功能跟踪所有系统调用,把检测相关的全部拦截掉。
1.4.2 用Shadow绕过
Shadow是一个越狱隐藏工具,它的原理更底层——直接hook了内核级别的检测点。Shadow可以:
- 隐藏越狱文件
- 拦截
stat()、access()等系统调用 - 伪装系统版本信息
- 隐藏进程列表
使用Shadow很简单,安装后开启「隐藏越狱」开关就行。但要注意,Shadow本身也会被检测——有些App会检查/Library/MobileSubstrate/DynamicLibraries/Shadow.dylib是否存在。
1.5 实战:让一个越狱检测App正常运行
我们来走一遍完整流程。假设你有一个App,一打开就提示「检测到越狱环境,即将退出」。
第一步:分析检测逻辑
用class-dump或Hopper反编译,搜索关键词:jailbreak、root、cydia、substrate。找到检测函数。
第二步:确定绕过方案
如果是文件检测,用Shadow隐藏文件。如果是API检测,用Frida hook。如果是环境检测,可能需要组合使用。
第三步:实施绕过
// 完整Frida绕过脚本
if (ObjC.available) {
// 1. 绕过文件检测
var fm = ObjC.classes.NSFileManager;
Interceptor.attach(fm["- fileExistsAtPath:"].implementation, {
onLeave: function(retval) {
var path = ObjC.Object(this.args[0]).toString();
var blacklist = ["cydia", "bash", "ssh", "substrate"];
for (var i = 0; i < blacklist.length; i++) {
if (path.toLowerCase().indexOf(blacklist[i]) > -1) {
retval.replace(0);
break;
}
}
}
});
// 2. 绕过API检测
var dlopen = Module.findExportByName(null, "dlopen");
Interceptor.attach(dlopen, {
onEnter: function(args) {
var path = Memory.readUtf8String(args[0]);
if (path.indexOf("MobileSubstrate") > -1) {
this.ret = 0; // 阻止加载
}
}
});
// 3. 绕过环境检测
var system = Module.findExportByName(null, "system");
Interceptor.attach(system, {
onEnter: function(args) {
var cmd = Memory.readUtf8String(args[0]);
console.log("[system] " + cmd);
// 可以修改命令或阻止执行
}
});
}
第四步:验证效果
运行App,看是否还能正常启动。如果还不行,检查日志,看看漏掉了哪个检测点。
我的经验:有些App会做双重检测——先检测越狱,如果没检测到,再检测是否在模拟器中运行。所以绕过时要全面,别只盯着越狱检测。
小结
iOS完整性校验与绕过,说白了就是攻防双方在信任链上的博弈。FairPlay DRM是苹果设的第一道防线,代码签名是第二道,越狱检测是第三道。作为逆向工程师,我们要做的就是理解每一道防线的原理,然后找到对应的绕过方法。
记住一点:没有绝对安全的系统。任何校验,只要是在用户态执行的,就一定能被绕过。区别只在于绕过的成本和复杂度。
好了,这一章就到这里。下一章我们会深入Mach-O文件格式,看看二进制层面还有哪些可以操作的空间。