pwnfreehard

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/
slub_freelist_poisoningsafe_linking_bypasstail_object_techniquestruct_file_spraycred_corruption

multifiles - b01lersc CTF

Description

build artifacts in build_out/ rebuild with pwn_build.sh run challenge with dev.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

  1. Allocate 50 objects (fills 2 slab pages)
  2. Delete objects 1 and 3 to create freelist: 3 -> 1 -> NULL
  3. Use OOB read from object 0 (at offset 152, reading 64 bytes) to leak obj1's encoded freelist pointer
  4. Use OOB read from object 2 to leak obj3's encoded freelist pointer
  5. 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:

  1. Write a fake encoded NULL freelist pointer at page+4080 (via OOB write from object 24)
  2. Poison obj3's freelist to point to page+4000 instead of page+160
  3. Allocate two objects: first gets obj3's original slot, second gets the "tail object" at page+4000
  4. 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

  1. Spray struct file objects by opening /dev/multifiles repeatedly (each open creates a struct file)
  2. 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)
  3. Detect struct file by checking field signatures:
    • f_count (offset 0): should be 1-8
    • f_op (offset 16): should be kernel pointer (0xffff...)
    • f_mapping (offset 24): should be kernel pointer
    • f_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

  1. Open new /dev/multifiles and create 25 objects
  2. Delete objects 1 and 3, compute page/random for this new page
  3. Poison freelist to allocate at cred - 16
  4. When allocating at cred-16:
    • name[0:7] (at cred+0) = 0x100 -> sets cred->usage (keeps cred alive)
    • name[8:15] (at cred+8) = 0 -> sets uid=0, gid=0
    • kmem_cache_zalloc zeros 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