Mach IPC Security on macOS
Introduction to Mach IPC and System Services security
INTRO
It is one of the few articles on XNU internals. As the main article about XNU has become lengthy, I decided to publish the one about Mach Inter-Process Communication separately to not overwhelm readers (and myself).
Enjoy!
Mach
Tasks and threads (in BSD layer mapped as processes and threads). Communication between tasks occurs via Mach IPC.
Mach IPC has one-way communication channels.
Mach IPC
It enables tasks (processes) to exchange information through ports asynchronously. Main components:
- Ports: Kernel-protected communication channel (like pipe)
- Port Rights: Define permissions to interact with ports (handle)
- Messages: Structured data packets exchanged between ports
- Service: Name registered in the Bootstrap Server for a given port
- Bootstrap server: Service registration and discovery (
launchd)
There is also MIG (Mach Interface Generator), which is unnecessary for communication, but it streamlines the coding process and makes it safer.
IPC part of the Mach:
osfmk/ipc/,/osfmk/mach/andosfmk/kern/.
Port
It lives in the kernel’s memory and maintains a message queue. It can be used to send/receive messages and has a globally unique identifier (name, for example 0x103) and specific security rights(send/receive/send_once).
Port Right
A port right is a handle to a port that grants the ability to either send (enqueue) or receive (dequeue) messages (similar to a file descriptor).
Message
The actual data packets that flow through ports. It contains a header and body (data to send). It can be used to send port rights (privilege delegation).
The header is standardized, while the body can contain any data structure — this is why we have, for example, MIG.
Mach Interface Generator
MIG is a tool that automates the creation of proper IPC code. Think of MIG as an automated translator (because writing IPC by hand is hard/error-prone).
Why use MIG?
Developers without MIG would need to:
- Manually pack their data structures into the message body
- Ensure proper byte alignment and padding
- Handle architecture differences (32-bit vs 64-bit, endianness)
- Manage memory allocation and deallocation
- Deal with type conversions
- Handle all error cases
MIG simplifies IPC development by hiding low-level mach_msg complexities. It is shown on the example in the Mach IPC Programming point of this article.
Task Special Ports
Some ports in XNU are privileged and provide access to critical system services and capabilities. Some are automatically assigned during task creation (task_create). SIP and entitlements protect most of them.
These two ports are essential for communication.
TASK_BOOTSTRAP_PORT
A special port that every task gets when it is created. It provides a SEND right to communicate with launchd. Through this port, our process can:
- Register its own services using
bootstrap_check_in - Get
SENDrights to other services’ ports usingbootstrap_look_up
// How to get the bootstrap port for our program:
mach_port_t bootstrap;
task_get_special_port(mach_task_self(),
TASK_BOOTSTRAP_PORT,
&bootstrap);It is inherited from the parent process, ultimately from
launchdwhich is PID 1.
TASK_KERNEL_PORT
The mach_task_self returns the TASK_KERNEL_PORT for the current task. It is a SEND right to our own task port. We can use it to interact with ourselves.
Every task has it. This is the port by which the kernel knows the new child task. If we have SEND rights to it, we can inject the task.
Mach IPC Flow
We have four phases (graph does not show phase 1, it is described later):
- When the server starts, it registers its port in the bootstrap server.
- When the client searches for service in the bootstrap server.
- When a client sends a message to a service.
- When a client receives a message from a server.
Communication can be done without MIG, but it is commonly used.
Bootstrap Phase (Port Discovery)
Client needs to find the server’s port before communication starts. It is described in the Bootstrap Phase (Service Registration) point. After that:
- Client uses
bootstrap_look_upto get a send right to the server's port - The kernel validates and returns the appropriate port right
Communication Phase (Message Sending)
- Client’s MIG-generated code creates a message.
- Client calls
mach_msg_send()to send the message - Kernel copies message to server’s message queue
- Server receives notification of pending message
- Server’s MIG code unpacks the received message
- Server processes the request
Communication Phase (Reply)
- After processing the request, the server’s MIG code creates a response
- Server calls
mach_msg_send()to send the response message - Kernel manages the message transfer
- Client receives and unpacks response
Bootstrap Phase (Service Registration)
It is managed by launchd. It allows different processes to communicate with each other for the first time. It works like this:
- Every process can talk to
launchd(the bootstrap server) - Process A creates a communication channel (
port) and registers it withinlaunchdunder a name (e.g.com.example.service) - Process B can then ask
launchdfor access to Process A using that name
// Process A registering
kern_return_t kr;
kr = bootstrap_check_in(bootstrap_port,
"com.example.myservice", // service name
service_port); // port to register
// Process B looking up
mach_port_t service_port;
kr = bootstrap_look_up(bootstrap_port,
"com.example.myservice", // service name
&service_port); // will receive the portBootstrap on macOS uses launchd as a central coordinator that enables first-time communication between processes by letting them register and request (lookup) communication channels (ports) through a shared server.
Access Control — Granting Port Right
Processes can exchange port rights (SEND/RECEIVE) through a launchd.
- Process A creates a port with
RECEIVErights, generates aSENDright, and registers it withlaunchdunder aname(e.g.com.whatever.name). - Process B can lookup this
namethroughlaunchdto obtain theSENDright, enabling direct Mach message communication with Process A.
// Process A (Server)
mach_port_t server_port;
// Create port with RECEIVE right
kern_return_t kr = mach_port_allocate(
mach_task_self(), // our task
MACH_PORT_RIGHT_RECEIVE, // want receive right
&server_port // port name to create
);
// Create SEND right
mach_port_insert_right(
mach_task_self(), // our task
server_port, // port
server_port, // same port
MACH_MSG_TYPE_MAKE_SEND // convert to send right
);
// Register with launchd
bootstrap_check_in(
bootstrap_port, // launchd port
"com.example.service", // service name
server_port // port with send right
);
// Process B (Client)
mach_port_t client_port;
bootstrap_look_up(
bootstrap_port, // launchd port
"com.example.service", // service name
&client_port // receives send right
);Only the holder of a RECEIVE right for a port can generate SEND rights for that port. The receiver(*can be only one) controls who can send messages to it.
Tasks Security
By default, launchd does not restrict who registers the service. Moreover, any process can lookup and get SEND rights to their ports. This is why:
- System services (in
/System/Library/LaunchDaemonsand/System/Library/LaunchAgents) are protected by SIP - Entitlements protect system service functions
- User services can be impersonated by default
Process B can always get the SEND right through bootstrap_lookup. The security check should happen in Process A when it receives messages from Process B.
System Services Protection — Bootstrap & SIP
launchd protects system services in the bootstrap phase:
- It creates and holds the
RECEIVErights for their names - When a service starts, it must register (
bootstrap_check_in) launchdverifies the binary path matches with the list (SIP protected)launchdtransfer theRECEIVEright, if path was valid
// System Service checkin
mach_port_t service_port;
bootstrap_check_in( // same as bootstrap_register - which is deprecated
bootstrap_port,
"com.apple.systemservice", // must match plist
&service_port // receives the RECEIVE right
);It means processes cannot register system service names (launchd already holds them), and only the legitimate binary can register and get the RECEIVE right. Users can only get SEND rights and cannot impersonate system services (SIP).
System Services Protection — Entitlements
SIP protects the service registration process, preventing impersonation. Still, there are functions in these services behind the Mach message.
We can always get the SEND right and send messages, yet the service will reject unauthorized requests based on entitlements our process has.
Taskgated
When the kernel is asked for the task port of a process, it invokes this daemon (via launchd) to make the decision. It is task_for_pid access control daemon and every process can talk to it via TASK_ACCESS_PORT.
# Daemon PLIST:
/System/Library/LaunchDaemons/com.apple.taskgated.plistBugs in its logic that allow for task_for_pid are critical for task security.
Task Injection Bypasses
The task_for_pid allows a process to get the task port of another process. Then, we can use mach_vm_allocate, mach_vm_write, mach_vm_protect and thread_create_running for injection. However, there are some rules:
- As root, we can inject into any app that is not Apple platform binary (
is_platform_binary) or is not compiled with hardened runtime. - The task port of the app with
com.apple.security.get-task-allowcan be accessed by any other process, but it must run at the same user level. - If we pwned the app with
com.apple.system-task-ports, we could get the task port for any process except for the kernel.
On top of that, we have the kernel task port (task_for_pid(0)), which is heavily restricted, but if compromised, it gives full control of the system.
Mach IPC Programming
Here, I provide a simple client-server Mach IPC communication, both with and without MIG, but also using Distributed Objects.
Distributed Objects, XPC, or NSNotification, are built on top of the Mach, providing a higher access level to a low-level IPC mechanism.
Example Client-Server without MIG
There are only client.c and server.c. We can compile it like every C:
gcc server.c -o server
gcc client.c -o clientWe can see that we are using mach_msg directly for receiving the message, and there are at least three places where a developer can mess up:
In the case of the client, we are also using mach_msg directly for sending the message, and there are at least 4places where developers can mess up:
There is also a lot of code to maintain in both cases, client and server. Every change in specification forces the developers to make changes in both files.
Example Client-Server with MIG
Instead of writing the IPC code directly in the client.c and server.c, we declare functions and structures in one definition file:
Then, we can use MIG to generate the server and client code that will be able to communicate with each other to call the send_message function:
mig message.defs
# message.h - Header file with type declarations and function prototypes
# messageUser.c - Client-side IPC code to be used by client.c
# messageServer.c - Server-side IPC code to be used by server.cNow, the developer only needs to declare the function prototypes and then use the generated by the MIG subsystem handler function message_server:
The client only needs a line with an imported MIG-generated routine:
The client and server work like before:
MIG automates writing IPC glue code, letting focus on the server/client logic.
Example Client-Server with Distributed Object
The Objective-C runtime supports a distributed object's IPC.
Below is an example using deprecated API NSConnection:
There is also CFMessagePort, while not formally deprecated, is considered a legacy API just like NSConnection:
For more direct access to mach ports from Obj-C, we can use NSMachPort:
This point was not intended to teach how to program in distributed objects but to draw attention how many higher-level APIs are built on Mach IPC and there are more e.g., NSDistributedNotificationCenter or NSXPCConnection (XPC).
Swift — Kass
There is also a cool security research tool written in Swift. We can also use it to build a client-server and interact with Kernel in many other ways:
I encourage everyone interested in Mach to read the repository documentation, as it goes far beyond this article and is a great introduction to Mach with Swift.
IPC Table
Each process has a structure that consists of ipc_entry entries. Each entry maps port names (numerical values) to their corresponding Mach port objects in the kernel and associated port rights (like send/receive).
// Example entry in a process's IPC table (WindowServer is just an example, it could be any kernel port object)
struct ipc_entry {
ie_object = 0xfffff80123456789 // Points to WindowServer's port
ie_bits = 0x00010003 // Generation 1, SEND right, 3 references
ie_index = 42 // Port name base is 42
}The table starts with 32 entries and can grow when a process wants to share a port. The kernel creates a new entry in the receiving process’s IPC table, granting specific rights. Entry 0 is always reserved (serves as the head of the free list).
ie_object
This is a pointer to the actual Mach port object in the kernel.
- When we create a new port with
mach_port_allocate, a new kernel port object is created andie_objectpoints to it. - In an entry for the
WindowServerconnection,ie_objectwould point to the Mach port that lets our app communicate with theWindowServer.
For a NULL entry (free/unused),
ie_objectwould be set toIO_NULL.
ie_bits
This field packs several pieces of information together:
If a malicious process tries to guess port names, it needs to guess both the index and the correct generation number, making the attack much harder.
ie_index
This is the port name/identifier number. For example, a port might have an index 42, making its name 0x2A. Combined with the generation number, a full port name might look like 0x1002A (generation 16, index 42)
When we see a port name like 0x1003 in debug output, the last digits (3) are the index, and the preceding digits (100) include the generation number.
Mach IPC Recon on macOS
Every service must be registered (bootstrap) through launchd to work on macOS, and because of that, we can use launchtl tool for active recon:
# Show all System Services running:
launchctl list
launchctl print system
# Inspect user services
launchctl print user/$UID
# Oneliner to list all services
launchctl list | awk 'NR>1{print $3}'; launchctl print user/$UID | grep -Eo '(com|org)\.[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+' | sort -u
# Find port by its service name
sudo launchctl dumpstate | grep com_example_service
# Show process info
sudo launchctl procinfo $PID
# Resolve System Service name of the port for the PID
sudo launchctl resolveport PID PORT
# Locations
/System/Library/LaunchDaemons
/System/Library/LaunchAgents
/Library/LaunchDaemons
/Library/LaunchAgents
$HOME/Library/LaunchAgentsThere is also lsmp for listing mach ports:
# Show all ports for all tasks in the server
sudo lsmp -a
# Show all ports for the given process
sudo lsmp -p $(pgrep PROCESS_NAME)With disabled SIP, we can also utilise dtrace (and ktrace):
# Tracing mach_traps in crimson_server
sudo dtrace -n 'mach_trap::: /execname == "crimson_server"/ { printf("%s %d\n", execname, arg0); }'
# Show timestamps and process names whenever a Mach port is registered through the notification system
sudo dtrace -n 'notify*:::register_mach_port { printf("%Y: %s registered mach port\n", walltimestamp, execname); }'See also taskinfo and taskpolicy commands.
Using IPC table
We can also use existing APIs mach_port_space_info, mach_task_self and task_for_pid for enumeration of all ports for the given PID:
However, we cannot use task_for_pid against system services even with root, which makes this a little useless and it is better to use lsmp -p which is allowed to do that with com.apple.system-task-ports.read entitlement.
Forward Service Lookup
We can use bootstrap_look_up to check if there is any port registered under the given service name and also check our process rights to it:
(we should always get SEND rights to them, as it is by design)
Unfortunately, there is no API to get a service name from a port number(name). The Mach IPC system and bootstrap server only provide forward lookup (service name -> port number) but not reverse lookup (port number -> service name).
Special Task Port Right
We can use task_get_special_port MIG routine. I created a tool that enumerates our task rights to these ports. Interesting is the first result:
I also made a version for checking other PIDs permissions, which does not throw this security error:
So, the
mach_task_selfgives us access to our ownTASK_KERNEL_PORTbut trying to get it again viatask_get_special_portis blocked by security policy.
Reverse Engineering Mach IPC
For structures, we can use osfmk/ directory as it is open source:
- IPC (Inter-Process Communication):
osfmk/ipc/ - Thread and task management:
osfmk/kern/ - Virtual memory management:
osfmk/vm/ - Low-level hardware abstractions:
osfmk/arm/
See also the Mach API collection and Mach Enumeration.
Debugging
Breakpoints of our interest are mach_msg and mach_msg_overwrite as they are used to sending and receiving messages:
br set -n mach_msg
br set -n mach_msg_overwrite # For MIGFor example, when debugging our simple client-server example, we can see the content on the stack just after hitting the breakpoint:
However, we can do better than that by navigating the message memory layout based on proper Objective-C types rather than hardcoded offsets:
# Import Foundation and use Objc++
expr -l objc++ -O -- @import Foundation
# Cast the x0 to a Mach message header
p *(mach_msg_header_t *)$x0
# Skip over the mach_msg_header_t (+1) and cast message content to char
p (char *)((mach_msg_header_t *)$x0 + 1)Similarly, we can set a breakpoint for client-server example that use MIG, but this time we need to use mach_msg_overwrite as mach_msg is not hit:
Using the previous trick, we can parse the Mach message header, but it will not work for content. We can also observe the MIG subsystem ID used:
MIG messages are not simple text like in our previous example — they are RPC data. Before we can cast it to the message structure, we need to understand it.
Debugging MIG
The structure is generated by MIG, but most of the time, we do not have the source code. However, we can use msgh_id to find routine it implements:
Then, we can find it in decompiled code and find the function that handles the request. As we can see below, the function takes 3 parameters:
# I hided casts on the image
SERVER_send_message(
*(unsigned int *)(a1 + 12), # First parameter at offset 12
*(_QWORD *)(a1 + 28), # Second parameter at offset 28
*(unsigned int *)(a1 + 40) # Third parameter at offset 40
);When debugging and reading the message content, we need to look at these specific offsets from our x0 register. Let’s try reading them:
# MIG message structure contains:
p/x *(unsigned int *)($x0 + 12) # offset 12: Port/ID (0xd03)
p/x *(unsigned long *)($x0 + 28) # offset 28: Message pointer (0x100014000)
p/x *(unsigned int *)($x0 + 40) # offset 40: Length (0x13)We could also get an idea of where is our message content by looking at the implementation of the MIG subsystem 400 ID routine(function):
The point is, every implementation on top of Mach will have some nuances, but we can always start RE from message header (mach_msg_header_t) structure.
Static Analysis
The below functions can be considered as Mach artifacts when we want to quickly identify if a given program implements Mach IPC in its code:
//===============================
// 1. Message Handling Functions
//===============================
// Core message sending/receiving
mach_msg // Primary function for sending/receiving Mach messages
mach_msg_overwrite // Variant that can reuse buffer space for efficiency
mach_msg_server // Framework for handling MIG-generated message servers
_NDR_record // Network Data Representation for MIG
// Message structure manipulation
mach_msg_type_number_t // Used for managing message size and buffer lengths
mach_msg_header_t // Basic message header structure
mach_msg_trailer_t // Message trailer for security/audit information
//===============================
// 2. Service Management
//===============================
// Service registration
bootstrap_check_in // Register a service with launchd
bootstrap_register // Legacy registration method
bootstrap_look_up // Find a registered service by name
// Port management
mach_port_insert_right // Add a port right to a task
mach_port_allocate // Create new port rights
mach_port_deallocate // Clean up port rights
//===============================
// 3. Task Operations
//===============================
// Task port access
mach_task_self // Get the task port for current task
task_set_special_port // Configure special ports for a task
task_get_special_port // Retrieve special ports from a task
// Task manipulation
task_for_pid // Get task port for a process ID
pid_for_task // Get process ID for a task portI also described in detail how to identify MIG subsystems in this article:
# Look for _NDR_record
CrimsonUroboros -p PATH_TO_BIN --migWorking with MIG is easier than it looks. We just look for subsystems (msgh_id) and follow the cross-references to the function implementation behind it.
Decompilation
For the server, most of the time, we should check for bootstrap_check_in or bootstrap_register and analyze the mach_msg logic that follows:
Similarly, for the client, but we check the bootstrap_look_up:
This general RE flow, but it depends on the implementation and the developer's imagination. In general, there is always mach_msg or mach_msg_overwrite
FINAL WORDS
The article is not the shortest. Still, many things have been omitted, such as privileged ports or an example task injection with task_for_pid.
Although I uploaded it to the repository with comments, the shellcoding and process injection techniques deserve a separate article.
Moreover, I did not write about the XPC which is also built on top of Mach.
All these topics will appear in future articles about process injections and XPC.
References
I did not focus on the low-level vulnerabilities in Mach IPC. If this is a point of interest, I think a good start would be Ian Beer's presentations:
While working on this article, I read some very good shorter and longer pieces about Mach IPC from many cool guys in security. Here they are:
- Notes on Mach IPC by Ignacio Sanmillan
- Introduction to macOS — Mach Ports by Jonathan Bar Or
- An Introduction to Mach Vouchers by Robert Sesek
- Kass: A security research tool by Noah Gregory
- macOS IPC — Inter Process Communication by HackTricks
For vulnerability research, these presentations are extraordinary:
- Auditing and Exploiting Apple IPC by Ian Beer
- Exception-oriented exploitation on iOS by Ian Beer
- Splitting atoms in XNU by Ian Beer
- macOS IPC MitM by Samuel Groß
- Fuzzing at Mach Speed by Dillon Franke
- XNU heap exploitation: From kernel bug to kernel control by tihmstar
I also used some of Apple’s official documentation and various other manuals that should be referenced here:
- Mach Overview
- Introduction to Distributed Objects
- The GNU Mach Reference Manual
- Mach 3 Kernel Interfaces
- Mach ports — Darling Docs
The best book about Mach: *OS Internals Volume II by Jonathan Levin:
Also, self-reference, if you are interested in MIG, read Snake&Apple VI — AMFI
Sssssssssstay tuned.