本文介绍了iOS和macOS中使用的内存管理单元zone的基本知识。以及通过zone的一些特性,来攻击最新版iOS和macOS内核的技巧。
0x00 Zone的简介
Zone在XNU(iOS和macOS的内核)中是非常重要的内存管理单元,用于快速分配和释放频繁使用的固定大小的对象。Zone的结构体定义如下:
free_element指向了一个链表,表里记录这个zone里的空闲块元素。elem_size记录了这个zone中元素分配的大小。Count, countfree, cur_size,max_size,alloc_size等记录了这个zone的各种状态。
在系统中使用sudo zprint可以看到当前zone的信息:
一般对安全研究者来说,iOS 10之前写exploit使用最多的zone都是kalloc.*相关的zone。当内核使用kalloc()分配一块内存的时候,内存分配器会选择能满足所需求size的elem_size最小的zone来进行分配。
在iOS 10之后,ipc.ports这个zone变的火了起来,因为很多POP(Mach Port-oriented Programming)攻击会伪造ipc_port结构体,虽然这个结构体的大小是168,但并不会分配到kalloc.*的zone中,而是有一个自己的ipc.ports zone。同样,很多ipc_port对应的kobject也有自己的zone,比如voucher, semaphores和task等。这一种类似隔离堆的设计,导致传统的uaf或者buffer overflow并不能影响到不同类型(却大小相同的)的对象,攻击者需要有新的攻击手段来做到跨zone攻击。
0x01 Zone元素随机化
iOS 9.2之前,zone在一个新的allocate_size大小的空间(根据页面大小而定)上分配元素的时候,这些元素在空间中分配的地址的是线性的。打个比方,在kalloc.512 zone上,你用for循环分配很多元素,每个页面会被分配8个元素:
for (int i=0;i<8;i++) {
vz[i] = (char*)calloc(4096,1);
memset(vz[i] ,i, 4096);
}
for (int i = 0; i < 0x1000; i++) {
send_kern_data(vz[i%8],512-0x18,&ports[i]);
}
一个可能的布局是:
但在最新的macOS或者iOS上,这个布局可能是这样的:
这是什么原因呢?因为,在iOS 9.2版本中,在zone相关的代码中,增加了一个叫random_free_to_zone()的函数:
这个函数会在一个zone初始化的时候将元素随机的从一个分配空间的头部和尾部插入到释放列表里,因此在元素分配的时候就会从中间开始,向两边扩散:
比如我们做了8次实验并观察内存的分配结果如下:
5 4 3 0 1 2 6 7
7 6 5 4 3 0 1 2
7 5 2 1 0 3 4 6
7 4 3 2 1 0 5 6
7 3 1 0 2 4 5 6
7 6 4 3 1 0 2 5
6 5 2 0 1 3 4 7
3 2 1 0 4 5 6 7
这种随机化的内存分布,会给攻击者的内存破坏攻击和堆风水产生不小的麻烦。
0x02 page的线性分配
虽然一个page内的元素的分配是随机的,但是page的分配并不是随机的,因此有一个有趣的zone出现了:kalloc.4096。这个zone元素的大小刚好是一个page的的大小,并且allocate_size也是一个page的大小,当我们进行这个zone中的元素分配时,完全是按照内存从低到高的顺序进行分配。比如我们尝试如下代码:
for (int i = 0; i < 0x1000; i++) {
send_kern_data(vz[i%8],4096-0x18,&ports[i]);
}
for (int i = 0x1000-0x10; i < 0x1000; i++) {
read_kern_data(ports[i]);
}
然后使用kdp进行调试,并检测kalloc.4096 zone的freelist:
我们可以用lldb的x指令查看一下释放后的数据,可以发现虽然元素已经释放了,但大多数原始数据还在:
利用元素释放后,原始数据大概率还在的这个特性,如果我们手头有信息泄露的漏洞的话,可以利用OOL port message来帮助我们获取port对象的地址,因为OOL port的name在传递到内核后会转换成ipc_port结构体在内核中的地址。
0x03 垃圾回收(garbage collection)和跨zone攻击
因为内存的资源是有限的,因此系统有了垃圾回收机制。如果一个page中的元素全部都被释放掉了,那么这个page会被标记成free page。当系统内存不足的时候,这个free page就会被回收,并且可能会被其他的zone使用。因此,攻击者可以利用这个特性进行跨zone的攻击。
根据公开的资料,在XNU上的这种攻击手法是beer最早在mach_portal[1]中提出的,随后在async_wake等exploit中也利用了跨zone攻击的技术来控制uaf后的port对象(图片来自beer的ppt):
那么今天我们就在可以进行kdp的macOS上测试一下跨zone攻击。我们的代码基本流程如下:
- 先分配10000个port对象(大概15M)和大概800M的kalloc.1024的对象。
- 随后选择一个port对象作为漏洞的攻击对象,比如利用info leak获取port在内核中的地址和利用UAF获取到一个dangling port。
- 随后释之前分配的port和kalloc.1024对象,让分配了这些对象的pages进入free page list。
- 接下来分配大量的kalloc.4096对象(在我们的测试中为2000M(macOS上),不同机型和系统版本因为内存不同,会有所差异)触发gc,并将dangling port指向的地址喷上我们伪造的ipc_port对象。
整个过程中,受影响的zone的状态信息如下:
通过调试我们可以看到,在进行gc和refill之前,dangling port所指向的地址还是一个在ipc.port zone中的ipc port对象:
进行了gc和refill之后,dangling port所指向的地址已经变成了kalloc.4096 zone中的内容了:
因此我们成功的实现了跨zone的攻击。
0x04 voucher_swap漏洞利用分析
CVE-2019-6225[3] 是最近被爆出来的影响iOS 12和macOS 10.14的UAF漏洞。原因是MIG自动生成的Xtask_swap_mach_voucher()代码中,没有正确的处理task_swap_mach_voucher()这个函数对new_voucher以及in_out_old_voucher对象的引用计数,从而造成了voucher对象的use after free。
攻击者可以利用task_swap_mach_voucher()和thread_set/get_voucher()来获取到一个悬空的voucher指针。再结合之前提到的跨zone攻击的方法,通过gc的方式将原本voucher的zone使用OOL port message填充上。这样悬空voucher指针指向的地址就和OOL port message中的某个port指针重合上了。随后,攻击者通过不断地调用task_swap_mach_voucher() (每次+1)来修改OOL port消息中的port指针的值,使其指向一个在pipe中伪造的ipc_port,最后再把OOL port接收回来,就可以在用户态获取到一个伪造port对象的端口了(图片来自Brandon的blog):
有了伪造port对象的端口,接下来通过在pipe中修改伪造port对象所指向的伪造task中的属性,即可实现内核内存的任意读写(具体代码请参考Brandon的exploit)。
0x05 总结
虽然苹果在XNU的zone中加入了很多的缓解机制,但是利用好其中的特性,依然可以在最新版本的iOS12以及macOS 10.14上实现有效的攻击。更多内容请参考我们的《iOS冰与火之歌》系列文章:https://github.com/zhengmin1989/iOS_ICE_AND_FIRE
0x06 参考文献
- Mach portal, ian beer https://bugs.chromium.org/p/project-zero/issues/detail?id=965
- iOS/MacOS kernel double free due to IOSurfaceRootUserClient not respecting MIG ownership rules, Google. https://bugs.chromium.org/p/project-zero/issues/detail?id=1417
- voucher_swap, Brandon https://googleprojectzero.blogspot.com/2019/01/voucherswap-exploiting-mig-reference.html