Show Notes

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.