Escaping the Google kCTF Container with a Data-Only Exploit

We discussed this vulnerability during Episode 218 on 10 October 2023

Writeup for exploiting an io_uring bug submitted to kCTF (before io_uring was disabled in kCTF). The vulnerability itself was extremely straightforward and was in the handling for IORING_OP_MSG_RING command for signaling another ring. In order to do so, it would take a file descriptor to the ring or file to signal, and after processing, it would indiscriminately do an io_put_file() call to decrement the file reference count. The problem is, in the case of fixed files, a later call to io_uring_unregister_files() would already handle the refcount, so fixed files would have the file object free’d while the file descriptor was still valid and installed in the process.

Arbitrary Read + kASLR Defeat Getting arbitrary read turned out to be fairly easy, as the F_GET_RW_HINT fcntl would copy data to userspace from the file’s inode write hint pointer (inode->i_write_hint), which was controllable via UAF overlap. The author went with reclaiming the page containing the file struct with pipe buffers for spraying into the UAF’d file.

To defeat kASLR, h0mbre abused the fact that cpu_entry_area objects (which are per-cpu objects that contain data necessary for interrupt handling amongst other things) are at a fixed mapping. The error_entry function pointer is particularly useful for defeating kASLR.

Arbitrary Write For arbitrary write, io_uring itself was used for deriving the primitive. The io_msg_ring() function would cast the file->private_data field to a context, which would later be used for copying the cached completion queue ring in io_commit_cqring() to an attacker-controllable pointer, giving 4-byte arbitrary write. The combined arbitrary read/write primitive was used to find and corrupt the task struct of a child process to escalate privileges.