Exploit-DB #40955
Description: Apple macOS < 10.12.2 / iOS < 10.2 Kernel - ipc_port_t Reference Count Leak Due to Incorrect externalMethod Overrides Use-After-Free
Type: dos
Platform: multiple
Date: 2016-12-22
Author: Google Security Research

Exploit:
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 l