一、iOS完整性校验与绕过:FairPlay DRM解密、代码签名校验、越狱检测与绕过

大家好,我是老张。今天我们来聊聊iOS逆向里一个绕不开的话题——完整性校验与绕过。说实话,这玩意儿我刚开始接触的时候也头大,但搞懂了核心逻辑,你会发现其实就那么几招。

iOS的完整性校验,说白了就是系统在问三个问题:

  • 你是谁?——代码签名校验
  • 你从哪来?——FairPlay DRM验证
  • 你环境安全吗?——越狱检测

这三个问题任何一个没答好,App要么闪退,要么直接罢工。我当年在做一个金融类App的渗透测试时,就遇到过App检测到越狱环境后直接清空本地数据的情况——嗯,那场面,客户脸都绿了。

核心观点:完整性校验的本质是信任链的验证。从硬件到系统,从系统到App,每一层都在问上一级「你可靠吗?」。逆向工程要做的,就是在这个链条上找到可以插入的缝隙。

iOS完整性校验知识体系 FairPlay DRM 应用解密与验证 • 解密密钥获取 • 二进制解密 • 完整性验证 绕过:dumpdecrypted 代码签名校验 Mach-O验证 • 签名验证 • 哈希校验 • 权限验证 绕过:CoreTrust 越狱检测 环境检测 • 文件检测 • API检测 • 环境检测 绕过:Frida/Shadow 目标:让越狱检测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,然后跟硬编码在代码里的值比对。一旦发现被修改,直接弹窗「应用已损坏」然后退出。

绕过方法其实不复杂:

  1. 静态patch:找到校验函数,nop掉比较逻辑
  2. 动态hook:用Frida hook住校验函数,返回正确值
  3. 重签名:修改二进制后,用ldidcodesign重新签名
// 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-dumpHopper反编译,搜索关键词: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文件格式,看看二进制层面还有哪些可以操作的空间。


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