Overview

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.

NSXPC

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 3.app/Contents/XPCServices ╰─$ 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

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. exit(EXIT_FAILURE);

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

CMLipoServiceDelegate

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 CMLipo protocol 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

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.

Instrumentation

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

to observe new incoming connections Hook interfaceWithProtocol to return the name of the specified protocol

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/ida.app/Contents/MacOS/Assistant [+] Hooked shouldAcceptNewConnection [!] [+] NSXPCConnection => 0x7fba464004f0 [+] Hooked interfaceWithProtocol [!] [+] Protocol => 0x10b6dc650 [+] Protocol Name => CMLipo

Conclusion I really hoped you enjoyed this three-part series on Frida. Scripts for each part in the series are located here => https://github.com/VerSprite/engage Thanks for reading! References https://developer.apple.com/library/content/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html

https://developer.apple.com/documentation/foundation/nsxpclistenerdelegate/1410381-listener?language=objc

https://developer.apple.com/documentation/objectivec/1418826-protocol_getname?language=objc

https://developer.apple.com/documentation/foundation/nsxpclistenerdelegate?language=objc

VerSprite's Research and Development division (a.k.a VS-Labs) is comprised of individuals who are passionate about diving into the internals of various technologies.



Our clients rely on VerSprite's unique offerings of zero-day vulnerability research and exploit development to protect their assets from various threat actors.



From advanced technical security training to our research for hire B.O.S.S offering, we help organizations solve their most complex technical challenges. Learn more about Research as a Service →