XPC Programming on macOS
Introduction to Cross Process Communication (XPC)
INTRO
This is a continuation of my previous article, where I described how Mach Inter-Process Communication (IPC) works, which is a foundation for XPC:
However, I did not write anything about the XPC itself, and this article will be exactly about it, focusing on building and reverse engineering XPC.
Enjoy!
XPC
Cross-process communication operates through a client-server model where services can be either integrated with an app or system-wide.
The core idea behind XPC is service-oriented architecture. Instead of having one large app that does everything, programs are split into smaller.
Why did Apple develop XPC?
Before XPC, macOS apps ran as single processes with shared privileges and memory space. This caused two key issues:
- Security: Bug in any part of an app gave access to all its privileges
- Stability: Component crashes would take down the entire application
Apple introduced XPC to enable splitting apps into isolated components with minimal necessary privileges to improve security and stability.
Architecture High-Level Overview
At its heart, XPC has two main components:
- Service: It is like a specialized worker that performs specific tasks. It runs in its own separate space, isolated from other processes.
- Client: It is an app that needs something done and requests a service to do it. It also runs in separate spaces, most often with fewer privileges.
There can be multiple XPC services for a single client.
It allows privilege separation by isolating processes with different privilege levels. Services expose only authorized methods and retain elevated privileges to perform sensitive operations that unprivileged clients cannot directly execute.
Communication
The communication happens through structured messages that support various data types. On a high level, messages are passed as dictionaries.
While developers work with the high-level XPC API, internally the system uses Mach ports to transfer the serialized message data between processes.
Service Manager
The XPC services are managed by launchd, which handles their lifecycle - launching on demand, restarting after crashes, and terminating when idle.
We can create XPC as a Daemon or a bundled app component. In the case of XPC Service as a Daemon, the process is spawned by launchd:
In the case of the XPC Bundle, the process is spawned by the main app:
// As a daemon service (then we connect using Mach name)
NSXPCListener *listener = [[NSXPCListener alloc] initWithMachServiceName:@"crimson.test-terminal-xpc-helper"];
// Bundled (then we connect from a Client using BundleID of the XPC service)
NSXPCListener *listener = [NSXPCListener serviceListener];Services can be killed anytime. They should be stateless or have minimal state.
XPC Services Types
They come in three types based on their location and accessibility:
# System-wide (MachServices) - LaunchDaemons
/System/Library/LaunchDaemons
/Library/LaunchDaemons
# User domain (MachServices) - LaunchAgents
$HOME/Library/LaunchAgents
/Library/LaunchAgents
# App Private (App Bundles) - XPC Service
EXAMPLE.app/Contents/XPCServices/We can also find Framework XPC services, but they are kind of system-wide XPC services, which are bundled like app Helpers:
# System Framework Private XPC
/System/Library/PrivateFrameworks/*/XPCServices/System-wide services (LaunchDaemons) are accessible to authorized clients with proper entitlements, and user session services (LaunchAgents) are accessible to apps within that user’s session. In contrast, app-bundled private XPC services are only accessible to their parent application.
Changes in macOS 13
In macOS 13+, helper executables like launch agents and daemons can be included directly in the app bundle, simplifying installation:
# Old locations - still works
Contents/XPCServices/
$HOME/Library/LaunchAgents
/Library/LaunchAgents
/Library/LaunchDaemons# New locations
Contents/Resources
Contents/Library/LaunchAgents
Contents/Library/LaunchDaemonsSee Updating helper executables from earlier versions of macOS.
Bundled XPC Service example
An example can be a Stickies.app that has bundled StickiesMigration XPC:
# XPC Binary
/System/Applications/Stickies.app/Contents/XPCServices/StickiesMigration.xpc/Contents/MacOS/StickiesMigrationYet, it does not run all the time. It is only invoked on data migration.
Framework XPC services example
However, when we inspect the Stickies.app in the Activity Monitor, we may see two XPC services that were not bundled:
# com.apple.appkit.xpc.openAndSavePanelService
/System/Library/Frameworks/AppKit.framework/Versions/C/XPCServices/com.apple.appkit.xpc.openAndSavePanelService.xpc/
# com.apple.quicklook.QuickLookUIService
/System/Library/Frameworks/QuickLookUI.framework/Versions/A/XPCServices/QuickLookUIService.xpc/Contents/MacOS/It is kind of a hybrid solution, because they can be spawned by all aps (if build with given framework), but on launch they are only available for parent app.
Bundle Structure
XPC services live in Contents/XPCServices/ with reverse-DNS naming (but not always. For example, below we can see StickiesMigration.xpc as name):
It is also possible to create not bundled XPC services and also without Info.plist. We will build later such solution as a system-wide service (LaunchDaemon).
Info.plist
Each bundle has its Info.plist which, among other things, contains:
# Example _AllowedClients value:
identifier = com.apple.Stickies and (anchor apple or (anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9]))At the moment we can only use Application as the ServiceType for the 3rd party apps. See also Managing Your App’s Information Property List.
Entitlements
They allow for privilege separation and sandbox boundaries. For example, Stickies.app has broader user file and print access, while its XPC service maintains minimal permissions only for migration. Both are sandboxed:
However, configuring JoinExistingSession in the XPC service’s Info.plist will break this separation and allow XPC service the same privileges as main app.
Bundled XPC Programming
Here, we will build a simple Client app with bundled helper XPC service. We have two APIs available for that NSXPCConnection and C-based API:
// Objective-C / Swift API in Foundation.framework
// /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/
NSXPCConnection // Connect to XPC Service (client)
NSXPCInterface // Defines connection behavior (both, but implemented in service)
NSXPCListener // Handles incoming connections (service)
NSXPCListenerDelegate // Rules that accept or reject connection (service)# C-based API header files (/usr/include/xpc/):
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/xpc/
# Implemented in libxpc.dylib (packed in Dyld Shared Cache)
/usr/lib/system/libxpc.dylibWe will start with NSXPCConnection API and learn how to build bundled XPC Helper. See also Creating XPC services — a shorter version of this “guide”.
NSXPC Communication
App (Client) creates NSXPCConnection to XPC services, defines the protocol interface, and sends messages via proxy objects. Simplified API flow:
# There can be many interfaces (protocols)
NSXPCConnection -(NSXPCInterface)-> NSXPCListenerXPC Service (Helper) implements protocol methods, runs in separate processes, and handles requests via NSXPCListener.
The core of communication is
NSXPCInterface(protocol) which describes allowed messages, kinds of objects as arguments, signatures of any reply blocks, and information about additional proxy objects.
Building Client App
Before we build a helper tool, we need an app as a foundation. Here, I prepared a simple code of the Cocoa app that has only write access to ~/Downloads/xpc_test_write but needs info from ~/Desktop/xpc_test_read.
We will never reach the write function, as we are blocked on readFromFile. This is restricted by the sandbox entitlement we set below:
The last thing is to create these two files and execute the app. When we click on the Read xpc_test_read button, we will see a permission error:
Here comes the helper.
Creating XPC Service in XCode
Now, we will build the XPC helper that can read ~/Desktop/xpc_test_read on request and return the content to the client as text.
We must first add XPC Service in Xcode (click File -> New -> Target):
XCode will automatically create a proper main.m file with the function that creates an XPC listener that waits for incoming connections:
On connection, delegate’s shouldAcceptNewConnection method sets up the communication protocol and exports the helper object handling requests.
This is also a crucial step for security as the XPC Service may conduct additional checks and reject the connection based on their result.
Everything runs in an infinite loop because [listener resume] does not return, making the helper continuously available for connections.
Building XPC Service
We need to define the XPC protocol the app and helper will use. It declares a single method readFileWithCompletion that takes a completion block that returns file content as NSString and any error via NSError:
The protocol defines a method in the XPC helper that the main app can call remotely — kind of RPC.
We also need to modify test_terminal_xpc_helper.m to implement the actual file reading functionality defined in our protocol:
Lastly, we need an entitlements file with sandbox restrictions set to off:
Now, we can implement the connection logic in the main app (client).
Adding XPC connection to Client App
Here, we are adding a method that will connect to the XPC service and ask it to read info from ~/Desktop/xpc_test_read and return it to us for use.
First, we must import our protocol for XPC communication and set the NSXPCConnection property that will maintain the connection.
Then, in the setupXPCConnection method, we create a new NSXPCConnection object targeting the crimson.test-terminal-xpc-helper XPC service. Here, we also define the protocol the connection will use for communication.
In readAndWriteAction we interact with our XPC helper. First, it retrieves the remote proxy object test_terminal_xpc_helperProtocol. It is used to send a request to the XPC service.
Then, we got readFileWithCompletion, which is called on the proxy object to read a file. Lines 44–52 are just error handling.
If no error occurs, dispatch_async updates run on the main thread. The file content is set to textView using setString . This confirms that XPC read the file, and we get the valid content so it appears in the window:
Finally, we call our writeToFile to write to ~/Downloads/xpc_test_write.
Let’s check if it works.
Final result
As previously, we execute the app and click on Read xpc_test_read button:
TCC prompts us for access to the Desktop. This comes from the XPC helper. When we click on Allow, we can see textView updated our Window:
Finally, we can double-check if xpc_text_write content was populated:
The graph below summarizes this small experiment. (Actually, the XPC Service had broader access as it runs unsandboxed, so it could read anything).
I uploaded the final version of the app compressed to the repository here.
Low-Level XPC Programming
Here, we create a Launch Daemon XPC service using C and a terminal-like client to communicate with it. The overview of the process is shown below:
This is based on Using the C XPC Services API. The below code is a minimal (and insecure) implementation of client-server using XPC.
Understanding the Core Components
Our implementation consists of three main parts:
- An XPC service (server) that receives and responds to messages
- A LaunchDaemon configuration that registers our service
- A client that sends messages and receives responses
Below is the complete code of these parts. Each of the functions used is well documented by Apple Documentation. I also provided additional comments on the files uploaded to the repository.
The XPC Service (Server)
A minimal server that listens for messages and sends replies:
// crimson_xpc_service.c
#include <xpc/xpc.h>
#include <dispatch/dispatch.h>
#include <stdio.h>
int main(void) {
// Create service listener for our Mach service
xpc_connection_t service = xpc_connection_create_mach_service(
"com.crimson.xpc.message_service",
dispatch_get_main_queue(),
XPC_CONNECTION_MACH_SERVICE_LISTENER
);
// Handle incoming connections
xpc_connection_set_event_handler(service, ^(xpc_object_t peer) {
if (xpc_get_type(peer) != XPC_TYPE_CONNECTION) return;
// Set up message handler for this peer
xpc_connection_set_event_handler(peer, ^(xpc_object_t message) {
if (xpc_get_type(message) == XPC_TYPE_DICTIONARY) {
// Get and print the received message
size_t len;
const void* data = xpc_dictionary_get_data(message, "message_data", &len);
if (data) printf("Received: %.*s\n", (int)len, (char*)data);
// Send a simple reply
xpc_object_t reply = xpc_dictionary_create_reply(message);
xpc_dictionary_set_string(reply, "status", "received");
xpc_connection_send_message(peer, reply);
xpc_release(reply);
}
});
xpc_connection_resume(peer);
});
xpc_connection_resume(service);
dispatch_main();
}The XPC Client
A minimal client that sends a message and handles the response:
// crimson_xpc_client.c
#include <xpc/xpc.h>
#include <dispatch/dispatch.h>
#include <stdio.h>
#include <stdlib.h>
int main(void) {
// Create connection to our service
xpc_connection_t conn = xpc_connection_create_mach_service(
"com.crimson.xpc.message_service",
dispatch_get_main_queue(),
XPC_CONNECTION_MACH_SERVICE_PRIVILEGED
);
// Basic error handling
xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
if (xpc_get_type(event) == XPC_TYPE_ERROR) {
fprintf(stderr, "Connection error: %s\n",
xpc_dictionary_get_string(event, XPC_ERROR_KEY_DESCRIPTION));
}
});
xpc_connection_resume(conn);
// Create and send a simple message
xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(message, "message_data", "Hello from Crimson!");
// Send message and handle reply
xpc_connection_send_message_with_reply(conn, message,
dispatch_get_main_queue(), ^(xpc_object_t reply) {
if (xpc_get_type(reply) == XPC_TYPE_DICTIONARY) {
printf("Reply status: %s\n",
xpc_dictionary_get_string(reply, "status"));
}
dispatch_async(dispatch_get_main_queue(), ^{
exit(0);
});
});
xpc_release(message);
dispatch_main();
}LaunchDaemon Configuration
To register our service with the system, we need this configuration:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.crimson.xpc.message_service</string>
<key>Program</key>
<string>/usr/local/bin/crimson_xpc_service</string>
<key>MachServices</key>
<dict>
<key>com.crimson.xpc.message_service</key>
<true/>
</dict>
<key>KeepAlive</key>
<false/>
</dict>
</plist>See Creating Launch Daemons and Agents and XPC Service Property List Keys.
Building and Deployment
I have created a Makefile to simplify the build and installation process. To build it and register the service in launchd manually we can use:
# Compile
gcc -o crimson_xpc_service crimson_xpc_service.c -framework Foundation
gcc -o crimson_xpc_client crimson_xpc_client.c -framework Foundation
# Install
sudo cp crimson_xpc_service /usr/local/bin/
sudo cp com.crimson.xpc.message_service.plist /Library/LaunchDaemons/
sudo launchctl load /Library/LaunchDaemons/com.crimson.xpc.message_service.plistRunning
After installing our XPC service, we can list it using launchctl command as a valid system service and communicate it using our client:
As long as the service is loaded, even if the service process is dead, we can use the client to connect, and it will work as launchd will start it for us:
Okay, it works, but let’s look at how.
Connection Creation and Management
Both client and service use xpc_connection_create_mach_service, but with different flags. The key difference is in the last parameter:
// Service side
xpc_connection_t service = xpc_connection_create_mach_service(
"com.crimson.xpc.message_service",
dispatch_get_main_queue(),
XPC_CONNECTION_MACH_SERVICE_LISTENER // Indicates we're the service
);
// Client side
xpc_connection_t conn = xpc_connection_create_mach_service(
"com.crimson.xpc.message_service",
dispatch_get_main_queue(),
XPC_CONNECTION_MACH_SERVICE_PRIVILEGED // It can also be just 0
);The service uses LISTENER flag to indicate it is accepting connections, while the client specifies namespace where to look for connection. It has nothing to do with privileges, it just speed up how launchd lookup for name of the service.
Event Handler Setup
Both sides need to handle connection events. Both use the same API but with different responsibilities xpc_connection_set_event_handler:
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
// Event handling logic varies between client and server
});That strange syntax
^(xpc_object_t event)is an Obj-C block (lambda). It is defining a function that will be called whenever an event occurs. The^symbol indicates “this is a block of code that will be executed later (on connection).”
Server Event Handler
The server operates as a listener, primarily handling new connection events and managing client connections. Server-Side Event Handling:
xpc_connection_set_event_handler(service, ^(xpc_object_t peer) {
// First, validate that we received a connection event
if (xpc_get_type(peer) != XPC_TYPE_CONNECTION) return;
// For each new client, set up a dedicated message handler
xpc_connection_set_event_handler(peer, ^(xpc_object_t message) {
if (xpc_get_type(message) == XPC_TYPE_DICTIONARY) {
// Process the client's message
}
});
// Enable communication with the new client
xpc_connection_resume(peer);
});When a new client(peer) connects, the server receives XPC_TYPE_CONNECTION event and establishes a separate message handler for that client's messages.
Client Event Handler
The client focuses on processing message responses and monitoring connection status. Client-Side Event Handling:
xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {
if (xpc_get_type(event) == XPC_TYPE_ERROR) {
// Handle connection problems
} else if (xpc_get_type(event) == XPC_TYPE_DICTIONARY) {
// Process server's response
}
});The client’s event handler processes events (errors and dictionary messages). While we cannot create new event types, we can implement custom message structures within the dictionary messages to handle different types.
Creating Message
The message creation API is xpc_dictionary_create on both sides. We must utilize a dictionary as a container for the data we want to send:
xpc_object_t message = xpc_dictionary_create(NULL, NULL, 0);
//
// Dictionary keys are typically reverse-DNS style, like com.crimson.service.data.All XPC objects can be created manually using xpc_object_t, but there are easier ways to do that using predefined functions like xpc_data_create.
Generally, the naming convention is like this xpc_*_create. We will now create a simple data buffer with the help of xpc_data_create:
xpc_object_t our_message, our_buffer;
char data[] = "Hello XPC"; // Example data to send
size_t data_len = sizeof(data);
// Create empty dictionary
our_message = xpc_dictionary_create(NULL, NULL, 0);
// Create data buffer object
our_buffer = xpc_data_create(data, data_len);
// Set the data buffer in the dictionary with a key "message_data"
xpc_dictionary_set_value(our_message, "message_data", our_buffer);It can be even easier using a direct setter API like xpc_dictionary_set_data:
xpc_object_t our_message;
char data[] = "Hello XPC";
our_message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_data(our_message, "message_data", data, sizeof(data));We can send any data type inside the dictionary, but the message container must be a dictionary so the event handler can understand it.
Message Response Creation
Both sides can create reply messages using xpc_dictionary_create_reply:
// Creating a reply (usually service side)
xpc_object_t reply = xpc_dictionary_create_reply(received_message);
// Setting reply data (common pattern)
xpc_dictionary_set_string(reply, "status", "received");The xpc_dictionary_create_reply function maintains the connection context necessary for routing the reply back to the sender. When this dictionary is sent across the reply connection, the remote end’s reply handler is invoked.
Connection Lifecycle Management
Both sides must activate their connections using xpc_connection_resume:
xpc_connection_resume(connection);Both sides need to handle the XPC object lifecycle using xpc_release:
xpc_release(message); // Release when done with message
xpc_release(reply); // Release when done with replyBoth sides integrate with Grand Central Dispatch (GCD):
dispatch_get_main_queue() // Used in connection creation
dispatch_main() // Used to run the event loopMessage routing is asymmetric — clients typically send and await replies, while services receive and send back responses. GCD manages this.
Grand Central Dispatch and XPC
At its core, XPC communication is asynchronous — messages can be sent and received anytime, and we need a way to handle these events.
This is where GCD comes in. Let’s break this down.
The Role of GCD in XPC
Think of GCD as the traffic controller for your XPC communication. Just as a traffic controller manages multiple lanes of traffic flowing in different directions, GCD manages multiple streams of XPC messages and events.
xpc_connection_t connection = xpc_connection_create_mach_service(
"com.example.service",
dispatch_get_main_queue(), // This is our traffic controller
flags
);When we create an XPC connection with dispatch_get_main_queue, we are telling XPC: “Handle all events for this connection on the main thread”.
The Event Loop
The dispatch_main function is like turning on the traffic light system. It:
- Creates an infinite loop that processes events
- Manages the scheduling of blocks submitted to dispatch queues
- Handles the delivery of XPC messages and events to our handlers
xpc_connection_resume(connection);
dispatch_main(); // Start the event processing loop (never returns)Without it, traffic (messages) would not flow.
GCD on example XPC Service
Below is an example of XPC Service to show how the main queue works:
#include <xpc/xpc.h>
#include <dispatch/dispatch.h>
int main(void) {
// Create our XPC connection
xpc_connection_t connection = xpc_connection_create_mach_service(
"com.example.service",
dispatch_get_main_queue(), // Messages will be handled on main queue
XPC_CONNECTION_MACH_SERVICE_LISTENER
);
// Set up our event handler
xpc_connection_set_event_handler(connection, ^(xpc_object_t event) {
// This block will always run on the main queue
// This is safe because we used dispatch_get_main_queue() above
process_event(event);
});
// Set up message handler
xpc_connection_set_message_handler(connection, ^(xpc_object_t message) {
// This also runs on the main queue
// We can safely update UI or access shared resources
handle_message(message);
});
// Start accepting connections
xpc_connection_resume(connection);
// Start the event loop
// This call never returns - it keeps processing events forever
dispatch_main();
// Code here will never be reached
return 0;
}With GCD, dispatch_get_main_queue ensures all messages are processed in an order, and dispatch_main keeps the service running and responding to clients.
XPC Recon on macOS
Using launchctl
As XPC is built on top of Mach, we can use similar techniques described in the previous article, Mach IPC Security on macOS.
# List registered services in system/gui/user domains:
launchctl print system
launchctl print gui/501/
launchctl print user/$UID
# List running XPC services (use also sudo for root level services)
launchctl list | grep xpc # This will show our Obj-C App
sudo launchctl list | grep xpc # This will show our LaunchDaemonUsing Process Status
As the naming convention convinces to use .xpc in the name of the service bundles, we can also just list processes on our macOS with xpc filter:
ps aux | grep -i xpcUsing lsof
We can examine all files and resources that the XPC service has open:
sudo lsof -p SERVICE_PIDUsing CrimsonUroboros
We can get many things from the XPC bundle or its binary with CrimsonUroboros. Please read the documentation to learn about it.
CrimsonUroboros --entitlements --path/--bundle
CrimsonUroboros --checksec --path/--bundle
CrimsonUroboros --
CrimsonUroboros --bundle_info --bundleUsing SandBlaster
Most of the time, XPC works under a sandbox. We can get it in compiled form using CrimsonUroboros and then decompile it with SandBlaster:
# Get compiled Sandbox Profile of the XPC (works also for binaries -p)
CrimsonUroboros --sandbox_profile_data -b XPC_BUNDLE.app > sandbox_profile
# Decompile profile (saved to sandbox_profile.sb)
python3 reverse_sandbox.py -o sonoma_sandbox_operations.txt sandbox_profile -r 17I described the technical details behind it in the Sandbox Validator article.
Reverse Engineering XPC
The core XPC functionality is divided into several logical components, each with its own header file in /usr/include/xpc/:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/xpc- Low-level messaging primitives (
xpc.h) - Connection management layer (
connection.h,endpoint.h) - Service management layer (
listener.h,session.h) - Task management layer (
activity.h)
The implementation of these components lives in
libxpc.dylib, which is part of the dyld shared cache. While the implementation is not open source, the headers show how XPC is structured. XPC is also well-documented by Apple.
Decompilation
The below functions can be considered as XPC artifacts when we want to quickly identify if a given program implements XPC in its code:
// Core connection creation
xpc_connection_create // Generic connection creation
xpc_connection_create_mach_service // Mach service-specific connection
// Essential setup and messaging
xpc_connection_set_event_handler // Message handling setup
xpc_connection_resume // Connection activation
xpc_dictionary_create // Message container creation
xpc_connection_send_message // Message transmission
xpc_release // Memory management
xpc_get_type // Get type of XPC object
xpc_type_dictionary // Type that always exists// Main XPC Classes - foundation for Objective-C XPC communication
NSXPCConnection // Manages client-side connections
NSXPCListener // Handles server-side connection acceptance
// Common class methods and symbols
// Client-side connection setup
-[NSXPCConnection initWithMachServiceName:] // Initialize connection to named service
-[NSXPCConnection setRemoteObjectInterface:] // Define expected remote interface
-[NSXPCConnection resume] // Start connection processing
-[NSXPCConnection setDelegate] // Set connection event handler
// Server-side connection handling
-[NSXPCListener setDelegate] // Set listener event handler
-[NSXPCListener shouldAcceptNewConnection] // Connection acceptance controlThe fastest way is to filter all functions to find those with xpc* in their names.
Static Analysis
When working with Obj-C code, decompiler will not show the above names in the function window. Instead, we should look at the imports tab:
Then, we should cross-reference from these imports to reach their implementation in the main code:
For the server, we should find how it registers itself as a service, then look for the event handler and start the analysis of the method implemented for messages.
Our simple example will have two blocks in the main function. The first block handles new peer connections and then executes the second block:
This is where we should focus, as here the message handling logic lies (an important thing to notice: there may be more than just two blocks!):
For the client, we should find out how it establishes the connection, then look for message construction/sending and reply logic.
Here, we may also see blocks invoked for specific events. For instance, in reply to our message, we would trigger the below function (block):
No matter what language, analyzing asynchronous programs that use multi-threading is hard just statically. This is why it is much better to use debugging.
To learn more about blocks read the Apple Block ABI.
Debugging — Service
As with every communication mechanism, we should focus on sending and receiving functions. XPC has a few sending functions and one receiving:
# Send
xpc_connection_send_message
xpc_connection_send_message_with_reply
xpc_connection_send_message_with_reply_sync
# Recv
xpc_connection_set_event_handler
# A frequently hit breakpoint for checking the type of XPC objects
xpc_get_typeWe can attach to services after they are loaded by launchd using their pids:
lldb -p $(pgrep SERVICE_NAME)On the service end on message arrival, we will trigger the handler first:
br set -n xpc_connection_set_event_handlerWe can get the XPC debugDescription from x0 register, where we can find the name and PID of the process that made the connection:
# po $x0 use underneath:
expr -l objc -O -- [(NSObject *)$x0 debugDescription]
# To inspect the Expression Evaluation Mechanism
log enable lldb exprWhat is more interesting for us is the Event Handler Block function, which will be hit next. We can get this information from the x1 register:
(lldb) po $x1We could also read now xpc_dictionary_t and see the content of the message inside, but it is complicated. Moreover, we often want to inspect the logic of the function implemented in the block, and the message is passed to it as argument.
It is easier to set a breakpoint on the block and inspect the x1 argument to get the XPC dictionary container. As we can see, there is also our data:
br set -n __main_block_invoke_2
c
po $x1Next, there is also custom logic, as we can do anything we want on the message. The final breakpoint should be set on one of the XPC send message functions.
Debugging — Client
When debugging an XPC client, we should focus on how messages are sent to the service, how responses are received, and what is done after with it.
lldb -p $(pgrep CLIENT_NAME)We should start with a breakpoint where the XPC initializes its connection and inspects the x0 register to see the service we want to connect to:
br set -n xpc_connection_create
br set -n xpc_connection_create_mach_service
x/s $x0Now, depends what we want to debug, we can break on the send functions to inspect outgoing messages or on dictionary create to inspect the logic behind the data creation we want to send to the service:
br set -n xpc_connection_send_message
br set -n xpc_connection_send_message_with_reply
br set -n xpc_connection_send_message_with_reply_sync
# Data creation
br set -n xpc_dictionary_createAt this point, we can also see in the x0 register where the data is sent to:
Then, a message is sent to the service, and we will receive a reply later. On the client side, it is handled the same way using xpc_connection_set_event_handler. We should make a breakpoint there and inspect handling logic, which is custom.
LLDB helper
To streamline setting breakpoints when working with XPC, I created a simple Python script for LLDB. It is available to download here.
command script import ~/.lldb/set_xpc_breaks.py
set_xpc_breaksI will probably do something better with task injections for tracing real-time XPC messages in the future and also for lldb to work in the background.
Final Words
Despite the article being very long, there are many things to work with.
First, both XPC implementations shown in this article are highly insecure, and the following article will be about how to secure them properly.
Speaking of security, I did not mention a single vulnerability in XPC itself, but they exist, and many were found in the past. Make sure to check links below if you are interested in vulnerability research with XPC!
References
However, for people who want to get into XPC security right now, there is a great presentation from Wojciech Reguła and Csaba Fitzl linked below:
While working on this article, I read some very good shorter and longer pieces about XPC from many cool security researchers. Here they are:
- Abusing & Securing XPC in macOS apps by Wojciech Reguła
- Reverse Engineering the XPC Objects by Kai Lu
- The Story Behind CVE-2019–13013 by Christian
- Breaking iOS: XPC by George Dan
- iOS containermanagerd XPC array OOB vulnerability by Marco Grassi
- Reversing Shorts: Apple’s Cross-Process Communication (XPC) by jiska
- Using debugDescription with XPC objects by Jeremy W. Sherman
- Don’t Trust the PID! AND Bits of launchd by Samuel Groß
- Abusing XPC Service to Elevate Privilege in macOS by Zhipeng Huo
I also used some of Apple’s official documentation and various other manuals that should be referenced here:
- Creating XPC Services
- Framework XPC
- Managing Your App’s Information Property List
- Modernizing Grand Central Dispatch Usage
Self-references
Here are some of my articles useful for mentioned in this article Sandbox, which is heavily used for XPC services and apps on macOS in general:
Also, check the Snake&Apple repository.
Sssssssssstay tuned.