@i41nbeer
漏洞: getvolattrlist 通过 fgetattrlist 系统调用接受一个用户可控的 bufferSize 参数。
当分配内核缓冲区以将属性列表序列化到其中时,有如下注释:
/*
* Allocate a target buffer for attribute results.
* Note that since we won't ever copy out more than the caller requested,
* we never need to allocate more than they offer.
*/
ab.allocated = ulmin(bufferSize, fixedsize + varsize);
if (ab.allocated > ATTR_MAX_BUFFER) {
error = ENOMEM;
VFS_DEBUG(ctx, vp, "ATTRLIST - ERROR: buffer size too large (%d limit %d)", ab.allocated, ATTR_MAX_BUFFER);
goto out;
}
MALLOC(ab.base, char *, ab.allocated, M_TEMP, M_ZERO | M_WAITOK);
问题在于,代码随后没有正确处理好用户提供的缓冲区大小小于所请求的报头大小的情况。如果我们传入 ATTR_CMN_RETURNED_ATTRS,就会执行以下代码:
/* Return attribute set output if requested. / if (return_valid) { ab.actual.commonattr |= ATTR_CMN_RETURNED_ATTRS; if (pack_invalid) { / Only report the attributes that are valid */ ab.actual.commonattr &= ab.valid.commonattr; ab.actual.volattr &= ab.valid.volattr; } bcopy(&ab.actual, ab.base + sizeof(uint32_t), sizeof (ab.actual)); }
这里没有检查所分配的缓冲区是否至少足够容纳该结构。
漏洞利用: 我希望之后能发布一篇更详细的白皮书,以下是关于该漏洞利用如何工作的一些粗略笔记:
该漏洞使您能够向 kalloc.16 分配块的末尾之外写入 8 个零字节。虽然看起来您可能能够控制这些字节中的少数位,但我不确定您实际上能否做到,因此我将重点放在利用上,就好像它在末尾之外写入一个 NULL 指针一样。
这是一个非常受限的原语,因此第一步是尝试枚举您可以做的可能事情:
最后我选择了第一种方案。于是还有两个进一步的要求:
我选择以 struct ipc_port 为目标,它的第二个双字是一个引用计数字段,因此满足了第一个要求。然而它并不在 kalloc.16 中分配;而是存在于自己的 zone(ipc_ports)中。
这意味着我们必须将一个 kalloc.16 zone 块对齐在 ipc_ports 块的正前方,然后从 kalloc.16 块中的最后一个 kalloc.16 分配溢出到 ipc_ports 中的第一个分配。
有两个技巧可以使这更容易:
空闲链表反转: zone 分配会首先来自中间(部分已满)页面。这意味着,如果我们开始在 groom 过程中的某处释放和分配 k.16 对象,那么在当前中间页面满或空之前,这些对象不会被重新使用。
这带来了一个挑战,因为新页面的空闲链表是半随机填充的,因此它们的分配会从内到外进行:
| 9 8 6 5 2 1 3 4 7 10 | <-- example "randomized" allocation order from a fresh all-free page
这意味着我们最终的中间 k.16 和 ports 页面看起来会有点像这样:
| - - - 5 2 1 3 4 - - | - - - 4 1 2 3 5 - - | kalloc.16 ipc_ports
如果我们利用溢出破坏空闲链表条目,那么一旦它被分配就会导致内核恐慌,因此我们需要避免这种情况。
诀窍在于,通过控制分配和释放顺序,我们可以反转空闲链表,使最终的中间页面看起来更接近这样: | 1 4 - - - - - 5 3 2 | 2 5 - - - - - 4 3 1 | kalloc.16 ipc_ports
此时,我们更有可能释放一个 kalloc.16 并重新分配它来进行溢出,从而命中 ipc_port 的第一个 qword。
可安全溢出的分配: 由于在命中目标分配(正好位于末尾,紧挨着 ipc_port 之前)之前,我们很可能需要从许多候选分配中溢出,因此我们需要确保 kalloc.16 页面上的已分配对象用 NULL 指针破坏是安全的。
我使用 mach message 的 ool_port 描述符来实现这一点,因为 NULL 是一个有效值。
漏洞利用流程: 我们执行 groom 来反转 kalloc.16 的空闲链表,并开始尝试溢出到 ipc_port 中。
我们知道包含将被破坏的端口的大致 mach 端口名称范围;在每次溢出尝试之后,我们检查这些端口,看看端口是否已被破坏。成功破坏的一个副作用是端口的 io_active 标志会被设置为零。我们可以使用 mach_port_kobject MIG 方法来检测这一点,而不会引起副作用。
一旦找到被破坏的端口,我们需要让它被引用一次并释放引用;更重要的是,我们需要执行此操作的代码路径不去检查 io_active 标志。mach_port_set_attributes 可以为我们做到这一点。
现在,我们已经把向 kalloc.16 末尾之外写入 NULL 指针变成了一个悬空的 mach 端口 :)
我们触发一次 zone gc,目标是让端口的内存被重新用作 kalloc.4096 页面。我们首先让它被重新用作 ool_ports 描述符,其中 ip_context 字段与我们发送给自己的指向一个金丝雀端口的 send right 重叠。这让我们能够获知内核中对象的大致地址。然后我们用 pipe 缓冲区替换 ool_desc,经过一番调试,我们就能确定悬空的 mach 端口在内存中的位置。
我们在其中构造一个伪造的内核任务端口,然后进行清理。
可靠性: 这个漏洞利用确实有效,这正是我的目标 :) 可靠性可能在 30% 左右,这完全取决于你多快能完成初始溢出和测试循环。如果有其他东西介入并在 kalloc.16 中分配或释放,那么你破坏空闲链表条目或其他内容并导致内核恐慌的概率就会增加。
我确信这个漏洞利用可以变得更可靠;我只是把它做到了证明该漏洞可利用的程度。如果你想以此作为起点并展示如何提高可靠性,我很想读一篇博客文章!我想这会涉及实际监控 kalloc.16 的分配,并理解失败案例是什么以及如何预防它们。
当设备重启后闲置一小段时间时,成功率似乎最高。
清理: 如果漏洞利用确实有效,它应当自行清理并且不会导致设备内核恐慌。伪造的内核任务端口会继续存在。
使用 kmem.h 中的函数来读写内核内存。如果你希望在此进程退出后仍保留内核内存访问权限,请在其中持久化一个指向 tfp0 的 send right。
我已在以下设备上测试:iPod Touch 6G、iPhone 6S、iPhone SE、iPhone 7、iPhone 8 它应该适用于 iOS 11 到 iOS 11.3.1。