首页
社区
课程
招聘
[翻译]如何利用 iOS 7 bootchain 中的递归堆栈溢出漏洞
发表于: 2018-6-16 21:19 40062

[翻译]如何利用 iOS 7 bootchain 中的递归堆栈溢出漏洞

2018-6-16 21:19
40062

这篇文章旨在阐释如何利用iOS 7 bootchain中的递归栈溢出的bug。无需漏洞利用的前置知识,只需稍微了解一些向下增长小端栈的机器的工作原理和一些ARM汇编的基础知识。不需要特定的硬件(线缆或调试设备)来实现对iBoot的确定性控制。在本文中我们将使用iPhone5,2/11B554a作为示例。

有两个看似无关的东西导致了这一漏洞的发现。

其中一个原因是我隐约记得2010年的年中,丰田因为问题固件引发意想不到的加速丑闻而饱受指责。这其中一些错误被认为源自于破坏全局变量的堆栈溢出[1]。

另一件事是Joshua Hill的“破碎的梦”演示,它在第85页有这个”BootROM漏洞利用方法”,提到“递归堆栈溢出”[2]。

iPhone的bootchain的一个简短的总结

SuffelRM是一小块掩模ROM或者写保护闪存。这是设备重启后第一个运行的东西。

LLB是Low Level Bootloader,负责硬件加载和加载主引导程序。

iBoot是主引导程序,主引导程序几乎做了你能想到的所有事情:USB、恢复模式,等等。它还处理FS访问,因为它需要从系统分区中读取内核并引导OS。

iBSS是LLB的DFU副本。它处理FS访问,因为它需要在升级过程中读取一个特殊的“恢复”分区。

实际上,引导逻辑比这更复杂一些,例如iBoot可以回退到恢复模式,它将接受一个IBEC,但我们不关注这一点,因为已经超出本文的范围。

由于它们的性质和目的,每个引导加载阶段带来了复杂性的增加。例如:LLB/IBSS不需要了解文件系统,但如上所述,iBoot/iBEC必须这样做。从统计上看,一个组件越复杂,其bug的几率就越高,因此,我们想要f寻找可轻易实现的目标:

首先,我们需要研究我们的目标,这听起来很复杂,但实际上不是。iBoot并不意味着可重定位,它被设计为在其首选的负载地址运行,即,memory_base + memory_size - 1MB,也就是:0x5FF00000 或 0x9FF00000 或 0xBFF00000。这意味着没有IASLR(iBoot地址空间布局没有随机化),这是好事。

在一个越狱的手机上iOS已经完全启动之后,我们应尽快dump iBoot。运气好的话,我们会发现iBoot仍然在自己的位置,几乎完好无损。为了dump,我们将使用winocm的ios-kexec-utils(3)工具,下面我们把它叫做kloader和它的小弟弟kdumper。

请注意,被dump的镜像与解密的镜像略有不同。好的是,数据在运行时保持不变,显著的例外是这个列表。我们通过对这些列表头进行清理,并基于旧的iBoots(如iPhone 4 iBoot)进行更多的清理工作,因为我们有那些的解密密钥[4 ]。

我们可以在上面的图中观察到任务堆栈被放置在数据后面,可以推断,破坏这些堆栈中的一个将导致数据损坏。刚开始,只有一个任务运行:引导任务。然后创建一些其他任务,如“idle”和“main”,然后又会产生更多的任务:poweroff, USB等,并运行它们。每个任务都有固定的堆栈大小:0x1C00字节。

还有一个简单的调度器管理这些任务,这些任务也执行一些轻量级的完整性检查,比如验证ARM重置向量基地址和创建任务的堆栈基地址。任务调度器是协作的,因此我们可以安全地假设一旦任务运行,它将继续这样做直到一些发生异常,例如IRQ。简而言之,堆栈检查不影响我们,只要task_yield()不在我们堆栈时运行。

在多次dump iBoot之后,我们不仅可以看到基地址,而且可以看到它的整个内存布局是完全可预测的,从漏洞利用的角度来看,这可太好了。我们可以找到堆中的代码、数据,甚至各种结构。当然,我们需要关注那些堆栈,这就是我们的目标,对吧?

请记住,递归堆栈溢出并不一定意味着直接递归,一个la函数F()调用它自己。这些都很难被发现,但并不总是有用的。例如,ResolvePathToCatalogEntry()被限制最大深度为64,因此对于这个目的是无用的。
让我们寻找F() -> G() -> ... -> F() 链,因为那些仍然会被算做递归。虽然在大量文件中发现这些有点困难,但是有一些图形算法能帮我们:我们有一个调用图,我们需要在这个调用图中标识“循环”。一个相当简单和有用的算法是Tarjan的强连通分量算法[5]。实际上,SCC可能包含多个循环,但是没关系;实际上,假设SCCs已经足够好了。ONS被正确识别,我们可以运行脚本[6 ]并打印出我们的调用图“循环”。

我们在找那些小巧的SCC。就像F()->G()->F(),因为那些容易跟踪。下面这个就不错:

上面的memalign()调用本身并没有对递归做出贡献,但它们在后面有重要作用 - memalign只是一个奇特的malloc。 另外,请记住,没有太多其他的事发生,这是很好的。

一个HFS+的快速入门会让我们更好地了解正在发生的事情。 有关HFS+[7]卷内所有文件和文件夹的信息保存在目录文件中。 目录文件是一个包含记录的B树[8],每个记录最多跟踪文件每个分支的8个范围[9]。 这被称为程度密度。 一旦所有8个扩展区都用完,附加扩展区就会记录在扩展区溢出文件中。 Extents溢出文件是另一个B-tree,它将记录分配给每个扩展文件的分配块。

在iBoot中发现的HFS+实现使用相同的ReadExtent()函数来读取目录和扩展溢出范围。我们观察到,如果在读取扩展溢出范围时超出了范围密度,则ReadExtent()将无限递归。

请注意:碰巧,一个过时版本的iBoot HFS+驱动程序是公开的[10]。源代码对漏洞利用必要的,但它可能有助于了解一些。

好消息:我们在"main"任务中找到了一个递归堆栈溢出,堆栈是高度可预测的。

坏消息:它会烧穿堆并且很快击中DATA,而且一旦触发无法阻止。

更坏的消息:iBoot堆(和部分数据)将完全失效。

当时,我只有一个设备易受这个bug的影响,我很担心失去它。只是盲目地拨弄iBoot是愚蠢的,所以我写了一个“iBoot loader”命名为iloader(附在本文中的源代码)。它的目的是在用户态伪造运行iBoot,利用iBoot的运行时内存布局的高度可预测的特性。

由于我们在用户态上是伪造的,所以我们必须跳过硬件。现在回想一下,堆栈上分配了任务堆栈,跳过整个代码块可能跳过一些alloc。为了弥补这一点,我们需要确保iloader保持主任务堆栈在iBoot基地址上的正确偏移。我们kloader修改了iBoot,并打印出“主”任务的栈顶例程中的堆栈指针:

对齐我们的堆栈,然后需要在创建主任务之前iloader以正确大小调用iBoot分配器一次。
Real iBoot heap: iloader iBoot heap:

在寻找触发漏洞的方法之前,有两件事要做。
考虑:
1。我们必须避免什么?
2。我们应该瞄准什么?

回答第1题,是的,在我们的路径上有些东西。还记得那些malloc调用吗?malloc函数使用enter_critical_section()/exit_critical_section()来防止竞争条件。那两个函数执行task::irq_disable_count而且大于999的话就会进入应急模式,触发递归将会清除他的路径上的一切,包括我们的"main"任务,也包括 task::irq_disable_count。所以我们需要确定main_task->irq_disable_count的位置被小于1000的值所重写。
To answer #2, yes there is something that could achieve an arbitrary write.
Remember those malloc calls again? Yeah, it follows that we should target
the allocator metadata. We have to put some values there and coerce the
allocator into an arbitrary write.
回答第2题,是的,有些东西可以实现任意的写入。还记得那些malloc调用吗?对,我们应该以分配器元数据为目标。我们在其中放一些值,并将分配器强制任意写入。
| |
+------------------+
| |
| DATA |
| |
| +--------------+ |
| |btree headers | | <-+ steer the allocator here
| +--------------+ | |
| +--------------+ | |
| | allocator | | --+
| | meta-data | | <-+ land here
| +--------------+ | |
| | |
+------------------+ |
| | |
| HEAP | |
| +--------------+ | |
| |task structure| | | <-- this gets nuked, avoid irq_disable_count
| |--------------| | |
| | task stack | | | <-- this is where we start
| +--------------+ |
| stuff |

要想在这些约束下起舞,我们需要确保在非常精确的点上开始递归。我们跨越分配器元数据的确切位置是由每个递归占用多少堆栈空间(在下面的例子中是208个字节)和起始SP。我们需要足够接近来操作分配器,但不能太接近,否则它会恐慌。我们稍后会看到,块“bin”阵列是甜蜜的地方着陆。SP调谐可以通过两种方法实现:

iBoot以简单的方式访问文件系统。它在"装载"分区时缓存两个BTree头,然后使用ReadExtent()用于文件读取以及目录扫描。

递归可以通过两种方式开始:

从一个简单的HFS+系统开始,有一个相当平坦的Btree:
trigger
|
v
+------+ +------------+
|HEADER|------>| root block |
+------+ +------------+
| ... |
+--------------+ |
v v
+-----+ +-----+
| | ... | |
+-----+ +-----+

上述BTree布局允许我们仅通过根节点触发递归。叶节点不能用于触发延迟递归,因为BTNodeDescriptor::kind在Read ReadBTreeEntry()中被检查,以防我们得到一个具有高编号的当前节点的任意索引节点。
配置示例就像这样:
HFSPlusVolumeHeader::catalogFile.logicalSize = 0xFFFFFE00;
HFSPlusVolumeHeader::extentsFile.logicalSize = 0x3FFC000;
Catalog BTree header:
BTHeaderRec::nodeSize = 512;
BTHeaderRec::totalNodes = 0x7FFFFF;
BTHeaderRec::rootNode = 0x7FFE; // initial trigger
Extents BTree header:
BTHeaderRec::nodeSize = 16384;
BTHeaderRec::totalNodes = 0xFFF;
BTHeaderRec::rootNode = 0x500; // must be big, but LSB must be zero

注意:这个Btree头的大部分空间都没有使用,也没有被iBoot检查,所以我们可以使用这个空间来放我们的payload。

上述策略虽然简单,却证明是相当不灵活的,因为我们不能调整启动SP太多。如前所述,我们希望使用ResolvePathToCatalogEntry()来为我们修复SP,这意味着在路径处理过程中触发任意点的递归。我们不得不求助于稍微不同的布局:复制根块,更新HFS+头,以在新根开始,并将克隆点内的所有记录都保留到原始根块。这将导致更高的树,但中间层不具有叶约束,但它仍然有效的HFS+(虽然略微浪费)。

和之前的配置不同,我们这次保持BTHeaderRec::rootNode不变,而是在相应的Btree记录中给block设置一个大的值。
PUT_DWORD_BE(block, 116, 0x10000); // see iloader source for reference

经过iloader的一尝试和错误后,我们找到了最佳的使用途径是 ${boot-ramdisk}="/a/b/c/d/e/f/g/h/i/j/k/l/m/disk.dmg"

一旦递归将堆栈指针靠近分配器元数据,我们的目标就是让memalign()为我们做任何地方的写入。memalign()有两个主循环,这里简化为:

下面是汇编,供参考:

free_list_remove()提供读/写。为了获得写的权限,我们需要让它从我们的payload中读取——这是方便地放置在HFS+ Btree头中的。每一个递归的堆栈帧都有一个指向bTree头的指针,尽管略微不对齐。

理想情况下,我们希望这个指针指向起始点,但是不管我们做了多少SP调整,都不能同步。注意,在执行任何工作之前,循环跳过所有的零点,给我们一些助力,尽管还不够。但是,起始库取决于分配大小。我们必须在memalign(blockSize),而不是memalign(64)在这里加载。原来这是最后一点操作空间:在512和65536的限制范围内调整块大小。不过,避免将块大小提升得太高:在递归过程中,较大的块大小会耗尽堆,因此对它有实际限制。

在我们的指针被选中之后,我们几乎可以“驱动”分配器逻辑,并且确保在使用分配器“写入”实现一个写堆栈(以MealIGN的帧为目标,特别是保存的链接寄存器(LR)值)时不会产生惊慌,以便获得PC控制。

最后的问题是, free_list_remove()正好要使用一个LDRD指令来读,这就意味着资源已经按照DWORD对齐了,但是我们的指针正好在DWORD+2的位置。为了避免出现错误,在第一次迭代中,我们没有块拟合,切换到一个对齐的地址,然后在第二次迭代中进行拟合。现在我们终于得到了一个任意位置写入,并且我们在memalign的堆栈上定位LR的位置。

我们现在希望memalign尽快返回,并尽可能少的副作用。通过仔细安排当前bin的假block中的一些值,现在已经在BTree头中移动了,我们可以跳过memalign逻辑中的所有其他内容,从而得到正确和快速的返回。这一返回将跳到我们选择的位置。由于整个映射是可执行的,所以我们只从Btree头中的某处返回,我们的shellcode就在这儿。

下面是在iPhone5,2/11B554a iBoot上iloader的输出:

Delaying boot for 0 seconds. Hit enter to break into the command prompt...

从这个iloader的输出汇编中,我们可以看到当PC指向0xBFF1A2F8时发生了什么。

因为我们已经控制了pc,最后一个问题就是怎么修复,iBoot堆完全被彻底删除,整个任务被垃圾覆盖。然而,DATA大部分幸存下来,除了较高的端部,分配器结构就在这个地方。无论是哪种方式,最好的做法是在内存中修补相关的安全检查,就像我们对dump做的一样,并完全重新启动iBoot,因为它将重新初始化BSS和堆。

为了清理DATA,我们将一小段代码存入BTree头,其目的是执行清理,起一个合适的名字“nettoyeur”。为了节省空间,我们实际上有一个压缩的nettoyeur,因为iBoot总会提供了一个lzss解压缩例程。

总结起来,顺序是这样的:

我们最后处于iBoot的控制台,所有的安全检查已经禁用, GID AES key还能用。

到目前为止,所有的东西都可以在一个ARM CPU上运行的iloader上进行尝试和测试。你会看到成功消息“suck sid”,然后iBoot重新启动,然后最终在恢复模式崩溃,因为有些iloader不支持的东西。

最后的非破坏性测试可以在进行真正的事情之前进行:

一旦处理了每一个小细节,我们就开始攻击真正的bootchain。记住,设备必须是越狱,这已经是dump的先决条件。它还必须运行我们正在瞄准的iBoot的精确版本。

在下面的例子中,${boot-ramdisk}在iPhone5,2/11B554a上可以执行
假乳iBEC没有公钥,我们只能这么做:
. have the jailbreak untether leave System partition read-only
让越狱离开系统分区只读
. 重启
. ssh登录到这台设备
. nvram boot-ramdisk="/a/b/c/d/e/f/g/h/i/j/k/l/m/disk.dmg"
. dd if=/dev/rdisk0s1s1 of=backup bs=512k count=1
. dd of=/dev/rdisk0s1s1 if=ramdiskF.dmg bs=512k count=1
. 重启
. 祈祷

一旦我们获得了iBEC的解密key,还有一个更安全的方法:

当设备重新启动时,您会注意到启动标志闪烁(即漏洞利用代码重新启动IBoot),然后进入恢复控制台。我们现在可以用irecovery[12]连接它:

要想退出这种模式,我们要这样做:
. in the pwned Recovery, upload a pwned iBEC and jump to it
. once in iBEC, upload dtre/rdsk/krnl and bootx
. ssh into the ramdisk
. dd of=/dev/rdisk0s1s1 if=backup bs=512k count=1
. nvram -d boot-ramdisk
. reboot

这个bug的实际应用是引导任何未签名的内核。考虑创建一个第三分区并触发那里的漏洞:

当然,payload需要这样修改:
. 移到sp
. 禁用解释器
. 应用那个补丁, 从0分区加载内核, 忽略ramdisk
. 解压 nettoyeur
. 静默安装
. 运行 nettoyeur
.跳转到iBoot起始点然后再次运行

这一特定漏洞及其利用方法的旅程到此结束。它已经在iOS 8上修复了,然而,回想起来我觉得它很有趣,因为发动攻击的环境非常恶劣。此外,触发它导致所有地狱挣脱和狙击的出路肯定是有趣的。最后,能从中学到一些教训。

对这种漏洞利用不起作用的缓解措施:

对这种漏洞利用起到作用的缓解措施:

[1] https://embeddedgurus.com/state-space/2014/02/are-we-shooting-ourselves-in-the-foot-with-stack-overflow/
[2] https://conference.hitb.org/hitbsecconf2013kul/materials/D2T1%20-%20Joshua%20'p0sixninja'%20Hill%20-%20SHAttered%20Dreams.pdf
[3] https://github.com/xerub/ios-kexec-utils
[4] https://www.theiphonewiki.com/wiki/InnsbruckTaos11B554a(iPhone3,1)
[5] https://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
[6] https://github.com/xerub/idastuff/blob/master/tarjan.py
[7] https://en.wikipedia.org/wiki/HFSPlus
[8] https://en.wikipedia.org/wiki/B-tree
[9] https://en.wikipedia.org/wiki/Extent
(file_systems)
[10] https://opensource.apple.com/tarballs/BootX/BootX-81.tar.gz
[11] https://www.theiphonewiki.com/wiki/GID_Key
[12] https://github.com/xerub/irecovery

-- 全文完
原文链接:https://xerub.github.io/ios/iboot/2018/05/10/de-rebus-antiquis.html


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//