CVE-2021-3491: Triggering a Linux Kernel io_uring Overflow
Introduction:
Linux kernel vulnerability research has been a hot topic lately. A lot of great research papers got published lately. One specific topic that was interesting is io_uring.
At Haboob, we decided to start a small research to investigate one of the published CVEs, specifically CVE-2021-3491.
Throughout this blogpost, we will explain io_uring fundamentals, its use-case and the advantage it offers. We’ll also walk through CVE-2021-3491 from root-cause to PoC development.
Everyone loves kernel bugs it seems, buckle up for a quick fine ride!
Why io_uring?
Io_uring is a new subsystem that is rapidly changing and improving. It’s ripe for research!
It’s very interesting to see how it internally works and how it interacts with the kernel.
- io_uring: what is it?
According to the manuals: io_uring is a Linux-specific API for asynchronous I/O. It allows the user to submit one or more I/O requests, which are processed asynchronously without blocking the calling process. io_uring gets its name from ring buffers which are shared between user space and kernel space. This arrangement allows for efficient I/O, while avoiding the overhead of copying buffers between them, where possible. This interface makes io_uring different from other UNIX I/O APIs, wherein, rather than just communicate between kernel and user space with system calls, ring buffers are used as the main mode of communication.
Root Cause:
After checking the io_uring source code commit changes in (fs/io_uring.c), we start tracing the differences between the patched version and the unpatched version, and try to realize the cause of the bug.
We first notice that in struct io_buffer the “len” is defined as sign int32 that is being used as the length for buffer.
Then, we also notice that in io_add_buffers, when attemping to access the struct: buf->len was assigned without checking the data type and MAX_RW_COUNT.
We found that there is a multiplication (p->len * p->nbufs) in io_provide_buffers_prep which leads to integer overflow when (p->len > 0x7fffffff) executes. Then it will bypass the access check during the access_ok() function call.
When we perform te IORING_OP_READV operation with the selected buffer, we can bypass the MAX_RW_COUNT:
Using “R/W” on “/proc/self/mem” will force the kernel to handle our request using mem_rw function. From the arguments the “count” is received as size_t then passed to min_t() as an integer which will return a negative number in “this_len”.
Access_remote_vm function will receive “this_len” as a negative number which will result in copying more PAGE_SIZE bytes to the page, which results to a heap overflow.
Triggering the Bug:
We will go through the details of how the bug is triggered to achieve a kernel panic that can lead to a heap overflow.
Step1:
The following code snippet will interact with “proc” to open a file descriptor for “/proc/self/mem” and extract an address from “/proc/self/maps” to attempt to read from it:
Step2:
We need to prepare the buffer using the function “io_uring_prep_provide_buffers()” with length 0x80000000 to trigger the integer overflow vulnerability:
Step3:
Using iovec struct with 2 dimensional buffer, we assign the “len” as 0x80000000 to bypass MAX_RW_COUNT:
Step4:
When we do IORING_OP_READV operation on “file_fd” using offset “start_address” we can read the content of “/proc/self/mem” with that offset using the selected buffer:
POC
We can trigger kernel panic with the following PoC: