virtpp
virtpp
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-24 | Status: Solved Techniques: vtable_dispatch_analysis, ollvm_bypass, call_target_redirection, rel32_recomputation
Summary
Task: x86-64 C++ ELF where main instantiates a no-op BadGuy and PrintFlag dispatches Decode through the vtable; the real flag decryptor GoodGuy::Decode is OLLVM-obfuscated. Solution: patch the constructor call in main to GoodGuy::GoodGuy so virtual dispatch invokes the real decryptor, then run the binary.
virtpp — spbctf
Description
We had two classes derived from the same Base: GoodGuy and BadGuy. Looks like GoodGuy decrypts the flag, and BadGuy just sits there and does nothing.
We are given virtpp.elf, an x86-64, dynamically-linked, non-stripped C++ binary. It defines a Base class with two derived classes, GoodGuy and BadGuy. GoodGuy::Decode decrypts the flag; BadGuy::Decode does nothing. The goal is to make the program print the flag.
Analysis
file/strings confirm the binary is x86-64 C++ built with Obfuscator-LLVM (clang 4.0.1 / OLLVM 4.0.1).
Relevant symbols (nm -C):
| Symbol | Address |
|---|---|
main | 0x40ed50 |
Base::PrintFlag() | 0x403400 |
BadGuy::Decode() | 0x403370 (no-op) |
GoodGuy::Decode() | 0x4036c0 (real decryptor, OLLVM-flattened) |
BadGuy::BadGuy() | 0x4033c0 |
GoodGuy::GoodGuy() | 0x403450 |
vtable Base | 0x40efc0 |
vtable BadGuy | 0x40ef70 |
vtable GoodGuy | 0x40efe0 |
Control flow:
maindoesnew BadGuy()(callsBadGuy::BadGuyat0x40ed7c), then callsBase::PrintFlag().Base::PrintFlag(0x403400) invokes the virtualDecodethrough the vtable (call qword ptr [rax]), then prints thestd::stringstored at object offsetthis+0x8.- Since the object is a
BadGuy, the vtable dispatches toBadGuy::Decode(no-op), so nothing useful is printed. GoodGuy::Decodeis the real decryptor, but it is heavily OLLVM control-flow-flattened. Statically reversing it is impractical — and unnecessary.
Key insight: the type of the object is decided solely by which constructor main calls. The constructor sets the vtable pointer, which determines which Decode virtual dispatch selects. So we don't need to reverse the decryptor at all — we just need to make the heap object a GoodGuy.
Solution
Redirect the constructor call in main from BadGuy::BadGuy to GoodGuy::GoodGuy. The object then carries GoodGuy's vtable, the virtual dispatch in PrintFlag calls GoodGuy::Decode, the flag is decrypted into the object, and PrintFlag prints it.
Patch details (this is a static-layout binary, so file offset == vaddr - 0x400000):
- At virtual address
0x40ed7cthe instruction iscall 0x4033c0=e8 3f 46 ff ff. - New target
GoodGuy::GoodGuy@0x403450. New rel32 =0x403450 - (0x40ed7c + 5) = 0xffff46cf. - Patched bytes:
e8 cf 46 ff ff.
python read-only • copy allowedimport struct data = bytearray(open('virtpp.elf', 'rb').read()) off = 0xed7c rel = (0x403450 - (0x40ed7c + 5)) & 0xffffffff data[off] = 0xe8 data[off+1:off+5] = struct.pack('<I', rel) open('virtpp_good.elf', 'wb').write(data)
Run the patched binary on Linux x86-64 (Docker used here from a macOS host):
bash read-only • copy allowedchmod +x virtpp_good.elf docker run --rm --platform linux/amd64 -v "$PWD":/w -w /w ubuntu:22.04 ./virtpp_good.elf
Output:
spbctf{REDACTED}