XPC Hello World in C on iOS


There are too many IPC methods and not nearly enough documentation in iOS, especially at lower-level interfaces. How is one to check and audit security if the basic hello world app using the C API is impossible to find! Now you might say doesn't apple provide useful documentation on the man pages and at developer.apple[1]. And you'd be half right while the man page does have a ton of information explaining how XPC works but it has well-formed working examples to build and run. The same is true of the developer.apple page it's kinda sad because these API are very interesting to work with. Thus let's build a basic XPC server and client and learn a little more about how XPC is works. 

All the code here is on Github at https://github.com/lquinn2015/xpcCDemo. and was generated with mostly with the help of a discord person named capt. Skip the explanation of everything else and skip to the code if you just want something working now

Whats IPC?

First, we need a little background on what IPCis and why it is the way it is. Thus let's take a slide from Ian Beer presentation on IPC on iOS[2] 


IPC or Interprocess communication is a Zoo. This is quite literally as who makes over 7 different interfaces for IPC. Well, Apple did because kernel design despite feeling monolithic is an iterative process. And so API's can and should be extended to make achieving certain things easier and faster. IPC is all about sending messages between 2 processes. After all, the whole idea of having an operating system is to write a bunch of processes each accomplishing a unique goal to get work done. What we want to send should be abstract as each process's or task's needs will differ. And the first thing to start with is  Mach Messages. These are the primitives on which most of iOS is built. It's very labored to explain them and they can be very error-prone to use however they are the most powerful IPC method in iOS because they are used to implement all other methods of IPC. However just because they are powerful doesn't make them usable, having a heavy sword is much harder to wield thus Apple has several other methods built on top of them. However, understanding how they function a little will shape our understanding of how anything built on top of them must work. Also from an operating systems perspective, they are really cool, and in my opinion the style of the XNU in general scales better to multicore systems which are the future. 

Brief Mach Messages  

Let's finally look at some code just some structure definitions

This is how Mach messages generally look like kinda. There is a lot of complex type descriptors that allow this message to contain just about anything but that also makes them almost impossible to use hence very few people use Mach messages rawly even apple they built MIG to autogen messages for the longest time. XPC is a step away from that approach and arguably uses the Mach (sub)system better. There are some interesting features we can gather specifically messages containing 2 mach_port_t a remote port and a local port. One's mind might jump straight to the idea that this must be, process A and process B but this is not the case. Mach_port_t's are far more granular than the process level. Now a cool feature of Mach ports is that can be sent through themselves in single-use or multi-use mode, but have only 1 dedicated listener/receiver but potentially multiple senders. They also allow for multiple out-of-line buffers which together can present the illusion of streaming. Now all those words basically mean they are complex state objects that can do anything but because of that, they are really difficult to use. XPC is built on these and builds on some of the strengths of Mach messages while removing the burden of a complex state machine to make it so that the average user can use this cool feature of the Mach Kernel. 

Finally XPC Mach Service

One of the hard-to-understand parts of Mach messaging is ports. They can mean a lot of things and to simplify them XPC instead uses the idea of connections. The easiest way to understand connections is to look at the two things they can be. The first connection is XPC connections residing inside of your App bundle think if you use a framework it might host an XPC service you can connect to change something about the UI. The other Service type is Mach Services which you can find by running the following 



These are a bunch of system-level Mach XPC services we could potentially connect to. Note you'll need the correct entitlements to connect to things. However, we can also just make a new service and work with that since that will be easier. So let's make a Mach global entry! 

Let's create com.johnappleseed.server.plist and put it in /Library/LaunchDaemons/



Now we need to change some permission and load it via launchctl -- note this will make it so it stays around even after boot. (granted you need to be jailbroken via checkra1n I think)



If you did this right it will make an entry you can see. Although no binary is running because it doesn't exist. If you read the plist from above you'll see we made a name for the service give it arguments, RunAtLoad which starts it automatically and Keep alive to restart on crashes. MachServices actually hosts this as a Mach service we can lookup using the correct XPC call in a later section. 

XPC Server Side




XPCListen


Now let's breakdown some of the code here starting with the XPCListen method. There are a complete of fundamental ideas to breakdown here. The first is the ^ that looks like it is a syntax error. It's not, it's actually a C language extension [3] which apple added to support their GCD thread model. By default (-fblocks if it is not working), clang can handle this however GCC does not necessarily support this so be aware of this. How you read this is the ^ is code block where the (xpc_object_t connection)  is a parameter passed to the "lambda function" in the curly braces. So to understand this method we are creating an xpc_connection using a Mach service lookup. We could add a custom dispatch handler queue where we have our NULL but there is no need. The XPC_CONNECTION_MACH_SERVICE_LISTENER designates us as the listener i.e anyone else who calls xpc_connection_create_mach_service we connect to our server as a peer connection. I.e we are getting all the traffic which helps explain the next line which is xpc_connection_set_event_handler. An important qualifier is that this is a 1-1 connection, not a broadcast connection so the server when handling multiple connections will always need to resolve their connection. One could wrap the event handler to make a broadcast feature.

In this method, we are receiving peer connections from a connection trying to use our service. when we do xpc_set_event_handler the first time it's a handler for the Mach service. Thus the first event handler is a service handler that accepts new connections we need to do as a services is accept the connection by setting that connections event handler and resuming it. Thus we established a peer connection whose incoming messages will be sent here. You also have the opportunity to end a message to the connection after we resume it. This is very similar to the idea in UNIX of forking after we accept a connection on a socket server. However, with the additional step of after we accept a new connection to our service and setting its event handler, we have run xpc_connection_resume to activate the connection on our side. When we make this call, our service will actually be validated by the remote end i.e our pid information is then available. The sky is the limit in this event handler function however XPC comes with the idea of being stateless to make things simpler. While we as programmers can violate this it comes with more risk of being unmanageable. 

XPCDelisten

This function cancels are service that's about it. It's pretty straightforward but should never be called because we never want to tear it down. This is mostly to show that people can cancel connections and if they do so that could potentially mess with our service i.e if that cancel while we are trying to reply we need to handle that.

XPCDispatch

This method is one we made the idea is that when a peer connection sends us a message we need to parse the request and give a logical response. This is where the majority of the code ends up being because you could have some complex messages the important thing is that if you need to retain part of the xpc_event from this dispatch function you need to call xpc_retain and then later xpc_release to handle memory allocation properly. I am fairly sure this is written such that there is no memory leak but I leave it to the reader to double-check here. This is the downside of the C library it has very few protections so if your service has a ton of throughput and you don't retain and release correctly you will eventually crash/hang due to memory pressure. 

Let's go over the semantics of parsing now. Basically, if you didn't know every xpc_*_t type is technically an xpc_object_t type. Thus you can always call xpc_get_type(xpc_object_t obj). This allows you to quickly check for errors, or dictionaries, or any value and slowly break it apart however eventually you will likely get to a dictionary type. Note I am skipping arrays but those exist too. We should really check that this is indeed a dictionary but for brevity, I don't here. Now if something is a dictionary we can call xpc_dictionary_get_{type}(obj, "key"). To extract the value from the key-value pair out if it exists or NULL if it does not. Avoid the temptation to use xpc_dictionary_get_value(obj, "key"). This function assumes type whereas the other functions do not. If you assume type you could shoot yourself in the foot because people might write clients incorrectly. Interestingly enough Apple has done this to themselves multiple times maybe you can find more. If you find one it is a way of privilege escalation on iOS.     

After you do some parsing you eventually will need to reply back. In this case you need to use xpc_dictionary_create_reply(event). With this reply you need to populate as your protocol seems fit and send it back with xpc_connection_send_message(remote, reply). The remote connection can be gotten by calling xpc_dictionary_get_remote_connection. You can save yourself a few lines of code by getting the remote connection earlier than but it is nice to see the style behind everything. 

Main

Finally our main program now this part is much easier. We only need to run XPCListen() and CFRunLoopRun(). This makes it so XPC will be hosted and the default dispatch queue will start processing. 

Great now that we have a server we can build it and codesign it so it runs (we have no entitlements needed here). I'll save the makefile for last let's look at how a client can interact with our server. 

Client Side




Now if you take a quick view over this there is not much that's different. You'll see XPC_CONNECTION_MACH_SERVICE_PRIVILEGED flag instead of a XPC_CONNECTION_MACH_SERVICE_LISTENER flag. We actually don't need this flag however it prevents people from spoofing our server's connection. I.e with the Priv flag we know that our service is getting the trusted server. The other big thing you'll notice is everything is syncronous in the client. We could do an Async loop by setting an event handler however that's identical to the server with the difference you'll only use xpc_connection_set_event_handler one time. This is because you're not listening for connections you are listening for events. We could also used code blocks to have async hooks something similar to this 


dispatch_queue_t replyq = dispatch_queue_create("com.johnappleseed.client.replyqueue", DISPATCH_QUEUE_CONCURRENT); 
xpc_connection_send_message_with_reply(server_conn, msg,  replyq, ^ (xpc_object_t reply) {
    
        // Parse
        // response
        xpc_release(msg);
        // Note that the xpc_object_t reply is managed by the dispatch queue. and thus should not be free
});

Now while this looks extremely straight forward this is logically really complex. Under the hood, you have two flows of logic and if one depends on the other you run the risk of race conditions and deadlocks. However, if it is important to generate max throughput this complexity is how you can get there. I'd argue for most cases at least at the C level you probably want to maintain synchronous replys to simplify your models until you are comfortable building something more complex.

Now let's show it running! Notice I start the server because it exists now and wasn't started earlier


Makefile

Now since this is my first blog post I thought I post a Makefile for all this nonsense. One disclaimer here is that I am building XPC services in C for iOS which is not supported by iOS by default this does not mean the functions we want to link to don't exist but we need need to copy the XPC header files from a mac into our project and switch there includes from <> to "". I provide those files on Github.

Now for the make file when you run make it will build everything and codesign it correctly. Put the server in /usr/local/  as that is what our plist had it as. And put the client in /usr/bin so they run. 







[1] https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingXPCServices.html
[2] https://thecyberwire.com/events/docs/IanBeer_JSS_Slides.pdf   -- This is an excellent presentation on the attack surface of IPC and is recommend to an reader. 
[3] https://en.wikipedia.org/wiki/Blocks_(C_language_extension)


Note if you have any concerns about the article please send me an email or comment below and I'll get it fixed

No comments:

Post a Comment

Bootstrapping LTE Physical channels

Demystifying the LTE Physical Downlink Control Channels Xphos Note to the reader:  This was a paper I wrote in latex and converted to...