Breaking iOS: XPC

George Dan
7 min readFeb 3, 2017

--

Note: This article at the beginning is more for beginners. I discuss more about XPC further down the article.

And so the long journey begins. For you and for me.

In summary, I decided to embark on a journey the entirely long way. I’ve received advice from lots of security researchers (on iOS) telling me to start on macOS, and then the lowest possible iOS version I can. But instead I decided to go straight to iOS 10.1.1 (and to 10.2), and begin attempting to create a jailbreak w/o any previous knowledge.

The first common step is escaping the sandbox. This is the step I will be focusing on until I manage to go through all the common ways of escaping this place (XPC, IOKit, Mach, Kernel Communication). The first way I decided to research is XPC.

XPC, or Cross Process Communication, is a form of IPC (Inter Process Communication). IPC is how process can talk to one another. This is implemented because Apple has been building up the number of processes running on the device, mainly for security reasons. XPC is built on Mach messages, but adds some features.

XPC messages are generally dictionaries, with keys as strings, and values as strongly typed values (most of the time). You can have strings, int64s, uint64s, booleans, dates, UUIDs, data, doubles, arrays, and dictionaries. Dictionaries have strict getters and setters. That is, if you have a value of type string, but the message handler tries to get an int64, the xpc function will return NULL as a string value is not found, only an int64.

XPC relies on process creating connections. Generally, you can create a connection to a service that is a listener or a client. When you have a listener, a handler must be set that will be called when a message is received. After that, all the message handling will occur.

Now, you might be thinking how do we find a bug out of all this?

There is one main case in which a bug would be found. And this is when the handler fails to check for correct values. Usually, this is probably because the process writers failed to think that a random application will connect to the app, and so they don’t do any NULL checking (remember? when getting a certain type through the getter method, NULL is returned when the value isn’t of the required type). An example of a bug like this can be found below:

The bug above is a clear example of the developer not intending for random applications to talk to this service. If we were to pass a value that is not a string, or no value at all, this would crash the service as it is expecting a value, but NULL is given.

Another type of bug is type confusion. This type of bug is very similar to our NULL Pointer Dereference bug above, but is generated differently.

XPC has some default functions when getting data from a dictionary. These are commonly in the format of xpc_dictionary_get_<type specified above>. Note that if you want to get a dictionary/array, you will have to use the unsafe xpc_dictionary_get_value, which is unsafe because of type confusions.

But Apple wanted an easier(?) way of sending messages through XPC. So they created some functions to convert NS/CFObjects to XPC types and vice versa. These functions are located in CoreFoundation (so you can access them via dlsym), and are known as _CFXPCCreateXPCMessageWithCFObject and _CFXPCCreateCFObjectFromXPCMessage. The same rules apply with the types. But because you can get NSObjects straight out of the dictionary, developers will tend to directly use Objective-C methods on these objects without checking if the objects support this selector.

Now that we have an understanding of (a small amount of) bugs that we can find, let’s find one now:
Note: Hopper on macOS is required for the below if you would like to do it yourself

Download a 32 bit version of iOS 10.2 (it doesn’t matter which device). We need 32bit because Hopper can only decompile 32 bit binaries (3rd of February 2017). Once that is done, extract the ipsw, and open the dmg that has the largest size. Then go to the folder System/Library/PrivateFrameworks/GeoServices.framework/, and open geod in Hopper.

Now that we are in Hopper, search in the left pane (with Procedures selected) for ‘xpc_connection_create_mach_service’. Select the one that does not have _ at the beginning. Now switch to Pseudo-code mode. Now press X on your keyboard to bring up a menu of all the functions in which ‘xpc_connection_create_mach_service’ is referenced. In this case, it will be the init method of GEODaemon. Before you scroll down to find our function, deselect ‘Remove potentially dead code’.

Now scroll down to ‘xpc_connection_create_mach_service’. You will then see roughly the following:

We see that the event handler is set to sp + 0x18. This is irrelevant. What is relevant are the hex numbers above this function (in stack[2007], and r1-r3). Usually it’s the 3rd hex number which is the actual handler address, but bruteforce it if it isn’t. We select the 3rd address, press G (go to), and paste our hex number in. We will then go to our handler.

Our handler says that if the event object is a XPC connection object, we will handle the incoming connection in _handleNewConnection. We then follow the GEOPeer creation, find our next event handler. We go to the correct address, and then follow _handleEvent. We notice that if we don’t set a “app_debug_version” or “peer_debug_identifier” key in our message, the message will then be handled at GEODaemon handleIncomingMessage:fromPeer.

Things get a little tricky here, and require some research and smart thinking. In summary, there are a whole bunch of servers that can handle incoming messages. These all conform to theGEOServer class. In the handleIncomingMessage... on GEODaemon, all the servers associated with this daemon object are iterated. We check if our message can be handled by the server by checking if the “message” key starts with the server identifier (specified in the identifier method). If so, the message gets sent to the handler.

For our purpose, I’m going to tell you that our message is going to start with “resourcemanifest”, and will be handled by GEOResourceManifestServer. When sent to the server to be handled, what happens is that based on our message, the handler will split at a “.” character, get the second item, append “WithMessage:” to it, and check if that string can be performed on the server as a selector. If so, then we can find our method. Our method we will be using is setConfiguration. We now go to GEOResourceManifestServer setConfigurationWithMessage.

We discover that there is also a key in our message called “userInfo”, which holds a dictionary of values. We then create a GEOResourceManifestConfiguration with our dictionary value at cfg in our userInfo dictionary. Now note that GEOResourceManifestConfiguration is actually defined in GeoServices which is loaded in the shared cache. Luckily, if you have Xcode 8, the binary exists in the simulator SDK. We load up the binary in Hopper, and go to the initialisation method. We see at the top this:

As you can see here, the developer checks if the value “tgi” exists in the dictionary. If it does, it presumes it is an object of NSNumber, and attempts to get the unsigned int value. But if you send a string, which is converted to NSString, this method does not exist on those types of objects. So the process crashes as the selector unsignedIntValue does not exist on type NSString.

PoC:

xpc_connection_t conn = xpc_connection_create_mach_service("com.apple.geod", NULL, 0); // Create the client to communicate with geodxpc_connection_set_event_handler(conn, ^(xpc_object_t object) {NSLog(@"Event: %@", object);}); // Set a dummy event handlerxpc_connection_resume(conn); // Start the clientxpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0); // Create our messagexpc_dictionary_set_string(msg, "message", "resourcemanifest.setConfiguration"); // Set the message key so that the ResourceManifestServer handles the messagexpc_object_t userInfo = xpc_dictionary_create(NULL, NULL, 0); // Create our user info dictionaryxpc_object_t cfg = xpc_dictionary_create(NULL, NULL, 0); // Create our cfg dictionaryxpc_dictionary_set_string(cfg, "tgi", "lol"); // Set our bad value for the keyxpc_dictionary_set_value(userInfo, "cfg", cfg); // Set the dictionary in user infoxpc_dictionary_set_value(msg, "userInfo", userInfo); // Set the user info in our messagexpc_object_t reply = xpc_connection_send_message_with_reply_sync(conn, msg); // Send the message. If you look in Console, you will see geod crashing :)NSLog(@"Reply: %@", reply);

And that’s about it!

There are other types of bugs in XPC which are beyond my knowledge. Ian Beer’s presentation on IPC covers this (doing stuff in memory so that a function is called), but I still can’t get my head around it…

Any questions, feel free to comment them.

Resources:

Ian Beer on IPC: https://vimeo.com/127859750
Pangu on XPC: https://www.blackhat.com/docs/us-15/materials/us-15-Wang-Review-And-Exploit-Neglected-Attack-Surface-In-iOS-8.pdf
Marco Grassi’s containermanagerd bug: https://marcograss.github.io/security/apple/xpc/2016/06/17/containermanagerd-xpc-array-oob.html

--

--