152 - 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 blogpost 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.
Background
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.
Vulnerability
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
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 IORING_OP_TEE
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.