本文原文来自From zero to tfp0 - Part 2: A Walkthrough of the voucher_swap exploit
在本文中将深入研究voucher_swap漏洞以及获取内核任务端口的所有步骤。
该漏洞的发现和POC都归功于@_bazad
(@S0rryMybad也发现了这个漏洞并用在天府杯上:https://blogs.360.cn/post/IPC%20Voucher%20UaF%20Remote%20Jailbreak%20Stage%202.html)

引用计数

本文中的漏洞是由于MIG生成的代码的引用计数问题造成的。什么是引用计数?引用计数是一种简单而有效的内存管理方式。创建或复制对象会将其引用计数加1,而销毁或覆盖对象会将其引用计数减1。如果对象的引用计数达到零,则将释放该对象。在内存有限的系统中,引用计数比垃圾回收(它是循环发生的,可能会耗费时间)更有效,因为可以在对象的引用计数为零时立即释放对象,从而提高了系统的整体响应能力。
对象结构体中的成员可以表示该对象的引用计数。例如,Mach端口(ipc_port_t)中io_references成员表示其引用计数,ip_references和ip_release用于增加和减少端口上的引用计数。简单搜索ip_reference将找到此函数用于操纵端口的引用计数的许多示例。

voucher也是一样的,其中iv_refs成员表示引用计数。

iv_refs的类型为os_refcnt_t,它是32位整数,因此其范围为0-0xffffffff(8个f)吗?其实并不是。在libkern/os/refcnt.c中,最大值定义为0x0fffffff(7个f)。这是一种新的防止整数溢出的缓解措施。

如下所示,访问超出此范围的任何值都会触发内核panic。

voucher的ipc_voucher_reference和ipc_voucher_release函数仅检查voucher是否不为NULL,然后调用iv_reference和iv_release,他们分别调用os_ref_retain和os_ref_release。

BUILD/obj/EXPORT_HDRS/libkern/os/refcnt.h中可以找到更多细节。

因此,可能会出现两种漏洞:一种是以某种方式增加引用计数,从而导致溢出。如前所述,由于存在上限,这实际上是无法利用的。但是仍然可以将引用计数增加到0x0fffffff(7个f),稍后我们将使用此技术。另一种是将对象的引用计数设置为0,但是仍然有一个指向它的指针。由于引用计数变为0,对象将被释放,因此指向该对象的指针变成了悬空指针。

漏洞

让我们看一下该漏洞。查看xnu-4903.221.2/osfmk/kern/task.c中的task_swap_mach_voucher函数。这是一个简单的函数,它应该将新的voucher和旧的voucher交换。但是它只是用new_voucher替换了old_voucher。

根据注释,task_swap_mach_voucher函数是一个占位符。可在xnu-4903.221.2/osfmk/mach/task.defs中找到它。

这证明它实际上是Mach API,因为MIG def文件正在为Mach接口生成代码。/BUILD/obj/RELEASE_X86_64/osfmk/mach/task.h中可以找到此函数的Mach消息格式。

/BUILD/obj/RELEASE_X86_64/osfmk/RELEASE/mach/task_server.c中可以看到对请求执行的检查。

下面是实际的实现。

简化一下。

convert_port_to_voucher函数通过调用ipc_voucher_reference函数将引用计数增加1。

convert_voucher_to_port函数通过调用ipc_voucher_release函数将引用计数减少1。

在task_swap_mach_voucher例程中,通过调用ipc_voucher_release函数(第4844行)将新voucher的引用计数减1。

此例程中引用计数的变化如下。
第4839行: new_voucher的引用计数+1
第4841行: old_voucher的引用计数+1
第4843行: task_swap_mach_voucher调用,old_voucher = new_voucher
第4844行: new_voucher的引用计数-1
第4857行: new_voucher的引用计数-1(因为old_voucher现在是new_voucher)
你可能已经看出问题了。可以将new_voucher的引用计数减少为0,从而释放该对象。并且old_voucher的引用计数可以增加很多。如果存储指向new_voucher的指针,然后使用漏洞将new_voucher的引用计数减少为0,这样就有可能获得指向new_voucher的悬空指针。

关于voucher

在继续之前,最好先查看ipc_voucher结构体并了解其中的成员。

第一件事是确定要在哪个对象中存储被释放的voucher的指针。最好的方法是在内核源代码中搜索ipc_voucher_t,并寻找易于获取和设置该指针的API。明显的地方之一是osfmk/kern/thread.h中的thread对象,该对象存储名为ith_voucher的voucher的引用。

可以使用thread_get_mach_voucher和thread_set_mach_voucher函数从用户态读取和写入voucher引用。查看MIG为该函数生成的代码。

一旦获得了指向已释放voucher对象的悬空指针,便可以使用其他对象占用已释放的voucher对象。但是这并不简单。voucher通常位于自己的ipc voucher zone中,如osfmk/ipc/ipc_voucher.c所示,其中zinit为voucher分配了一个新zone。

因此,被释放的voucher的内存将被放置在zone的freelist中,并在创建新voucher时分配给新voucher。为了用其他对象占用,唯一可行的方法是触发zone垃圾收集,它会将被释放的voucher的内存(最小大小为1页)移到zone map中,然后这些内存就可以重新分配给其他对象。可以通过分配大量voucher并释放它们来做到这一点。
让我们再次仔细查看MIG为thread_get_mach_voucher生成的代码。假设我们确实用其他对象占用了已释放的voucher对象,则调用thread_get_mach_voucher应该成功而内核不会panic。在第2688行的thread_get_mach_voucher函数调用ipc_voucher_reference(voucher),这意味着该voucher应该具有有效的iv_refs成员。


点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖