154 lines
No EOL
7 KiB
Text
154 lines
No EOL
7 KiB
Text
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 |