Frida Engage Part Three | You Down With XPC?

Written By: Ben Watson


In the final installment of the Frida Engage blog series, we will demonstrate how to use Frida for hooking and inspecting Apple’s NSXPC API using the CleanMyMac 3 application as our guinea pig.


XPC is one flavor of the Inter-Process Communication technologies provided by Apple.

“The XPC Services API, part of libSystem, provides a lightweight mechanism for basic interprocess communication integrated with Grand Central Dispatch (GCD) and launchd. The XPC Services API allows you to create lightweight helper tools, called XPC services, that perform work on behalf of your application.” [1]

Apple provides developers two XPC API formats:

  • NSXPCConnection API
    • Objective-C based API that is part of the Foundation framework
  • XPC Services API
    • C-based XPC Services API

The NSXPCConnection API contains the following components:

  • NSXPCConnection
    • A class that represents the bidirectional communication channel between two processes [1]
  • NSXPCConnection
    • A class that describes the expected programmatic behavior of the connection [1]
  • NSXPCListener
    • A class that listens for incoming XPC connections [1]
  • NSXPCListenerEndpoint
    • A class that listens for incoming XPC connections [1]

Our goal is to understand how the NSXPConnection API is implemented and utilized. From the serpent’s mouth, the NSXPConnection API is used to create lightweight “helper tools”, called XPC services [1]. Let’s check out the XPC services that come bundled up with the CleanMyMac 3 application.

CleanMyMac 3 XPC Services

The CleanMyMac 3 application contains the following XPC services:

╭─[email protected] /Applications/CleanMyMac
╰─$ ls -la
total 0
drwxr-xr-x 4 rotlogix staff 128 Feb 9 08:47 .
drwxr-xr-x 12 rotlogix staff 384 Feb 9 08:47 ..
drwxr-xr-x 3 rotlogix staff 96 Feb 9 08:47 com.macpaw.CleanMyMac.IconsService.xpc
drwxr-xr-x 3 rotlogix staff 96 Feb 9 08:47 com.macpaw.CleanMyMac.LipoService.xpc

The LipoService looks pretty interesting! After some investigation, I discovered that this service simply wraps the lipo command itself.

“The lipo command creates or operates on “universal” (multi-architecture) files. It only ever produces one output file, and never alters the input file. The operations that lipo performs are: listing the architecture types in a universal file; creating a single universal file from one or more input files; thinning out a single universal file to one specified architecture type; and extracting, replacing, and/or removing architectures types from the input file to create a single new universal output file.”

If we give the application a spin, we can see that the service has been launched. Let’s dive into LipoService and see what XPC stuff is going on under the hood.

ps -x | grep macpaw
11257 ?? 0:00.02 ... /Contents/MacOS/com.macpaw.CleanMyMac.LipoService


XPC Service Creation

In order to create an XPC Service using the NSXPCConnection API you will need to configure a listener object [1].

int main(int argc, const char *argv[]) {
MyDelegateClass *myDelegate = ...
NSXPCListener *listener =
[NSXPCListener serviceListener];
listener.delegate = myDelegate;
[listener resume];
// The resume method never returns.

If we check out LipoService's imports, we can observe the existence of the NSXPCListener class!

0000000100006AF0 _OBJC_CLASS_$_NSXPCInterface /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
0000000100006AF8 _OBJC_CLASS_$_NSXPCListener /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation

The class NSXPCListener contains a single cross reference, originating from the start function.

__objc_classrefs:00000001000064F0 ; id classRef_NSXPCListener
__objc_classrefs:00000001000064F0 classRef_NSXPCListener dq offset _OBJC_CLASS_$_NSXPCListener
__objc_classrefs:00000001000064F0 ; DATA XREF: start+13↑r
public start
start proc near
push rbp
mov rbp, rsp
push r15
push r14
push r12
push rbx
call _objc_autoreleasePoolPush
mov r14, rax
mov rdi, cs:classRef_NSXPCListener ; id
mov rsi, cs:selRef_serviceListener ; SEL
mov r12, cs:_objc_msgSend_ptr

The next requirement for creating an XPC service is creating a connection delegate class that conforms to the protocol NSXPCListenerDelegate [1]. This class will be responsible for delegating to the XPC listener the ability to accept or reject new connections. This connection delegate will be the class CMLipoServiceDelegate.

mov rbx, rax
mov rdi, cs:classRef_CMLipoServiceDelegate ; id
mov rsi, cs:selRef_new ; SEL
call r12 ; _objc_msgSend
mov r15, rax
mov rsi, cs:selRef_setDelegate_ ; SEL
mov rdi, rbx ; id
mov rdx, r15
call r12 ; _objc_msgSend


The CMLipoServiceDelegate class implements a very important method:

-[CMLipoServiceDelegate listener:shouldAcceptNewConnection:]

The method shouldAcceptNewConnection is responsible for handling new inbound XPC connections, which takes the form of the class NSXPCConnection. The method shouldAcceptNewConnection is also responsible for setting up everything the XPC service needs for performing the operations sent to it from an XPC client.

Remote Procedure Calls

The Objective-C NSXPCConnection API provides a high-level remote procedure call interface that allows you to call methods on objects in one process from another via the XPC service [1].

To use the NSXPCConnection API, you must create the following:

  • An interface, which mainly consists of a protocol that describes what methods should be callable from the remote process
  • This is created via the NSXPCInterface's static initializer method interfaceWithProtocol

Within the CMLipoServiceDelegate class’s shouldAcceptNewConnection implementation, the CMLipoprotocol is used to initialize the NSXPCInterface instance.

mov r15, cs:classRef_NSXPCInterface
mov r12, cs:protocolRef_CMLipo
mov rbx, cs:selRef_interfaceWithProtocol_
mov rdi, r14 ; id
call cs:_objc_retain_ptr
mov [rbp+var_30], rax
mov r13, cs:_objc_msgSend_ptr
mov rdi, r15 ; id
mov rsi, rbx ; SEL
mov rdx, r12
call r13 ; _objc_msgSend

A quick and dirty way of identifying all of the methods provided by the protocol CMLipo in IDA is inspecting the protocol’s Objective-C method list.

__objc_const:0000000100005C68 _OBJC_INSTANCE_METHODS_CMLipo __objc2_meth_list <18h, 1>
__objc_const:0000000100005C68 ; DATA XREF: __data:_OBJC_PROTOCOL_$_CMLipo↓o
__objc_const:0000000100005C70 __objc2_meth <offset sel_obtainArchitecturesForBinary_withReply_, \ ; "obtainArchitecturesForBinary:withReply:" ...
__objc_const:0000000100005C70 offset aV32081624, 0>

Each NSXPCConnection object provides three key features [1]:

  • An exportedInterface property that describes the methods that should be available to the opposite side of the connection
  • An exportedObject property that contains a local object to handle method calls coming in from the other side of the connection

The exportedInterface will conform to the CMLipo protocol it was created from. The exportedObject will end up being the CMLipoTask class, which is set via the setExportedObject method.

mov rdi, cs:classRef_CMLipoTask ; id
mov rsi, cs:selRef_new ; SEL
call r13 ; _objc_msgSend
mov rbx, rax
mov rsi, cs:selRef_setExportedObject_ ; SEL
mov rdi, r14 ; id
mov rdx, rbx

The CMLipoTask class only contains a single method:

-[CMLipoTask obtainArchitecturesForBinary:withReply:]

We’ve also observed the obtainArchitecturesForBinary:withReply: method in the CMLipo protocol’s Objective-C method list. So, it should be clear that the CMLipoTask is responsible for implementing the methods within the CMLipo protocol.


We should have everything we need to create a simple Frida script to start instrumenting the XPC service. The goals of the script are straightforward:

  • Hook the -[CMLipoServiceDelegate listener:shouldAcceptNewConnection:] to observe new incoming connections
  • Hook interfaceWithProtocol to return the name of the specified protocol
  • Hook the -[CMLipoTask obtainArchitecturesForBinary:withReply:] method, and print out the first argument, which should be the name of a binary on disk
var protocol_getName = Module.findExportByName("/usr/lib/libobjc.A.dylib", "protocol_getName");
// const char * protocol_getName(Protocol *proto);
var my_protocol_getName = new NativeFunction(protocol_getName, 'pointer', ['pointer']);
var shouldAcceptNewConnection = ObjC.classes.CMLipoServiceDelegate["- listener:shouldAcceptNewConnection:"];
var interfaceWithProtocol = ObjC.classes.NSXPCInterface["+ interfaceWithProtocol:"];
var obtainArchitecturesForBinaryWithReply = ObjC.classes.CMLipoTask["- obtainArchitecturesForBinary:withReply:"];

Interceptor.attach(shouldAcceptNewConnection.implementation, {
    onEnter: function(args) {
        console.log("[+] Hooked shouldAcceptNewConnection [!]");
        console.log("[+] NSXPCConnection => " + args[2]);


Interceptor.attach(interfaceWithProtocol.implementation, {
    onEnter: function(args) {
        console.log("[+] Hooked interfaceWithProtocol [!]");
        console.log("[+] Protocol => " + args[2]);
        // Returns a const char *
        var name = my_protocol_getName(args[2]);
        console.log("[+] Protocol Name => " + Memory.readUtf8String(name));

Interceptor.attach(obtainArchitecturesForBinaryWithReply.implementation, {
    onEnter: function(args) {
        console.log("[+] Hooked obtainArchitecturesForBinaryWithReply [!]");
        console.log("[+] Binary => " + ObjC.Object(args[2]).toString());

If we kick off the CleanMyMac 3 application, we get the following output.

[+] Hooked obtainArchitecturesForBinaryWithReply [!]

[+] Binary => /Applications/IDA Pro 7.1/
[+] Hooked shouldAcceptNewConnection [!]
[+] NSXPCConnection => 0x7fba464004f0
[+] Hooked interfaceWithProtocol [!]
[+] Protocol => 0x10b6dc650
[+] Protocol Name => CMLipo

We are an international squad of professionals working as one.