multifiles
b01lersc
Task: Linux kernel module with custom slab cache and OOB read/write via incomplete bounds check. Solution: SLUB freelist poisoning with safe-linking bypass, tail object technique for cross-page access, struct file spray to leak cred pointer, then cred corruption for privilege escalation.
$ ls tags/ techniques/
multifiles - b01lersc CTF
Description
build artifacts in
build_out/rebuild withpwn_build.shrun challenge withdev.sh
A Linux kernel module called "multifiles" implements a custom slab cache for managing file-like objects. The challenge runs on Linux 6.12.81 with full kernel hardening enabled:
- KASLR, SMEP, SMAP, PTI
- CONFIG_SLAB_FREELIST_HARDENED=y
- CONFIG_HARDENED_USERCOPY=y
- CONFIG_STATIC_USERMODEHELPER is not set
User runs as ctf (uid=1000), flag is in /root/flag.txt.
Analysis
Kernel Module Structure
The module creates a custom slab cache for MultiFile objects:
typedef struct { u64 type; // offset 0 u64 flags; // offset 8 char name[16]; // offset 16 u64 data[16]; // offset 32 (128 bytes) } MultiFile; // Total: 160 bytes
The cache is created with SLAB_NO_MERGE to prevent merging with other caches:
multifiles_cache = kmem_cache_create_usercopy( MODULE_NAME "_cache", sizeof(MultiFile), // 160 bytes 0, SLAB_NO_MERGE, offsetof(MultiFile, name), // 16 USERCOPY_SIZE, // 144 (name + data) NULL );
With 160-byte objects, each 4096-byte slab page holds 25 objects (25 x 160 = 4000 bytes), leaving 96 bytes unused at the end of each page.
Vulnerability: Incomplete Bounds Check
The read/write operations have a flawed bounds check:
// check read/write bounds if ( count > MAX_RW_SIZE // count <= 64 || (count % sizeof(u64)) != 0 // count % 8 == 0 || *offset >= sizeof(MultiFile) // offset < 160 || *offset < 0 ) { ret = -EINVAL; goto out_unlock; }
Bug: The check verifies offset < sizeof(MultiFile) but NOT offset + count <= sizeof(MultiFile).
With offset = 152 and count = 64, we can read/write bytes 152-215, which extends 56 bytes past the object boundary into adjacent memory (or the next object's freelist pointer when freed).
SLUB Safe-Linking
Linux SLUB uses safe-linking with random XOR and bswap to protect freelist pointers:
encoded_ptr = real_ptr ^ random ^ bswap(ptr_location)
To bypass this, we need to recover the random value by leaking two encoded freelist pointers and solving algebraically.
Solution
Step 1: SLUB Freelist Pointer Leak
- Allocate 50 objects (fills 2 slab pages)
- Delete objects 1 and 3 to create freelist:
3 -> 1 -> NULL - Use OOB read from object 0 (at offset 152, reading 64 bytes) to leak obj1's encoded freelist pointer
- Use OOB read from object 2 to leak obj3's encoded freelist pointer
- Compute page base and SLUB random value:
# Object layout: obj0 at page+0, obj1 at page+160, obj2 at page+320, obj3 at page+480 # Freelist pointer stored at offset 0 of freed object # From obj0, reading at offset 152+32=184 reaches obj1's freelist at offset 184-160=24... # Actually: read starts at &data[0]+offset, data is at +32, so offset 152 -> byte 184 # obj1 starts at 160, so we read obj1's bytes 24-87, freelist is at obj1+0 # The math for decoding: # enc1 = ptr1 ^ random ^ bswap(loc1) where ptr1=page+240 (NULL encoded), loc1=page+160 # enc3 = ptr3 ^ random ^ bswap(loc3) where ptr3=page+160, loc3=page+480 page = enc1 ^ bswap(240) ^ enc3 ^ 160 ^ bswap(560) random = enc3 ^ (page + 160) ^ bswap(page + 560)
Step 2: Tail Object Technique
The key insight is that 25 objects x 160 bytes = 4000 bytes, leaving 96 bytes unused at the end of each slab page. We can create a "tail object" at offset 4000 that spans into the next physical page:
- Write a fake encoded NULL freelist pointer at
page+4080(via OOB write from object 24) - Poison obj3's freelist to point to
page+4000instead ofpage+160 - Allocate two objects: first gets obj3's original slot, second gets the "tail object" at
page+4000 - The tail object can read/write bytes 4000-4159, which spans 96 bytes in current page + 64 bytes into the NEXT physical page
Step 3: Struct File Spray and Detection
- Spray
struct fileobjects by opening/dev/multifilesrepeatedly (each open creates a struct file) - After each spray, use tail object to read 64 bytes starting at offset 63 (reads 1 byte from current page + 63 bytes from next page)
- Detect
struct fileby checking field signatures:f_count(offset 0): should be 1-8f_op(offset 16): should be kernel pointer (0xffff...)f_mapping(offset 24): should be kernel pointerf_inode(offset 40): should be kernel pointer
Step 4: Cred Pointer Leak
Extract f_cred from detected struct file (at offset 56). Due to alignment, only 7 bytes are readable (bytes 57-63). Reconstruct full pointer:
cred = (leaked_7_bytes) | 0xff00000000000000
Step 5: Cred Corruption
- Open new
/dev/multifilesand create 25 objects - Delete objects 1 and 3, compute page/random for this new page
- Poison freelist to allocate at
cred - 16 - When allocating at
cred-16:name[0:7](at cred+0) = 0x100 -> setscred->usage(keeps cred alive)name[8:15](at cred+8) = 0 -> sets uid=0, gid=0kmem_cache_zalloczeros remaining bytes including suid, sgid, euid, egid, fsuid, fsgid
Step 6: Read Flag
With uid=0, directly open and read /root/flag.txt.
Critical: Must read flag immediately after cred corruption. Any operation that triggers prepare_creds() or similar will access NULL pointers in zeroed cred fields (user_ns, group_info, etc.) and crash the kernel.
Exploit Code
The final exploit is a ~6.5KB NASM binary that performs all steps:
bits 64 default rel org 0x400000 ; Minimal static Linux/amd64 ELF - All-in-one exploit for multifiles ; Stage1: leak f_cred from adjacent struct file ; Stage2: poison allocation to overlap cred-16, zero uid/gid ; Stage3: read /root/flag.txt %define SYS_read 0 %define SYS_write 1 %define SYS_open 2 %define SYS_close 3 %define SYS_lseek 8 %define SYS_ioctl 16 ; ... (full exploit in solve_all.asm)
Key exploit functions:
leak_qword: ; rdi=fd, esi=pred_idx -> rax=leaked qword ; Sets active object, seeks to offset 152, reads 64 bytes ; Returns the qword at offset 56 (the OOB freelist pointer) overwrite_qword: ; rdi=fd, esi=pred_idx, rdx=new_value ; Reads 64 bytes at offset 152, modifies byte 56-63, writes back ; Used to poison freelist pointers
Remote deployment script uploads the binary via base64:
#!/usr/bin/env python3 import socket, ssl, base64, time HOST = "multifiles.opus4-7.b01le.rs" PORT = 8443 # Upload exploit via base64 chunks # Decode and execute: base64 -d exp.b64 > exp && chmod +x exp && ./exp
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar
Similar writeups
- [pwn][Pro]Common Offset— srdnlen
- [pwn][Pro]pwn8_logger — logger_easy!(not) — UAF/alias + tcache poison— spbctf
- [pwn][free]throughthewall— b01lersc
- [pwn][Pro]Taste— grodno_new_year_2026
- [pwn][Pro]iz_heap_lv1 — BSS-pointer overlap + tcache poisoning— spbctf