New issue
Advanced search Search tips
Starred by 4 users

Issue metadata

Status: Fixed
Owner:
Closed: Jan 22
Cc:

Restricted
  • Only users with EditIssue permission may comment.



Sign in to add a comment
link

Issue 1731: iOS/macOS: task_swap_mach_voucher() does not respect MIG semantics leading to use-after-free

Reported by bazad@google.com, Dec 6 Project Member

Issue description

The dangers of not obeying MIG semantics have been well documented: see issues  926  (CVE-2016-7612), 954 (CVE-2016-7633), 1417 (CVE-2017-13861, async_wake), 1520 (CVE-2018-4139), 1529 (CVE-2018-4206), and 1629 (no CVE), as well as CVE-2018-4280 (blanket). However, despite numerous fixes and mitigations, MIG issues persist and offer incredibly powerful exploit primitives. Part of the problem is that MIG semantics are complicated and unintuitive and do not align well with the kernel's abstractions.

Consider the MIG routine task_swap_mach_voucher():

	routine task_swap_mach_voucher(
			task		: task_t;
			new_voucher	: ipc_voucher_t;
		inout	old_voucher	: ipc_voucher_t);

Here's the (placeholder) implementation:

	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;
	}

The correctness of this implementation depends on exactly how MIG ownership semantics are defined for each of these parameters.

When dealing with Mach ports and out-of-line memory, ownership follows the traditional rules (the ones violated by the bugs above):

1. All Mach ports (except the first) passed as input parameters are owned by the service routine if and only if the service routine returns success. If the service routine returns failure then MIG will deallocate the ports.

2. All out-of-line memory regions passed as input parameters are owned by the service routine if and only if the service routine returns success. If the service routine returns failure then MIG will deallocate all out-of-line memory.

But this is only part of the picture. There are more rules for other types of objects:

3. All objects with defined MIG translations that are passed as input-only parameters are borrowed by the service routine. For reference-counted objects, this means that the service routine is not given a reference, and hence a reference must be added if the service routine intends to keep the object around.

4. All objects with defined MIG translations that are returned in output parameters must be owned by the output parameter. For reference-counted objects, this means that output parameters consume a reference on the object.

And most unintuitive of all:

5. All objects with defined MIG translations that are passed as input in input-output parameters are owned (not borrowed!) by the service routine. This means that the service routine must consume the input object's reference.

Having defined MIG translations means that there is an automatic conversion defined between the object type and its Mach port representation. A task port is one example of such a type: you can convert a task port to the underlying task object using convert_port_to_task(), and you can convert a task to its corresponding port using convert_task_to_port().

Getting back to Mach vouchers, this is the MIG definition of ipc_voucher_t:

	type ipc_voucher_t = mach_port_t
			intran: ipc_voucher_t convert_port_to_voucher(mach_port_t)
			outtran: mach_port_t convert_voucher_to_port(ipc_voucher_t)
			destructor: ipc_voucher_release(ipc_voucher_t)
		        ;

This definition means that MIG will automatically convert the voucher port input parameters to ipc_voucher_t objects using convert_port_to_voucher(), convert the ipc_voucher_t output parameters into ports using convert_voucher_to_port(), and discard any extra references using ipc_voucher_release(). Note that convert_port_to_voucher() produces a voucher reference without consuming a port reference, while convert_voucher_to_port() consumes a voucher reference and produces a port reference.

To confirm our understanding of the MIG semantics outlined above, we can look at the function _Xtask_swap_mach_voucher(), which is generated by MIG during the build process:

	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;
	}

Tracing where each of the references are going, we can deduce that:

1. The new_voucher parameter is deallocated with ipc_voucher_release() after invoking the service routine, so it is not owned by task_swap_mach_voucher(). In other words, task_swap_mach_voucher() is not given a reference on new_voucher.

2. The old_voucher parameter has a reference on it before it gets overwritten by task_swap_mach_voucher(), which means task_swap_mach_voucher() is being given a reference on the input value of old_voucher.

3. The value returned by task_swap_mach_voucher() in old_voucher is passed to convert_voucher_to_port(), which consumes a reference on the voucher. Thus, task_swap_mach_voucher() is giving _Xtask_swap_mach_voucher() a reference on the output value of old_voucher.

Finally, looking back at the implementation of task_swap_mach_voucher(), we can see that none of these rules are being followed:

	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;
	}

This results in two separate reference counting issues:

1. By overwriting the value of in_out_old_voucher without first releasing the reference, we are leaking a reference on the input value of old_voucher.

2. By assigning the value of new_voucher to in_out_old_voucher without adding a reference, we are consuming a reference we don't own, leading to an over-release of new_voucher.

Now, Apple has previously added a mitigation to make reference count leaks on Mach ports non-exploitable by having the reference count saturate before it overflows. However, this mitigation is not relevant here because we're leaking a reference on the actual ipc_voucher_t, not on the voucher port that represents the voucher. And looking at the implementation of ipc_voucher_reference() and ipc_voucher_release() (as of macOS 10.13.6), it's clear that the voucher reference count is tracked independently of the port reference count:

	void
	ipc_voucher_reference(ipc_voucher_t voucher)
	{
		iv_refs_t refs;

		if (IPC_VOUCHER_NULL == voucher)
			return;

		refs = iv_reference(voucher);
		assert(1 < refs);
	}

	void
	ipc_voucher_release(ipc_voucher_t voucher)
	{
		if (IPC_VOUCHER_NULL != voucher)
			iv_release(voucher);
	}

	static inline iv_refs_t
	iv_reference(ipc_voucher_t iv)
	{
		iv_refs_t refs;

		refs = hw_atomic_add(&iv->iv_refs, 1);
		return refs;
	}

	static inline void
	iv_release(ipc_voucher_t iv)
	{
		iv_refs_t refs;

		assert(0 < iv->iv_refs);
		refs = hw_atomic_sub(&iv->iv_refs, 1);
		if (0 == refs)
			iv_dealloc(iv, TRUE);
	}

(The assert()s are not live on production builds.)

This vulnerability can be triggered without crossing any privilege/MACF checks, so it should be reachable within every process and every sandbox.

On iOS 11 and macOS 10.13, both the over-reference and over-release vulnerabilities can be independently exploited to free an ipc_voucher_t while it is still in use. On these platforms these are incredibly powerful vulnerabilities, since they also let us receive a send right to a freed-and-reallocated Mach port back in userspace. For some examples of why this is dangerous, see Ian's thoughts in  issue 941 : <https://bugs.chromium.org/p/project-zero/issues/detail?id=941#c3>.

As of iOS 12 and macOS 10.14, the voucher reference count is checked for underflow and overflow, which does make the over-reference vulnerability non-exploitable. However, the over-release vulnerability is still fully exploitable, and probably can still be used as a single, direct-to-kernel bug from any process.

Additionally, while this report is of a single bug, it should indicate a wider problem with the complexity of obeying MIG semantics. It might be worth reviewing other edge cases of MIG semantics not covered by previous bugs.

(There's a variant of the over-reference vulnerability in thread_swap_mach_voucher(), but it is no longer exploitable as of iOS 12.)

This proof-of-concept demonstrates the vulnerability by creating a Mach voucher, saving a reference to it in the current thread's ith_voucher field via thread_set_mach_voucher(), decreasing the reference count back to 1 using task_swap_mach_voucher(), and then freeing the voucher by deallocating the voucher port in userspace. This leaves a dangling pointer to the freed voucher's memory in ith_voucher, which can subsequently be accessed with a call to thread_get_mach_voucher(), triggering a panic.

Tested on macOS 10.13.6 (17G4015), macOS 10.14.2, and iOS 12.1 (16B92).
 
voucher_swap-poc.c
14.1 KB View Download

Comment 1 by bazad@google.com, Jan 18

Project Member
Labels: CVE-2019-6225

Comment 2 by bazad@google.com, Jan 22

Project Member
Labels: Fixed-2019-Jan-22
Status: Fixed (was: New)
Fixed in iOS 12.1.3: https://support.apple.com/en-us/HT209443
Fixed in macOS 10.14.3: https://support.apple.com/en-us/HT209446

Comment 3 by bazad@google.com, Jan 24

Project Member
Labels: -Restrict-View-Commit
Derestricting as @S0rryMybad released his blog post describing the vulnerability.

Comment 4 Deleted

Comment 5 Deleted

Comment 6 Deleted

Comment 7 Deleted

Comment 8 Deleted

Comment 9 by hawkes@google.com, Jan 25

Project Member
Labels: Restrict-AddIssueComment-EditIssue

Comment 10 by bazad@google.com, Jan 29

Project Member
voucher_swap exploit attached. Obtains the kernel task port and establishes a kernel function calling primitive on the iPhone XS, iPhone XR, and iPhone 8 running iOS 12.1.2.
voucher_swap.zip
74.3 KB Download

Sign in to add a comment