← Back to Writeups
HTBN/APwn

multifiles

XESXOR8/23/20267 min read
#pwn#htb#n/a

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
PortServiceVersionNotes
<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

  1. Cred_corruption
  2. Safe_linking_bypass
  3. Slub_freelist_poisoning
  4. Struct_file_spray
  5. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR