Underneath the BSD process model that macOS shows the world is Mach, and Mach has a very different idea of what a process is. To Mach a process is a task: a container holding a virtual address space, a set of threads, and a collection of ports. If you can get the right kind of handle to another task’s special control port, you do not need a loader trick or an environment variable to inject code. You can allocate memory in that task, write your shellcode into it, set up a stack, and spawn a thread to run it: all from outside, all through documented Mach APIs. It is the most surgical injection primitive on the platform.

This post works through Mach IPC from the ground up: ports and rights, the bootstrap server that brokers them, the special ports that matter, and then the full injection sequence built on task_for_pid. It ends on a BlockBlock case study, where this exact technique injects execv shellcode into a security product. The dylib techniques from last post relied on the loader trusting a path; this relies on the kernel handing you control of another task.

Ports, Rights, and the Bootstrap Server

A Mach port is a kernel-managed message queue. You do not touch a port directly; you hold a right to it, and the right’s type decides what you can do:

  • A receive right lets you read messages from the queue. Exactly one task holds it at a time: it is ownership of the port.
  • A send right lets you put messages on the queue. Many tasks can hold a send right to the same port, and a send right can be copied and passed in messages.

That asymmetry (one receiver, many senders, rights movable through messages) is the entire IPC model. A server allocates a port, keeps the receive right, and hands send rights to clients who want to talk to it.

The obvious problem: how does a client get that send right in the first place, without already having a channel to ask for it? That is the job of the bootstrap server, which on macOS is launchd. Every task automatically holds a send right to the bootstrap server. The dance:

# filename: bootstrap.txt
1. Task A allocates a port (receive right) and makes a send right to it.
2. Task A registers:  bootstrap_register(bootstrap_port, "com.example.svc", port)
3. Task B looks up:   bootstrap_look_up(bootstrap_port, "com.example.svc", &port)
4. The bootstrap server hands Task B a send right to A's port.
5. A and B now exchange mach_msg() messages directly.

A minimal server allocates the port, inserts a send right, and registers a name:

// filename: server.c
mach_port_t port;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
bootstrap_register(bootstrap_port, "org.darlinghq.example", port);
// then mach_msg(...) to receive

The bootstrap server does not verify that a task claiming a service name has any right to that name. Whoever registers first owns it. That missing check is the seed of an entire class of attacks: register a service name before the legitimate owner and clients connect to you. The XPC post later in this series builds on exactly this trust gap.

The Special Ports, and the One That Owns a Task

Beyond service ports, every task has access to a handful of special ports that grant privileged capabilities:

  • HOST_PORT: read non-privileged host information.
  • HOST_PRIV_PORT: the privileged version, gating sensitive host operations (loading kernel extensions among them, which the kernel-code-execution post returns to).
  • The task port (the task’s kernel port): the one that matters here. A send right to a task’s port is control over that task. With it you can read and write its memory, change its threads’ register state, and create new threads. It is the Mach equivalent of Windows’ OpenProcess(PROCESS_ALL_ACCESS).

So injection reduces to a single question: can you get a send right to the target’s task port? Access is guarded. Historically taskgated mediated task_for_pid; today it is governed by entitlements and SIP. Two entitlements are worth memorizing:

  • com.apple.security.get-task-allow: if the target carries this, any process at the same user level can grab its task port. This is the debug entitlement, and on a target it is a gift.
  • com.apple.system-task-ports: if the caller carries this, it can obtain the task port of other processes. It is the keys to the kingdom and Apple hands it out sparingly.

The reason get-task-allow is so dangerous is that it converts “I can run code as this user” into “I can take over this specific privileged-ish process,” with no further bug required. Auditing a target’s entitlements for it is step one.

The Injection Sequence

Assume you can get the task port. The injection is a clean, five-API sequence, each call the Mach analogue of a Windows process-injection primitive.

1. Get the task port with task_for_pid:

// filename: inject-1.c
mach_port_t remoteTask;
kern_return_t kr = task_for_pid(mach_task_self(), pid, &remoteTask);
// KERN_SUCCESS (0) means we now hold a send right to the target's task port.

2. Allocate memory in the target (one region for the shellcode, one for a stack) with mach_vm_allocate (the VirtualAlloc analogue):

// filename: inject-2.c
mach_vm_address_t remoteStack64 = 0, remoteCode64 = 0;
mach_vm_allocate(remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
mach_vm_allocate(remoteTask, &remoteCode64,  CODE_SIZE,  VM_FLAGS_ANYWHERE);

VM_FLAGS_ANYWHERE lets the kernel pick the addresses and reports back where it put them: which is exactly why the payload must be the position-independent shellcode from the earlier post. You do not get to choose where your code lands.

3. Write the shellcode into the remote code region with mach_vm_write (the WriteProcessMemory analogue):

// filename: inject-3.c
mach_vm_write(remoteTask, remoteCode64, (vm_address_t)shellcode, CODE_SIZE);

4. Fix the page permissions. Freshly written memory is not executable; the stack must not be executable. Set them explicitly with vm_protect: read+execute on the code, read+write on the stack:

// filename: inject-4.c
vm_protect(remoteTask, remoteCode64,  CODE_SIZE,  FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE,  VM_PROT_READ | VM_PROT_WRITE);

This W^X split is not optional hygiene: modern macOS enforces it, and skipping it faults the thread the instant it runs.

5. Build a thread state and launch it with thread_create_running. You hand the kernel an x86_THREAD_STATE64 describing the new thread’s registers: RIP pointing at remoteCode64, and RSP/RBP pointing into the stack region: shifted to the middle of the allocation so the stack has room to grow downward without immediately running off the start:

// filename: inject-5.c
remoteStack64 += (STACK_SIZE / 2);          // start mid-region, grow down
state.__rip = remoteCode64;
state.__rsp = remoteStack64;
state.__rbp = remoteStack64;
thread_create_running(remoteTask, x86_THREAD_STATE64,
                      (thread_state_t)&state, x86_THREAD_STATE64_COUNT, &thread);

The moment thread_create_running returns, a brand-new thread is executing your shellcode inside the target task. No file touched disk, no library was loaded, no environment variable was set. The target’s own code keeps running on its threads while yours runs alongside.

BlockBlock: Injecting Into a Security Tool

This post closes by turning the technique on a real target: Objective-See’s BlockBlock, a popular macOS persistence monitor. The chain is instructive precisely because the victim is a security product:

  1. Find the process: enumerate PIDs to locate BlockBlock’s running process.
  2. Get the task port: the vulnerability is that the process is reachable; obtain its task port.
  3. Inject execv shellcode: write a payload that runs an arbitrary binary, using the position- independent C-derived shellcode from the earlier post.
  4. Promote the Mach thread to a POSIX thread, and this is the subtle, important step. A thread created by thread_create_running is a bare Mach thread; it has no pthread structure, so any payload that calls into libc (and execv ultimately does) will misbehave or crash. The fix is to have the shellcode call pthread_create_from_mach_thread, which bootstraps a proper POSIX thread around the Mach thread before doing real work. Skip it and the injection “works” right up until the first libc call.

That last point is the one people miss. Getting code running in another task is the easy 90%; making that code able to call normal library functions without the runtime falling over is the other 90%. The Mach world and the POSIX world have to be reconciled, and pthread_create_from_mach_thread is the bridge.

Why This Matters

Mach task-port injection is the cleanest injection on macOS because it does not depend on the target cooperating with the loader: no hijackable dylib, no injectable environment, no file on disk. It depends only on getting a send right to the task port, which is why the entire defensive story reduces to who may obtain that right. get-task-allow on a target and system-task-ports on a caller are the two entitlements that decide it, and auditing for them is how you find injectable processes.

It also surfaces the bootstrap server’s missing-verification problem, which is not an injection bug at all but the foundation of the XPC attacks coming next. The bootstrap server brokers trust between processes and verifies almost nothing about who is claiming what. Mach gives you the primitives; XPC builds a richer protocol on top of them, and inherits the same questions about who is really on the other end of the connection. That is where the series goes next.

References

  1. Apple. task_for_pid, mach_vm_allocate, mach_vm_write, thread_create_running. Kernel Framework Reference, developer.apple.com.
  2. Wardle, P. Objective-See: BlockBlock. objective-see.org.
  3. Apple. launchd / bootstrap.h source. opensource.apple.com.