An iOS Bug, Attacking Titan-M, and MTE Arrives
A fairly trivial OOB write in the XNU kernel that was introduced in an iOS 15.0 beta and patched in iOS 15.4. It seems
ipc_kmsg_get_from_user() in conjunction with pre-allocated kmsgs (via
mktimer) can receive sizes that are larger than the pre-allocated buffer. It’s possible to set the
mktimer port as an exception port, and use something such as a breakpoint to get an exception message with controlled message contents (via the thread state) to overflow inside the
ipc_kmsg structure. One primitive the writeup details is an arbitrary 4-byte write via the
kmsg->ikm_header field, which is used as the destination in a memcpy in
ipc_kmsg_get_from_kernel(). This blog post mostly is just a root cause analysis and exploration of the arbitrary write primitive though, as Pointer Authentication (PAC) would get in the way of exploiting this. You’d also need some kind of read primitive to full chain this to know where to write to.
The Titan-M is a security chip found on Google’s Pixel devices from Pixel 3 onwards. This post goes through some of the background and reversing they did on the chip, as well as exploring some fuzzing routes through blackbox as well as emulation via unicorn. Through emulated fuzzing, they found a vuln in the Keymaster task’s
ImportKey handler, which had a wild one-byte write of
1. It seems the
KeyParameters field was used to index into a stack buffer to set a byte to
1, but there was no bounds checking. At first this seems a weak primitive, but the Titan-M’s memory is static, and lacks mitigation for memory corruption beyond W^X.
They were able to smash one byte of the address where incoming keymaster requests get stored, giving a more useful out-of-bounds write. They then were able to corrupt a return pointer to ROP, and setup a second stage ROP chain to exfil data over SPI. The secondary ROP chain would be triggered by smashing the command handler with the first ROP chain. Unfortunately they couldn’t find any stack-pivot gadgets that were suitable for launching the secondary chain, but they found a cool trick. A stack pivot could be achieved as they discovered the
DestroyAttestationIds handler had a 32-bit slot in the stack-frame that wasn’t overwritten by a previous function, giving a trampoline to stack-pivot. They were then able to break root of trust, exfil keys, etc.
An io_uring race UAF that gets chained into 4 UAFs! First, a bit of background on io_uring.
io_uring is a (primarily) async I/O subsystem that was introduced to linux in 5.1. It consists of a submission queue (SQ) and completion queue (CQ), which are shared ring buffers between kernel and userspace. As userspace, you write your entries into the submission queue, dubbed “SQE”, and the kernel writes results back to the completion queue. While io_uring is commonly used asynchronously, it’s possible to use synchronous requests as well via setting the
IOSQE_LINK_FLAG in the SQE. There’s also an
IORING_OP_TIMEOUT opcode, which allows you to specify a timeout for a batch of n I/O operations. For targeted timeouts of specific requests,
IORING_OP_LINK_TIMEOUT is provided. Relevant for exploitation is the
IORING_OP_TEE type, which is used for splicing data from an input file to an output file.
The researchers had a question with the two timeout related opcodes - what happens if an
IORING_OP_TIMEOUT races with an
IORING_OP_LINK_TIMEOUT on the same request? It turns out, memory corruption happens, because the
io_flush_timeouts() function will unconditionally try to cancel the timeout and remove it from the timeout list without consideration that something else might be triggering or cancelling it as well. This leads to a deferred UAF on the
io_kiocb object for the timeout.
Exploitation was tricky. It’s a race condition with a somewhat narrow window, and heap isolation makes it so it’s not easy to overlap the free’d
io_kiocb with a different type of object. It seems they decided to overlap an
io_kiocb with that of the free’d one, and used the request free on the overlap to corrupt the reference count of the
file object backing the
IORING_OP_TEE request. This allowed them to get a controlled UAF on
file. Now this puts them in a somewhat similar situation to before as heap isolation is still a factor, but they’re not working inside of a tight race window.
By doing some trickery to get the page released back to the slab allocator and get it used for a different object cache, they were able to overlap the
file with a sprayed
msg_msgseg object. Then, they triggered a third UAF by closing the
file to overlap
msg_msgseg with a
tls_context object for sockets. They used this overlap to leak the contents of
tls_context by receiving the
msg_msgseg object, which in-turn also free’d it, chaining into their fourth and final UAF. By overlapping
tls_context with another
msg_msgseg of a fake
tls_context, they could hijack the
sk_proto pointer and get code execution from the
getsockopt() function pointer.