
Ghidra Server 리포지토리와 분석을 양방향으로 동기화하는 Binary Ninja 플러그인으로, Java 브리지 서브프로세스를 통해 동작합니다.
Ghidra Server 저장소에 연결하여 해당 분석 결과(심볼, 함수 이름, 주석)를 열려 있는 Binary Ninja 바이너리 뷰로 직접 가져오는 Binary Ninja 플러그인입니다.
Ghidra와 Binary Ninja는 각각 강점이 있습니다. 이 플러그인을 사용하면 이름이나 주석을 수동으로 복사하지 않고도 동일한 바이너리에서 두 도구를 모두 사용할 수 있습니다. 실행 중인 Ghidra Server에 연결하고, 저장소를 탐색한 뒤, 프로젝트 파일을 더블클릭하여 해당 분석 결과를 현재 열려 있는 BN 뷰로 가져옵니다.
동기화는 양방향으로 이루어집니다.
| BN ← Ghidra (가져오기) | BN → Ghidra (체크인) | |
|---|---|---|
| 심볼 (레이블, 함수 이름) | ✓ | ✓ |
| 주석 (EOL/PRE/POST/PLATE/REP) | ✓ | ✓ |
| 함수 시그니처 (반환 타입, 호출 규약) | ✓ | ✓ |
| 함수 매개변수 (이름 변경, 타입 변경, 추가) | ✓ | ✓ |
| 데이터 타입 (struct/union/enum/typedef + pointer/array) | ✓ | ✓ |
| Equate (상수 이름 + 참조) | ✓ | ✓ |
| 북마크 | ✓ | ✓ |
| 타입이 지정된 데이터 항목 | ✓ | ✓ |
| 함수 플래그 (thunk, no-return, inline) | ✓ BN 태그로 | — |
| 지역 변수 (스토리지 인식) | ✓ | 부분적 — 레지스터 스토리지 매핑 미구현 |
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)
플러그인은 로드 시 Java 하위 프로세스("브리지")를 생성합니다. 브리지는 Ghidra Server에 대한 RMI 연결을 유지하고, 로컬 TCP 소켓을 통해 플러그인과 간단한 JSON 프로토콜로 통신합니다. 이렇게 하면 모든 Java/RMI 코드가 C++ 프로세스 외부에 유지되고, BN이 로딩을 마치는 동안 JVM이 백그라운드에서 시작될 수 있습니다.
브리지 JVM은 시작 시 Ghidra의 Application 프레임워크도 초기화하므로, 쓰기 경로에서 원시 db.Table.putRecord() 쓰기 대신 Ghidra의 고수준 프로그램 모델 API(ProgramDB, DataTypeManager, SymbolTable, FunctionManager)를 사용할 수 있습니다 — 아래 체크인 쓰기 경로를 참조하세요.
| 경로 | 언어 | 역할 |
|---|---|---|
plugin/ | C++ / Qt6 | Binary Ninja 사이드바 플러그인 |
bridge/ | Java 17 | Ghidra RMI 클라이언트 + JSON 브리지 서버 |
플러그인 (C++):
plugin.cpp — 설정과 사이드바 위젯을 등록하고, 로드 시 브리지 JVM을 즉시 시작GhidraConnection.cpp — 싱글턴; 브리지 수명 주기와 모든 RMI 기반 작업을 관리BridgeProcess.cpp — 브리지 JAR을 stdout/stderr 파이프와 함께 하위 프로세스로 실행하고, READY port=N 핸드셰이크 라인을 읽음BridgeClient.cpp — TCP 클라이언트; JSON 요청을 보내고 응답을 받으며 비동기 이벤트를 디스패치SyncEngine.cpp — GhidraDbExport를 BinaryView에 적용 (심볼, 주석, 플래그)ui/ProjectPanel.cpp — 사이드바 위젯: 저장소 트리, 연결 대화상자, 활동 로그ui/ConnectDialog.cpp — 호스트/포트/사용자/비밀번호 대화상자브리지 (Java):
BridgeMain.java — 인수 파싱; UniversalIdGenerator와 Ghidra Application 프레임워크를 초기화하고, TCP 서버를 시작한 뒤 stdout에 READY port=N을 출력BridgeServer.java — 하나의 TCP 클라이언트 연결을 수락하고 BridgeConnection을 넘겨줌BridgeConnection.java — JSON 요청 디스패처; Ghidra API 응답을 JSON으로 직렬화하고, opCheckin(서버에 새 프로그램 버전 생성)을 처리GhidraSession.java — 인증된 RMI 세션; RemoteRepositoryServerHandle을 래핑EventStreamer.java — 열린 저장소마다 하나씩 있는 백그라운드 스레드; RepositoryChangeEvent를 비동기 JSON 이벤트로 플러그인에 푸시DatabaseExporter.java — 읽기 경로: 원시 db.jar 접근을 통해 ManagedBufferFileHandle(Ghidra의 원격 DB 버퍼)에서 심볼/주석/함수 플래그/데이터 타입/equate/북마크 테이블을 추출ProgramApplier.java — 쓰기 경로: 버퍼 파일을 실제 ProgramDB로 열고 Ghidra의 고수준 API를 통해 모든 BN 측 변경 사항을 적용 (아래 체크인 쓰기 경로 참조)DatabaseImporter.java — 테스트 시임으로만 유지되는 레거시 원시 쓰기 헬퍼; 프로덕션 apply(...)는 ProgramApplier에 위임opCheckin은 프로그램의 관리 버퍼 파일을 쓰기 모드로 열고, 그 위에 ProgramDB를 생성한 뒤, Ghidra의 프로그램 모델 API를 통해 BN 측 변경 사항을 적용합니다. 원시 db.Table.putRecord() 쓰기는 피합니다 — 이는 우리가 겪은 모든 체크인 손상 버그의 원인이었습니다:
| 잘못된 계층의 쓰기 | 실패 모드 |
|---|---|
Function Data 테이블에 setIntValue(col, longTypeId) | IntField.setLongValue가 조용히 l2i로 잘라냄 → 모든 시그니처 업데이트에서 StackPurge 손상 |
V5V6 Composite Data Types에 setByteValue(col, isUnion) | Ghidra 12.x에서 해당 컬럼은 BooleanField → IllegalFieldAccessException ("Illegal field access") |
V2 Typedef Flags 컬럼에 setIntValue(col, 0) | 해당 컬럼은 ShortField → 동일한 크래시, 다른 스키마 |
| 컴포넌트 설정 행 없이 composite 헤더 작성 | Ghidra에서 struct를 열 때 CompositeEditorModel.cloneAllComponentSettings가 ArrayIndexOutOfBoundsException을 던짐 |
SYM_ADDR_COL = RAM 주소로 PARAMETER 심볼 작성 | 어떤 함수에 접근하든 FunctionDB.loadSymbolBasedVariables가 Address is not a VariableAddress를 던짐 |
DBHandle.save()에 null DBChangeSet 전달 | 서버가 0바이트 변경 데이터 파일을 작성 → 다음 체크아웃이 ProgramContentHandler.loadProgramChangeSet에서 EOFException으로 실패 |
ProgramApplier에는 이러한 함정이 없습니다. DataTypeManager.addDataType, SymbolTable.createLabel, Listing.setComment, Function.setReturnType 등을 통해 라우팅하기 때문입니다 — 이 API들은 Ghidra의 상호 잠금 테이블 불변성을 자동으로 유지합니다. 또한 모든 체크인 시작 시 cleanupBadVariableSymbols 패스를 실행하여 이전 브리지 버전이 데이터베이스에 남긴 손상을 제거합니다.
server-package/CleanupBadVariableSymbols.java는 analyzeHeadless를 통해 동일한 정리를 실행하는 독립 실행형 GhidraScript입니다 — 파일이 너무 손상되어 Ghidra GUI에서 열 수 없을 때 유용합니다.
./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)
테스트 스위트는 다섯 계층으로 구성됩니다. 계층 2–4는 한 가지 속성을 증명하기 위해 존재합니다: 동일한 호환 데이터가 .bndb와 Ghidra 프로그램 데이터베이스 양쪽에 저장된다 (이 README 상단의 호환성 매트릭스).
| 계층 | 내용 | 위치 | 게이트 |
|---|---|---|---|
| 0 | 순수 단위 테스트 | plugin/test/*.cpp (binja-ghidra-tests), 브리지 *Test.java | 항상 |
| 1 | Ghidra-DB 왕복 | 브리지 *RoundTripTest.java (실제 ProgramDB에 대한 ProgramApplier) | ghidra.home / GHIDRA_HOME 필요 |
| 2 | BN BinaryView/.bndb 왕복 | plugin/test/bn/ (binja-ghidra-bn-tests; headless binaryninjacore) | headless 지원 BN 라이선스 없으면 깔끔하게 SKIP (BN_LICENSE 환경 변수 존중) |
| 3 | 교차 DB 패리티 | testdata/parity/fixtures/의 공유 골든에 대한 CanonicalParityTest (C++ 및 Java) | 계층 1+2와 함께 |
| 4 | 라이브 서버 E2E | 브리지 LiveServerE2ETest — 임시 디렉터리에서 실제 ghidraSvr를 부팅하고, analyzeHeadless로 시드한 뒤, RMI를 통해 체크아웃 → 내보내기 → 체크인 → 재내보내기를 구동 | test.bat --e2e (GHIDRA_E2E=1 설정) |
패리티 오라클 (계층 3). 양쪽은 동일한 체크인된 정규 JSON(브리지 DatabaseExporter 형태)에 대해 독립적으로 검증합니다. 가져오기 방향: 골든이 ProgramDB(Java)와 SyncEngine을 통한 BinaryView(C++)에 로드되고, 각각의 재내보내기가 골든과 같아야 합니다. 체크인 방향: 스크립트된 BN 편집이 정확히 fixtures/checkin/*/expected-preview.json(C++)을 생성해야 하며, 해당 프리뷰를 ProgramApplier로 적용하면 expected-after.json(Java)으로 재내보내져야 합니다. 양쪽이 공유 골든과 일치하면 두 데이터베이스는 추이성에 의해 일치합니다. 필드 비교 모드와 타입 이름 정규화 테이블은 testdata/parity/RULES.md에 있으며, 테스트 바이너리는 testdata/bin/parity_x64.bin입니다 (레이아웃은 parity_x64.md).
Java 측의 오래된 회귀 핀:
DataTypesRoundTripTest.struct_cloneSettings_doesNotThrow — composite 설정이 헤더와 일관성을 유지해야 함 (cloneAllComponentSettings 크래시)FunctionSignaturesRoundTripTest.returnType_doesNotCorruptStackPurge — IntField 잘림ParametersRoundTripTest.noParameterSymbol_endsUpAtRamAddress — VariableAddress 불변성왕복 및 패리티 테스트에는 Ghidra 설치가 필요합니다 (런타임에 언어 서비스에 사용됨). 경로는 ghidra.home Gradle 시스템 속성 또는 GHIDRA_HOME 환경 변수에서 읽습니다; build.gradle은 기본적으로 ghidraHome을 전달합니다. 계층 2/3 C++ 테스트는 추가로 binaryninjacore가 로드 가능해야 합니다 (스크립트가 BN 설치 디렉터리를 PATH에 추가함).
| 종속성 | 비고 |
|---|---|
| Binary Ninja (상용) | BN 설치의 api_REVISION.txt와 일치하는 버전에 대해 테스트됨 |
| Ghidra Server | Ghidra 12.0.4로 테스트됨. 실행 중이어야 하며 RMI/SSL을 통해 접근 가능해야 함 |
| Java 17+ JDK | Eclipse Adoptium JDK 21 권장 |
| CMake 3.24+ | |
| Ninja | |
| C++ 컴파일러 | Windows에서는 MSVC 2022+; macOS에서는 clang; Linux에서는 gcc/clang |
| Qt 6.7+ | 아래 Qt 설정 참조; 빌드 시 qmake가 PATH에 있어야 함 |
| Gradle (wrapper 경유) | 브리지는 Gradle wrapper를 사용 — 별도 설치 불필요 |
| Poetry (Qt 빌드 전용) | qt-build 서브모듈에서 Qt를 빌드할 때만 필요. pip install poetry 또는 pipx install poetry로 설치. |
| libclang 19 (Qt 빌드 전용) | Qt의 빌드 시스템에서 필요. 다운로드 지침은 qt-build/README.md 참조. |
플러그인은 Binary Ninja가 사용하는 것과 동일한 Qt 6 빌드에 링크됩니다. 두 가지 옵션이 있습니다:
옵션 A — 기존 Qt 설치 사용 (이미 Qt가 있다면 가장 빠름)
Qt CMake 디렉터리를 가리키는 Qt6_DIR을 전달하세요:
Qt6_DIR=/path/to/Qt/6.x.y/clang_64/lib/cmake/Qt6 ./build.sh
macOS에서는 Qt 온라인 설치 프로그램이 /usr/local/Qt* 아래에 설치한 경우 빌드 스크립트가 Qt를 자동 감지합니다.
옵션 B — qt-build 서브모듈에서 Qt 빌드 (머신당 한 번, 약 1-2시간)
qt-build 서브모듈(Vector35의 Qt 빌드 스크립트)은 Binary Ninja의 패치와 함께 Qt 6을 컴파일합니다. Poetry와 libclang 19가 필요합니다 (위 사전 요구 사항 및 qt-build/README.md 참조).
Qt는 저장소 내 qt/<version>/<compiler>/에 설치됩니다: