
Binary-Ninja-Plugin, das die Analyse bidirektional mit einem Ghidra-Server-Repository über einen Java-Bridge-Subprozess synchronisiert.
Ein Binary-Ninja-Plugin, das sich mit einem Ghidra-Server-Repository verbindet und dessen Analyse – Symbole, Funktionsnamen und Kommentare – direkt in eine geöffnete Binary-Ninja-Binary-View importiert.
Ghidra und Binary Ninja haben jeweils ihre Stärken. Dieses Plugin ermöglicht es, beide auf derselben Binary zu verwenden, ohne Namen oder Kommentare manuell zwischen ihnen kopieren zu müssen. Verbinde dich mit einem laufenden Ghidra-Server, durchsuche dessen Repositories und doppelklicke auf eine beliebige Projektdatei, um deren Analyse in die aktuell geöffnete BN-View zu übernehmen.
Die Synchronisierung läuft in beide Richtungen.
| BN ← Ghidra (Import) | BN → Ghidra (Checkin) |
|---|
| Symbole (Labels, Funktionsnamen) | ✓ | ✓ |
| Kommentare (EOL/PRE/POST/PLATE/REP) | ✓ | ✓ |
| Funktionssignaturen (Rückgabetyp, Aufrufkonvention) | ✓ | ✓ |
| Funktionsparameter (Umbenennen, Retypisieren, Hinzufügen) | ✓ | ✓ |
| Datentypen (struct/union/enum/typedef + Zeiger/Array) | ✓ | ✓ |
| Equates (Konstantennamen + Referenzen) | ✓ | ✓ |
| Lesezeichen | ✓ | ✓ |
| Typisierte Datenelemente | ✓ | ✓ |
| Funktions-Flags (Thunk, No-Return, Inline) | ✓ als BN-Tags | — |
| Lokale Variablen (Storage-bewusst) | ✓ | teilweise — Register-Storage-Mapping nicht implementiert |
Binary Ninja (C++ plugin)
│ TCP / newline-delimited JSON
▼
ghidra-bridge-*.jar (Java, runs as a subprocess)
│ Java RMI / SSL
▼
Ghidra Server (ghidraSvr, running on the network)
Das Plugin startet beim Laden einen Java-Subprozess (die „Bridge"). Die Bridge hält die RMI-Verbindung zum Ghidra-Server und spricht ein einfaches JSON-Protokoll über einen lokalen TCP-Socket zurück zum Plugin. Dadurch bleibt jeglicher Java/RMI-Code aus dem C++-Prozess heraus, und die JVM kann im Hintergrund starten, während BN das Laden abschließt.
Die Bridge-JVM initialisiert beim Start außerdem das Application-Framework von Ghidra, damit der Schreibpfad die High-Level-Program-Model-APIs von Ghidra (ProgramDB, DataTypeManager, SymbolTable, FunctionManager) verwenden kann statt roher db.Table.putRecord()-Schreibvorgänge — siehe Checkin-Schreibpfad unten.
| Pfad | Sprache | Rolle |
|---|---|---|
plugin/ | C++ / Qt6 | Binary-Ninja-Sidebar-Plugin |
bridge/ | Java 17 | Ghidra-RMI-Client + JSON-Bridge-Server |
Plugin (C++):
plugin.cpp — registriert Einstellungen und das Sidebar-Widget; startet die Bridge-JVM beim Laden eifrigGhidraConnection.cpp — Singleton; verwaltet den Bridge-Lebenszyklus und alle RMI-gestützten OperationenBridgeProcess.cpp — startet das Bridge-JAR als Subprozess mit stdout/stderr-Pipes; liest die READY port=N-Handshake-ZeileBridgeClient.cpp — TCP-Client; sendet JSON-Anfragen, empfängt Antworten, verteilt asynchrone EventsSyncEngine.cpp — wendet ein GhidraDbExport auf eine BinaryView an (Symbole, Kommentare, Flags)ui/ProjectPanel.cpp — Sidebar-Widget: Repo-Baum, Verbindungsdialog, Aktivitätsprotokollui/ConnectDialog.cpp — Host/Port/Benutzer/Passwort-DialogBridge (Java):
BridgeMain.java — Argument-Parsing; initialisiert UniversalIdGenerator und das Ghidra-Application-Framework; startet den TCP-Server; gibt READY port=N auf stdout ausBridgeServer.java — akzeptiert eine TCP-Client-Verbindung und übergibt ihr eine BridgeConnectionBridgeConnection.java — JSON-Request-Dispatcher; serialisiert Ghidra-API-Antworten nach JSON; behandelt opCheckin (erstellt eine neue Programmversion auf dem Server)GhidraSession.java — authentifizierte RMI-Session; umschließt RemoteRepositoryServerHandleEventStreamer.java — Hintergrund-Thread pro geöffnetem Repo; schiebt RepositoryChangeEvents als asynchrone JSON-Events zum PluginDatabaseExporter.java — Lesepfad: extrahiert Symbol-/Kommentar-/Funktions-Flag-/Datentyp-/Equate-/Lesezeichen-Tabellen aus einem ManagedBufferFileHandle (Ghidras Remote-DB-Buffer) über rohen db.jar-ZugriffProgramApplier.java — Schreibpfad: öffnet die Buffer-Datei als echte ProgramDB und wendet alle BN-seitigen Änderungen über Ghidras High-Level-APIs an (siehe Checkin-Schreibpfad unten)DatabaseImporter.java — Legacy-Raw-Write-Helfer, nur als Test-Seam beibehalten; das produktive apply(...) delegiert an ProgramApplieropCheckin öffnet die Managed-Buffer-Datei des Programms im Schreibmodus, konstruiert eine ProgramDB darüber und wendet BN-seitige Änderungen über Ghidras Program-Model-APIs an. Rohe db.Table.putRecord()-Schreibvorgänge werden vermieden — sie waren die Quelle jedes Checkin-Korruptionsfehlers, den wir je hatten:
| Falsch-Tier-Schreibvorgang | Fehlermodus |
|---|---|
setIntValue(col, longTypeId) auf Function-Data-Tabelle | IntField.setLongValue schneidet still per l2i ab → StackPurge bei jedem Signatur-Update korrumpiert |
setByteValue(col, isUnion) auf V5V6 Composite Data Types | Spalte ist BooleanField in Ghidra 12.x → IllegalFieldAccessException („Illegal field access") |
setIntValue(col, 0) auf V2 Typedef-Flags-Spalte | Spalte ist ShortField → gleicher Absturz, anderes Schema |
| Schreiben des Composite-Headers ohne Component-Settings-Zeilen | CompositeEditorModel.cloneAllComponentSettings wirft ArrayIndexOutOfBoundsException, wenn das Struct in Ghidra geöffnet wird |
Schreiben eines PARAMETER-Symbols mit SYM_ADDR_COL = RAM-Adresse | Address is not a VariableAddress wird von FunctionDB.loadSymbolBasedVariables bei jedem Funktionszugriff geworfen |
Übergeben von null DBChangeSet an DBHandle.save() | Server schreibt eine 0-Byte-Change-Data-Datei → nächster Checkout schlägt mit EOFException in ProgramContentHandler.loadProgramChangeSet fehl |
ProgramApplier hat diese Fallen nicht, weil er über DataTypeManager.addDataType, SymbolTable.createLabel, Listing.setComment, Function.setReturnType usw. läuft — APIs, die Ghidras ineinandergreifende Tabelleninvarianten automatisch wahren. Er führt außerdem zu Beginn jedes Checkins einen cleanupBadVariableSymbols-Durchlauf aus, um Korruption zu bereinigen, die ältere Bridge-Versionen in der Datenbank hinterlassen haben.
server-package/CleanupBadVariableSymbols.java ist ein eigenständiges GhidraScript, das dieselbe Bereinigung über analyzeHeadless ausführt — nützlich, wenn eine Datei zu korrupt ist, um sie in der Ghidra-GUI zu öffnen.
./test.sh # macOS / Linux: tiers 0-3 (C++ unit + BN-headless + Java)
test.bat # Windows equivalent
test.bat --parity # cross-DB parity tier only (C++ BN tests + gradlew parityTest)
test.bat --e2e # live Ghidra-server E2E (starts a local ghidraSvr)
Die Suite ist in fünf Tiers organisiert. Tiers 2–4 existieren, um eine Eigenschaft zu beweisen: dieselben kompatiblen Daten landen sowohl in der .bndb als auch in der Ghidra-Programmdatenbank (die Kompatibilitätsmatrix oben in dieser README).
| Tier | Was | Wo | Gate |
|---|---|---|---|
| 0 | Reine Unit-Tests | plugin/test/*.cpp (binja-ghidra-tests), Bridge *Test.java | immer |
| 1 | Ghidra-DB-Round-Trip | Bridge *RoundTripTest.java (ProgramApplier gegen eine echte ProgramDB) | benötigt ghidra.home / GHIDRA_HOME |
| 2 | BN BinaryView/.bndb-Round-Trip | plugin/test/bn/ (binja-ghidra-bn-tests; headless binaryninjacore) | SKIPt sauber ohne headless-fähige BN-Lizenz (BN_LICENSE-Env wird berücksichtigt) |
| 3 | Cross-DB-Parität | CanonicalParityTest (C++ und Java) gegen die gemeinsamen Goldens in testdata/parity/fixtures/ | mit Tiers 1+2 |
| 4 | Live-Server-E2E | Bridge LiveServerE2ETest — bootet einen echten ghidraSvr in einem Temp-Verzeichnis, seedet via analyzeHeadless, fährt Checkout → Export → Checkin → Re-Export über RMI | test.bat --e2e (setzt GHIDRA_E2E=1) |
Parity-Oracle (Tier 3). Beide Seiten verifizieren unabhängig gegen dasselbe
eingecheckte kanonische JSON (die Form des Bridge-DatabaseExporter). Import-
Richtung: Das Golden lädt in eine ProgramDB (Java) und in eine BinaryView
via SyncEngine (C++), und jeder Re-Export muss dem Golden entsprechen. Checkin-
Richtung: Skriptgesteuerte BN-Änderungen müssen exakt
fixtures/checkin/*/expected-preview.json erzeugen (C++), und das Anwenden dieser Preview via
ProgramApplier muss als expected-after.json re-exportieren (Java). Wenn beide Seiten
mit den gemeinsamen Goldens übereinstimmen, stimmen die beiden Datenbanken per Transitivität überein. Feld-
Vergleichsmodi und die Typnamen-Normalisierungstabelle stehen in
testdata/parity/RULES.md; die Test-Binary ist
testdata/bin/parity_x64.bin (Layout in parity_x64.md).
Langjährige Regressions-Pins auf der Java-Seite:
DataTypesRoundTripTest.struct_cloneSettings_doesNotThrow — Composite-Settings müssen konsistent mit dem Header bleiben (cloneAllComponentSettings-Absturz)FunctionSignaturesRoundTripTest.returnType_doesNotCorruptStackPurge — IntField-TrunkierungParametersRoundTripTest.noParameterSymbol_endsUpAtRamAddress — VariableAddress-InvarianteRound-Trip- und Paritätstests erfordern eine Ghidra-Installation (zur Laufzeit für
Language Services verwendet). Der Pfad wird aus der Gradle-System-Property ghidra.home oder der Env-Variable GHIDRA_HOME gelesen; build.gradle reicht ghidraHome standardmäßig durch. Tier-2/3-C++-Tests benötigen zusätzlich ein ladbares binaryninjacore
(die Skripte legen das BN-Installationsverzeichnis auf den PATH).
| Abhängigkeit | Hinweise |
|---|---|
| Binary Ninja (kommerziell) | Getestet gegen die Version, die zu api_REVISION.txt in der BN-Installation passt |
| Ghidra Server | Getestet mit Ghidra 12.0.4. Muss laufen und über RMI/SSL erreichbar sein |
| Java 17+ JDK | Eclipse Adoptium JDK 21 empfohlen |
| CMake 3.24+ | |
| Ninja | |
| C++-Compiler | MSVC 2022+ unter Windows; clang unter macOS; gcc/clang unter Linux |
| Qt 6.7+ | Siehe Qt-Setup unten; qmake muss zur Build-Zeit auf dem PATH sein |
| Gradle (via Wrapper) | Die Bridge verwendet den Gradle-Wrapper — keine separate Installation nötig |
| Poetry (nur Qt-Build) | Nur erforderlich, wenn Qt aus dem qt-build-Submodul gebaut wird. Installation mit pip install poetry oder pipx install poetry. |
| libclang 19 (nur Qt-Build) | Vom Qt-Build-System benötigt. Siehe qt-build/README.md für Download-Anweisungen. |
Das Plugin linkt gegen denselben Qt-6-Build, den Binary Ninja verwendet. Du hast zwei Optionen:
Option A — Eine vorhandene Qt-Installation verwenden (am schnellsten, wenn du bereits Qt hast)
Übergib Qt6_DIR mit Verweis auf dein Qt-CMake-Verzeichnis:
Qt6_DIR=/path/to/Qt/6.x.y/clang_64/lib/cmake/Qt6 ./build.sh
Unter macOS erkennt das Build-Skript Qt automatisch, wenn es vom Qt-Online-Installer unter /usr/local/Qt* installiert wurde.
Option B — Qt aus dem qt-build-Submodul bauen (~1-2 Stunden, einmal pro Maschine)
Das qt-build-Submodul (Vector35s Qt-Build-Skripte) kompiliert Qt 6 mit Binary Ninjas Patches. Es erfordert Poetry und libclang 19 (siehe Voraussetzungen oben und qt-build/README.md).
Qt wird nach qt/<version>/<compiler>/ innerhalb des Repos installiert:
| Plattform | Installationspfad |
|---|---|
| macOS | qt/6.10.1/clang_64/ |
| Linux x86-64 | qt/6.10.1/gcc_64/ |
| Windows | qt/6.10.1/msvc2022_64/ |
# First time on a new machine:
./build.sh qt # compiles Qt — takes 1-2 hours
# All subsequent builds (Qt cached in qt/, reused automatically):
./build.sh
Der qt-Schritt ist nur einmal nötig. CMake und die Build-Skripte erkennen das gebaute Qt in qt/ bei jedem weiteren Lauf und überspringen das Submodul vollständig. Das qt/-Verzeichnis ist gitignored.
git clone https://github.com/mutinylaboratories/ghidra_svr_bridge.git
cd ghidra_svr_bridge
git submodule update --init # populates binaryninja-api and qt-build (~seconds)
Dann folge dem Qt-Setup oben (Option A oder B) und führe aus:
./build.sh install
# Incremental build of both components
./build.sh
# Full clean rebuild + install into BN plugins folder
./build.sh clean install
# Build only the C++ plugin
./build.sh plugin
# Build only the Java bridge
./build.sh bridge
# Build Qt once on a machine without Qt installed
./build.sh qt
Umgebungsvariablen (alle optional — das Skript setzt sinnvolle Standardwerte):
BN_INSTALL=/Applications/Binary\ Ninja.app/Contents/MacOS
Qt6_DIR=/usr/local/Qt-6.7.2/lib/cmake/Qt6
Bearbeite die Pfade oben in build.bat, damit sie zu deiner Umgebung passen, bevor du es zum ersten Mal verwendest:
set "JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.11.10-hotspot"
set "VSDEVCMD=C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\Tools\VsDevCmd.bat"
set "Qt6_DIR=C:\qt\v6.7.2\lib\cmake\Qt6"
set "BN_INSTALL=C:\Program Files\Vector35\BinaryNinja"
rem Incremental build of both components
build.bat
rem Full clean rebuild + install into BN plugins folder
build.bat clean install
rem Build only the C++ plugin
build.bat plugin
rem Build only the Java bridge
build.bat bridge
rem Build Qt once on a machine without Qt installed
build.bat qt
Der C++-Build verwendet CMake FetchContent, um binaryninja-api beim exakten Commit zu klonen, der in api_REVISION.txt festgehalten ist, sodass die Plugin-ABI immer zur installierten BN-Version passt. Ghidra wird beim ersten Konfigurieren automatisch von CMake heruntergeladen, wenn GHIDRA_HOME nicht gesetzt ist.
Setze nach der Installation Folgendes in den Binary-Ninja-Einstellungen (Edit → Preferences → Settings, suche nach „Ghidra"):
| Einstellung | Beschreibung |
|---|---|
ghidra.javaExe | Vollständiger Pfad zu java.exe |
ghidra.ghidraHome | Wurzel deiner Ghidra-Installation (enthält Ghidra/Framework/…) |
ghidra.trustAllCerts | Auf true setzen, wenn dein Ghidra-Server ein selbstsigniertes Zertifikat verwendet |
ghidra.defaultHost | Füllt den Verbindungsdialog vor |
ghidra.defaultPort | Standard: 13100 |
ghidra.defaultUser | Füllt den Verbindungsdialog vor |
Voraussetzung für Schritt 5: Die Programmdatei muss in das Ghidra-Server-Repository eingecheckt sein (nicht nur lokal in Ghidra geöffnet). In Ghidra: Rechtsklick auf die Datei im Project-Fenster → Version Control → Add to Version Control….
Das Plugin und die Bridge kommunizieren über einen lokalen TCP-Socket mittels newline-delimited JSON. Jede Anfrage trägt eine ganzzahlige id und einen String op; jede Antwort gibt die id zurück. Asynchrone Events (serverseitige Repository-Änderungen) tragen stattdessen einen "event"-Schlüssel.
| Op | Richtung | Zweck |
|---|---|---|
ping, status, connect, disconnect | Anfrage/Antwort | Session-Lebenszyklus |
list_repos, open_repo, close_repo | Anfrage/Antwort | Repo-Aufzählung |
list_items, get_subfolders | Anfrage/Antwort | Repo-Browsing |
get_versions, get_checkouts | Anfrage/Antwort | Versionskontrollstatus |
checkout, terminate_checkout | Anfrage/Antwort | exklusiver Schreib-Lock |
open_db | Anfrage/Antwort | vollständige Ghidra-DB lesen → JSON (schwer) |
checkin | Anfrage/Antwort | BN-seitige Änderungen anwenden → neue Repo-Version (schwer, via ProgramApplier) |
download_binary, upload_binary | Anfrage/Antwort | die Original-Binary rein/raus bewegen |
delete_item | Anfrage/Antwort | Datei aus dem Repo entfernen |
repo_changed | Event (asynchron) | serverseitiger RepositoryChangeEvent-Push |
Das Repository enthält alles, was zum Neubauen von Grund auf nötig ist. Pro-Entwickler-Setup, das nicht in git ist:
git clone https://github.com/mutinylaboratories/ghidra_svr_bridge.git
cd ghidra_svr_bridge
git submodule update --init --recursive
bridge/gradle.properties:
ghidraHome=C:/Users/<you>/ghidra/ghidra_12.0.4_PUBLIC
Qt6_DIR auf eine vorhandene Installation zeigen lassen oder einmal ./build.sh qt ausführen (Windows: build.bat qt).binaryninja-api-Commit von GitHub:
./build.sh --channel stable # default — latest stable release (from GitHub)
./build.sh --channel dev # latest dev (dev branch head, from GitHub)
./build.sh --bn-api <commit> # explicit commit, no GitHub lookup (escape hatch)
--channel und --bn-api schließen sich gegenseitig aus; mit keinem von beiden wird der stable-Kanal verwendet. --channel fragt die Vector35/binaryninja-api GitHub ab (neuestes stable/*-Release oder der dev-Branch-Head), benötigt also Netzwerkzugriff. Wenn dein installiertes BN hinter dem neuesten Release zurückliegt, übergib --bn-api mit dem exakten SHA aus der api_REVISION.txt dieser Installation.Beim Öffnen einer frischen Claude-Code-Session sind die besten Onboarding-Hinweise diese README plus der aktuelle Stand auf dev:
bridge/src/main/java/com/ghidra_svr/bridge/ProgramApplier.javabridge/src/test/java/com/ghidra_svr/bridge/ProgramTestBase.javabridge/src/test/java/com/ghidra_svr/bridge/*RoundTripTest.javagit log --oneline — jede Betreffzeile sagt, was sich geändert hat und warumProgramApplier überspringt is_local-Parametereinträge, weil das Mapping von BN-Registerindizes auf Ghidra-Storage eine pro-Architektur-Registertabellen-Übersetzung erfordert. Parameter funktionieren; Locals synchronisieren noch nicht.DatabaseExporter geht von einem einzelnen RAM-Adressraum aus. Overlay-Spaces oder Harvard-Architekturen können falsche Adressen erzeugen.DBChangeSet, um Checkouts funktionsfähig zu halten. Ghidras Merge-on-Checkout-Maschinerie kann daher gleichzeitige Bearbeitungen zwischen BN- und Ghidra-Nutzern nicht automatisch auflösen — der letzte Schreiber gewinnt.