
on Mac 10.12.2
This privilege escalation exploits a vulnerability in mach_voucher_extract_attr_recipe_trap, and the core of the exploitation method is through MACH_MSG_OOL_PORTS_DESCRIPTOR messages.
Regarding mach_msg ool, simply put, when sending an msg containing an ool descriptor, the kernel copies the specified data from user space to kernel space, and the kernel keeps this data until the target task processes the message. Similarly, when the target process receives a message containing an ool descriptor, the kernel copies the data from kernel space to user space (not necessarily a true copy). Therefore, this technique can be used to write data to the kernel heap or read data from the kernel.
Since the exploitation of this vulnerability is much more complicated than Trident, I will analyze it step by step, from the vulnerability's origin to its gradual exploitation. The code can be referenced on my github
Among the new features added in iOS 10 and macOS 10.12, there is a function called mach_voucher_extract_attr_recipe_trap, which is a Mach trap that can be called within the sandbox. Below is the source code of this function:
kern_return_t
mach_voucher_extract_attr_recipe_trap(struct mach_voucher_extract_attr_recipe_args *args)
{
ipc_voucher_t voucher = IV_NULL;
kern_return_t kr = KERN_SUCCESS;
mach_msg_type_number_t sz = 0;
//将recipe_size的地址拷贝到sz中,此时sz存放的就是kalloc_size的值了
if (copyin(args->recipe_size, (void *)&sz, sizeof(sz))) <---------- (a)
return KERN_MEMORY_ERROR;
if (sz > MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE)
return MIG_ARRAY_TOO_LARGE;
voucher = convert_port_name_to_voucher(args->voucher_name);
if (voucher == IV_NULL)
return MACH_SEND_INVALID_DEST;
mach_msg_type_number_t __assert_only max_sz = sz;
if (sz < MACH_VOUCHER_TRAP_STACK_LIMIT) {
/* keep small recipes on the stack for speed */
uint8_t krecipe[sz];
if (copyin(args->recipe, (void *)krecipe, sz)) {
kr = KERN_MEMORY_ERROR;
goto done;
}
kr = mach_voucher_extract_attr_recipe(voucher, args->key,
(mach_voucher_attr_raw_recipe_t)krecipe, &sz);
assert(sz <= max_sz);
if (kr == KERN_SUCCESS && sz > 0)
kr = copyout(krecipe, (void *)args->recipe, sz);
} else {
uint8_t *krecipe = kalloc((vm_size_t)sz); <---------- (b)
if (!krecipe) {
kr = KERN_RESOURCE_SHORTAGE;
goto done;
}
if (copyin(args->recipe, (void *)krecipe, args->recipe_size)) { <----------- (c)
kfree(krecipe, (vm_size_t)sz);
kr = KERN_MEMORY_ERROR;
goto done;
}
kr = mach_voucher_extract_attr_recipe(voucher, args->key,
(mach_voucher_attr_raw_recipe_t)krecipe, &sz);
assert(sz <= max_sz);
if (kr == KERN_SUCCESS && sz > 0)
kr = copyout(krecipe, (void *)args->recipe, sz);
kfree(krecipe, (vm_size_t)sz);
}
kr = copyout(&sz, args->recipe_size, sizeof(sz));
done:
ipc_voucher_release(voucher);
return kr;
}
args->recipe_size is written into sz.sz is between MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120) and MACH_VOUCHER_TRAP_STACK_LIMIT (256), a kernel heap buffer is allocated according to the value of sz.sz used to allocate the kernel heap, but a user space pointer, thus causing a heap overflow. This is precisely the point we exploit for the attack. Moreover, the copyin function has a feature: it stops copying when encountering an unmapped page. This feature will be utilized in our poc:freelist randomization, we no longer know the location of the reallocated memory blocks.First, we need to understand the handling of MACH_MSG_OOL_PORTS_DESCRIPTOR in mach_msg. When the kernel receives a complex message and finds it is a ports descriptor, it hands it to the ipc_kmsg_copyin_ool_ports_descriptor function (called by ipc_kmsg_copyin) to read all port objects. This function calls kalloc to allocate the required memory (under 64-bit, the allocated memory is twice the input, and the length of name is 4 bytes). Then it converts valid ports from name to the actual ipc_port object address and saves them. For name inputs that are MACH_PORT_NULL or MACH_PORT_DEAD, they remain unchanged.
/* calculate length of data in bytes, rounding up */
if (os_mul_overflow(count, sizeof(mach_port_t), &ports_length)) {
*mr = MACH_SEND_TOO_LARGE;
return NULL;
}
if (os_mul_overflow(count, sizeof(mach_port_name_t), &names_length)) {
*mr = MACH_SEND_TOO_LARGE;
return NULL;
}
if(ports_length == 0){
return user_desc;
}
data = kalloc(ports_length); // 分配空间
...
objects = (ipc_object_t *) data;
dsc->address = data;
for ( i = 0; i < count; i++) {
mach_port_name_t name = names[i];
ipc_object_t object;
if (!MACH_PORT_VALID(name)) {
objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name);// IPC_PORT_DEAD continue;
}
...
}
Therefore, during the attack, we send a large number of MACH_PORT_DEAD to fill the memory area with 0xFFFFFFFFFFFFFFFF (MACH_PORT_DEAD). Then we trigger the vulnerability to modify one IPC_PORT_DEAD into a memory area arranged by the attacker. If the pointed area is a legitimate ipc port structure, then after receiving the OOL PORTS message, we can obtain the port name corresponding to this ipc_port in user space, proceeding to the next stage of the attack.
ipc_objectFirst, we have obtained this fake port. Next, to perform information leakage, we must know which parameters the kernel uses to handle it differently. First, let's look at the structure of ipc_port:
struct ipc_port {
//ipc_object的指针就在前八个字节,是我们溢出攻击的对象
struct ipc_object ip_object; // port对象的类型 struct ipc_mqueue,ip_messages;
struct ipc_mqueue ip_messages; //消息队列
union {
struct ipc_space *receiver;
struct ipc_port *destination;
ipc_port_timestamp_t timestamp;
}data;
union {
ipc_importance_task_t imp_task;
ipc_kobject_t kobject; // port对应的内核对象
uintptr_t alias;
}kdata;
...
} __attribute__((__packed__));
Among them, there is a kernel object corresponding to the port. The type of kernel object corresponding to this ipc_port is determined by the attributes of ipc_object. Therefore, we actually construct for ipc_object.
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11; //设置port锁处于活动状态,防止死锁
The kernel will then recognize this ipc_port as a port for communicating with the IKOT_CLOCK object. The next goal is to leak the kernel base address:
Forge this ipc_port as an IKOT_CLOCK object, then set its kdata.kobject pointer to a kernel address. Each time this kernel address is modified, calling clock_sleep_trap in user space causes the kernel to invoke port_name_to_clock to obtain this kernel address and pass it as the clock parameter to clock_sleep_internal. The source code is as follows: