218 - A Chrome RCE, WebP 0day, and glibc LPE
A rather complex-bug to trigger that was found being exploited in the wild against libwebp’s VP8L compression and was reachable through an iMessage.
There is a fair degree of complexity when it comes to reaching the bug, but it can be summed up decent: The application allocates a buffer it believes will hold a maximum sized Huffman table. It then decoded the data into the huffman table segments producing five huffman table segments (each segment is a huffman table in the traditional sense, but WebP here treated the whole group as a Huffman Table).
The problem is that even through there is a consistency check at the end of building the table to ensure it produced the right number of nodes that will end processing of the file. This check happens after the data has already been written into the Huffman Table segments and too late to stop any malformed table from causing an overflow and out-of-bounds write as those writes happen during the decoding process before the check.
In order to actually trigger this vulnerability the author had to first provide four maximally sized table segments to push the code as far into the allocated buffer as possible, and then the final, and smallest huffman table of the bunch could be malformed with extra nodes allowing it to overflow.
Definitely tricky to reach and as the author notes, even though the vulnerable code was well covered by fuzzing, it would be somewhat hard but not impossible for a fuzzer to fulfill these conditions and trigger the bug.
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.
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.
A buffer overflow vulnerability was introduced in a 2021 patch to glibc’s dynamic loader when processing
GLIBC_TUNABLES environment variables. Tunables are basically configuration settings of the runtime that can be set via key/value pairs in the form of
GLIBC_TUNABLES variables will be processed via
__tunables_init(), and will pass the duplicated tunable name and value through
parse_tunables(). Before the 2021 commit, you could pass any tunables you wanted including malformed ones, including tunables with the
TUNABLE_SECLEVEL_SXID_ERASE security level. The commit addressed this to remove
SXID_ERASE tunables as they were considered dangerous tunables that shouldn’t be passed through.
Buffer overflow in tunable processing
The problem is, due to how the argument parsing code is written, you can set a tunable to a tunable key/value pair string, and it’ll then also copy and process that second tunable, which goes out of bounds of the duplicated string when attempting to append the value. This duplicated string also resides in
mmap()‘d memory, which they could leverage to overflow into adjacent mappings.
This bug was leveraged to target
link_map objects used for dynamic linking. Since the map came from
mmap(), it was assumed to be zeroed memory and thus not explicitly zero-initialized. This is fine in normal cases, however, due to the overflow, this assumption could be violated. By overwriting the zeroed data with setup pointers, they could create an uninitialized use type scenario to setup an attacker-controlled
l_info field, which is used for searching for libraries. An attacker could smash this pointer to point to a controlled path setup on the stack to load arbitrary libraries.
Post from Man Yue Mo at GitHub Security Lab on an RCE in Chrome due to a bug in Chrome’s JIT compiler (TurboFan). As a bit of background, modern browsers will often compile code/functions that are deemed as ‘hotpaths’ (aka executed a lot). The JIT compiler and optimizer has to take a lot of factors into consideration to assume security checks and type checks are only removed when it can be safely assumed that some callback or other code running can’t taint the state of the object while executing. This is also important as the fields and offsets of those fields in an object are tracked via that object’s mapping, which can be changed in the optimization process.
The main culprit of corruption here is the
CompilationDependency::PrepareInstall() method, which calls
EnsureHasInitialMap which can implicitly chain the layout or map of an object via optimization. Interestingly, this function is called on the main thread and not the background thread, so it’s not a typical race as was the initial concern. However,
PrepareInstall() can be executed on the main thread through an interrupt in something like a loop. So if you have a function that accesses a field on some object, run the loop for a while and allow the interrupt to fire, and have
PrepareInstall() called, the next access on that field after the loop may be executed using a changed mapping, and no additional checks are inserted after the loop. This ultimately leads to an access at an incorrect offset, which leads to a sort of type confusion scenario.
This was used to overwrite a
capacity field to derive an out-of-bounds access, which was then used to confuse objects and arrays to get fakeobj and leakobj type primitives.