Mach IPC (Inter-Process Communication)
Mach IPC is the backbone of Darwin. All communication between processes, between userspace and the kernel, and between kernel subsystems goes through Mach messages and ports. This is the most important component for kernel exploitation.
Why This Is the Most Important Component
- Mach messages are allocated on the kernel heap – used for heap spraying/grooming
- Port objects are kernel objects – targets for type confusion, UAF
- OOL (out-of-line) descriptors – controlled kernel allocations with arbitrary data
- Task ports – anyone with a send right to the kernel task = full kernel r/w
- MIG-generated handlers – large attack surface, historically buggy
Mach Ports
Concept
A Mach port is a kernel-managed endpoint for a message queue. Conceptually similar to a file descriptor but more powerful.
// Port in the kernel
struct ipc_port {
struct ipc_object ip_object; // Header (type, refs, lock)
struct ipc_mqueue ip_messages; // Message queue
natural_t ip_mscount; // Make-send count
natural_t ip_srights; // Send rights count
mach_port_rights_t ip_sorights; // Send-once rights count
union {
struct ipc_kmsg *imq_messages; // Message list
struct task *task; // For IKOT_TASK ports
struct thread *thread; // For IKOT_THREAD ports
struct semaphore *semaphore; // For IKOT_SEMAPHORE
IOUserClient *iokit_object; // For IKOT_IOKIT_CONNECT
} kdata;
struct ipc_port *ip_nsrequest; // No-senders notification
struct ipc_port *ip_pdrequest; // Port-destroyed notification
// ...
};
Port Rights
| Right | Meaning | Limit |
|---|---|---|
| Receive right | Allows receiving messages from the port | Only 1 holder in the entire system |
| Send right | Allows sending messages | Multiple holders, reference counted |
| Send-once right | Send exactly 1 message then self-destructs | Used for reply ports |
Process A ─── send right ───→ Port ←── receive right ─── Process B
Process C ─── send right ──╱ (only B receives messages)
Capability model:
- Having a right = having a capability
- No other mechanism controls access (not ACL-based)
- Transferring a right = transferring a capability (via Mach messages)
Port Lifecycle
// 1. Allocate port (get receive right)
mach_port_t port;
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
// 2. Insert send right
mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
// 3. Send message
mach_msg_header_t msg = {
.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0),
.msgh_size = sizeof(msg),
.msgh_remote_port = port, // destination
.msgh_local_port = MACH_PORT_NULL,
.msgh_id = 0x1234
};
mach_msg(&msg, MACH_SEND_MSG, sizeof(msg), 0, 0, 0, 0);
// 4. Receive message
mach_msg(&msg, MACH_RCV_MSG, 0, sizeof(msg), port, 0, 0);
// 5. Destroy port
mach_port_destroy(mach_task_self(), port);
Mach Messages
Message Structure
// Simple message (no ports or OOL data)
struct {
mach_msg_header_t header; // 24 bytes
// optional: inline data
char data[256];
} simple_msg;
// Complex message (contains ports or OOL data)
struct {
mach_msg_header_t header;
mach_msg_body_t body; // descriptor count
// Descriptors:
mach_msg_port_descriptor_t port_desc; // Send port right
mach_msg_ool_descriptor_t ool_desc; // OOL memory pointer
// Trailing data
} complex_msg;
Header
typedef struct {
mach_msg_bits_t msgh_bits; // Type bits (complex, send/receive types)
mach_msg_size_t msgh_size; // Total message size
mach_port_t msgh_remote_port; // Destination port
mach_port_t msgh_local_port; // Reply port (optional)
mach_port_name_t msgh_voucher_port;// Voucher port
mach_msg_id_t msgh_id; // Message ID (for MIG dispatch)
} mach_msg_header_t;
msgh_bits Encoding
// Remote port type (destination)
MACH_MSG_TYPE_COPY_SEND // Copy send right
MACH_MSG_TYPE_MOVE_SEND // Move send right (sender loses it)
MACH_MSG_TYPE_MAKE_SEND // Make send from receive right
MACH_MSG_TYPE_MOVE_SEND_ONCE // Move send-once right
// Complex bit
MACH_MSGH_BITS_COMPLEX // Message contains descriptors
// Helper macro
MACH_MSGH_BITS(remote, local) // Combine remote + local types
Out-of-Line (OOL) Data – Exploitation Primitive
OOL Memory Descriptor
When message data is too large, use OOL: the kernel allocates kernel memory and copies the data in.
mach_msg_ool_descriptor_t ool = {
.address = user_buffer, // Userspace buffer (source)
.count = size, // Size in bytes
.deallocate = FALSE, // Don't free source after copy
.copy = MACH_MSG_VIRTUAL_COPY, // Copy strategy
.type = MACH_MSG_OOL_DESCRIPTOR
};
Why this matters for exploitation:
1. Attacker sends an OOL message with controlled data
2. Kernel allocates a buffer on the kernel heap (kalloc zone)
3. Kernel copies the attacker's data into the buffer
4. Message enqueued → buffer stays allocated
5. Attacker has controlled data on the kernel heap!
Used for:
- Heap spraying: send many OOL messages of the same size → fill the zone
- Heap grooming: spray → free selected → allocate target object
- Fake object creation: OOL data = fake kernel struct
OOL Ports Descriptor
Send an array of port rights:
mach_msg_ool_ports_descriptor_t ports_desc = {
.address = port_array, // Array of mach_port_t
.count = num_ports, // Number of ports
.deallocate = FALSE,
.disposition = MACH_MSG_TYPE_COPY_SEND,
.type = MACH_MSG_OOL_PORTS_DESCRIPTOR
};
Exploitation: Each port right in OOL causes the kernel to allocate an ipc_port pointer entry. Controlling port array content = controlling kernel heap layout.
Task Ports – The Ultimate Goal
Task Port Concept
struct task {
// ...
struct ipc_port *itk_self; // Task's self port
struct ipc_port *itk_nself; // Task's notify port
struct vm_map *map; // Address space
// ...
};
task_self()returns a send right to your own task porttask_for_pid(target_task, pid, &port)gets a send right to another task
tfp0 (Task-For-Pid 0 = Kernel Task Port)
mach_port_t kernel_task_port;
task_for_pid(mach_task_self(), 0, &kernel_task_port);
// If successful: kernel_task_port = send right to kernel task
// → Use mach_vm_read/mach_vm_write to read/write kernel memory
On modern iOS:
task_for_pid(0)is blocked by entitlement checks + sandbox- Must create a fake kernel task port after obtaining a kernel r/w primitive
- Or use
mach_vm_*through alternative primitives
Fake Task Port Attack (Classic)
1. Spray IOSurface objects on the kernel heap
2. Trigger vulnerability → kernel write primitive
3. Craft a fake ipc_port structure in controlled memory
4. Overwrite port pointer → point to fake port
5. Fake port's kdata.task → pointer to fake task struct
6. Fake task's map → pointer to kernel_map
7. mach_vm_read(fake_port, kernel_addr, ...) → reads kernel memory!
MIG (Mach Interface Generator)
MIG generates C code for Mach IPC interfaces:
.defs file → MIG compiler → client stubs + server stubs
MIG Definition Example
routine task_threads(
target_task : task_inspect_t;
out act_list : thread_act_array_t;
out act_listCnt : mach_msg_type_number_t
);
MIG generates:
- Client stub: serialize arguments, mach_msg, deserialize reply
- Server stub: deserialize message, call implementation, serialize reply
Attack surface:
- MIG handlers parse complex message structures
- Historically many bugs in size validation, port handling
ipc_kmsg_copyin()handles complex descriptor parsing – bugs here = kernel exploitation
Exploitation Patterns Summary
| Pattern | Mechanism | Example |
|---|---|---|
| Heap spray | OOL messages of the same size fill a kalloc zone | Fill zone before triggering vuln |
| Heap grooming | Spray, free selected, allocate target | Control adjacency on heap |
| Port UAF | Free a port but keep a reference | Overwrite freed port memory |
| Fake port | Craft a fake ipc_port struct | Redirect port operations |
| Port replacement | Replace kernel object via dangling port | Type confusion |
| Message race | Race between send and receive | TOCTOU on message data |
Resources
- Apple Developer – Mach Overview
- Notes on Mach IPC – ulexec
- XNU IPC – Mach Messages – dmcyk
- Damien Deville – IPC on iOS with Mach Messages
- Ali Pourhadi – IPC Mach Message
- XNU source:
osfmk/ipc/,osfmk/kern/ipc_tt.c
Exercises
- Write a Mach IPC client/server: Create 2 processes that communicate via Mach messages on macOS
- Spray the kernel heap: Send 1000 OOL messages of 256 bytes, observe kernel memory usage
- Enumerate ports: Write a tool to list all Mach ports of a target process (using
mach_port_names) - Send a complex message: Send a message containing a port right + OOL data, receive in another process
- Trace a MIG call: Identify the MIG subsystem/routine ID for
task_threads, trace the kernel handling path