
Proof-of-concept exploit and detailed analysis of CVE-2022-0847 (Dirty Pipe) Linux kernel privilege escalation vulnerability, including Docker environment for testing and debugging.
[toc]
This article was first published on Huawei Security's official account, this is the blog version (more complete)
First published link: https://mp.weixin.qq.com/s/6VhWBOzJ7uu80nzFxe5jpg
Vulnerability ID: CVE-2022-0847 (Alias: dirty pipe)
Vulnerable product: linux kernel - splice syscall
Affected versions: Linux 5.8 patch f6dd975583bd introduced ~ 5.16.11, 5.15.25, 5.10.102 fixed
Hazard: Write content not exceeding one page to any readable file (sufficient), can be used for local privilege escalation.
Vulnerability analysis docker: chenaotian/cve-2022-0847 (If still inaccessible, then I haven't uploaded it yet)
Provides:
Start:
cd ~/cve-2022-0847
gcc exp.c -o exp --static && cp exp ./rootfs && cd rootfs
find . | cpio -o --format=newc > ../rootfs.img
cd ../
./boot.sh
Debug:
gdb ./vmlinux
target remote :10086
directory /root/linux-5.13
b do_splice
b copy_page_to_iter_pipe
b pipe_write
ignore 3 15
...
p *(struct pipe_inode_info *) pipe
p (struct pipe_buffer)pipe->bufs[0]
The brief principle of the vulnerability is that calling the
splicefunction can send a file to thepipein a "zero-copy" manner. The zero-copy at the code level directly uses the file cache page (page cache) as thebufpage of thepipe. However, this introduces an uninitialized variable vulnerability, causing the file cache page to be treated as a normalpipecache page in subsequentpipechannels and thus "overwritten" and modified. In this case, the kernel does not mark this cache page as "dirty", and it will not be flushed to disk in the short term (until the next reboot or similar). During this period, all scenarios accessing the file will use the modified file cache page, achieving a "temporary arbitrary write to any readable file" operation. This can complete local privilege escalation.
According to the patch, the vulnerability trigger point is in the copy_page_to_iter_pipe function, which adds an initialization operation for buf->flags, so this is an uninitialized variable vulnerability.

The call point of copy_page_to_iter_pipe appears in the splice system call. The splice function (system call) transports file content into the pipe through a "zero-copy" method. Compared to the traditional method of directly sending file content into the pipe, performance is better. Details are introduced below.
First, the vulnerability alias is dirty pipe, so let's first understand the pipe. pipe is a communication channel provided by the kernel, created by the pipe/pipe2 function, returning two file descriptors, one for sending data and the other for receiving data, similar to the two ends of a pipe. The specific usage is not elaborated.

Briefly talk about the implementation in the kernel. Usually, the total length of the pipe cache space is 65536 bytes, managed in the form of pages, a total of 16 pages (one page is 4096 bytes). The pages are not contiguous but managed through an array, forming a circular linked list. Two linked list pointers are maintained, one for writing (pipe->head) and one for reading (pipe->tail). Here we mainly analyze the pipe_write function:
linux-5.13\fs\pipe.c : 400 : pipe_write
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
struct file *filp = iocb->ki_filp;
struct pipe_inode_info *pipe = filp->private_data;
unsigned int head;
ssize_t ret = 0;
size_t total_len = iov_iter_count(from);
ssize_t chars;
bool was_empty = false;
bool wake_next_writer = false;
··· ···
··· ···
head = pipe->head;
was_empty = pipe_empty(head, pipe->tail);
chars = total_len & (PAGE_SIZE-1);
if (chars && !was_empty) {
//[1] If the pipe cache is not empty, try to write "continuously" from the current last page
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
int offset = buf->offset + buf->len;
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
/*[2] Key: if the PIPE_BUF_FLAG_CAN_MERGE flag exists, it means this page allows continuous writing
* If the write length does not cross a page, continue writing; otherwise start a new page */
ret = pipe_buf_confirm(pipe, buf);
···
ret = copy_page_from_iter(buf->page, offset, chars, from);
···
}
buf->len += ret;
···
}
}
for (;;) {//[3] If the previous page cannot be written continuously, start a new page
··· ···
head = pipe->head;
if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[head & mask];
struct page *page = pipe->tmp_page;
int copied;
if (!page) {//[4] Allocate a new page
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}
spin_lock_irq(&pipe->rd_wait.lock);
head = pipe->head;
··· ···
pipe->head = head + 1;
spin_unlock_irq(&pipe->rd_wait.lock);
/* Insert it into the buffer array */
buf = &pipe->bufs[head & mask];
buf->page = page;//[5] Put the newly allocated page into the page array
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
if (is_packetized(filp))
buf->flags = PIPE_BUF_FLAG_PACKET;
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
//[6] Set flag, default PIPE_BUF_FLAG_CAN_MERGE
pipe->tmp_page = NULL;
copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
//[7] Copy operation
··· ···
ret += copied;
buf->offset = 0;
buf->len = copied;
··· ···
}
··· ···
}
··· ···
return ret;
}
head==tail indicates an empty pipe), it means there is unread data in the pipe. Then get the head pointer, which points to the latest page used for writing, check the len and offset of that page (to find the end of data), and try to continue writing on the current page.PIPE_BUF_FLAG_CAN_MERGE flag; if not, continuous writing on the current page is not allowed. Or if the written data appended to the previous data exceeds one page (i.e., the write operation crosses a page), if it crosses a page, continuous writing is not possible.alloc_page allocates a new page.buf->flag is initialized to PIPE_BUF_FLAG_CAN_MERGE by default, because the default state allows the page to be written continuously.The key to exploitation is the uninitialized PIPE_BUF_FLAG_CAN_MERGE flag in splice, which determines whether we can continue writing on a "not fully written" pipe page.
As mentioned above, the pipe manages 16 pages as cache. The zero-copy method of splice is to directly replace the cache pages in pipe with the file cache pages (change the pipe cache page pointer to point to the file cache page).