플러그인·MCP 서버
1e98025 기준이며, 줄 수에는 공백과 주석이 포함됩니다.
GhidraMCP는 Ghidra의 바이너리 분석 기능을 MCP 클라이언트가 호출할 수 있게 만드는 연결 계층입니다. AI가 실행 파일을 이해하는 데 필요한 근거를 Ghidra에서 가져오고, 분석 결과를 이름과 주석으로 다시 남기게 해줍니다.
예를 들어 AI에게 “이 프로그램의 계산 흐름을 설명해 줘”라고 요청하면,
클라이언트는 함수 목록을 조회하고, 특정 함수를 디컴파일하고, 그 함수가 호출하는 다른 함수를 조사할 수 있습니다.
적절한 이름을 찾았다면 rename_function_by_address로 Ghidra의 함수 이름도 바꿀 수 있습니다.
현미경의 렌즈와 관찰 능력은 Ghidra가 제공합니다. GhidraMCP는 “이 부분을 보여 줘”, “여기에 이름표를 붙여 줘”라는 버튼을 외부에 내놓습니다.
AI 클라이언트는 관찰 순서를 정하는 연구 조수에 가깝습니다. 관찰 결과를 해석할 수는 있어도, 그 해석의 정확성까지 제어판이 보증하지는 않습니다.
| 작업 | 대표 도구 | 얻는 것 |
|---|---|---|
| 대상 확인 | get_program_info | 파일 형식, 아키텍처, 주소 범위, SHA-256 등 |
| 구조 탐색 | list_functions, list_strings | 함수 이름·진입 주소, 문자열 값·주소 |
| 동작 조사 | decompile_function_by_address, get_function_calls | C 형태 의사 코드, 한 단계 호출 관계 |
| 분석 기록 | rename_function_by_address, set_decompiler_comment | Ghidra 분석 데이터의 이름·주석 변경 |
src/ghidra_mcp/server.py에는 @mcp.tool() 선언이 30개 있습니다.
이 중 현재 GUI 주소·함수 조회 2개는 조건부라, headless에서는 28개가 등록됩니다.
list_methods 같은 호환용 별칭도 포함된 숫자이므로 독립적인 분석 알고리즘 30개라는 뜻은 아닙니다.
모델 가중치·추론 엔진·자체 대화 화면은 없습니다. 모델 선택과 대화, 도구 실행 승인 정책은 연결한 MCP 클라이언트 쪽에서 다룹니다.
분석 중에는 함수 목록을 훑고, 문자열을 찾고, 호출자를 확인한 뒤, 발견한 의미를 이름과 주석으로 남기는 작업이 이어집니다. 의사 코드를 대화창에 수동으로 옮기는 방식에서는 다음 조사에 필요한 주소와 주변 정보가 빠지기 쉽습니다. GhidraMCP는 그 과정을 작은 도구 호출로 나누어, 클라이언트가 필요한 정보를 추가로 요청할 수 있게 합니다.
이 프로젝트의 매력은 검증 가능한 분석 도구를 AI의 작업 흐름에 연결한다는 데 있습니다. 조회 결과에 주소·호출 위치·오류 코드를 남기는 구현도 이 연결을 뒷받침합니다.
GitHub Trending Java Monthly #3 선정과 스타 11,495개는 관심의 크기를 보여줍니다. 다만 순위나 스타만으로 분석 정확도와 실사용 성능을 판단할 수는 없습니다. 반복 탐색의 자동화, MCP 클라이언트 연결, GUI·headless 공유 구조는 코드에서 읽을 수 있는 매력 요인이며, 순위 상승의 인과관계가 검증되었다는 뜻은 아닙니다.
| 선택지 | 강점과 접근 방식 | GhidraMCP에서 배울 차이 |
|---|---|---|
| Ghidra 직접 사용 | GUI에서 분석 결과를 사람이 살피고 수정합니다. Java·Python 확장도 가능합니다. | 동일한 분석 기능을 원격 도구 계약으로 감싸는 방법 |
| 직접 만든 분석 스크립트 | 반복할 절차를 코드로 고정하기 좋습니다. | 함수별 도구와 구조화된 결과로 다음 조사 순서를 클라이언트에 맡기는 방법 |
| IDA / Hex-Rays | 디컴파일과 이름·타입 수정 기능을 제공하는 별도 분석 환경입니다. | Ghidra를 쓰는 환경에서 연결 계층의 Java·Python 구현을 함께 읽을 수 있음 |
| Binary Ninja MCP | 공식 문서에 GUI·headless MCP 서버와 분석 정보 조회가 설명되어 있습니다. | 이 저장소에는 조회뿐 아니라 이름·주석·타입을 바꾸는 경로도 명시적으로 구현됨 |
| GhidraMCP | Ghidra의 기능을 MCP 도구로 공개하고 두 실행 모드에서 공유합니다. | Apache-2.0 소스에서 프로토콜·분석·환경 의존성을 분리한 작은 구조를 학습 |
제품 기능의 근거는 Ghidra 공식 저장소, Hex-Rays 디컴파일러 개요, Binary Ninja MCP 공식 문서입니다. 이 비교는 워크플로와 공개된 연결 기능에 관한 것이며 디컴파일 정확도의 순위표는 아닙니다.
Python의 타입 힌트는 도구 입력을 설명하고, HTTP는 Java와 데이터를 주고받고,
Java의 ProgramProvider는 실행 환경 차이를 숨깁니다.
GUI 모드의 수정 작업은 Swing 이벤트 스레드와 Ghidra 트랜잭션이라는 두 가지 제약을 함께 지켜야 합니다.
웹 프레임워크나 대규모 데이터베이스 없이도, 서비스 경계와 실패 처리를 공부할 수 있다는 점이 초보자에게 유용합니다.
| 기술 | 실제 버전·설정 | 근거와 역할 |
|---|---|---|
| Python 패키지 | ghidra-mcp 2.0.0, Python >=3.10 | pyproject.toml의 프로젝트 메타와 실행 진입점 |
| MCP Python SDK | mcp>=1.30,<2 → 잠금 1.30.0 | FastMCP로 도구 등록·통신 처리 |
| Requests | requests>=2.32,<3 → 잠금 2.34.2 | src/ghidra_mcp/backend.py에서 Java HTTP 호출 |
| PyGhidra | 선택 의존성 pyghidra>=3.1 → 잠금 3.1.0 | [headless] 설치 시 JVM 시작·분석·프로젝트 관리 |
| JPype | 잠금 파일 jpype1 1.5.2 | PyGhidra 경로의 전이 의존성, Java 클래스 접근 |
| Ghidra | README·CI 기준 12.1.4 | Java API, 분석 데이터 모델, 디컴파일러 제공 |
| Java | README·CI 기준 JDK 21 | 확장 빌드와 Ghidra 실행 환경 |
| HTTP / JSON | JDK HttpServer + Gson | extension/src/main/java/com/lauriewired/ghidramcp/http/BackendHttpServer.java, extension/src/main/java/com/lauriewired/ghidramcp/http/Json.java |
버전 범위는 설치할 때 허용하는 후보이고, 잠금 파일의 버전은 이 스냅샷에서 실제 선택한 결과입니다.
예를 들어 mcp>=1.30,<2를 언제나 1.30.0만 쓴다는 뜻으로 읽으면 안 됩니다.
extension/build.gradle의 dependencies·repositories 블록은 비어 있으며,
Gson과 Ghidra API의 개별 버전은 이 저장소에서 따로 고정하지 않습니다.
package.json이나 React·Vue 코드, 별도 프론트엔드 빌드는 없습니다.
사용자 화면은 Ghidra의 기존 GUI와 외부 MCP 클라이언트가 제공합니다.
extension/src/main/java/com/lauriewired/GhidraMCPPlugin.java는 포트 설정과 서버 생명주기를 연결하며, 새 대화 화면을 그리지 않습니다.
이 구조는 “UI가 없으니 미완성”이 아니라 기존 앱의 확장으로 제품 범위를 정한 사례입니다.
GuiProgramProvider.getCurrentAddress()가 기존 코드 뷰어의 선택 위치를 읽어오는 것이 대표적입니다.
| 층 | 실제 설정 | 학습 포인트 |
|---|---|---|
| Python 빌드 | hatchling>=1.25, uv build | wheel·소스 배포본 생성, Hatchling은 빌드 요구 범위이며 확정 버전은 별도 미기재 |
| 명령 진입점 | ghidra-mcp = "ghidra_mcp.cli:main" | 패키지 설치 후 CLI 함수 연결 |
| Java 확장 빌드 | CI Gradle 8.14 | 설치된 Ghidra의 support/buildExtension.gradle 사용 |
| 테스트 | pytest>=8 → 잠금 9.1.1 | 가짜 HTTP 백엔드 검사와 실제 headless 통합 검사 분리 |
| GitHub Actions | ubuntu-latest, Temurin 21, setup-uv@v6 | .github/workflows/build.yml에 빌드·테스트·산출물 업로드 정의 |
| Ghidra 다운로드 | GHIDRA_VERSION: 12.1.4, GHIDRA_DATE: 20260921 | CI에서 분석 도구 버전까지 지정 |
| 배포 묶음 | 확장 ZIP, Python wheel·tar.gz, 호환 진입 스크립트 | 서로 다른 언어의 배포물을 함께 조립 |
CI는 main에 대한 push·pull request에서 실행되도록 선언되어 있습니다.
설정 파일은 검사 의도를 보여주며, 특정 실행이 성공했다는 증거는 아닙니다.
별도 Dockerfile, Node·Rust·Go 의존성 파일은 없고, 이 워크플로에 Python·운영체제 버전 행렬도 없습니다.
# pyproject.toml의 실행 경로와 빌드 범위
[project.scripts]
ghidra-mcp = "ghidra_mcp.cli:main"
[tool.hatch.build.targets.wheel]
packages = ["src/ghidra_mcp"]
# Java 확장은 Ghidra 설치 위치에서 빌드 규칙을 가져옵니다.
# extension/build.gradle: GHIDRA_INSTALL_DIR가 없으면 예외 발생
MCP 연결과 Java 백엔드 HTTP 연결을 구분해야 합니다.
클라이언트용 HTTP 모드의 기본 주소는 127.0.0.1:8081/mcp이고,
Ghidra GUI 백엔드의 기본 주소는 127.0.0.1:8080입니다.
--mcp-port와 --ghidra-server는 서로 다른 연결을 설정합니다.
MCP 전송 규격은
표준 입출력과 Streamable HTTP를 설명합니다.
이 저장소의 Java HTTP 응답은 success/data/error 봉투를 쓰는 내부 API이며,
외부 MCP 메시지 처리는 Python SDK가 맡습니다.
src/ghidra_mcp/server.py의 create_server()는
Python 함수에 @mcp.tool()을 붙여 도구를 공개합니다.
decompile_function_by_address(address)는 내부 경로 decompile_function을 GET으로 호출하고,
Java의 registerRoutes()는 이를 service.decompileByAddress()에 연결합니다.
# src/ghidra_mcp/server.py의 실제 도구 연결
@mcp.tool()
def decompile_function_by_address(address: str) -> Result:
# 도구 설명 문자열은 생략했습니다.
return get("decompile_function", address=address)
# 호출 입력 예시. 주소는 실제 함수 목록에서 얻습니다.
{"address": "0x401000"}
어댑터는 서로 다른 인터페이스 사이를 번역하는 부품이고, 파사드는 복잡한 내부 기능의 간단한 입구입니다. Python 도구 함수가 MCP 입력을 HTTP 파라미터로 번역하고, Java 서비스가 여러 Ghidra API를 “함수 하나 디컴파일”이라는 작업으로 묶는 모습에서 두 역할을 읽을 수 있습니다.
GhidraBackend.request()는 값이 None인 파라미터를 제거하고,
GET에서는 params=, POST에서는 data=를 사용합니다.
Java의 Request.parse()는 쿼리 문자열과 폼 본문을 합칩니다.
따라서 이 내부 API를 JSON 본문을 받는 API라고 설명하면 실제 계약과 어긋납니다.
extension/src/main/java/com/lauriewired/ghidramcp/core/ProgramProvider.java는
getProgram(), mode(), capabilities(), runWrite()를 정의합니다.
GUI 구현은 ProgramManager의 현재 프로그램을 얻고,
headless 구현은 생성 시 받은 하나의 Program을 반환합니다.
// BackendHttpServer 생성자의 핵심 두 줄
this.provider = provider;
this.service = new GhidraAnalysisService(provider::runWrite);
// ProgramProvider의 쓰기 실행 계약
<T> T runWrite(Supplier<T> action);
분석 서비스는 GUI 객체를 직접 찾지 않습니다. 필요한 프로그램은 메서드 인자로 받고, 쓰기 실행 방식은 생성자로 받습니다. 이 덕분에 GUI와 headless를 위해 함수 목록·디컴파일·이름 변경 알고리즘을 두 벌 만들 필요가 없습니다.
같은 프린터에 USB와 네트워크 어댑터를 붙이는 것과 비슷합니다. 인쇄하는 핵심은 같고, 어느 연결에서 데이터를 받는지만 바뀝니다. 여기서는 프로그램을 얻는 방법과 수정 작업을 실행하는 스레드가 교체 대상입니다.
GuiProgramProvider.runWrite()는 이미 Swing 이벤트 스레드라면 바로 실행하고,
그렇지 않으면 SwingUtilities.invokeAndWait()로 작업을 전달합니다.
HeadlessProgramProvider.runWrite()는 직접 실행합니다.
headless HTTP 요청은 BackendHttpServer의 단일 실행기에서 처리됩니다.
그 위에서 GhidraAnalysisService.write()가 수정의 성공·실패 범위를 관리합니다.
이름 변경, 데이터 라벨, 주석, 함수 원형, 변수 타입 변경이 이 공통 경로를 사용합니다.
// GhidraAnalysisService.write()의 흐름을 줄인 의사 코드
return writeExecutor.execute(() -> {
int tx = program.startTransaction(transactionName);
boolean commit = false;
try {
T result = action.run();
commit = true;
return result;
} finally {
program.endTransaction(tx, commit);
}
});
수정이 안전하려면 올바른 실행 스레드와 성공했을 때만 확정되는 트랜잭션을 함께 설계해야 합니다.
실제 메서드는 중복 이름을 NAME_CONFLICT로,
잘못된 입력을 INVALID_PARAMETER로 번역하는 예외 처리도 포함합니다.
여기서 확정되는 것은 Ghidra 프로그램의 트랜잭션입니다.
디스크에 프로젝트를 저장하는 일은 별도입니다.
headless의 종료 시 저장 여부는 HeadlessSession.close()와 _should_save()가 결정합니다.
cli.run_headless()는 JVM을 시작한 뒤 MCP 서버를 별도 Python 스레드에 띄우고,
메인 스레드에서 HeadlessSession.run()으로 프로그램을 열고 분석합니다.
소스 주석은 JVM을 시작한 스레드와 다른 스레드에서 프로젝트 API를 처음 사용하면 멈출 수 있다는 제약을 설명합니다.
DeferredBackend는 threading.Event로 준비 완료 또는 실패를 알립니다.
HeadlessSession._serve()가 Java 서버의 실제 포트를 얻어 set_ready()에 전달합니다.
기본 --ready-timeout은 30초이며, 분석 전체를 30초 뒤 취소하는 옵션은 아닙니다.
기능 공개도 환경에 맞춥니다. GUI의 /health에는 gui_context가 들어가고,
headless에서는 빈 기능 집합을 넘겨 현재 GUI 위치 도구를 숨깁니다.
GUI 백엔드가 시작 시 응답하지 않아 기능이 None이면 모든 도구를 우선 등록합니다.
이후 자동으로 도구 목록을 다시 협상하는 코드는 없습니다.
| 계약 | 실제 구현 | 독자가 주목할 이유 |
|---|---|---|
| 페이지 결과 | Page<T>: items/offset/limit/total/has_more | 응답 크기와 다음 요청을 결정할 수 있습니다. |
| 페이지 상한 | Page.clampLimit(): 1~2,000, Page.Builder 생성자에서 음수 offset은 0 | 잘못된 범위를 정규화합니다. |
| 이름 중복 | functionByName(): 일치 함수가 여러 개면 AMBIGUOUS_NAME | 첫 일치 항목을 임의로 고르지 않고 주소 사용을 안내합니다. |
| 주소 조회 | functionAt(): 진입점 확인 후 포함 함수 탐색 | 함수 내부 주소도 다룰 수 있습니다. |
| 성공·실패 봉투 | Json.success() / Json.error() | Java 오류 → Python BackendError → MCP ToolError로 전달됩니다. |
# Java 백엔드 응답 형식 예시. 수치는 설명용입니다.
{
"success": true,
"data": {
"items": [{"name": "compute", "address": "00401000"}],
"offset": 0, "limit": 100, "total": 1, "has_more": false
}
}
# 실패는 코드와 설명을 분리합니다.
{"success": false,
"error": {"code": "NO_PROGRAM", "message": "프로그램이 열려 있지 않습니다"}}
extension/src/main/java/com/lauriewired/ghidramcp/http/Json.java는 Gson의 LOWER_CASE_WITH_UNDERSCORES 정책을 써서
Java의 hasMore를 JSON의 has_more로 내보냅니다.
Page.Builder.offer()는 요청 범위의 항목만 객체로 만들지만 전체 항목을 세기 위한 순회는 계속합니다.
응답을 작게 만든다고 검색 시간까지 항상 작아지는 것은 아닙니다.
GhidraAnalysisService는 디컴파일 60초, 결과 문자열 200,000자(Java의 UTF-16 코드 단위),
메모리 읽기 4,096바이트 상한을 둡니다.
결과가 길면 truncated=true를 반환하고, withDecompiler()는
finally에서 DecompInterface.dispose()를 호출합니다.
src/ghidra_mcp/backend.py의 연결 제한은 5초, 읽기 제한은 90초입니다.
90초는 60초 디컴파일보다 길지만, 요청 대기와 준비 대기까지 포함한 전체 작업의 완료 보장은 아닙니다.
Java 요청이 직렬 처리되므로 느린 디컴파일 뒤의 조회도 기다릴 수 있습니다.
BackendHttpServer.start()는 루프백 주소에 바인딩합니다.
dispatch()는 Origin 헤더가 있거나 허용하지 않은 Host이면 403을 반환합니다.
Python 세션은 trust_env=False로 환경 프록시를 사용하지 않습니다.
Java 내부 API에는 토큰 인증이 구현되어 있지 않아, 로컬 프로세스 간 신뢰까지 해결한 구조는 아닙니다.
extension/src/main/java/com/lauriewired/ghidramcp/core/에는 HTTP 요청 객체나 Swing 화면 코드가 들어가지 않습니다.
extension/src/main/java/com/lauriewired/ghidramcp/http/는 경로와 파라미터를 분석 메서드에 연결하고,
extension/src/main/java/com/lauriewired/ghidramcp/gui/·extension/src/main/java/com/lauriewired/ghidramcp/headless/는 실행 환경 차이를 담당합니다.
Python도 도구 선언, 네트워크 호출, 생명주기를 서로 다른 파일에 둡니다.
bridge_mcp_ghidra.py는 별도의 구형 분석 엔진이 아닙니다.
패키지의 cli.main을 가져오고, 가져오기에 실패하면 저장소의 Python 소스 경로를 추가하는 20줄짜리 호환 진입점입니다.
| 범위 | 파일 수 · 물리 줄 수 | 해석 |
|---|---|---|
| Java 확장 | 12개 · 1,705줄 | 분석·HTTP·환경 어댑터 전체 |
| Python 구현 | 7개 · 895줄 | 패키지 6개와 호환 진입점 1개 |
| Python 테스트 | 5개 · 441줄 | 공유 fixture 포함 |
| Python 전체 | 12개 · 1,336줄 | 구현과 테스트를 합한 값 |
| C / 셸 | C 1개 · 48줄 / 셸 1개 · 13줄 | 통합 검사 샘플 원본과 재생성 절차 |
| 빌드·CI | Gradle 2개 · 34줄 / YAML 1개 · 60줄 | 실행 코드와 구분한 설정 파일 |
줄 수는 공백·주석을 포함하는 물리 줄 수이며 문서·잠금 파일·실행 파일·Git 내부 파일은 제외했습니다.
가장 큰 파일은 extension/src/main/java/com/lauriewired/ghidramcp/core/GhidraAnalysisService.java 880줄이고,
Python의 src/ghidra_mcp/server.py는 263줄입니다.
Java·Python 구현 합계는 2,600줄이라 호출 경로 하나를 끝까지 읽기에도 부담이 비교적 작습니다.
src/ghidra_mcp/server.py의 list_functions()부터 시작해
src/ghidra_mcp/backend.py, BackendHttpServer.registerRoutes(),
GhidraAnalysisService.listFunctions(), extension/src/main/java/com/lauriewired/ghidramcp/core/Page.java를 읽어 보세요.
마지막으로 test_list_functions_pagination_and_filter()가 무엇을 보장하려는지 확인하면 됩니다.
src/ghidra_mcp/server.py의 도구 입력에는 str | None 같은 선택형과
Literal["both", "callers", "callees"] 같은 제한된 선택지가 등장합니다.
get_function_calls()의 방향 값은 Java에서도 다시 검사합니다.
외부 입력을 받는 각 경계가 자신의 계약을 검증하는 사례입니다.
학습 실습으로 get_function_xrefs()를 읽고, 이름·주소가 둘 다 없을 때
왜 HTTP 요청 전에 MISSING_PARAMETER를 내는지 설명해 보세요.
이어 BackendError의 코드·설명·HTTP 상태가 어느 계층까지 유지되는지 표로 정리해 보세요.
extension/src/main/java/com/lauriewired/ghidramcp/core/Model.java는 결과를 record로 선언합니다.
FunctionRef는 이름과 주소만, FunctionInfo는 서명·본문 범위 등 자세한 정보를 담습니다.
모든 조회가 무거운 객체 하나를 반환하게 만들지 않은 점을 눈여겨보세요.
// extension/src/main/java/com/lauriewired/ghidramcp/core/Page.java: 요청 범위에 있을 때만 항목을 만듭니다.
public void offer(Supplier<T> item) {
if (total >= offset && items.size() < limit) {
items.add(item.get());
}
total++;
}
Supplier<T>는 “필요할 때 T를 만드는 함수”입니다.
offset 10, limit 2인 목록에 15개 항목을 넣으면 항목 객체는 두 개만 생성하지만 total은 15까지 증가합니다.
종이에 이 흐름을 추적한 뒤 객체 생성 비용과 전체 순회 비용을 따로 설명해 보세요.
Request.required()는 빈 문자열도 거부하지만 present()는 존재 여부만 봅니다.
주석을 지울 때는 comment=""가 유효하기 때문입니다.
누락과 빈 값의 의미가 다를 수 있다는 API 설계의 작은 사례입니다.
| 상황 | 대표 결과 | 확인 위치 |
|---|---|---|
| 프로그램이 없음 | 409 · NO_PROGRAM | dispatch(), McpException.noProgram() |
| 주소 문법 오류 | 400 · BAD_ADDRESS | parseAddress() |
| 함수 이름 중복 | 409 · AMBIGUOUS_NAME | functionByName() |
| 지원하지 않는 메서드 | 405 · METHOD_NOT_ALLOWED | dispatch() |
| 오래된 형식의 응답 | Python BAD_RESPONSE | GhidraBackend.request() |
연습으로 “연결 실패”, “유효한 JSON이지만 실패 봉투”, “JSON이 아닌 응답”을 구분해 보세요.
tests/test_backend.py가 각 경우의 기대 동작을 어떻게 표현하는지 함께 읽으면 좋습니다.
functionByName()는 단순 이름과 네임스페이스가 붙은 이름을 모두 비교합니다.
이름은 수정될 수 있고 중복될 수도 있으므로,
도구 안내문 INSTRUCTIONS는 수정 시 주소 사용을 권합니다.
주소도 다른 바이너리에서는 의미가 달라지므로 get_program_info로 대상을 먼저 확인해야 합니다.
getFunctionCalls()는 재귀적으로 모든 경로를 복구하지 않고 한 단계의 호출 관계를 반환합니다.
callees()는 함수 본문의 명령어를 걷고,
calleeThroughPointer()는 포인터를 통한 호출을 일부 해석합니다.
해석할 수 없는 간접 호출은 목적지가 null일 수 있습니다.
direct=false를 “호출이 없다”로 해석하면 안 됩니다.좋은 분석 도구는 알아낸 사실과 아직 해석하지 못한 빈칸을 구분해서 돌려줍니다.
readMemory()가 읽을 수 없는 영역을 0으로 꾸미지 않고 ??와
gaps로 표현하는 것도 같은 교훈을 줍니다.
이 메모리는 Ghidra에 로드된 Program의 메모리이며, 실행 중인 프로세스의 실시간 메모리를 읽는 기능은 아닙니다.
cli.main()은 로그를 stderr로 보냅니다.
headless.protect_stdout()는 원래 stdout을 복제한 뒤,
파일 디스크립터 1은 stderr로 향하게 하고 Python의 sys.stdout은 복제본을 사용하게 합니다.
이렇게 하면 JVM의 일반 출력이 MCP stdio 메시지에 섞이는 일을 막을 수 있습니다. MCP stdio 규격도 stdout에 유효한 MCP 메시지 외의 내용을 쓰지 않도록 요구합니다. 실습으로 로그와 프로토콜 출력을 같은 채널에 섞었을 때 생기는 문제를 도식화해 보세요.
tests/conftest.py의 FakeBackend는 작은 HTTP 서버입니다.
Python의 요청 인코딩과 오류 변환을 빠르게 검사하지만 Ghidra의 분석 정확도까지 검사하지는 않습니다.
실제 ELF·PE 분석은 tests/test_headless_integration.py가 담당합니다.
테스트 함수 정의는 백엔드 9개, CLI 3개, 서버 6개, 통합 12개입니다. 매개변수화로 실행 사례 수는 더 늘어날 수 있습니다. 통합 검사는 Ghidra 경로나 확장 JAR이 없으면 건너뛰도록 선언되어 있으므로, “테스트 명령이 끝남”과 “실제 Ghidra 검사가 수행됨”을 구분해서 읽어야 합니다.
| 항목 | 요구·권장 사항 | 근거 또는 범위 |
|---|---|---|
| 분석 도구 | Ghidra 12.1.4와 GhidraMCP 확장 | README·CI의 기준 조합 |
| Java | JDK 21 | README의 빌드 안내와 CI 설정 |
| Python | 일반 패키지 3.10+, headless는 README 기준 3.10~3.13 | 패키지 선언에는 상한이 없지만 headless 안내 범위는 더 좁음 |
| 패키지 도구 | uv 또는 Python·pip | README가 두 경로를 안내 |
| 소스 빌드 | GHIDRA_INSTALL_DIR, Gradle | Java 확장 빌드 시 필요, CI Gradle은 8.14 |
| 운영체제 | Ghidra의 Windows·macOS·Linux 환경 | 상위 도구 지원 범위이며 이 저장소 CI는 Ubuntu 하나 |
| 메모리·저장 공간 | 저장소에 자체 최소 수치 명시 없음 | 바이너리 크기·분석 결과·프로젝트 보관량에 따라 달라짐 |
| GPU | 이 저장소의 분석 서버에 GPU 의존성 없음 | 로컬 LLM을 별도로 실행하면 그 모델의 요구사항이 추가됨 |
운영체제 범위는 Ghidra 공식 설명 기준입니다. 모든 운영체제·Python 조합에서 GhidraMCP가 같은 수준으로 검증되었다는 의미는 아닙니다. 작은 연습용 바이너리에는 RAM 8GB 이상, IDE·GUI·AI 클라이언트를 함께 쓰면 16GB 정도를 실습용 출발점으로 권할 수 있습니다. 이 수치는 프로젝트의 공식 최소 사양이나 성능 측정값이 아닙니다.
GUI 모드는 Ghidra에서 프로그램을 열고 플러그인을 켜야 합니다.
포트 옵션은 GhidraMCP HTTP Server의 Server Port이며 기본값은 8080입니다.
GhidraMCPPlugin의 옵션 설명에 따르면 변경 후 재시작 또는 플러그인 재로딩이 필요합니다.
headless 모드는 PyGhidra 선택 의존성과 확장 클래스가 필요합니다.
설치된 확장 대신 --extension-jar나 GHIDRA_MCP_EXTENSION_JAR로 JAR을 지정할 수도 있습니다.
HeadlessSession._start_jvm()은 실제 로드된 클래스의 위치까지 확인해 다른 JAR이 가려지는 문제를 검사합니다.
macOS의 디컴파일러 네이티브 빌드 절차는 README에 별도로 안내되어 있습니다. 플랫폼 준비가 끝났다는 가정 없이, 해당 Ghidra 설치와 네이티브 구성부터 확인하는 편이 좋습니다.
--project가 없으면 임시 프로젝트를 만들고 종료 시 정리합니다.
--project를 주면 변경 저장이 기본값이 되며,
--save·--discard로 종료 시 변경 저장 여부를 지정할 수 있습니다.
임시 프로젝트에서 --save만 켜도 종료 후 그 임시 디렉터리는 삭제되므로 장기 보관에는 명시적 프로젝트 경로가 필요합니다.
_open_program()은 프로젝트 안에서 바이너리의 파일 이름으로 기존 항목을 찾습니다.
같은 이름의 파일 내용을 바꿨다고 자동으로 새 바이너리를 재가져오는 구조는 아닙니다.
다른 파일·버전을 비교할 때는 프로젝트를 분리하고 해시를 확인하는 습관이 필요합니다.
Java 백엔드는 루프백 전용입니다. Python에 URL 옵션이 있다고 Java 서버가 외부 인터페이스에 열리는 것은 아닙니다. AI 클라이언트가 외부 모델을 사용하면 도구 결과가 그 모델 제공자에게 전달될 수 있으며, 이 저장소는 모델 호출이나 데이터 전송 정책을 직접 관리하지 않습니다.
실습 대상은 tests/fixtures/sample.c와 동봉된 x86-64 ELF·PE 파일입니다.
직접 만든 프로그램이나 분석 권한이 있는 파일을 사용하세요.
아래 명령은 준비된 학습 환경에서 따라 할 실행 예시입니다.
목표: 디컴파일 결과를 읽기 전에 원본 동작을 설명할 수 있게 됩니다.
tests/fixtures/sample.c에서 add_numbers, mul_numbers, compute를 찾습니다.compute(2, add_numbers)의 계산을 종이에 추적합니다.SECRET 문자열을 어디서 사용하는지도 연결합니다.완료 기준: 2 → 5 → 10 → 12의 계산을 설명하고,
op(total, x)의 대상이 인자로 달라지는 이유를 말할 수 있습니다.
이 값은 compute 호출의 결과이며 main이 출력하는 전체 계산과 구분합니다.
목표: MCP 도구 입력과 Ghidra 결과의 관계를 익힙니다.
get_program_info로 이름·형식·아키텍처·해시를 기록합니다.list_functions를 filter="compute"로 호출해 얻은 주소로 디컴파일합니다.# 저장소 루트에서 사용하는 Python 서버 실행 예시
uv run ghidra-mcp --ghidra-server http://127.0.0.1:8080/
# URL로 연결하는 클라이언트를 위한 별도 실행 예시
uv run ghidra-mcp --transport streamable-http \
--mcp-host 127.0.0.1 --mcp-port 8081 \
--ghidra-server http://127.0.0.1:8080/
완료 기준: 조회한 함수의 실제 주소와 호출한 도구 이름을 적고,
디컴파일 결과의 function·code·truncated를 설명할 수 있습니다.
첫 명령의 기본 stdio 모드는 MCP 클라이언트가 프로세스를 실행하고 연결하는 방식입니다.
목표: 수정 결과를 다시 조회해 확인하고, 트랜잭션과 저장을 구분합니다.
mul_numbers의 진입 주소와 원래 이름을 기록합니다.rename_function_by_address로 이름을 multiply로 바꿉니다.get_function_by_address로 이름이 바뀌었는지 확인한 뒤 원래 이름으로 되돌립니다.set_disassembly_comment로 주석을 남기고 다시 디스어셈블합니다.null인지 확인합니다.완료 기준: 수정 전·후·복구 후의 이름과 주석을 표로 남깁니다. 함수 주석이라고 생각해 임의 주소를 쓰지 말고, 실제 명령어 위치와 주석 종류도 확인하세요.
목표: 정상 응답뿐 아니라 API의 경계 조건도 이해합니다.
limit=2로 요청하고 has_more가 참이면 offset을 늘립니다.limit=0·3000이 실제 응답에서 어떻게 정규화되는지 확인합니다.read_memory의 length=5000이 왜 거부되는지 소스와 연결합니다.완료 기준: 목록을 중복 없이 모으는 절차와 오류별 다음 행동을 적습니다. 검사 중 프로그램의 함수 집합이 바뀌면 offset 기반 페이지도 달라질 수 있다는 조건을 덧붙입니다.
목표: GUI 없는 분석의 준비 상태와 저장 생명주기를 확인합니다.
# Ghidra 설치·확장을 준비한 뒤 사용하는 예시
# 패키지의 headless 선택 의존성이 필요합니다.
uv run --extra headless ghidra-mcp --headless \
--binary tests/fixtures/sample_x86_64.elf \
--project ./practice-project --ready-timeout 300
--discard를 쓴 실행과 비교합니다.DeferredBackend의 이벤트로 설명합니다.완료 기준: 준비 상태·도구 공개·종료 시 저장을 한 장의 순서도로 설명합니다. 같은 프로젝트를 다른 GUI나 headless 세션에서 동시에 열면 잠금 오류가 날 수 있습니다.
실습의 완성은 그럴듯한 AI 설명을 얻는 일이 아니라, 주소와 조회 결과로 설명을 다시 확인하는 일입니다.
주당 3~5시간을 기준으로 한 학습 계획입니다. Python·Java가 모두 처음이라면 1~2주를 더 사용해 함수, 클래스, 예외, HTTP 기초를 익히세요. 매주 읽을 파일과 만들 결과물을 함께 정하면 학습 범위를 유지하기 쉽습니다.
| 주차 | 학습 주제 · 읽을 코드 | 완료할 결과물 |
|---|---|---|
| 1주 | C 함수·포인터·컴파일 결과의 차이tests/fixtures/sample.c | 직접·간접 호출을 표시한 함수 관계도 |
| 2주 | Python 패키지·CLI·HTTP 예외pyproject.toml, src/ghidra_mcp/cli.py, src/ghidra_mcp/backend.py | CLI 옵션이 HTTP 요청으로 바뀌는 경로 설명 |
| 3주 | MCP 도구·타입·통신 채널src/ghidra_mcp/server.py, protect_stdout() | 도구 하나의 입력·결과·오류 계약표 |
| 4주 | Java 제네릭·레코드·Ghidra APIextension/src/main/java/com/lauriewired/ghidramcp/core/Model.java, extension/src/main/java/com/lauriewired/ghidramcp/core/Page.java, listFunctions() | 페이지 결과와 전체 순회 비용을 설명한 메모 |
| 5주 | 전략·의존성 주입·트랜잭션ProgramProvider, runWrite(), write() | GUI/headless 쓰기 경로 비교도와 실패 복구 설명 |
| 6주 | JVM 생명주기·통합 검사·배포src/ghidra_mcp/headless.py, tests/, .github/workflows/build.yml | 독립된 학습용 사본에서 읽기 전용 도구 하나를 설계한 변경안 |
새 도구 하나를 끝까지 연결해 보면, 언어 문법을 넘어 시스템의 경계를 설계하는 감각을 얻을 수 있습니다.
큰 도시의 모든 도로 이름을 외우기보다 집에서 학교까지 한 경로를 직접 걸어 보세요. 이 저장소에서는 도구 함수에서 Ghidra API까지 이어지는 한 경로가 그 산책로입니다. 다음 도구를 읽을 때는 이미 익숙한 교차로를 다시 만나게 됩니다.
| 용어 | 쉬운 설명 |
|---|---|
| 바이너리 | 컴퓨터가 처리할 명령과 데이터가 담긴 파일입니다. 여기서는 주로 컴파일된 실행 파일을 말합니다. |
| Ghidra | 실행 파일의 명령어·함수·데이터를 분석하고 결과를 관리하는 리버스 엔지니어링 도구 모음입니다. |
| 디스어셈블 | 기계어 바이트를 어셈블리 명령으로 읽는 과정입니다. disassemble_function은 Ghidra가 이미 분석한 함수 본문의 명령어를 조회합니다. |
| 디컴파일 | 기계어 분석 결과를 C와 비슷한 의사 코드로 표현하는 과정입니다. 원본 소스의 완전한 복원은 아닙니다. |
| 의사 코드 | 동작을 이해하기 쉽게 표현한 코드입니다. 이 결과만으로 원래 변수명이나 설계 의도를 확정할 수는 없습니다. |
| MCP 클라이언트 | 서버의 도구 목록을 보고 도구를 호출하는 AI 애플리케이션 쪽입니다. |
| MCP 서버 | 호출 가능한 도구와 결과를 공개하는 쪽입니다. 이 프로젝트에서는 Python의 FastMCP가 맡습니다. |
| JSON-RPC | 메서드 이름과 파라미터, 요청 식별자 등을 JSON 메시지로 주고받는 방식입니다. MCP 통신의 기반입니다. |
| stdio | 프로세스의 표준 입력과 출력으로 메시지를 주고받는 방식입니다. 로그는 별도 stderr로 보냅니다. |
| Streamable HTTP | HTTP를 통해 MCP 메시지를 교환하는 통신 방식입니다. 내부 Java HTTP API와는 구분합니다. |
| headless | 그래픽 화면을 띄우지 않는 실행 방식입니다. 이 저장소에서는 PyGhidra로 프로그램을 열고 분석합니다. |
| JVM / JPype | JVM은 Java 실행 환경이고, JPype는 Python에서 JVM의 Java 클래스에 접근하는 연결 수단입니다. |
| Program | Ghidra가 관리하는 분석 대상과 분석 정보의 중심 객체입니다. 원본 파일 바이트와 분석 기록을 구분해서 봐야 합니다. |
| 어댑터 / 파사드 | 어댑터는 인터페이스를 번역하고, 파사드는 복잡한 내부 기능에 단순한 입구를 제공합니다. |
| 의존성 주입 | 필요한 협력 객체나 동작을 내부에서 고정하지 않고 밖에서 전달하는 설계입니다. provider::runWrite가 예입니다. |
| 트랜잭션 | 묶인 변경을 성공 시 확정하고 실패 시 되돌리는 작업 단위입니다. 디스크 저장과는 별개입니다. |
| Swing 이벤트 스레드 | Java GUI의 이벤트와 화면 관련 작업을 처리하는 스레드입니다. GUI 모드의 수정 작업이 여기로 전달됩니다. |
| 페이지네이션 | 큰 목록을 offset·limit로 나누어 받는 방법입니다. has_more가 다음 페이지 존재 여부를 알려줍니다. |
| 교차 참조 / xref | 어떤 주소를 다른 명령이나 데이터가 참조하는 관계입니다. 호출 외에 데이터 접근도 포함할 수 있습니다. |
| 호출 그래프 | 어느 함수가 어느 함수를 부르는지 그린 관계도입니다. 여기서는 한 단계씩 조회합니다. |
| 네임스페이스 | 이름을 묶는 범위입니다. 같은 함수 이름도 다른 범위에 존재할 수 있습니다. |
| 루프백 | 현재 컴퓨터 안으로 돌아오는 네트워크 주소입니다. 외부 인터페이스에 공개하는 것과 다릅니다. |
| ELF / PE | 각각 Linux 계열과 Windows에서 널리 쓰는 실행 파일 형식입니다. 테스트 샘플이 두 형식으로 들어 있습니다. |
| fixture | 테스트에서 재사용하는 준비물입니다. 여기서는 가짜 서버와 작은 분석용 바이너리가 예입니다. |
| SHA-256 | 파일 내용을 식별하는 데 사용하는 해시입니다. 파일 이름이 같아도 내용이 다른지 확인하는 단서가 됩니다. |
| 잠금 파일 | 의존성의 실제 해결 결과를 기록한 파일입니다. 이 저장소의 Python 환경은 uv.lock에서 살펴봅니다. |
| 항목 | 상태 · 기준 |
|---|---|
| 선정 | GitHub Trending Java Monthly #3 · 2026-10-23 분석 |
| 스타 / 포크 | 11,495 / 1,207 · 2026-10-23 GitHub 기준 |
| 생성일 | 2025-03-23 · GitHub 저장소 생성일 |
| 최근 push | 2026-10-10 · GitHub 기준 |
| 최신 릴리스 | 2.0 · 2026-10-10, Python 패키지 내부 버전은 2.0.0 |
| 최근 커밋 | 1e98025 · 2026-10-10 · Update readme |
| 주 언어 | Java · GitHub 분류 |
| 코드 규모 | Java 1,705줄, Python 1,336줄 중 테스트 441줄, C 48줄 · 공백·주석 포함 |
| 주요 구성 | Java 확장 + Python MCP 서버, GUI·headless 공유 분석 서비스 |
| 도구 선언 | 30개, headless 등록 28개 · 별칭 포함 |
| 테스트 정의 | 테스트 함수 30개 · 매개변수화와 환경별 건너뛰기 있음 |
| 라이선스 | Apache-2.0 · LICENSE 기준 |
① 자동화의 범위. README는 LLM의 자율 리버스 엔지니어링을 목표로 소개하지만, 이 저장소에는 모델 호출·계획 반복·정확도 평가 엔진이 없습니다. 어떤 순서로 도구를 쓰고 결과를 검증할지는 클라이언트와 사용자에게 달려 있습니다.
② 수정 기능과 저장. 이름·주석·프로토타입·타입 변경은 Ghidra의 분석 정보를 바꿉니다. 원본 실행 파일을 패치하거나 대상 프로그램을 실행하는 도구로 소개하면 부정확합니다. 잘못 붙인 이름도 분석을 오도할 수 있으니 근거를 남기고, 보관할 결과는 저장 경로까지 확인하세요.
③ 동시성과 대상 선택. Java 요청은 직렬 처리되고, GUI 모드의 대상은 각 요청 시점의 현재 프로그램입니다. 대화 도중 다른 프로그램으로 바꾸면 다음 호출의 대상도 바뀔 수 있습니다. 여러 호출을 하나의 고정된 분석 세션으로 묶어 주는 보장은 없습니다.
④ 로컬 경계와 정보 전송. 루프백·Host·Origin 검사는 방어 장치지만 Java 내부 API의 사용자 인증은 아닙니다. 외부 인터페이스에 노출하거나 프록시를 붙이면 별도 접근 통제가 필요합니다. 외부 LLM을 쓰는 클라이언트에는 바이너리에서 추출한 문자열·의사 코드가 전달될 수 있습니다.
⑤ 결과와 검증 범위. 디컴파일은 원본 복원이 아니고, 호출 관계는 한 단계이며, 미해석 간접 호출과 메모리 빈 영역이 남을 수 있습니다. 샘플 ELF·PE 검사 코드가 있어도 모든 CPU·대형 파일의 정확도와 성능이 입증되는 것은 아닙니다. 테스트 정의는 기대 동작을 나타내며, 실행 성공이나 성능 측정 결과와는 구분해야 합니다.
⑥ 버전의 경계. 일반 Python 요구는 >=3.10이지만 README 기준 headless의 Python 범위는
3.10~3.13입니다. Ghidra 12.1.4도 README·CI의 기준 조합입니다.
더 새로운 Python·Ghidra와의 호환성을 이 선언만으로 단정하지 마세요.