原文链接:https://media.weibo.cn/article?id=2309404245794218721506 作者:Xiaolong Bai and Min (Spark) Zheng @ Alibaba Security Lab 译者:tylinux 博客地址: https://tylinux.com

0x0 简介

对于越狱来讲,一个可读写的 Root 分区是必须的,因为越狱之后需要安装一些非沙盒的应用以及修改部分系统设置。但是,未越狱的 iOS 系统中, Root 文件系统默认是只读的,因此,现代越狱中的很重要的一步就是将 Root 文件系统重新挂载为可读写的。显然,苹果不会让你这么轻易地做到。

在本文中,我们将介绍苹果在 iOS 11.3 之后,针对 remount Root 文件系统为可读写问题新引入的缓解措施,同时,我们会介绍一种全新的,可以绕过这种缓解措施的技术。根据我们的研究,这种绕过技术可以与 Ian Beer 即将释出的 iOS 11.3.1 tfp0 漏洞协同工作,这就意味着,11.3.1 可以越狱了!

0x01 在 iOS 11.3 之前 remount Root 文件系统

当我们使用 mount() 系统调用重新挂载一个文件系统的时候,内核会调用 __mac_mount(),最终调用 mount_common() 函数进行挂载。在 mount_common() 函数中,存在一个 MACF 安全检查: mac_mount_check_remount(),用来检查本次重新挂载是否被允许,而这个检查最终执行的是 Sandbox Kext 中的 hook_mount_check_remount()函数,检查本次重新挂载是否为 Root 文件系统(RootFS),如果是,则终止本次挂载。如下图:

在 iOS 11.3 之前的系统中,通常采用绕过沙盒检测的方式来 remount Root 文件系统,该方案由 Xurub 提出,先移除 Root 文件系统的 ROOTFS RDONLY, NOSUID标识再进行挂载。Jonathan LevinHITB 18 上对此有更加详细的描述。

0x02 iOS 文件系统基础

出现了绕过技术,苹果自然也会升级自己的防御。自 iOS 11.3 开始,如果按照之前的方式remount Root 文件系统 ,当我们尝试修改 Root 文件系统时,内核会直接崩溃,日志如下:

这说明苹果在文件系统层面引入了新的缓解措施来阻止我们 remount Root 分区。我们仔细研究了 iOS 11.3 的文件系统,找出了几种方式来绕过新的缓解措施。在本文中,我们会详细介绍其中的一种。在开始之前,我们首先介绍一下 iOS 文件系统的一些基本结构以及刚刚内核为何会崩溃。

Darwin 内核支持许多不同的文件系统,比如 APFS、HFS+、FAT等等。当一个文件系统被挂载的同时,会生成一个通用的 mount 结构体(如下图)用来描述此文件系统是如何被挂载的。

在这个结构体中,mnt_flag 包含一些诸如 RDONLYROOTFSNOSUID 这样的标志,来描述基本的挂载状态。mnt_ops 中则保存这一些函数指针,这些函数是由文件系统实现的 mountunmountioctl 等等的已一些操作。而 mnt_data 则记录着具体的文件系统,比如 APFS,是如何组织和存储文件系统需要操作的关键数据。在具体的文件系统实现中,mount/unmount/remount 这些操作都是通过操作私有的 “mount” 结构体来使操作生效的。

0x03 根本问题

从崩溃日志中可以看到,com.apple.xbs/Sources/apfs/apfs-748.52.14 这条路径表明崩溃来源是 APFS 文件系统,同时也说明 iOS 的 Root 文件系统是 APFS 格式的。APFS(Apple File System)是有苹果开发的新型文件系统,具备克隆、加密及快照等等先进的功能,目前已经被应用于所有的苹果设备中。

所以,APFS 中什么地方触发了内核崩溃呢?想要回答这个问题,我们首先需要知道, Root 文件系统是如何被挂载到系统中的。在 iOS 中执行命令 mount,我们可以得到当前系统中挂载的所有分区信息,如下:

在 Root 文件系统的挂载路径中,有个奇怪的前缀:com.apple.os.update-CA59XXXX@,这个又是什么?我们先来在 macOS 上多个小实验。

上图中,tmutil localsnapshot / 是为 Root 分区创建了一个快照,名字是 com.apple.TimeMachine.2018-05-30-154704。而 mount -t apfs -o -s=com.apple.TimeMachine.2018-05-30-154704 / /tmp 这条命令则是把创建的快照挂载到 /tmp 目录。挂载完成之后,我们会发现,挂载路径中也有个类似的奇怪前缀:com.apple.TimeMachine.2018-05-30-154704@。现在我们知道了,com.apple.os.update-CA59XXXX@ 这个前缀表明,iOS 的 Root 分区,挂载的是一个文件系统快照!

苹果对文件系统快照的解释是:快照是文件系统某一时刻的只读实例,操作系统使用快照功能来更高效地创建备份,提供回滚到某一特定时间点的能力。显然,iOS 11.3 中新的缓解措施就是,把一个只读的文件系统快照挂载为 Root 分区。这也就解释了为什么我们改变 Root 文件系统的时候内核会崩溃了。

所以我们之所以重新挂载失败了是因为,尽管我们去掉了 RDONLY 的标志以使 Root 文件系统能被挂载为可读写,然而实际挂载的依然还是一个快照,而快照是只读的。而且,文件系统的私有 mount 结构中的 mnt_dat 项未被修改,依然是一个只读的快照。我们之前的方式,其实是尝试把一个只读的快照重新挂载为“可读写的”只读快照,自然是不可行的,所以就崩溃了。

为了了解这个问题的技术细节,我们首先回到之前的崩溃日志。在日志中有提到,崩溃原因是缺少某个范围的 extent。在 APFS 文件系统中,extent 是用来存储文件的位置和大小的数据结构。通过逆向 APFS 的内核扩展,确认 extent 以 B树 的形式存储在 APFS 私有 mount 结构体的 mnt_data项中。而在 APFS 快照的 mnt_data 中,不包含有效的 extent 结构,当我们尝试使用原有方式重新挂载 Root 分区并对它进行修改的时候,文件系统会去查找相应的 extent 结构,因为 Root 分区是快照,无法找到正确的 extent 结构,也就触发了内核崩溃。

0x04 新的绕过方案

通过以上的分析,一个很明确的解决方案出现了:我们需要让文件系统成功地从快照的 mnt_data 中获取 extent 结构。为了达到这个目的,我们需要创建一个和可读写文件系统一样的,包含有效文件 extentmnt_data 结构。然而,创建一个真是有效的 mnt_data 结构是一项复杂而又困难的工作。那么,让 APFS 文件系统像挂载一个可读写的 Root 分区一样,帮我们创建一个 mnt_data 行不行呢?没问题,只要我们给 Root 文件系统创建一个新的可读写挂载,然后再从这个可读写挂载中获取 mnt_data 结构就可以了。

所以,新的绕过姿势的完整思路已经出来了,总共分以下几步:

  1. 把 Root 分区所在的块设备 /dev/disk0s1s1 以可读写的方式挂载到某个目录下,比如 /var/mobile/tmp
  2. 从新的可读写挂载中获得 mnt_data 结构
  3. 像 11.3 之前的方式一样,修改 Root 文件系统的 mnt_flag,然后 remount
  4. 用刚刚获得的 mnt_data 结构,覆盖 Root 分区挂载的 mnt_data 结构

新的问题来了:iOS 不允许同一个块设备被挂载两次。具体判断是在 mount_common() 方法中:

vfs_mountedon() 方法中,函数从设备的 vnode 中获取 v_specflags,然后检查其中是否包含 SI_MOUNTEDONSI_ALIASED 标志:

不过,我们可以通过清空设备 vnode 的 v_specflags 属性来绕过 vfs_moutedon() 中的检测。

至此,我们就基本实现了绕过 iOS 11.3 中的缓解措施,实现将 Root 分区 remount 为可读写的目的了。伪代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void remountRootAsRW() {
    char *devpath = strdup(/dev/disk0s1s1);
    uint64_t devVnode = getVnodeAtPath(devpath);
    writeKern(devVnode + off_v_specflags, 0); // clear dev vnode’s v_specflags

    /* 1. 将 Root 分区所在块设备挂载到某个目录下 */
    char *newMPPath = strdup(/private/var/mobile/tmp);
    createDirAtPath(newMPPath);
    mountDevAtPathAsRW(devPath, newMPPath);

    /* 2. 从新的挂载中获得 mnt_data 结构 */
    uint64_t newMPVnode = getVnodeAtPath(newMPPath);
    uint64_t newMPMount = readKern(newMPVnode + off_v_mount);
    uint64_t newMPMountData = readKern(newMPMount + off_mnt_data);

    /* 3. 修改 Root 分区的挂载方式并重新挂载 */
    uint64_t rootVnode = getVnodeAtPath(/);
    uint64_t rootMount = readKern(rootVnode + off_v_mount);
    uint32_t rootMountFlag = readKern(rootMount + off_mnt_flag);
    writeKern(rootMount + off_mnt_flag, rootMountFlag & ~ ( MNT_NOSUID | MNT_RDONLY | MNT_ROOTFS));
    mount(apfs, /, MNT_UPDATE, &devpath);

    /* 4. 用新的 mnt_data 结构覆盖原有的 mnt_data */
    writeKern(rootMount + off_mnt_data, newMPMountData);
}

在代码中, readKern() 方法用于从某个内存地址中读取数据,writeKern() 用于向某个内存地址中写入数据。相关实现可以从 XerubElectraV0rtexmach_portal 以及 Qilin 等工具中找到。getVnodeAtPath() 是我们开发的一个小工具,用来获取某个路径的 vnode 地址,使用 Ian Beer 提供的技术实现内核代码执行。

使用本文讲述的新的绕过方式,就可以在 iOS 设备上获得一个可读写的 Root 文件系统。你可以在此之上修改系统文件、安装非沙盒的可执行文件等等。下图展示了一个已经成功挂载为可读写状态的 Root 文件系统和 iOS 11.3.1 系统的越狱环境。

0x05 结论

关于本文提到的绕过方式有一点要注意,当你重启设备后,对文件系统所做的所有修改都会被丢弃,所以这是一种不完美的 remount 方案(我们将在近期讨论一种完美的 remount 方案)。但是相信对于大部分的越狱群众来讲这已经足够了。

最后,我们将于 2018年8月9日指12日在拉斯维加斯举办的 DEFCON 黑客大会上,就 iOS 越狱与 macOS 漏洞检测等话题发表演讲。同时,欢迎大家关注我们的 Twitter 账户:@bxl1989、@SparkZheng.