diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..868268ac --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: Build with Maven + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + env: + GHIDRA_VERSION: 11.3.2 + GHIDRA_DATE: 20250415 + GHIDRA_LIBS: >- + Features/Base/lib/Base.jar + Features/Decompiler/lib/Decompiler.jar + Framework/Docking/lib/Docking.jar + Framework/Generic/lib/Generic.jar + Framework/Project/lib/Project.jar + Framework/SoftwareModeling/lib/SoftwareModeling.jar + Framework/Utility/lib/Utility.jar + Framework/Gui/lib/Gui.jar + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: maven + + - name: Download Ghidra + run: | + wget --no-verbose -O ghidra.zip https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${{ env.GHIDRA_VERSION }}_build/ghidra_${{ env.GHIDRA_VERSION }}_PUBLIC_${{ env.GHIDRA_DATE }}.zip + 7z x -bd ghidra.zip + + - name: Copy Ghidra libs + run: | + mkdir -p ./lib + for libfile in ${{ env.GHIDRA_LIBS }} + do echo "Copying ${libfile} to lib/" + cp ghidra_${{ env.GHIDRA_VERSION }}_PUBLIC/Ghidra/${libfile} ./lib/ + done + + - name: Build with Maven + run: mvn clean package assembly:single + + - name: Assemble release directory + run: | + mkdir release + cp target/GhidraMCP-*-SNAPSHOT.zip release/ + cp bridge_mcp_ghidra.py release/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: GhidraMCP-artifact + path: | + release/* diff --git a/.gitignore b/.gitignore index 0701e633..2bdf632f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ mvnw.cmd hs_err_pid* replay_pid* +# Third party JAR files from Ghidra +lib/*.jar + diff --git a/README.md b/README.md index 08890f99..b5b92bc0 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ https://github.com/user-attachments/assets/75f0c176-6da1-48dc-ad96-c182eb4648c3 ## MCP Clients -Theoretically, any MCP client should work with ghidraMCP. Two examples are given below. +Theoretically, any MCP client should work with ghidraMCP. Three examples are given below. ## Example 1: Claude Desktop To set up Claude Desktop as a Ghidra MCP client, go to `Claude` -> `Settings` -> `Developer` -> `Edit Config` -> `claude_desktop_config.json` and add the following: @@ -99,7 +99,16 @@ Another MCP client that supports multiple models on the backend is [5ire](https: 3. Command: `python /ABSOLUTE_PATH_TO/bridge_mcp_ghidra.py` # Building from Source -Build with Maven by running: +1. Copy the following files from your Ghidra directory to this project's `lib/` directory: +- `Ghidra/Features/Base/lib/Base.jar` +- `Ghidra/Features/Decompiler/lib/Decompiler.jar` +- `Ghidra/Framework/Docking/lib/Docking.jar` +- `Ghidra/Framework/Generic/lib/Generic.jar` +- `Ghidra/Framework/Project/lib/Project.jar` +- `Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar` +- `Ghidra/Framework/Utility/lib/Utility.jar` +- `Ghidra/Framework/Gui/lib/Gui.jar` +2. Build with Maven by running: `mvn clean package assembly:single` diff --git a/bridge_mcp_ghidra.py b/bridge_mcp_ghidra.py index e171a7ad..c492562d 100644 --- a/bridge_mcp_ghidra.py +++ b/bridge_mcp_ghidra.py @@ -10,6 +10,7 @@ import requests import argparse import logging +from urllib.parse import urljoin from mcp.server.fastmcp import FastMCP @@ -29,7 +30,7 @@ def safe_get(endpoint: str, params: dict = None) -> list: if params is None: params = {} - url = f"{ghidra_server_url}/{endpoint}" + url = urljoin(ghidra_server_url, endpoint) try: response = requests.get(url, params=params, timeout=5) @@ -43,10 +44,11 @@ def safe_get(endpoint: str, params: dict = None) -> list: def safe_post(endpoint: str, data: dict | str) -> str: try: + url = urljoin(ghidra_server_url, endpoint) if isinstance(data, dict): - response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data, timeout=5) + response = requests.post(url, data=data, timeout=5) else: - response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data.encode("utf-8"), timeout=5) + response = requests.post(url, data=data.encode("utf-8"), timeout=5) response.encoding = 'utf-8' if response.ok: return response.text.strip() @@ -222,6 +224,69 @@ def set_local_variable_type(function_address: str, variable_name: str, new_type: """ return safe_post("set_local_variable_type", {"function_address": function_address, "variable_name": variable_name, "new_type": new_type}) +@mcp.tool() +def get_xrefs_to(address: str, offset: int = 0, limit: int = 100) -> list: + """ + Get all references to the specified address (xref to). + + Args: + address: Target address in hex format (e.g. "0x1400010a0") + offset: Pagination offset (default: 0) + limit: Maximum number of references to return (default: 100) + + Returns: + List of references to the specified address + """ + return safe_get("xrefs_to", {"address": address, "offset": offset, "limit": limit}) + +@mcp.tool() +def get_xrefs_from(address: str, offset: int = 0, limit: int = 100) -> list: + """ + Get all references from the specified address (xref from). + + Args: + address: Source address in hex format (e.g. "0x1400010a0") + offset: Pagination offset (default: 0) + limit: Maximum number of references to return (default: 100) + + Returns: + List of references from the specified address + """ + return safe_get("xrefs_from", {"address": address, "offset": offset, "limit": limit}) + +@mcp.tool() +def get_function_xrefs(name: str, offset: int = 0, limit: int = 100) -> list: + """ + Get all references to the specified function by name. + + Args: + name: Function name to search for + offset: Pagination offset (default: 0) + limit: Maximum number of references to return (default: 100) + + Returns: + List of references to the specified function + """ + return safe_get("function_xrefs", {"name": name, "offset": offset, "limit": limit}) + +@mcp.tool() +def list_strings(offset: int = 0, limit: int = 2000, filter: str = None) -> list: + """ + List all defined strings in the program with their addresses. + + Args: + offset: Pagination offset (default: 0) + limit: Maximum number of strings to return (default: 2000) + filter: Optional filter to match within string content + + Returns: + List of strings with their addresses + """ + params = {"offset": offset, "limit": limit} + if filter: + params["filter"] = filter + return safe_get("strings", params) + def main(): parser = argparse.ArgumentParser(description="MCP server for Ghidra") parser.add_argument("--ghidra-server", type=str, default=DEFAULT_GHIDRA_SERVER, @@ -234,6 +299,8 @@ def main(): help="Transport protocol for MCP, default: stdio") args = parser.parse_args() + # Use the global variable to ensure it's properly updated + global ghidra_server_url if args.ghidra_server: ghidra_server_url = args.ghidra_server diff --git a/lib/.gitignore b/lib/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/lib/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/pom.xml b/pom.xml index aa7c31dc..6452d96e 100644 --- a/pom.xml +++ b/pom.xml @@ -15,56 +15,56 @@ ghidra Generic - 11.3.1 + 11.3.2 system ${project.basedir}/lib/Generic.jar ghidra SoftwareModeling - 11.3.1 + 11.3.2 system ${project.basedir}/lib/SoftwareModeling.jar ghidra Project - 11.3.1 + 11.3.2 system ${project.basedir}/lib/Project.jar ghidra Docking - 11.3.1 + 11.3.2 system ${project.basedir}/lib/Docking.jar ghidra Decompiler - 11.3.1 + 11.3.2 system ${project.basedir}/lib/Decompiler.jar ghidra Utility - 11.3.1 + 11.3.2 system ${project.basedir}/lib/Utility.jar ghidra Base - 11.3.1 + 11.3.2 system ${project.basedir}/lib/Base.jar ghidra Gui - 11.3.1 + 11.3.2 system ${project.basedir}/lib/Gui.jar diff --git a/src/main/java/com/lauriewired/GhidraMCPPlugin.java b/src/main/java/com/lauriewired/GhidraMCPPlugin.java index 0716c2e8..88583bab 100644 --- a/src/main/java/com/lauriewired/GhidraMCPPlugin.java +++ b/src/main/java/com/lauriewired/GhidraMCPPlugin.java @@ -7,6 +7,10 @@ import ghidra.program.model.listing.*; import ghidra.program.model.mem.MemoryBlock; import ghidra.program.model.symbol.*; +import ghidra.program.model.symbol.ReferenceManager; +import ghidra.program.model.symbol.Reference; +import ghidra.program.model.symbol.ReferenceIterator; +import ghidra.program.model.symbol.RefType; import ghidra.program.model.pcode.HighFunction; import ghidra.program.model.pcode.HighSymbol; import ghidra.program.model.pcode.LocalSymbolMap; @@ -305,6 +309,38 @@ private void startServer() throws IOException { sendResponse(exchange, responseMsg.toString()); }); + server.createContext("/xrefs_to", exchange -> { + Map qparams = parseQueryParams(exchange); + String address = qparams.get("address"); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, getXrefsTo(address, offset, limit)); + }); + + server.createContext("/xrefs_from", exchange -> { + Map qparams = parseQueryParams(exchange); + String address = qparams.get("address"); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, getXrefsFrom(address, offset, limit)); + }); + + server.createContext("/function_xrefs", exchange -> { + Map qparams = parseQueryParams(exchange); + String name = qparams.get("name"); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, getFunctionXrefs(name, offset, limit)); + }); + + server.createContext("/strings", exchange -> { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + String filter = qparams.get("filter"); + sendResponse(exchange, listDefinedStrings(offset, limit, filter)); + }); + server.setExecutor(null); new Thread(() -> { try { @@ -494,7 +530,7 @@ private boolean renameFunction(String oldName, String newName) { Msg.error(this, "Error renaming function", e); } finally { - program.endTransaction(tx, successFlag.get()); + successFlag.set(program.endTransaction(tx, successFlag.get())); } }); } @@ -616,7 +652,7 @@ private String renameVariableInFunction(String functionName, String oldVarName, Msg.error(this, "Failed to rename variable", e); } finally { - program.endTransaction(tx, true); + successFlag.set(program.endTransaction(tx, true)); } }); } catch (InterruptedException | InvocationTargetException e) { @@ -839,7 +875,7 @@ private boolean setCommentAtAddress(String addressStr, String comment, int comme } catch (Exception e) { Msg.error(this, "Error setting " + transactionName.toLowerCase(), e); } finally { - program.endTransaction(tx, success.get()); + success.set(program.endTransaction(tx, success.get())); } }); } catch (InterruptedException | InvocationTargetException e) { @@ -1206,6 +1242,177 @@ private void updateVariableType(Program program, HighSymbol symbol, DataType dat } } + /** + * Get all references to a specific address (xref to) + */ + private String getXrefsTo(String addressStr, int offset, int limit) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + if (addressStr == null || addressStr.isEmpty()) return "Address is required"; + + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + ReferenceManager refManager = program.getReferenceManager(); + + ReferenceIterator refIter = refManager.getReferencesTo(addr); + + List refs = new ArrayList<>(); + while (refIter.hasNext()) { + Reference ref = refIter.next(); + Address fromAddr = ref.getFromAddress(); + RefType refType = ref.getReferenceType(); + + Function fromFunc = program.getFunctionManager().getFunctionContaining(fromAddr); + String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : ""; + + refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName())); + } + + return paginateList(refs, offset, limit); + } catch (Exception e) { + return "Error getting references to address: " + e.getMessage(); + } + } + + /** + * Get all references from a specific address (xref from) + */ + private String getXrefsFrom(String addressStr, int offset, int limit) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + if (addressStr == null || addressStr.isEmpty()) return "Address is required"; + + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + ReferenceManager refManager = program.getReferenceManager(); + + Reference[] references = refManager.getReferencesFrom(addr); + + List refs = new ArrayList<>(); + for (Reference ref : references) { + Address toAddr = ref.getToAddress(); + RefType refType = ref.getReferenceType(); + + String targetInfo = ""; + Function toFunc = program.getFunctionManager().getFunctionAt(toAddr); + if (toFunc != null) { + targetInfo = " to function " + toFunc.getName(); + } else { + Data data = program.getListing().getDataAt(toAddr); + if (data != null) { + targetInfo = " to data " + (data.getLabel() != null ? data.getLabel() : data.getPathName()); + } + } + + refs.add(String.format("To %s%s [%s]", toAddr, targetInfo, refType.getName())); + } + + return paginateList(refs, offset, limit); + } catch (Exception e) { + return "Error getting references from address: " + e.getMessage(); + } + } + + /** + * Get all references to a specific function by name + */ + private String getFunctionXrefs(String functionName, int offset, int limit) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + if (functionName == null || functionName.isEmpty()) return "Function name is required"; + + try { + List refs = new ArrayList<>(); + FunctionManager funcManager = program.getFunctionManager(); + for (Function function : funcManager.getFunctions(true)) { + if (function.getName().equals(functionName)) { + Address entryPoint = function.getEntryPoint(); + ReferenceIterator refIter = program.getReferenceManager().getReferencesTo(entryPoint); + + while (refIter.hasNext()) { + Reference ref = refIter.next(); + Address fromAddr = ref.getFromAddress(); + RefType refType = ref.getReferenceType(); + + Function fromFunc = funcManager.getFunctionContaining(fromAddr); + String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : ""; + + refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName())); + } + } + } + + if (refs.isEmpty()) { + return "No references found to function: " + functionName; + } + + return paginateList(refs, offset, limit); + } catch (Exception e) { + return "Error getting function references: " + e.getMessage(); + } + } + +/** + * List all defined strings in the program with their addresses + */ + private String listDefinedStrings(int offset, int limit, String filter) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + + List lines = new ArrayList<>(); + DataIterator dataIt = program.getListing().getDefinedData(true); + + while (dataIt.hasNext()) { + Data data = dataIt.next(); + + if (data != null && isStringData(data)) { + String value = data.getValue() != null ? data.getValue().toString() : ""; + + if (filter == null || value.toLowerCase().contains(filter.toLowerCase())) { + String escapedValue = escapeString(value); + lines.add(String.format("%s: \"%s\"", data.getAddress(), escapedValue)); + } + } + } + + return paginateList(lines, offset, limit); + } + + /** + * Check if the given data is a string type + */ + private boolean isStringData(Data data) { + if (data == null) return false; + + DataType dt = data.getDataType(); + String typeName = dt.getName().toLowerCase(); + return typeName.contains("string") || typeName.contains("char") || typeName.equals("unicode"); + } + + /** + * Escape special characters in a string for display + */ + private String escapeString(String input) { + if (input == null) return ""; + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (c >= 32 && c < 127) { + sb.append(c); + } else if (c == '\n') { + sb.append("\\n"); + } else if (c == '\r') { + sb.append("\\r"); + } else if (c == '\t') { + sb.append("\\t"); + } else { + sb.append(String.format("\\x%02x", (int)c & 0xFF)); + } + } + return sb.toString(); + } + /** * Resolves a data type by name, handling common types and pointer types * @param dtm The data type manager diff --git a/src/main/resources/extension.properties b/src/main/resources/extension.properties index b0c5ea9b..3ca8018c 100644 --- a/src/main/resources/extension.properties +++ b/src/main/resources/extension.properties @@ -2,5 +2,5 @@ name=GhidraMCP description=A plugin that runs an embedded HTTP server to expose program data. author=LaurieWired createdOn=2025-03-22 -version=11.3.1 -ghidraVersion=11.3.1 \ No newline at end of file +version=11.3.2 +ghidraVersion=11.3.2 \ No newline at end of file