作者:Gh0u1L5
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送!
投稿邮箱:[email protected]
9月27号,黑客 ani0mX 在推特上公布了苹果公司的“史诗级安全漏洞”。该漏洞的影响范围极其广泛,囊括了绝大部分型号的苹果手机、平板、手表及智能电视等。而且由于它是一个“半硬件层”的漏洞,所以苹果永远无法通过软件更新修补这个漏洞。
漏洞发布当天,我在推特上看到另一名黑客 littlelailo 公布了一段30多行的草稿,简略聊了聊 checkm8 的攻击原理。我以为接下来很快国内安全社区也会有人放出更多细节,然而等了很久也没等到,索性自己开篇文章聊聊吧。我希望这篇文章:
- 能够让读者对 iPhone 启动机制有简单的了解。
- 能够让读者初步掌握如何逆向 iPhone 的 Secure ROM 。
- 能够讲明白 checkm8 漏洞攻击的基本思路和相关技术。
只要不以盈利为目的,任何个人或组织均可在注明出处的情况下自由转载,转载前通过评论区/私信简单告知本人即可。
0x00 iOS 安全启动机制简介
已熟知 iOS 安全启动链与 Secure ROM 防护机制的读者可直接跳过本节。
为了保证 iOS 系统的代码不被恶意篡改,苹果公司使用了一套名为 安全启动链(Secure Boot Chain) 的技术。他们将开机过程分为四到五个阶段,每个阶段负责检查下个阶段的代码,如果检查出任何问题,比如签名错误、安全模式不符,就立马中止开机。
在一些过时的资料里, iPhone 的开机过程分为以下五个阶段:
虽然这五个阶段被人引用过很多次,但其实它已经错了三年多了。从 A10 处理器以来,苹果就已经放弃了双阶段加载,也就是说上图的那个LLB
已经被删掉了,更新后的启动流程如下:
这四个阶段从左到右分别是:
- 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 的帮助下,窃取苹果公司层层保护的代码仅需三步:
- 使用网上搜到的按键组合,把 iPhone 手机重启到 DFU 模式(固件升级模式)。
- 执行
./ipwndfu -p
命令植入 payload,如果显示漏洞利用失败的话就多试几次。 - 执行
./ipwndfu --dump-rom
命令读取 ROM 并保存到当前文件夹下,完工。
这套操作,真的,猴子训练一下都能做。checkm8 光靠这一个功能,我觉得就无愧于“史诗级”这个评价了。
成功拿到 ROM 的二进制机器码之后,接下来扔给反编译器就可以了。
苹果的 CPU 从 A7 开始都是 AArch64 架构, little-endian 字序, ROM 的起始地址都是0x100000000
,设定好这三项之后,反编译器就能直出正确的汇编代码了。
除了反编译得到的这些代码外,网上还有一些开源的 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 应急启动的八个步骤:
- 手机以 DFU 模式开机后,负责处理 USB 的主模块会先调用
usb_dfu_init()
函数,初始化 DFU 子模块。初始化过程主要做两件事:- 分配一块 2048 字节的内存作为缓冲区,我们叫它
io_buffer
。 - 把 DFU 事件处理函数提交给 USB 驱动 ,等待用户发来的 DFU 请求。
- 分配一块 2048 字节的内存作为缓冲区,我们叫它
- 当用户想要加载临时系统时,会先发送一个
DFU_DNLOAD
请求。主模块将它转发给 DFU 事件处理函数。 - DFU 检查这个请求,如果用户想要发来一段长度为
wLength
的数据,那么 DFU 将会检查wLength
是否超过 2048 字节。- 超过的话,发送一个 STALL 包掐断 USB 会话,向主模块返回-1。
- 不超过的话,用指针将
io_buffer
传递给一个全局变量 ,向主模块返回wLength
。
- 主模块把
wLength
等信息记录到另一个全局变量中,为接下来接收数据做好准备。 - 用户接下来将数据陆续发送给主模块,主模块将这些数据复制到
io_buffer
中。等到所有的数据都接收完毕后,主模块通知 DFU 模块处理这些数据。 - DFU 模块拿到
io_buffer
,确认里面数据的长度确实是用户刚开始允诺的wLength
,然后将这些数据复制到临时系统的加载地址,比如0x18001C000
(iPhone 8/X)。 - 缓冲区数据处理完毕之后,主模块 清空之前的所有全局变量 ,准备接受下一个 USB 请求。
- 当用户分批发送完临时系统的所有内容后,会发送一个
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 ,我整理出了这个请求的具体数据结构:
这个结构里面,我们主要看两个成员:
next
指针,用来指向下一个要处理的请求对象,构成一串请求链表。callback
回调函数,虽然图里我把它标成一个void *
,但它实际的类型是一个函数指针,void (*callback) (struct usb_device_io_request *io_request)
。
整个攻击思路是这样的:
- 构建一串假的 IO 请求,让它们的
callback
依次指向我们想执行的 gadget 。 - 布置一套堆风水布局,操纵 malloc 把一个真请求放到我们掌控的
io_buffer
上。 - 向
io_buffer
写数据,把那串假请求写进内存,接到真请求的后面。 - 发送 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 也应该会做到位吧?感觉有点搞不懂苹果为什么在这里会漏下一个大坑漏了这么多年,感觉有点不可思议。
嘛,这次的文章就写到这里吧,正文有什么错误欢迎在评论区指正,就这样了。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1065/