When XPC serializes large xpc_data objects it creates mach memory entry ports
to represent the memory region then transfers that region to the receiving process
by sending a send right to the memory entry port in the underlying mach message.
By crafting our own xpc message (or using an interposition library as this poc does)
we can pass different flags to mach_make_memory_entry_64 such that the memory entry
received by the target process actually represents a region of shared memory such that
when the xpc_data deserialization code maps the memory entry port the memory region remains
mapped in the sender's address space and the sender can still modify it (with the receiver
seeing the updates.)
Perhaps this is intended behaviour but there's definitely plenty of code which doesn't expect
the contents of xpc_data objects to change.
In this PoC I target NSXPC, a high-level RPC mechanism which uses XPC for its low-level transport layer.
NSXPC is widely used across privilege boundaries.
NSXPCDecoder is implemented in Foundation. Clients send serialized NSInvocation objects
representing the methods they wish to call on the remote objects. These NSInvocations are serialized
using the NSSecureCoding method which ends up creating a bplist16 serialized byte stream.
That bplist16 buffer gets sent in an xpc message as an xpc_data object.
NSXPCDecoder wraps the bplist16 deserialization and for selectors such as decodeCStringForKey:
,if the key is present, the value returned will be a pointer directly into the
xpc_data object in which it was received.
By crafting our own memory entry object this means the pointers returned by decodeCStringForKey:
actually point into shared memory which can still be modified by the caller.
This can be turned directly into controlled memory corruption by targetting the serialized method
type signature (key 'ty') which is parsed by [NSMethodSignature signatureWithObjCTypes].
This method is implemented in CoreFoundation. If the method signature string isn't in a cache of
parsed signatures then the string is passed to __NSMS1. This function calls __NSGetSizeAndAlignment
to determine the size of a buffer required to parse the signature string which __NSMS1 then allocates
using calloc before parsing the signature string into the allocated buffer. If we change the
types represented by the signature string (which is in shared memory) between these two calls
we can cause the parsing code to write out of bounds as it assumes that the length computed by
__NSGetSizeAndAlignment is correct.
The most direct path to trigger memory controlled memory corruption is to use a type signature like this:
That will cause 7 bytes of buffer space to be allocated for the parsed signature
(which will just contain a copy of the string.)
If we increase the length of the string in shared memory eg to:
then __NSMS1 will copy the extra bytes up until it encounters a '"' character.
This PoC targets the airportd daemon which runs as root but should work for any NSXPC service.
This is a race condition so you may have to run the PoC multiple times (./run.sh) and also use
libgmalloc to see the corruption directly rather than its effects.
This is an exploit for CVE-2017-7047, a logic error in libxpc which allowed
malicious message senders to send xpc_data objects that were backed by shared memory.
Consumers of xpc messages did not seem to expect that the backing buffers of xpc_data objects
could be modified by the sender whilst being processed by the receiver.
This project exploits CVE-2017-7047 to build a proof-of-concept remote lldb debugserver
stub capable of attaching to and allowing the remote debugging all userspace
processes on iOS 10.0 to 10.3.2.
Please see the README in the nsxpc2pc folder in the attached archive for further discussion and details.
The exploit isn't hugely reliable - the race condition needs quite exact timing and sometimes it just doesn't work or it does but the heap groom fails. You should just hard reboot the device and try again. It may take a couple of attempts but it should work. Once the debugserver is running it should be stable. If you take a look at the xcode stdout/debugger window you can see some more status information.