记得去年暑假,大概也是这会,我刚刚入门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 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就不需要检查可执行了?
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混淆,并没有系统学过反编译混淆的处理方法所以这里我不献丑了)
- 内核收到mremap_encrypt,根据com.apple.fairplay这个标号转到 FairplayIOKit 内核 extension去处理
- 内核 extension 给用户态 fairplayd 发送 mig 请求,fairplayd 收到请求读取 sinf /supf,还有激活(MobileActivation,苹果的另一个安全保护逻辑)后下载的 icinfo (sidb, sidv)
- 而后fairplayd解析上面的几个不同凭据之后会把东西丢回到内核态的IOKit继续处理,使用 SEP 独立硬件 (以及硬件绑定的AP GID Key,也许不是)进行解密。获得sinf的解密结果,最后返回给fairplayd
- fairplayd再用软件aes解密supf拿到真实的aes key。再扔回到内核,最后内核解密macho。
另一个逻辑是关于MobileActivation:
- 在下载了一个app并且运行后,fairplayd 发现自己的icinfo 没办法解密!
- 去找激活相关框架,上报给苹果服务器自己的设备序列号/ecid/…一系列环境信息/账号信息。苹果验证后返回对应的icinfo,本地更新。
这两个逻辑配上苹果的从SEP开始往上的信任链,导致在解密过程中break,拿到某一环的内容从而一劳永逸地解密变得不现实。
The END?
最后,关于fairplay的研究最开始是想在unicorn里直接运行SEP的固件。。。于是被迫/主动了解了一些详细的知识,顺带往上多看了看,顺着顺着就看到了之前一直在研究的拆壳。
接下来我想做的是通过分析得到一个状态:我们只需要输入SINF/SUPF,就能自动拿到macho解密的aes key / iv。 希望可以做到。
好吧,现在被codex安全限制卡住了,那我把research的仓库放在下面吧。
大佬牛
🐮