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]
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