
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>/에 설치됩니다:
| 플랫폼 | 설치 경로 |
|---|---|
| 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
qt 단계는 한 번만 필요합니다. CMake와 빌드 스크립트는 이후 실행마다 qt/에 빌드된 Qt를 감지하고 서브모듈을 완전히 건너뜁니다. qt/ 디렉터리는 gitignore됩니다.
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)
그런 다음 위의 Qt 설정(옵션 A 또는 B)을 따르고 다음을 실행하세요:
./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
환경 변수 (모두 선택 사항 — 스크립트가 합리적인 기본값을 설정함):
BN_INSTALL=/Applications/Binary\ Ninja.app/Contents/MacOS
Qt6_DIR=/usr/local/Qt-6.7.2/lib/cmake/Qt6
처음 사용하기 전에 build.bat 상단의 경로를 환경에 맞게 편집하세요:
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
C++ 빌드는 CMake FetchContent를 사용하여 api_REVISION.txt에 기록된 정확한 커밋에서 binaryninja-api를 클론하므로, 플러그인 ABI가 항상 설치된 BN 버전과 일치합니다. GHIDRA_HOME이 설정되지 않은 경우 첫 구성 시 CMake가 Ghidra를 자동으로 다운로드합니다.
설치 후 Binary Ninja 설정(Edit → Preferences → Settings, "Ghidra" 검색)에서 다음을 설정하세요:
| 설정 | 설명 |
|---|---|
ghidra.javaExe | java.exe의 전체 경로 |
ghidra.ghidraHome | Ghidra 설치의 루트 (Ghidra/Framework/… 포함) |
ghidra.trustAllCerts | Ghidra Server가 자체 서명 인증서를 사용하는 경우 true로 설정 |
ghidra.defaultHost | 연결 대화상자를 미리 채움 |
ghidra.defaultPort | 기본값: 13100 |
ghidra.defaultUser | 연결 대화상자를 미리 채움 |
5단계의 전제 조건: 프로그램 파일이 Ghidra Server 저장소에 커밋되어 있어야 합니다 (Ghidra에서 로컬로 열려 있는 것만으로는 부족). Ghidra에서: Project 창의 파일을 마우스 오른쪽 버튼으로 클릭 → Version Control → Add to Version Control….
플러그인과 브리지는 줄바꿈으로 구분된 JSON을 사용하여 로컬 TCP 소켓을 통해 통신합니다. 모든 요청은 정수 id와 문자열 op를 포함하며, 모든 응답은 id를 반향합니다. 비동기 이벤트(서버 측 저장소 변경)는 대신 "event" 키를 포함합니다.
| Op | 방향 | 목적 |
|---|---|---|
ping, status, connect, disconnect | 요청/응답 | 세션 수명 주기 |
list_repos, open_repo, close_repo | 요청/응답 | 저장소 열거 |
list_items, get_subfolders | 요청/응답 | 저장소 탐색 |
get_versions, get_checkouts | 요청/응답 | 버전 관리 상태 |
checkout, terminate_checkout | 요청/응답 | 배타적 쓰기 잠금 |
open_db | 요청/응답 | 전체 Ghidra DB 읽기 → JSON (무거움) |
checkin | 요청/응답 | BN 측 변경 사항 적용 → 새 저장소 버전 (무거움, ProgramApplier 경유) |
download_binary, upload_binary | 요청/응답 | 원본 바이너리 이동 |
delete_item | 요청/응답 | 저장소에서 파일 제거 |
repo_changed | 이벤트 (비동기) | 서버 측 RepositoryChangeEvent 푸시 |
저장소에는 처음부터 다시 빌드하는 데 필요한 모든 것이 포함되어 있습니다. git에 없는 개발자별 설정:
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을 설정하거나 ./build.sh qt(Windows: build.bat qt)를 한 번 실행합니다.binaryninja-api 커밋을 가져옵니다:
./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과 --bn-api는 상호 배타적입니다; 둘 다 없으면 stable 채널이 사용됩니다. --channel은 Vector35/binaryninja-api GitHub(최신 stable/* 릴리스 또는 dev 브랜치 헤드)를 쿼리하므로 네트워크 접근이 필요합니다. 설치된 BN이 최신 릴리스보다 뒤처져 있다면 해당 설치의 api_REVISION.txt에 있는 정확한 SHA로 --bn-api를 전달하세요.새 Claude Code 세션을 열 때 가장 좋은 온보딩 포인터는 이 README와 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 — 각 제목 줄이 무엇이 왜 변경되었는지 설명합니다ProgramApplier는 is_local 매개변수 항목을 건너뜁니다. BN 레지스터 인덱스를 Ghidra 스토리지에 매핑하려면 아키텍처별 레지스터 테이블 변환이 필요하기 때문입니다. 매개변수는 작동하지만 지역 변수는 아직 동기화되지 않습니다.DatabaseExporter는 단일 RAM 주소 공간을 가정합니다. 오버레이 공간이나 하버드 아키텍처는 잘못된 주소를 생성할 수 있습니다.DBChangeSet을 씁니다. 따라서 Ghidra의 체크아웃 시 병합 메커니즘은 BN과 Ghidra 사용자 간의 동시 편집을 자동으로 해결할 수 없습니다 — 마지막 작성자가 이깁니다.