Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=926
mach ports are really struct ipc_port_t's in the kernel; this is a reference-counted object,
ip_reference and ip_release atomically increment and decrement the 32 bit io_references field.
Unlike OSObjects, ip_reference will allow the reference count to overflow, however it is still 32-bits
so without either a lot of physical memory (which you don't have on mobile or most desktops) or a real reference leak
this isn't that interesting.
** MIG and mach message rights ownership **
ipc_kobject_server in ipc_kobject.c is the main dispatch routine for the kernel MIG endpoints. When userspace sends a
message the kernel will copy in the message body and also copy in all the message rights; see for example
ipc_right_copyin in ipc_right.c. This means that by the time we reach the actual callout to the MIG handler any port rights
contained in a request have had their reference count increased by one.
After the callout we reach the following code (still in ipc_kobject_server):
if ((kr == KERN_SUCCESS) || (kr == MIG_NO_REPLY)) {
// The server function is responsible for the contents
// of the message. The reply port right is moved
// to the reply message, and we have deallocated
// the destination port right, so we just need
// to free the kmsg.
ipc_kmsg_free(request);
} else {
// The message contents of the request are intact.
// Destroy everthing except the reply port right,
// which is needed in the reply message.
request->ikm_header->msgh_local_port = MACH_PORT_NULL;
ipc_kmsg_destroy(request);
}
If the MIG callout returns success, then it means that the method took ownership of *all* of the rights contained in the message.
If the MIG callout returns a failure code then the means the method took ownership of *none* of the rights contained in the message.
ipc_kmsg_free will only destroy the message header, so if the message had any other port rights then their reference counts won't be
decremented. ipc_kmsg_destroy on the other hand will decrement the reference counts for all the port rights in the message, even those
in port descriptors.
If we can find a MIG method which returns KERN_SUCCESS but doesn't in fact take ownership of any mach ports its passed (by for example
storing them and dropping the ref later, or using them then immediately dropping the ref or passing them to another method which takes
ownership) then this can lead to us being able to leak references.
** indirect MIG methods **
Here's the MIG request structure generated for io_service_add_notification_ool_64:
typedef struct {
mach_msg_header_t Head;
// start of the kernel processed data
mach_msg_body_t msgh_body;
mach_msg_ool_descriptor_t matching;
mach_msg_port_descriptor_t wake_port;
// end of the kernel processed data
NDR_record_t NDR;
mach_msg_type_number_t notification_typeOffset; // MiG doesn't use it
mach_msg_type_number_t notification_typeCnt;
char notification_type[128];
mach_msg_type_number_t matchingCnt;
mach_msg_type_number_t referenceCnt;
io_user_reference_t reference[8];
mach_msg_trailer_t trailer;
} Request __attribute__((unused));
This is an interesting method as its implementation actually calls another MIG handler:
static kern_return_t internal_io_service_add_notification_ool(
...
kr = vm_map_copyout( kernel_map, &map_data, (vm_map_copy_t) matching );
data = CAST_DOWN(vm_offset_t, map_data);
if( KERN_SUCCESS == kr) {
// must return success after vm_map_copyout() succeeds
// and mig will copy out objects on success
*notification = 0;
*result = internal_io_service_add_notification( master_port, notification_type,
(char *) data, matchingCnt, wake_port, reference, referenceSize, client64, notification );
vm_deallocate( kernel_map, data, matchingCnt );
}
return( kr );
}
and internal_io_service_add_notification does this:
static kern_return_t internal_io_service_add_notification(
...
if( master_port != master_device_port)
return( kIOReturnNotPrivileged);
do {
err = kIOReturnNoResources;
if( !(sym = OSSymbol::withCString( notification_type )))
err = kIOReturnNoResources;
if (matching_size)
{
dict = OSDynamicCast(OSDictionary, OSUnserializeXML(matching, matching_size));
}
else
{
dict = OSDynamicCast(OSDictionary, OSUnserializeXML(matching));
}
if (!dict) {
err = kIOReturnBadArgument;
continue;
}
...
} while( false );
return( err );
This inner function has many failure cases (wrong kernel port, invalid serialized data) which we can easily trigger and these error paths lead
to this inner function not taking ownership of the wake_port argument. However, MIG will only see the return value of the outer internal_io_service_add_notification_ool
which will always return success if we pass a valid ool memory descriptor. This violates ipc_kobject_server's ownership model where success means ownership
was taken of all rights, not just some.
What this leads to is actually quite a nice primitive for constructing an ipc_port_t reference count overflow without leaking any memory.
If we call io_service_add_notification_ool with a valid ool descriptor, but fill it with data that causes OSUnserializeXML to return an error then
we can get that memory freed (via the vm_deallocate call above) but the reference on the wake port will be leaked since ipc_kmsg_free will be called, not
ipc_kmsg_destroy.
If we send this request 0xffffffff times we can cause a ipc_port_t's io_references field to overflow to 0; the next time it's used the ref will go 0 -> 1 -> 0
and the object will be free'd but we'll still have a dangling pointer in our process's ports table.
As well as being a regular kernel UaF this also gives us the opportunity to do all kinds of fun mach port related logic attacks, eg getting send rights to
other task's task ports via our dangling ipc_port_t pointer.
** practicality **
On my 4 year old dual core MBA 5,2 running with two threads this PoC takes around 8 hours after which you should see a kernel panic indicative of a UaF.
Note that there are no resources leaks involved here so you can run it even on very constrained systems like an iPhone and it will work fine,
albeit a bit slowly :)
This code is reachable from all sandboxed environments.
** fixes **
One approach to fixing this issue would be to do something similar to OSObjects which use a saturating reference count and leak the object if the reference count saturates
I fear there are a great number of similar issues so just fixing this once instance may not be enough.
Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40955.zip