iPhone史诗级漏洞checkm8攻击原理浅析

iPhone史诗级漏洞checkm8攻击原理浅析

9月27号,黑客 axi0mX 在推特上公布了苹果公司的“史诗级安全漏洞”。该漏洞的影响范围极其广泛,囊括了绝大部分型号的苹果手机、平板、手表及智能电视等。而且由于它是一个“半硬件层”的漏洞,所以苹果永远无法通过软件更新修补这个漏洞。

漏洞发布当天,我在推特上看到另一名黑客 littlelailo 公布了一段30多行的草稿,简略聊了聊 checkm8 的攻击原理。我以为接下来很快国内安全社区也会有人放出更多细节,然而等了很久也没等到,索性自己开篇文章聊聊吧。我希望这篇文章:

  1. 能够让读者对 iPhone 启动机制有简单的了解。
  2. 能够让读者初步掌握如何逆向 iPhone 的 Secure ROM 。
  3. 能够讲明白 checkm8 漏洞攻击的基本思路和相关技术。
  4. 尽量做到深入浅出,争取让刚入行的萌新甚至没有相关背景的程序员也能看个大概。

只要不以盈利为目的,任何个人或组织均可在注明出处的情况下自由转载,转载前通过评论区/私信简单告知本人即可。


0x00 iOS 安全启动机制简介

已熟知 iOS 安全启动链与 Secure ROM 防护机制的读者可直接跳过本节。

为了保证 iOS 系统的代码不被恶意篡改,苹果公司使用了一套名为安全启动链(Secure Boot Chain)的技术。他们将开机过程分为四到五个阶段,每个阶段都会检查下个阶段的代码有没有被篡改。如果检查出任何问题,比如签名错误、安全模式不符,就立马中止开机。

在一些过时的资料里, iPhone 的开机过程分为以下五个阶段:

iPhone 旧版启动流程

虽然这五个阶段被一些公众号和自媒体复读了很多遍,但其实……它已经错了三年多了。从 A10 处理器以来,苹果就已经放弃了双阶段加载,也就是说上图的那个“LLB”已经被删掉了,更新后的启动流程如下:

iPhone 新版启动流程

这四个阶段从左到右分别是:

  • ROM / Secure ROM:开机启动时执行的第一段程序,负责检查并加载接下来的 iBoot 。
  • iBoot:苹果开发的引导程序,负责检查并加载系统内核。
  • Kernel:iOS 系统内核。
  • OS:iOS 系统的用户界面、后台服务等非核心组件。

Secure ROM 作为系统启动时执行的第一段程序,扮演着整个安全启动链技术的信任基石。一旦黑客攻破了它,接下来所有阶段的代码都能随意篡改,因此苹果公司下了很大功夫来保护这段 ROM 程序:

封杀写权限:这段程序烧写在 CPU 的硅片内部,无法拆解,无法替换。在工厂里一次性烧录完之后,就连苹果自己都没办法改动它。

封杀读权限:这段程序完成工作后,会直接把自己所在的储存器锁掉,再没有任何办法能读取它。也就是说,启动之后哪怕你攻陷了整个系统,也读不到这段程序的内容。

苹果的想法很单纯——如果一段程序黑客读都读不到,改也改不了,那么这段程序应该就会很安全。等到文章结尾的时候,我会再花点笔墨聊聊这个想法为什么不现实。但现在,苹果的这些安全措施确实给我们造成了一点麻烦:我们连程序内容都看不到,怎么分析程序漏洞?

0x01 抓取 Secure ROM

刚刚我们提到, Secure ROM 完成工作后,才会把储存器锁住。换句话说,只要 Secure ROM 还没完成工作,我们就有机会从内存里读到它的内容。​

如何抓住这个机会呢?这就轮到 checkm8 出场了。

checkm8 是一个能实现任意代码执行的漏洞,允许我们在 ROM 运行期间执行恶意代码。更贴心的是, axi0mX 还在自己发布的 ipwndfu 项目里附上了一段现成的恶意代码,我们攻击成功后,就可以运行一个 Python 脚本给后门发消息,执行各种高权限的操作,比如:

  • --dump-rom将 iPhone 的 Secure ROM 直接从内存里抓取出来,保存为文件。
  • --demote:启用 JTAG 模式。配合一条5800多元的 Bonobo 线,你就可以用 gdb 随意调试 iPhone 内核了。如果公司或者实验室给报销的话,我真的强烈建议买一条(笑)。

另外, axi0mX 的后门里还有一个 execute 命令非常好用,但是没有放出命令行接口,只能自己写 Python 代码来调用。这个命令允许你调用内存里存在的任意函数,能传递参数,还能拿到返回值。但他的代码有个问题,传第8个参数的时候会传成第7个,用之前需要自己动手改一下。

好了,我们书归正传。在 checkm8 的帮助下,窃取苹果公司层层保护的代码仅需三步:

  1. 使用网上搜到的按键组合,把 iPhone 手机重启到 DFU 模式(固件升级模式)。
  2. 执行./ipwndfu -p命令植入后门,如果显示漏洞利用失败的话就多试几次。
  3. 执行./ipwndfu --dump-rom命令读取 ROM 并保存到当前文件夹下,完工。

这套操作,真的,猴子训练一下都能做。checkm8 光靠这一个功能,我觉得就无愧于“史诗级”这个评价了。

成功拿到 ROM 的二进制机器码之后,接下来扔给反编译器就可以了。

苹果的 CPU 从 A7 开始都是 AArch64 架构, little-endian 字序, ROM 的起始地址都是0x100000000,设定好这三项之后,反编译器就能直出正确的汇编代码了。

iPhone 8 Secure ROM 反编译结果

除了反编译得到的这些代码外,网上还有一些开源的 iBoot 项目,以及苹果某实习生泄露出来的一份四五年前的旧版 Secure ROM 代码,这些材料对我们的逆向分析都非常有帮助。

但是,由于发布这些泄露代码铁定会吃一张苹果的律师函,所以我不会在这篇文章里引用或发布那份泄露代码,有需要的读者还请自己动手搜索一下。

最后要说的是,刚才那套轻松的招数最多只能用到 iPhone X 上,从 Xs / Xr 开始 checkm8 漏洞就没法用了。对于这些手机,目前我们也没有什么好办法,只能用黑盒测试、旧 ROM 代码和 iBoot 代码这三样凑活着挖漏洞。

iBoot 的代码能用来挖 ROM 的漏洞,是因为 iBoot 和 ROM 有一部分功能重叠,所以代码也有重叠。比如这次的 checkm8 漏洞,就是 axi0mX 在分析一个 iBoot 补丁的时候发现的。

至于解密 iBoot 的具体方法,因为好像有点偏题了,所以将来有机会的话再开篇文章讲讲吧。有兴趣的读者可以先自行了解一下 iOS 的 GID Key、IMG3/IMG4、KBAG 这几个概念。

0x02 漏洞原理解析

我前文中提到过,利用 checkm8 前需要先把手机重启到 DFU 模式,因为这次的漏洞正是出在这个 DFU 模式上。

苹果的 DFU 模式大致相当于一个“应急启动模式”,重启到这个模式后,用户可以用 USB 传入一个临时系统,用临时系统开机启动。(当然,这个临时系统必须是苹果官方系统。)

基于 littlelailo 的草稿、 iPhone 8 的逆向结果,以及一些“开源”的 iBoot 项目,我整理出了 DFU 应急启动的八个步骤:

  1. 手机以 DFU 模式开机后,负责处理 USB 的主模块会先调用usb_dfu_init()函数,初始化 DFU 子模块。初始化过程主要做两件事:
    1. 分配一块 2048 字节的内存作为缓冲区,我们叫它io_buffer
    2. 把 DFU 事件处理函数提交给 USB 驱动 ,等待用户发来的 DFU 请求。
  2. 当用户想要加载临时系统时,会先发送一个DFU_DNLOAD请求。主模块将它转发给 DFU 事件处理函数。
  3. DFU 检查这个请求,如果用户想要发来一段长度为wLength的数据,那么 DFU 将会检查wLength是否超过 2048 字节。
    1. 超过的话,发送一个 STALL 包掐断 USB 会话,向主模块返回-1
    2. 不超过的话,用指针将io_buffer传递给一个全局变量,向主模块返回wLength
  4. 主模块把wLength等信息记录到另一个全局变量中,为接下来接收数据做好准备。
  5. 用户接下来将数据陆续发送给主模块,主模块将这些数据复制到io_buffer中。等到所有的数据都接收完毕后,主模块通知 DFU 模块处理这些数据。
  6. DFU 模块拿到io_buffer,确认里面数据的长度确实是用户刚开始允诺的wLength,然后将这些数据复制到临时系统的加载地址,比如0x18001C000(iPhone 8/X)。
  7. 缓冲区数据处理完毕之后,主模块清空之前的所有全局变量,准备接受下一个 USB 请求。
  8. 当用户分批发送完临时系统的所有内容后,会发送一个DFU_DONE请求。主模块将它转发给 DFU ,通知 DFU 开机,于是 DFU 模块释放掉io_buffer,尝试开机。如果开机失败,再次执行usb_dfu_init() ,开始第二轮 DFU 启动。

有了我加黑标粗的几个关键点,有人也许已经能看出来这次漏洞的原理了。

第3步 DFU 将 io_buffer 地址记录到了一个全局变量里,如果用户接着发送一个DFU_DONE请求的话,5~7步就会被直接跳过。第8步 DFU 释放掉io_buffer这块内存,开机失败跳回到第1步,开始第二轮 DFU 启动。这时之前那个全局变量记录的,还是已经释放掉的io_buffer,这就构成了一个 Use-After-Free 漏洞。

如果我们事先在堆上精妙地占下一些位置,就能准确诱导 malloc 算法的行为,迫使它把一些关键的内存安排到已经 free 过的io_buffer里。接着只要往io_buffer里面写数据,就能改写这些关键内存的内容。上述操作,用安全界的术语来讲叫堆风水(Heap Feng-shui),一般认为是由保加利亚安全专家 Alexander Sotirov 正式提出。

说到这里,我忍不住想说句八卦。 littlelailo 在推特上抱怨说,自己早在今年3月就发现了 checkm8 漏洞,但由于他只攻破了 A8 和 A9 处理器,所以就没掀起什么波澜。我没看过他的攻击代码,不知道跟 axi0mX 的代码比起来到底差了哪里。但既然大家原理一模一样,那搞不好就是堆风水的时候出了差别。由此可见,玩风水的造诣确实是能决定一个黑客的运势,古人诚不欺我啊。

最后,给想要自己逆向的读者指个路吧。在 iPhone 8 / iPhone X 的 ROM 中,几个关键函数的位置分别位于:

  • USB 主模块代码:0x10000B24C
  • DFU 请求处理代码:0x10000BCCC
  • DFU 数据处理代码:0x10000BEF4

0x03 构建ROP

这一节我其实本来想顺着聊聊 checkm8 里面堆风水的处理的,然而由于我这篇文章写得三天打鱼两天晒网,所以写到这里的时候外网已经有人发文章详细讨论过 checkm8 堆风水的处理了,还配了好看又细致的插图。那我觉得就没必要再写一遍了,反正也写不过人家,干脆直接贴个链接(Technical analysis of the checkm8 exploit),然后往下跳到构建 ROP 的部分。

目前我们已经可以修改一块堆内存了,接下来想要执行代码的话,就需要在堆上找到一个可控的函数指针,通过修改它来实现代码执行。

在这里, axi0mX 盯上了一个名叫usb_device_io_request的数据结构。这个数据结构里面保存着发给 USB 驱动的 IO 请求,正常情况下,USB 驱动会挨个处理这些请求,完成数据收发。但是如果用户要求重置 USB 会话的话,驱动就会一口气清空所有请求,并且调用每个请求的回调函数

通过逆向 iPhone 8 的 Secure ROM ,我整理出了这个请求的具体数据结构:

struct usb_device_io_request

这个结构里面,我们主要看两个成员:

  • next指针,用来指向下一个要处理的请求对象,构成一串请求链表。
  • callback回调函数,虽然图里我把它标成一个void *,但它实际的类型是一个函数指针,void (*callback) (struct usb_device_io_request *io_request)

整个攻击思路是这样的:

  1. 构建一串假的 IO 请求,让它们的callback依次指向我们想执行的代码。
  2. 布置一套堆风水布局,操纵 malloc 把一个真请求放到我们掌控的io_buffer上。
  3. io_buffer写数据,把那串假请求写进内存,接到真请求的后面。
  4. 发送 USB reset 请求,重置会话,让 USB 驱动执行那串恶意callback

有了这套思路之后,剩下的就是选 gadget 之类的细节了,我们暂不赘述。至此,checkm8 的攻击原理已经算是基本揭露完了。

想要自己动手逆向本节内容的读者,我再给你们指个路吧。在iPhone 8 / X 的 Secure ROM 中,几个关键的函数分别位于:

  • USB 主模块 reset 请求处理函数:0x10000B84C
  • USB 驱动 reset 请求处理函数:0x100004A44

0x04 后记

从这次的 checkm8 漏洞里我们能学到什么?

首先,我觉得最重要的一点就是再次强调了那个业界共识:“保密不等于安全”

当然啦,一定会有人反问我:苹果的这套保密体系不是效果很好吗?这么显眼的漏洞,将近十年都没被黑客发现啊?这还不够安全吗?

然而我们要注意一点, ROM 漏洞并不是将近十年没人发现,而是将近十年没人公布,这两字之差就是天壤之别。

在漏洞挖掘这个领域,大家所求的东西各不相同,但顶尖玩家一般就三种:有求名的,比如腾讯、360、知道创宇这些公司的实验室,需要 Apple、Google 时不时发感谢信来维护实验室的招牌。有求财的,比如 Zerodium 这些网络军火商,同样的漏洞苹果顶多悬赏 20~100 万美金,而这帮军火商开口就是150万美金,因为这些漏洞落到他们手里能变现出更大的利益。剩下一批顶尖玩家是各国的国家队,揣着明确的军事目标在挖掘漏洞。

当某个产品漏洞挖掘的门槛抬得过高时(比如 Secure ROM),各家实验室会迫于经营压力/指标压力,转去寻找更好拿下的山头。整个赛场上就只剩下军火商和国家队,这两种人目标明确,苹果悬赏区区50万、100万根本打动不了他们,挖出的漏洞也就全被他们悄悄吞下来了。

所以对于大公司来说,最好的安全策略其实是拥抱透明,把求名的伙计们更多地拉下场,把愿意公布漏洞赚干净钱的白帽黑客拉下场。如果 Apple 采用这个战略的话, checkm8 可能根本没机会发展成一个横跨7、8代苹果产品的史诗级漏洞,而是会在 iPhone 5、iPhone 6 发布的时候就被腾讯玄武实验室之类的白帽组织报了出来。然后苹果只要发发锦旗、奉上20万50万美元的赏金,事情就解决了,哪有今天这个尴尬局面?

其次,我觉得这个漏洞还说明了一点:对所有出现数据吞吐的地方,都应该进行细致的 fuzz 测试。

这次 checkm8 的成因,主要是对 USB 请求处理不当造成的 UAF 漏洞,我个人感觉这个完全有可能通过 fuzz 技术检测出来啊?发完 setup 包之后跳过 data phase ,这个 ROM 程序应该就直接炸了啊?Secure ROM 作为安全启动链的起点,就算不做彻底的形式化验证, fuzz 也应该会做到位吧?感觉有点搞不懂苹果为什么在这里会漏下一个大坑漏了这么多年,感觉有点不可思议。

嘛,这次的文章就写到这里吧。觉得有帮助的话欢迎点赞、关注支持一下,正文有什么错误欢迎在评论区指正,就这样了。

注:页首 checkm8 图标由 Reddit 用户 devabdulsalam 所设计,使用前已征得原作者同意。

编辑于 2019-11-14

文章被以下专栏收录