Skip to content

Add C++ RTTI extraction#70

Open
Veryyes wants to merge 2 commits into
devfrom
rtti
Open

Add C++ RTTI extraction#70
Veryyes wants to merge 2 commits into
devfrom
rtti

Conversation

@Veryyes

@Veryyes Veryyes commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds `ClassInfo` and `VTableEntry` Pydantic primitives to represent recovered C++ class metadata (vtable layout, inheritance, virtual/multiple inheritance flags)
  • Implements `get_classes()` in the Ghidra (dragon/pyghidra), Rizin, and Binary Ninja backends, populating `Binary.classes` with demangled class names, vtable addresses, vtable entries, and base/derived class relationships
  • Adds `binocular/rtti_util.py` with a shared Itanium ABI name decoder used by all three backends
  • Adds three test C++ binaries (`rtti_simple.cpp`, `rtti_inherit.cpp`, `rtti_diamond.cpp`) covering single, single-inheritance, and diamond-inheritance scenarios
  • Adds `test/test_rtti.py` with test suite covering Ghidra, Rizin, and Binary Ninja RTTI extraction

Test plan

  • `cd test && make` to build the RTTI test binaries
  • `pytest test/test_rtti.py` passes for all installed backends
  • `pytest test/` — full suite passes with no regressions
  • Verify `Binary.classes` is JSON-serializable (Pydantic round-trip)

🤖 Generated with Claude Code

Implements get_classes() on the dragon and rizin disassembler backends,
exposing vtable addresses, virtual method slots, base class chains, and
multiple/virtual inheritance flags via structural Itanium ABI parsing.
Adds C++ test binaries (no-inheritance, 3-level chain, diamond) and a
full test suite covering all three scenarios on both backends.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Veryyes Veryyes changed the title Add C++ RTTI extraction (Ghidra dragon + Rizin) Add C++ RTTI extraction (Ghidra + Rizin) Jun 8, 2026
- Extract shared itanium_name() decoder into binocular/rtti_util.py,
  removing duplicated implementations in dragon.py and binja.py
- Refactor rizin.py get_classes() nested functions into private _rz_*
  methods for readability and testability
- Add symbol-size-based slot cap in Ghidra and Binja vtable readers to
  prevent over-reading into the VTT in multiple-inheritance layouts
- Fix VMI flags bound: flags > 7 -> flags > 3 (only bits 0-1 are ABI-defined)
- Fix TestRTTIRizin skip check to call Rizin.is_installed() as a class
  method, avoiding a FileNotFoundError before make_cpp runs
- Assert make returncode in make_cpp fixture for a clear failure message

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Veryyes Veryyes changed the title Add C++ RTTI extraction (Ghidra + Rizin) Add C++ RTTI extraction Jun 8, 2026
@Veryyes

Veryyes commented Jun 8, 2026

Copy link
Copy Markdown
Owner Author

Binja broken

___________________________________________________________________ TestRTTIBinaryNinja.test_simple ___________________________________________________________________

self = <test_rtti.TestRTTIBinaryNinja object at 0x705854776030>, make_cpp = None

    def test_simple(self, make_cpp):
        from binocular import BinaryNinja
    
        with BinaryNinja("rtti_simple") as g:
            g.analyze()
>           _assert_simple_rtti(g.binary)

test_rtti.py:244: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

binary = Binary(endianness=Endian.LITTLE, architecture=x86_64, bitness=64, len(functions)=28, filename=rtti_simple, len(names)=...98624, base_addr=4194304, len(dynamic_libs)=3, is_stripped=False, has_debug_info=True, len(strings)=35, len(classes)=0)

    def _assert_simple_rtti(binary) -> None:
        """Logger: one class, virtual methods, no inheritance."""
        classes = binary.classes
>       assert "Logger" in classes, f"expected 'Logger' in classes, got: {list(classes)}"
E       AssertionError: expected 'Logger' in classes, got: []
E       assert 'Logger' in {}

test_rtti.py:37: AssertionError
__________________________________________________________________ TestRTTIBinaryNinja.test_inherit ___________________________________________________________________

self = <test_rtti.TestRTTIBinaryNinja object at 0x705857dbda60>, make_cpp = None

    def test_inherit(self, make_cpp):
        from binocular import BinaryNinja
    
        with BinaryNinja("rtti_inherit") as g:
            g.analyze()
>           _assert_inherit_rtti(g.binary)

test_rtti.py:251: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

binary = Binary(endianness=Endian.LITTLE, architecture=x86_64, bitness=64, len(functions)=43, filename=rtti_inherit, len(names)...98624, base_addr=4194304, len(dynamic_libs)=4, is_stripped=False, has_debug_info=True, len(strings)=42, len(classes)=0)

    def _assert_inherit_rtti(binary) -> None:
        """Shape → Polygon → Rectangle → Square: 3-level single inheritance chain."""
        classes = binary.classes
        for name in ("Shape", "Polygon", "Rectangle", "Square"):
>           assert name in classes, f"expected '{name}' in classes, got: {list(classes)}"
E           AssertionError: expected 'Shape' in classes, got: []
E           assert 'Shape' in {}

test_rtti.py:60: AssertionError
__________________________________________________________________ TestRTTIBinaryNinja.test_diamond ___________________________________________________________________

self = <test_rtti.TestRTTIBinaryNinja object at 0x705854776210>, make_cpp = None

    def test_diamond(self, make_cpp):
        from binocular import BinaryNinja
    
        with BinaryNinja("rtti_diamond") as g:
            g.analyze()
>           _assert_diamond_rtti(g.binary)

test_rtti.py:258: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

binary = Binary(endianness=Endian.LITTLE, architecture=x86_64, bitness=64, len(functions)=58, filename=rtti_diamond, len(names)...02720, base_addr=4194304, len(dynamic_libs)=3, is_stripped=False, has_debug_info=True, len(strings)=43, len(classes)=0)

    def _assert_diamond_rtti(binary) -> None:
        """Vehicle ←(virt) Car, Vehicle ←(virt) Boat, Car+Boat → Amphibious."""
        classes = binary.classes
        for name in ("Vehicle", "Car", "Boat", "Amphibious"):
>           assert name in classes, f"expected '{name}' in classes, got: {list(classes)}"
E           AssertionError: expected 'Vehicle' in classes, got: []
E           assert 'Vehicle' in {}

test_rtti.py:114: AssertionError
========================================================================== warnings summary ===========================================================================
test/test_rtti.py::TestRTTIGhidra::test_simple
test/test_rtti.py::TestRTTIGhidra::test_simple
test/test_rtti.py::TestRTTIGhidra::test_inherit
test/test_rtti.py::TestRTTIGhidra::test_inherit
test/test_rtti.py::TestRTTIGhidra::test_diamond
test/test_rtti.py::TestRTTIGhidra::test_diamond
  /home/brandon/Documents/BINocular/binocular/ghidra_impl/core.py:55: DeprecationWarning: 'pkgutil.get_loader' is deprecated and slated for removal in Python 3.14; use importlib.util.find_spec() instead
    os.path.dirname(pkgutil.get_loader("binocular").path), "data", "ghidra"

test/test_rtti.py::TestRTTIRizin::test_simple
test/test_rtti.py::TestRTTIRizin::test_simple
test/test_rtti.py::TestRTTIRizin::test_inherit
test/test_rtti.py::TestRTTIRizin::test_inherit
test/test_rtti.py::TestRTTIRizin::test_diamond
test/test_rtti.py::TestRTTIRizin::test_diamond
  /home/brandon/Documents/BINocular/binocular/rizin.py:40: DeprecationWarning: 'pkgutil.get_loader' is deprecated and slated for removal in Python 3.14; use importlib.util.find_spec() instead
    os.path.dirname(pkgutil.get_loader("binocular").path), "data", "rizin"

test/test_rtti.py::TestRTTIBinaryNinja::test_simple
test/test_rtti.py::TestRTTIBinaryNinja::test_inherit
test/test_rtti.py::TestRTTIBinaryNinja::test_diamond
  /home/brandon/Documents/BINocular/binocular/primitives.py:199: PydanticDeprecatedSince211: Accessing the 'model_fields' attribute on the instance is deprecated. Instead, you should access this attribute from the model class. Deprecated in Pydantic V2.11 to be removed in V3.0.
    for f in self.model_fields.keys():

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================================================================= short test summary info =======================================================================
FAILED test_rtti.py::TestRTTIBinaryNinja::test_simple - AssertionError: expected 'Logger' in classes, got: []
FAILED test_rtti.py::TestRTTIBinaryNinja::test_inherit - AssertionError: expected 'Shape' in classes, got: []
FAILED test_rtti.py::TestRTTIBinaryNinja::test_diamond - AssertionError: expected 'Vehicle' in classes, got: []
============================================================== 3 failed, 6 skipped, 15 warnings in 5.79s ==============================================================

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant