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

作者:Gh0u1L5

本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送!

投稿邮箱:paper@seebug.org

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

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

  1. 能够让读者对 iPhone 启动机制有简单的了解。
  2. 能够让读者初步掌握如何逆向 iPhone 的 Secure ROM 。
  3. 能够讲明白 checkm8 漏洞攻击的基本思路和相关技术。

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


0x00 iOS 安全启动机制简介

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

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

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

iOS Secure Boot Chain Outdated

虽然这五个阶段被人引用过很多次,但其实它已经错了三年多了。从 A10 处理器以来,苹果就已经放弃了双阶段加载,也就是说上图的那个LLB已经被删掉了,更新后的启动流程如下:

iOS Secure Boot Chain Latest

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

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

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

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

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

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


0x01 抓取 Secure ROM

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

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

checkm8 是一个任意代码执行漏洞,允许我们在 ROM 运行期间植入 payload。更贴心的是, ani0mX 还在自己发布的 exploit 里附上了一段高质量的 payload 。允许我们通过 USB 给 payload 发送指令,执行各种高权限的操作,比如:

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

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

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

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

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

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

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

iOS Secure ROM Decompile Result

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

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

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

iBoot 的代码能用来挖 ROM 的漏洞,是因为 iBoot 和 ROM 有一部分功能重叠,所以代码也有重叠。比如这次的 checkm8 漏洞,就是 ani0mX 在分析一个 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 字节。

    • 超过的话,发送一个 STALL 包掐断 USB 会话,向主模块返回-1。
    • 不超过的话,用指针将 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 漏洞。

在这个 UAF 漏洞的基础上,只要找到一个合适的攻击目标,用堆风水引导 malloc 把攻击目标分配到io_buffer上,就能通过写io_buffer修改这个攻击目标的内容了。

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

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

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


0x03 构建ROP

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

为了构建 ROP 调用链, ani0mX 盯上了一个名叫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依次指向我们想执行的 gadget 。
  2. 布置一套堆风水布局,操纵 malloc 把一个真请求放到我们掌控的io_buffer上。
  3. io_buffer写数据,把那串假请求写进内存,接到真请求的后面。
  4. 发送 USB reset 请求,重置会话,让 USB 驱动执行 ROP 链。

有了这套思路之后,剩下的就是选 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 也应该会做到位吧?感觉有点搞不懂苹果为什么在这里会漏下一个大坑漏了这么多年,感觉有点不可思议。

嘛,这次的文章就写到这里吧,正文有什么错误欢迎在评论区指正,就这样了。

以上是 iPhone 史诗级漏洞 checkm8 攻击原理浅析 的全部内容, 来源链接: utcz.com/p/199460.html

回到顶部