← Back to Writeups
CUSTOMMediumReversing

virtpp

XESXOR9/2/20263 min read
#reversing#custom#medium#vtable_dispatch_analysis#ollvm_bypass#call_target_redirection#rel32_recomputation#linux

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):

SymbolAddress
main0x40ed50
Base::PrintFlag()0x403400
BadGuy::Decode()0x403370 (no-op)
GoodGuy::Decode()0x4036c0 (real decryptor, OLLVM-flattened)
BadGuy::BadGuy()0x4033c0
GoodGuy::GoodGuy()0x403450
vtable Base0x40efc0
vtable BadGuy0x40ef70
vtable GoodGuy0x40efe0

Control flow:

  1. main does new BadGuy() (calls BadGuy::BadGuy at 0x40ed7c), then calls Base::PrintFlag().
  2. Base::PrintFlag (0x403400) invokes the virtual Decode through the vtable (call qword ptr [rax]), then prints the std::string stored at object offset this+0x8.
  3. Since the object is a BadGuy, the vtable dispatches to BadGuy::Decode (no-op), so nothing useful is printed.
  4. GoodGuy::Decode is 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 0x40ed7c the instruction is call 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
import 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
chmod +x virtpp_good.elf
docker run --rm --platform linux/amd64 -v "$PWD":/w -w /w ubuntu:22.04 ./virtpp_good.elf

Output:

spbctf{REDACTED}

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