查看原文
其他

不到4个小时,我找到了一枚苹果 0day

Brandon Azad 代码卫士 2022-04-06
 聚焦源代码安全,网罗国内外最新资讯!
编译:奇安信代码卫士团队
2020523日下午3点,有人发布了 iOS 13.5(当时发布的最新签名版)的 unc0ver 越狱版,它利用的是一个 0day和深度混淆。当天下午7点,我已经找到了这个 0day 并告知苹果公司。第二天凌晨1点,我将 PoC 和分析一并发送。本文将重温整个过程。


我希望快速找到 unc0ver 利用的 expoit 并将其告知苹果公司,证明混淆 exploit 基本无法阻止该 0day 落入恶意人员手中的厄运。
下载并提取 unc0ver IPA 后,我将主可执行文件加载到 IDA 中进行分析。遗憾的是,该二进制被深度混淆,因此我无法以静态的方式找到该 0day。
如下是含有被深度混淆代码的 IDA Pro 截屏。


然后,我将 unc0ver app 加载到在 iOS 13.2.3 上的 iPod Touch 7上。探索该 app 的接口并不代表用户可以控制用于利用该设备的任何漏洞,因此我希望 unc0ver 仅支持该 0day且没有使用 iOS 13.3 及更低版本上的oob_timestamp bug。
当点击“越狱”按钮时,我脑海中突然闪现出一个想法:之前我也写过一些内核 exploit,了解多数基于内存损坏的exploit 含有“关键章节”的部分说明损坏的内核状态是什么,而且如果exploit不含其它内容的话系统是不稳定的。于是,就凭这个突发奇想,我双击了主页按钮打开 app 切换开关,杀死 unc0ver app。
设备立即进入 panic 模式。
panic(cpu 1 caller 0xfffffff020e75424): "Zone cache element was used after free! Element 0xffffffe0033ac810 was corrupted at beginning; Expected 0x87be6c0681be12b8 but found 0xffffffe003059d90; canary 0x784193e68284daa8; zone 0xfffffff021415fa8 (kalloc.16)"Debugger message: panicMemory ID: 0x6OS version: 17B111Kernel version: Darwin Kernel Version 19.0.0: Wed Oct 9 22:41:51 PDT 2019; root:xnu-6153.42.1~1/RELEASE_ARM64_T8010KernelCache UUID: 5AD647C26EF3506257696CF29419F868Kernel UUID: F6AED585-86A0-3BEE-83B9-C5B36769EB13iBoot version: iBoot-5540.40.51secure boot?: YESPaniclog version: 13Kernel slide: 0x0000000019cf0000Kernel text base: 0xfffffff020cf4000mach_absolute_time: 0x3943f534bEpoch Time: sec usec Boot : 0x5ec9b036 0x0004cf8d Sleep : 0x00000000 0x00000000 Wake : 0x00000000 0x00000000 Calendar: 0x5ec9b138 0x0004b68b
Panicked task 0xffffffe0008a4800: 9619 pages, 230 threads: pid 222: unc0verPanicked thread: 0xffffffe004303a18, backtrace: 0xffffffe00021b2f0, tid: 4884 lr: 0xfffffff007135e70 fp: 0xffffffe00021b330 lr: 0xfffffff007135cd0 fp: 0xffffffe00021b3a0 lr: 0xfffffff0072345c0 fp: 0xffffffe00021b450 lr: 0xfffffff0070f9610 fp: 0xffffffe00021b460 lr: 0xfffffff007135648 fp: 0xffffffe00021b7d0 lr: 0xfffffff007135990 fp: 0xffffffe00021b820 lr: 0xfffffff0076e1ad4 fp: 0xffffffe00021b840 lr: 0xfffffff007185424 fp: 0xffffffe00021b8b0 lr: 0xfffffff007182550 fp: 0xffffffe00021b9e0 lr: 0xfffffff007140718 fp: 0xffffffe00021ba30 lr: 0xfffffff0074d5bfc fp: 0xffffffe00021ba80 lr: 0xfffffff0074d5d90 fp: 0xffffffe00021bb40 lr: 0xfffffff0075f10d0 fp: 0xffffffe00021bbd0 lr: 0xfffffff00723468c fp: 0xffffffe00021bc80 lr: 0xfffffff0070f9610 fp: 0xffffffe00021bc90 lr: 0x00000001bf085ae4 fp: 0x0000000000000000

看来有希望!我得到的一个 panic 消息是,kalloc.16 分配区域(通用分配,最大为16字节)中存在一个释放后重用漏洞。然而,很可能这是内存损坏的迹象而非内存损坏的来源(或者甚至是诱因!)。为了进一步调查,我需要分析回溯跟踪。
在等待 IDA 处理iPod内核缓存的过程中,我尝试了更多的即时实验。由于很多 exploit 将 Mach 端口用作基本原语,我编写了一款 app 来搅乱 ipc.ports 区域,创建分段并混合空闲列表。当我之后运行 unc0ver app 时,exploit 仍然起作用,这说明它可能不依赖于对 Mach 端口分配的堆整理 (heap grooming)。
接着,由于panic 日志提到了 kalloc.16,我决定编写一款app在 unc0ver exploit 过程中继续在后台分配并释放给 kalloc.16。这样做的想法是,如果unc0ver 依靠重新分配 kalloc.16 的分配区域,那么我编写的 app 应该会抓住该slot,从而可能导致 exploit 策略失败并可能导致内核处于 panic 状态。当然,如果我的 app 在后台挫败了 kalloc.16,那么点击“越狱”按钮将导致内核立即处于 panic 状态。
作为完整性检查,我尝试让我编写的 app 挫败另外一个区域 kalloc.32而不是 kalloc.16。这次,exploit成功运行,说明 kalloc.16 确实是该 exploit 使用的关键分配区域。
最终,当 IDA 分析完 iPod 内核内存时,我开始用符号表示从 panic 日志中收集的堆栈跟踪。

Panicked task 0xffffffe0008a4800: 9619 pages, 230 threads: pid 222: unc0verPanicked thread: 0xffffffe004303a18, backtrace: 0xffffffe00021b2f0, tid: 4884 lr: 0xfffffff007135e70 lr: 0xfffffff007135cd0 lr: 0xfffffff0072345c0 lr: 0xfffffff0070f9610 lr: 0xfffffff007135648 lr: 0xfffffff007135990 lr: 0xfffffff0076e1ad4 # _panic lr: 0xfffffff007185424 # _zcache_alloc_from_cpu_cache lr: 0xfffffff007182550 # _zalloc_internal lr: 0xfffffff007140718 # _kalloc_canblock lr: 0xfffffff0074d5bfc # _aio_copy_in_list lr: 0xfffffff0074d5d90 # _lio_listio lr: 0xfffffff0075f10d0 # _unix_syscall lr: 0xfffffff00723468c # _sleh_synchronous lr: 0xfffffff0070f9610 # _fleh_synchronous lr: 0x00000001bf085ae4

lio_listio() 的调用立即引起了我的注意。不久前我曾撰写了一份关于近期 iOS 内核利用的调查报告,碰巧记得 lio_listio() 是基于LightSpeed 的exploit中使用的易受攻击的系统调用。我重新阅读了 Synacktiv 写的博客文章回忆了这个 bug 并立即想到:LightSpeed 竞争中双重释放的目标对象是存在于 kalloc.16 中的一个aio_lio_context 对象。另外,unc0ver app 中的大量线程进一步印证了竞争条件的想法。
这时,我认为已经有足够的证据向苹果公司发出初步报告,说明该 bug 就是 LightSpeed,或者是其变体或者是一个回归。


漏洞确认和 PoC

接着,我想通过编写触发该问题的 PoC 来确认该 bug。我尝试了 LightSpeed 博客文章中提到的原始 PoC,但运行一分钟后还没出现 panic 的情况,这说明这个 0day 可能是原始 LightSpeed bug 的一个变体。
为发现更多的东西,我开启了双线调查:一条线是查看 XNU 来源尝试并发现该 bug,一条线是使用 checkra1n/pongoOS 修复内核缓存中的lio_listio(),并运行该 exploit。从来源中我并没有理解这个原始漏洞是如何被修复的。所以我专攻内核补丁的过程。
由于 checkm8 的存在,启动已修复的内核缓存虽然困难但可以办到。我下载了 checkra1n 并将 iPod 启动为 pongoOS shell。以 pongoOS 仓库的一个例子作为指南,我创建了一个可加载 pongo 模块,它将禁用 checkra1n 内核补丁,启用我自己的补丁(我禁用 checkra1n 补丁的原因是担心unc0ver 将检测到 checkra1n并采取反分析措施)。
第一次测试时,我只是在 lio_listio() 函数中插入无效的指令 opcode,这样调用时设备就会 panic。令人惊讶的是,启动设备的过程很顺利,之后我点击“越狱”按钮时,它就会panic。这说明unc0ver是唯一一个调用lio_listio() 函数的进程。
然后,我修复了负责分配在原始 LightSpeed bug中双重释放的aio_lio_context对象的代码,这样它就会从 kalloc.48 中而非 kalloc.16 中分配:
FFFFFFF0074D5D54 MOV W8, #0xC ; patched to #0x23FFFFFFF0074D5D58 STR X8, [SP,#0x40] ; alloc sizeFFFFFFF0074D5D5C ADRL X2, _lio_listio.site.5FFFFFFF0074D5D64 ADD X0, SP, #0x40FFFFFFF0074D5D68 MOV W1, #1 ; can blockFFFFFFF0074D5D6C BL kalloc_canblockFFFFFFF0074D5D70 CBZ X0, loc_FFFFFFF0074D6234FFFFFFF0074D5D74 MOV X19, X0 ; lio_contextFFFFFFF0074D5D78 MOV W1, #0xC ; size_tFFFFFFF0074D5D7C BL _bzero

这样做的想法是,增加该对象的分配大小将挫败 unc0ver 的 exploit 策略,因为它将尝试用 kalloc.16 的一个替代对象来取代偶然释放的 kalloc.48 的上下文对象。如果我注意到任何重大差异的话,那么我将会关注来源中的变体。然而,除了aio_reqprio 字段被设置为 ‘gang’ 以外,unc0ver 和原始 PoC 中传递给lio_listio() 的参数之间并不存在任何区别。
到这一步,这个 0day 看似可能真的是原始的 LightSpeed bug 本身而非其变体,因此我返回原始 PoC来分析它可能未被触发的原因是某种特定技术被缓解了。负责重新分配 kalloc.16 分配区域的代码引起了我的注意:

/* not mandatory but used to make the race more likely *//* this poll() will force a kalloc16 of a struct poll_continue_args *//* with its second dword as 0 (to collide with lio_context->io_issued == 0) *//* this technique is quite slow (1ms waiting time) and better ways to do so exists */int n = poll(NULL, 0, 1);

之前,我从未见过 poll()被当做重新分配原语的用法。从直觉上看,似乎使用基于 Mach 端口的重新分配策略更有希望,于是我将这段代码替换为从oob_timestamp 中复制的脱机 Mach 端口喷洒。当然,这是在几秒内可靠地触发 PoC 的唯一一个变化。


补丁历史



得到能运行的 PoC 后,我重新尝试了原始的 LightSpeed PoC,发现如果运行时间足够长的话,最终会引发 panic。因此,这是我本来可以通过回归测试找到的另外一个重新引入的 bug。
我们再返回来源,分析一下整个过程。入之前所述,但我首次检查 XNU 来源分析 lio_listio() 补丁是如何崩溃的,我实际上并没有理解这个 bug 原来到底是怎么修复的。回过头来看,并没有那么难懂。
博客文章对原始的 LightSpeed 漏洞描述得很清楚,在此不再赘述:强烈建议大家阅读这篇文章。从较高层次来说,该 bug 是,并不清楚释放 aio_lio_context 对象的语法是哪个函数,因为执行异步 I/O 的工作线程和 lio_listio() 函数本身都能释放。
该博客文章指出,该 bug 最初的修复方案只是,在可能被双重释放的情境下,不去释放aio_lio_context对象。
从一方面看,该补丁修复了 lio_context 上的潜在 UaF,但从另一方面看,在修复方案发布之前处理的出错案例现在被忽略了。结果,就可能使 lio_listio() 分配一个将永远不会被内核释放的aio_lio_context。这就使得我们会遭受 DoS 攻击,导致最近发布的内核(包括 iOS 12 在内)崩溃。
接下来,就是看苹果公司是否会修复由补丁引发的这个 DoS 攻击了。
结果证明,苹果最终却是决定在 iOS 13 中修复这个内存泄露问题,但在过程中他们似乎重新引入了这个竞争条件双重释放问题:

case LIO_NOWAIT:+ /* If no IOs were issued must free it (rdar://problem/45717887) */+ if (lio_context->io_issued == 0) {+ free_context = TRUE;+ } break;

虽然iOS 13 中的代码和iOS 11并不完全一致,但它在语法上是相等的。记得并理解原始 LightSpeed bug 的任何人都能够轻松通过审计 XNU 来源之间的差异将其认定为回归问题。任何人只要运行简单的回归测试都能轻松发现这个0day。
所以,总结一下:LightSpeed bug 在 iOS 12 中修复但其补丁并未解决根因问题,只是将条件竞争双重释放漏洞转变为内存泄露漏洞。之后在 iOS 13中,该内存泄露漏洞被找到并“被修复”,然而又引入原来的 bug,再次为解决根本问题。而我们只要运行一下博客文章中提到的原始 PoC 就能发现这个安全回归问题。
结论

组合利用 iOS 12.4 中的 SockPuppet 回归问题和 iOS 13 中的 LightSpeed 回归问题,说明苹果公司至少在这些老旧的安全 bug(他们是引发大量关注的公开 bug)上并未运行有效的回归测试。运行有效的回归测试是必要的基础软件测试流程,也是 exploit 的常见起点。
尽管如此,非常高兴地看到苹果在 exploit 遭公开后及时修复了这个问题。现实是,攻击者在公开的 PoC 发布之前就非常快速地找到了这些问题,因此利用回归的窗口期是巨大的。
同时,我找出被 unc0ver 利用的漏洞的目标是为了演示混淆无法阻止攻击者快速武器化遭利用的漏洞。我在分析中显得非常幸运:编写内核 exploit 的经验让我能够快速找到发现该 bug 的其它策略,由于我一直坚持追踪之前的 exploit,因此我碰巧熟悉所使用的这个具体漏洞。但是,利用 exploit 攻击苹果用户的任何人也一样具备这些优势。

 

 


推荐阅读

苹果会修复这个 0day吗?有人借机发布越狱包且适用于当前iOS 版本

谷歌又放大招:公布可实现 iOS 11 越狱的PoC 利用代码



原文链接

https://googleprojectzero.blogspot.com/2020/07/how-to-unc0ver-0-day-in-4-hours-or-less.html 




题图:Pixabay License


本文由奇安信代码卫士编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 www.codesafe.cn”。


奇安信代码卫士 (codesafe)

国内首个专注于软件开发安全的

产品线。

    点个 “在看” ,加油鸭~

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存