This is the writeup for the challenge Pillow, created by Samuel Groß(@saelo) of Project Zero, of 35C3 CTF annually organized by @EatSleepPwnRpt happening at the end of year 2018.
I didn’t solve this challenge during the CTF, when revisiting this challenge after checkout @LinusHenze repo, I have a big learning oppuntunity to checkout XNU exploitation, which was completely new to me.
You may want to checkout the exploit code
Feel free to skip this part if you have already had a basic knowledge in Mach.
Mach 3.0 was originally conceived as a simple, extensible, communications microkernel. It is capable of running as a stand-alone kernel, with other traditional operating-system services such as I/O, file systems, and networking stacks running as user-mode servers.
Mach is used to send messages or do remote procedure calls (RPC) between separate tasks. This modular structure results in a more robust and extensible system than a monolithic kernel would allow, without the performance penalty of a pure microkernel.
In Mach kernel, port is a very important concept, especially at its reference counting.
Port is a one-way transmission channel. The corresponding object in kernel is the
There can only be one receiver but there can be multiple senders.
To be able to send a message through a port, you must have a send right to it.
When sending and receiving mach messages from userspace there are two important kernel objects, which are the foundation of Mach Port: ipc_entry and
ipc_entryare the per-process handles or names which a process uses to refer to a particular ipc_object.
ipc_objectis the actual message queue (or kernel object) which the port refers to.
ipc_entryhave a pointer to the
ipc_objectthey are a handle for along with the ie_bits field which contains
the urefs and capacility bits for this name/handle (whether this is a send right, receive right etc.)
Each time a new right(send or receive) is received by a process, if it already had a name for that right the kernel will
increment the urefs count. Userspace can also arbitrarily control this reference count via
mach_port_deallocate. When the reference count hits 0 the entry is free’d and the name can be re-used to
name another right.
-Ian Beer(@i41nbeer) of Google Project Zero-
Port has two different usage. The first is for inter-process-communication (IPC); the second is for representing a kernel object.
For this writeup, we will only focus on IPC usage.
In Apple’s code, there is one called MIG, which is automatically generated according to the defs file. It usually does some inter-core object conversion (such as from port to kernel object) and object reference count management, and then call the real kernel functions. If the kernel developer is not familiar with the meaning of defs or MIG’s management of object reference counts, there is high possibility to manage the reference counts of the kernel objects improperly in the real kernel API of this MIG package, thus causing leaks of the reference counts or double free.
-Qixun Zhao(@S0rryMybad) of Qihoo 360 Vulcan Team-
The distribution gives you 4 files, 2 Launch Daemons config and 2 executable act at daemon.
shelld looks promising, there is a function shell_exec with call an arbitrary command after do some verification with capsd
This function is called by the MIG server.
The problem is that it does not respect the MIG schematics
If a MIG method returns KERN_SUCCESS it means that the method took ownership of all the arguments passed to it.
If a MIG method returns an error code, then it took ownership of none of the arguments passed to it.
What does it mean that if the function return
KERN_SUCCESS, it is responsible to manage all the resources passed in.
Otherwise, MIG will responsible for freeing all of it.
mach_port_deallocate, the listener port will be double-freed (by the function and MIG) and the uref(userspace-reference count) will be decreased.
When the uref reaches zero, it means that all connection to that port is deallocated, the port will be freed and be reused later
When the receive right/port have already have a reference(name) in the task, the uref will be increased by one
and decreased by one when it is deallocated
If we pass in the capsd port to the listener and an invalid session, the port that shelld communicates with capsd will be freed and we can attach our port to it by using
=> IPC Man-in-the-middle
One more thing, even if we have passed capsd check, we still have the macOS Sandbox enforced to a session-name
To bypass this, we create a session with a super long name, then the sandbox will refused to enforce due to long path.
=> Arbitrary Code Execution outside the sandbox.
Other technical/implementation is noted in the exploit.c, please check it out.