记得去年暑假,大概也是这会,我刚刚入门iOS安全方向的研究。那会老师给的第一个工作是,你能不能给我砸壳这几个ipa。然后简单学习了下大概知道了砸壳是个什么样的事情:

简而言之,苹果给分发的ipa进行了加密,导致我们从公开渠道(包括ipatool/Asspp/…/哪怕是越狱的iPhone)上获得的ipa是没法直接拖进ida进行分析的。但是又因为ipa肯定是要在设备上运行的,也就是说我们只需要把运行时解密好的程序dump下来就可以了。

然而这个做法会有一个缺陷——在旧版的手机上没法安装并dump新版的ipa,只有等到新版本的iPhone被越狱了才能dump(吗?

于是有了第二个方法:假装我们自己是dyld,手动加载可执行,并且让设备主动给我们解密。

mremap_encrypt

大概的系统调用就是这个mremap_encrypt,还有mmap之类的,先mmap映射可执行文件到内存(这里要用 PROT_READ | PROT_EXEC),然后mremap_encrypt去解密。于是我们发现了flexdecrypt(或者类似unfaird)但是大家发现它只能在macOS(11.3之前的)arm版上才能使用~~(可恶,为什么x86版不行,为什么Virtualization的虚拟机框架只给到了 12+)~~

那为什么iOS不行呢?众所周知iOS是一个权限管理非常非常非常严格的设备,如果权限不够什么都没法用。骗你的,权限够了还是有拿不到的东西。

我们只需要简单伪造一下entitlements,coretrust绕过了就好吧?

xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>application-identifier</key>
    <string>wiki.qaq.unfaird</string>
    <key>com.apple.developer.team-identifier</key>
    <string>-</string>
    <key>platform-application</key>
    <true/>
    <key>com.apple.private.security.no-container</key>
    <true/>
    <key>com.apple.private.security.no-sandbox</key>
    <true/>
    <key>dynamic-codesigning</key>
    <true/>
    <key>com.apple.private.skip-library-validation</key>
    <true/>
    <key>com.apple.security.exception.iokit-user-client-class</key>
    <array>
        <string>IOSurfaceRootUserClient</string>
    </array>
    <key>com.apple.private.security.storage-exempt.heritable</key>
    <true/>
    <key>com.apple.security.exception.files.absolute-path.read-write</key>
    <array>
        <string>/</string>
    </array>
</dict>
</plist>

高兴的尝试了下发现mmap这里就eperm了。原来因为权限不够,iOS不让你做可执行代码映射。还需要在内核里改一下进程权限:另外这玩意似乎是放在ro区的,只有darksword搞不了,必须有ppl bypass才行。(哦好吧mmap在a15+设备上也是ppl管理的,而且这个ppl 权限还绕不过,不是bypass拿到physrw就行的)

代码
int unfair_prepare_app_bundle_decryption(char *error, size_t error_size) {
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
    static int prepared = 0;
    if (prepared) {
        return 0;
    }
 
    if (geteuid() != 0) {
        set_error(error, error_size, "root privileges required for jailbreak primitives");
        return -1;
    }
 
    void *handle = open_libjailbreak(error, error_size);
    if (handle == NULL) {
        return -1;
    }
 
    jbclient_initialize_primitives_fn jbclient_initialize_primitives =
        (jbclient_initialize_primitives_fn)required_symbol(handle, "jbclient_initialize_primitives", error, error_size);
    jbclient_root_steal_ucred_fn jbclient_root_steal_ucred =
        (jbclient_root_steal_ucred_fn)required_symbol(handle, "jbclient_root_steal_ucred", error, error_size);
    jbclient_root_set_mac_label_fn jbclient_root_set_mac_label =
        (jbclient_root_set_mac_label_fn)required_symbol(handle, "jbclient_root_set_mac_label", error, error_size);
    proc_find_fn proc_find =
        (proc_find_fn)required_symbol(handle, "proc_find", error, error_size);
    proc_csflags_set_fn proc_csflags_set =
        (proc_csflags_set_fn)required_symbol(handle, "proc_csflags_set", error, error_size);
 
    if (jbclient_initialize_primitives == NULL ||
        jbclient_root_steal_ucred == NULL ||
        jbclient_root_set_mac_label == NULL ||
        proc_find == NULL ||
        proc_csflags_set == NULL) {
        return -1;
    }
 
    if (jbclient_initialize_primitives() != 0) {
        set_error(error, error_size, "jbclient_initialize_primitives failed");
        return -1;
    }
 
    uint64_t original_ucred = 0;
    if (jbclient_root_steal_ucred(0, &original_ucred) != 0) {
        set_error(error, error_size, "jbclient_root_steal_ucred failed");
        return -1;
    }
 
    uint64_t original_label = 0;
    if (jbclient_root_set_mac_label(1, UINT64_MAX, &original_label) != 0) {
        set_error(error, error_size, "jbclient_root_set_mac_label failed");
        return -1;
    }
 
    uint64_t proc = proc_find(getpid());
    if (proc == 0) {
        set_error(error, error_size, "proc_find(%d) failed", getpid());
        return -1;
    }
 
    proc_csflags_set(proc, UNFAIR_CS_PLATFORM_BINARY);
    prepared = 1;
    return 0;
#else
    (void)error;
    (void)error_size;
    return 0;
#endif
}
 

好然后这里做完了你终于能mmap了,但是为什么mremap_encrypt还是会阻止!

经过简单的尝试我们发现,放在一般目录的macho可以mmap 加载但是 mremap_encrypt会被拒绝,只有把内容放到/var/mobile 下才可以mremap_encrypt。

并且这里没有gatekeeper也不会做完整性校验什么的,那也就相当于只需要把下载的macho搭配上 SC_Info 还有里面的东西,一个macho上传解密一次就好了。

同时,用于解密的手机需要登录App Store随意下载一个软件并启动一次,这样才会更新IC_Info 里面关于这个账号的解密凭据。

于是我们就可以配上 ipatool 或者 AssppWeb 来做一套全自动下载砸壳服务了。稍微让codex写一写还能自动上传到telegram频道的bot。

https://github.com/lbr77/AssppJailbroken

update 8.4:由于我们有了在A15 + 17.0 上进行 krw的能力并且有了TXM的符号可以用来逆向,经过研究找到了一条路径从而获得了理论上通用的一套方案:

首先 mmap(R—) ,而后修改pmap中当前页的属性(添加vnode + R-X)权限。最后就可以美美mremap_encrypt 直接砸壳了。砸壳完成之后,用 mmap(R—) 再改回来,读内容,取消映射。也许后续会开源到unfair的仓库里。

update 8.5 这条路径依旧复杂且在ppl检查里好像,,没法用。

所以经过观察我们看到了这里:

代码
		/* ensure mapped memory is mapped as executable except
		 *  except for model decryption flow */
		if ((cryptid != CRYPTID_MODEL_ENCRYPTION) &&
		    !(map_entry->protection & VM_PROT_EXECUTE)) {
			kr = KERN_INVALID_ARGUMENT;
			goto done;
		}

嗯…嗯?原来直接用CRYPTID_MODEL_ENCRYPTION就不需要检查可执行了?

c
	case CRYPTID_APP_ENCRYPTION:
	case CRYPTID_MODEL_ENCRYPTION:
		cryptname = "com.apple.unfree";
		break;

并且,这里cryptname 是一样的?

那就简单了,直接走mmap(PROT_READ) → mremap_encrypt(xxx,CRYPTID_MODEL_ENCRYPTION) → read&save → unmap 就行了。我们只需要一个 platform-application 的权限就可以简单做完了,不需要复杂的 PROT_READ|PROT_EXEC映射和取消了。

Dive deeper?

有没有办法看到更底层呢?比如,这个ipa包里放的到底是什么凭据可以用来解密呢?以及,为什么一定要下载一个运行程序之后才能解密?

找到了这一篇文章:

其中SUPF文件和IPA一起分发,每个用户的IPA和SUPF文件都是一致的,其中SUPF文件中保存了加密Mach-O的密钥,但是密钥本身被另外的机制加密。而SINF文件则作为每个用户的DRM许可,记录了购买用户的标识符和姓名,以及解密SUPF需要的信息,因此在Sandbox策略下,App无法读取自身的SINF文件,以防止其被作为唯一ID追踪用户。

经过又一番研究,我们把整个运行前解密链的后半段逻辑猜出来:(因为代码用了ollvm混淆,并没有系统学过反编译混淆的处理方法所以这里我不献丑了)

  1. 内核收到mremap_encrypt,根据com.apple.fairplay这个标号转到 FairplayIOKit 内核 extension去处理
  2. 内核 extension 给用户态 fairplayd 发送 mig 请求,fairplayd 收到请求读取 sinf /supf,还有激活(MobileActivation,苹果的另一个安全保护逻辑)后下载的 icinfo (sidb, sidv)
  3. 而后fairplayd解析上面的几个不同凭据之后会把东西丢回到内核态的IOKit继续处理,使用 SEP 独立硬件 (以及硬件绑定的AP GID Key,也许不是)进行解密。获得sinf的解密结果,最后返回给fairplayd
  4. fairplayd再用软件aes解密supf拿到真实的aes key。再扔回到内核,最后内核解密macho。

另一个逻辑是关于MobileActivation:

  1. 在下载了一个app并且运行后,fairplayd 发现自己的icinfo 没办法解密!
  2. 去找激活相关框架,上报给苹果服务器自己的设备序列号/ecid/…一系列环境信息/账号信息。苹果验证后返回对应的icinfo,本地更新。

这两个逻辑配上苹果的从SEP开始往上的信任链,导致在解密过程中break,拿到某一环的内容从而一劳永逸地解密变得不现实。

The END?

最后,关于fairplay的研究最开始是想在unicorn里直接运行SEP的固件。。。于是被迫/主动了解了一些详细的知识,顺带往上多看了看,顺着顺着就看到了之前一直在研究的拆壳。

接下来我想做的是通过分析得到一个状态:我们只需要输入SINF/SUPF,就能自动拿到macho解密的aes key / iv。 希望可以做到。

好吧,现在被codex安全限制卡住了,那我把research的仓库放在下面吧。