multifiles
multifiles
Platform: B01Lersc | Category: Pwn | Type: Challenge | Difficulty: Hard | OS: Linux | Author: D3v0o0Nu11 | Date: 2026-04-19 | Status: Solved Techniques: cred_corruption, safe_linking_bypass, slub_freelist_poisoning, struct_file_spray, tail_object_technique
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
b01lersc| ID:20260419_b01lersc_multifiles - Tags: oob_read, oob_write, kernel, slub, slab_cache, struct_file, cred, linux_6_12
- Indicators: custom kmem_cache with SLAB_NO_MERGE, bounds check missing offset+count validation, CONFIG_SLAB_FREELIST_HARDENED=y, 160-byte object size (25 per page)
- Source:
20260419_b01lersc_multifiles.md
Foothold
Vulnerability / Misconfiguration
- Cred_corruption
- Safe_linking_bypass
- Slub_freelist_poisoning
- Struct_file_spray
- Tail_object_technique
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- cred_corruption
- safe_linking_bypass
- slub_freelist_poisoning
- struct_file_spray
- tail_object_technique
- Tags: oob_read, oob_write, kernel, slub, slab_cache, struct_file, cred, linux_6_12
Original Writeup
<details><summary>Click to expand original content</summary>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:
</details>#!/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
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR