diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..78e258d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Added docstrings for all @mcp.tool functions +- Variable manipulation tools (rename/retype variables) +- New endpoints for function variable management +- Dynamic version output in API responses +- Enhanced function analysis capabilities +- Support for searching variables by name +- New tools for working with function variables: + - get_function_by_address + - get_current_address + - get_current_function + - decompile_function_by_address + - disassemble_function + - set_decompiler_comment + - set_disassembly_comment + - rename_local_variable + - rename_function_by_address + - set_function_prototype + - set_local_variable_type + +### Changed +- Improved version handling in build system +- Reorganized imports in bridge_mcp_hydra.py +- Updated MANIFEST.MF with more detailed description + +## [1.2.0] - 2024-06-15 + +### Added +- Enhanced function analysis capabilities +- Additional variable manipulation tools +- Support for multiple Ghidra instances + +### Changed +- Improved error handling in API calls +- Optimized performance for large binaries + +## [1.0.0] - 2024-03-15 + +### Added +- Initial release of GhydraMCP bridge +- Basic Ghidra instance management tools +- Function analysis tools +- Variable manipulation tools diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index d59c834..9c5a278 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -6,12 +6,14 @@ # ] # /// import os +import signal import sys -import time -import requests import threading -from typing import Dict +import time from threading import Lock +from typing import Dict + +import requests from mcp.server.fastmcp import FastMCP # Track active Ghidra instances (port -> info dict) @@ -251,6 +253,7 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict: # Updated tool implementations with port parameter from urllib.parse import quote + @mcp.tool() def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: """List all functions with pagination""" @@ -278,30 +281,175 @@ def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: st @mcp.tool() def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all memory segments in the current program with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of segments to return (default: 100) + + Returns: + List of segment information strings + """ return safe_get(port, "segments", {"offset": offset, "limit": limit}) @mcp.tool() def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all imported symbols with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of imports to return (default: 100) + + Returns: + List of import information strings + """ return safe_get(port, "symbols/imports", {"offset": offset, "limit": limit}) @mcp.tool() def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all exported symbols with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of exports to return (default: 100) + + Returns: + List of export information strings + """ return safe_get(port, "symbols/exports", {"offset": offset, "limit": limit}) @mcp.tool() def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all namespaces in the current program with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of namespaces to return (default: 100) + + Returns: + List of namespace information strings + """ return safe_get(port, "namespaces", {"offset": offset, "limit": limit}) @mcp.tool() def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: + """List all defined data items with pagination + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum number of data items to return (default: 100) + + Returns: + List of data item information strings + """ return safe_get(port, "data", {"offset": offset, "limit": limit}) @mcp.tool() def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", offset: int = 0, limit: int = 100) -> list: + """Search for functions by name with pagination + + Args: + port: Ghidra instance port (default: 8192) + query: Search string to match against function names + offset: Pagination offset (default: 0) + limit: Maximum number of functions to return (default: 100) + + Returns: + List of matching function information strings or error message if query is empty + """ if not query: return ["Error: query string is required"] return safe_get(port, "functions", {"query": query, "offset": offset, "limit": limit}) +@mcp.tool() +def get_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str: + """ + Get a function by its address. + """ + return "\n".join(safe_get(port, "get_function_by_address", {"address": address})) + +@mcp.tool() +def get_current_address(port: int = DEFAULT_GHIDRA_PORT) -> str: + """ + Get the address currently selected by the user. + """ + return "\n".join(safe_get(port, "get_current_address")) + +@mcp.tool() +def get_current_function(port: int = DEFAULT_GHIDRA_PORT) -> str: + """ + Get the function currently selected by the user. + """ + return "\n".join(safe_get(port, "get_current_function")) + +@mcp.tool() +def list_functions(port: int = DEFAULT_GHIDRA_PORT) -> list: + """ + List all functions in the database. + """ + return safe_get(port, "list_functions") + +@mcp.tool() +def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str: + """ + Decompile a function at the given address. + """ + return "\n".join(safe_get(port, "decompile_function", {"address": address})) + +@mcp.tool() +def disassemble_function(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> list: + """ + Get assembly code (address: instruction; comment) for a function. + """ + return safe_get(port, "disassemble_function", {"address": address}) + +@mcp.tool() +def set_decompiler_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str: + """ + Set a comment for a given address in the function pseudocode. + """ + return safe_post(port, "set_decompiler_comment", {"address": address, "comment": comment}) + +@mcp.tool() +def set_disassembly_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str: + """ + Set a comment for a given address in the function disassembly. + """ + return safe_post(port, "set_disassembly_comment", {"address": address, "comment": comment}) + +@mcp.tool() +def rename_local_variable(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", old_name: str = "", new_name: str = "") -> str: + """ + Rename a local variable in a function. + """ + return safe_post(port, "rename_local_variable", {"function_address": function_address, "old_name": old_name, "new_name": new_name}) + +@mcp.tool() +def rename_function_by_address(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", new_name: str = "") -> str: + """ + Rename a function by its address. + """ + return safe_post(port, "rename_function_by_address", {"function_address": function_address, "new_name": new_name}) + +@mcp.tool() +def set_function_prototype(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", prototype: str = "") -> str: + """ + Set a function's prototype. + """ + return safe_post(port, "set_function_prototype", {"function_address": function_address, "prototype": prototype}) + +@mcp.tool() +def set_local_variable_type(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", variable_name: str = "", new_type: str = "") -> str: + """ + Set a local variable's type. + """ + return safe_post(port, "set_local_variable_type", {"function_address": function_address, "variable_name": variable_name, "new_type": new_type}) + @mcp.tool() def list_variables(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100, search: str = "") -> list: """List global variables with optional search""" @@ -339,10 +487,6 @@ def retype_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: s encoded_var = quote(name) return safe_put(port, f"functions/{encoded_function}/variables/{encoded_var}", {"dataType": data_type}) -# Handle graceful shutdown -import signal -import os - def handle_sigint(signum, frame): os._exit(0) diff --git a/pom.xml b/pom.xml index 5602599..033978c 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,14 @@ - + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + ghidra Generic @@ -72,13 +79,6 @@ ${ghidra.jar.location}/Base.jar - - - com.googlecode.json-simple - json-simple - 1.1.1 - - junit @@ -265,7 +265,14 @@ false + ghidra:Generic + ghidra:SoftwareModeling + ghidra:Project ghidra:Docking + ghidra:Decompiler + ghidra:Utility + ghidra:Base + junit:junit ghidra:* @@ -318,4 +325,4 @@ - \ No newline at end of file + diff --git a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java index 89a8533..16f91ef 100644 --- a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java +++ b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java @@ -13,6 +13,8 @@ import ghidra.program.model.pcode.HighSymbol; import ghidra.program.model.pcode.VarnodeAST; import ghidra.program.model.pcode.HighFunction; import ghidra.program.model.pcode.HighFunctionDBUtil; +import ghidra.program.model.pcode.LocalSymbolMap; +import ghidra.program.model.pcode.HighFunctionDBUtil.ReturnCommitOption; import ghidra.program.model.symbol.*; import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompileResults; @@ -46,6 +48,23 @@ import java.util.concurrent.atomic.*; // For JSON response handling import org.json.simple.JSONObject; +import ghidra.app.services.CodeViewerService; +import ghidra.app.util.PseudoDisassembler; +import ghidra.app.cmd.function.SetVariableNameCmd; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.model.listing.LocalVariableImpl; +import ghidra.program.model.listing.ParameterImpl; +import ghidra.util.exception.DuplicateNameException; +import ghidra.util.exception.InvalidInputException; +import ghidra.program.util.ProgramLocation; +import ghidra.util.task.TaskMonitor; +import ghidra.program.model.pcode.Varnode; +import ghidra.program.model.data.PointerDataType; +import ghidra.program.model.data.Undefined1DataType; +import ghidra.program.model.listing.Variable; +import ghidra.app.decompiler.component.DecompilerUtils; +import ghidra.app.decompiler.ClangToken; + @PluginInfo( status = PluginStatus.RELEASED, packageName = ghidra.app.DeveloperPluginPackage.NAME, @@ -421,7 +440,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { List names = new ArrayList<>(); for (Function f : program.getFunctionManager().getFunctions(true)) { - names.add(f.getName()); + names.add(f.getName() + " @ " + f.getEntryPoint()); } return paginateList(names, offset, limit); } @@ -723,75 +742,128 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { } private String renameVariable(String functionName, String oldName, String newName) { - if (oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty()) { - return "Both old and new variable names are required"; - } - Program program = getCurrentProgram(); if (program == null) return "No program loaded"; - - AtomicReference result = new AtomicReference<>("Variable rename failed"); - + + DecompInterface decomp = new DecompInterface(); + decomp.openProgram(program); + + Function func = null; + for (Function f : program.getFunctionManager().getFunctions(true)) { + if (f.getName().equals(functionName)) { + func = f; + break; + } + } + + if (func == null) { + return "Function not found"; + } + + DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); + if (result == null || !result.decompileCompleted()) { + return "Decompilation failed"; + } + + HighFunction highFunction = result.getHighFunction(); + if (highFunction == null) { + return "Decompilation failed (no high function)"; + } + + LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap(); + if (localSymbolMap == null) { + return "Decompilation failed (no local symbol map)"; + } + + HighSymbol highSymbol = null; + Iterator symbols = localSymbolMap.getSymbols(); + while (symbols.hasNext()) { + HighSymbol symbol = symbols.next(); + String symbolName = symbol.getName(); + + if (symbolName.equals(oldName)) { + highSymbol = symbol; + } + if (symbolName.equals(newName)) { + return "Error: A variable with name '" + newName + "' already exists in this function"; + } + } + + if (highSymbol == null) { + return "Variable not found"; + } + + boolean commitRequired = checkFullCommit(highSymbol, highFunction); + + final HighSymbol finalHighSymbol = highSymbol; + final Function finalFunction = func; + AtomicBoolean successFlag = new AtomicBoolean(false); + try { - SwingUtilities.invokeAndWait(() -> { - int tx = program.startTransaction("Rename variable via HTTP"); + SwingUtilities.invokeAndWait(() -> { + int tx = program.startTransaction("Rename variable"); try { - Function function = findFunctionByName(program, functionName); - if (function == null) { - result.set("Function not found: " + functionName); - return; + if (commitRequired) { + HighFunctionDBUtil.commitParamsToDatabase(highFunction, false, + ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource()); } - - // Initialize decompiler - DecompInterface decomp = new DecompInterface(); - decomp.openProgram(program); - DecompileResults decompRes = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor()); - - if (decompRes == null || !decompRes.decompileCompleted()) { - result.set("Failed to decompile function: " + functionName); - return; - } - - HighFunction highFunction = decompRes.getHighFunction(); - if (highFunction == null) { - result.set("Failed to get high function"); - return; - } - - // Find the variable by name - HighSymbol targetSymbol = null; - Iterator symbolIter = highFunction.getLocalSymbolMap().getSymbols(); - while (symbolIter.hasNext()) { - HighSymbol symbol = symbolIter.next(); - if (symbol.getName().equals(oldName)) { - targetSymbol = symbol; - break; - } - } - - if (targetSymbol == null) { - result.set("Variable not found: " + oldName); - return; - } - - // Rename the variable - HighFunctionDBUtil.updateDBVariable(targetSymbol, newName, targetSymbol.getDataType(), - SourceType.USER_DEFINED); - - result.set("Variable renamed from '" + oldName + "' to '" + newName + "'"); - } catch (Exception e) { - Msg.error(this, "Error renaming variable", e); - result.set("Error: " + e.getMessage()); - } finally { + HighFunctionDBUtil.updateDBVariable( + finalHighSymbol, + newName, + null, + SourceType.USER_DEFINED + ); + successFlag.set(true); + } + catch (Exception e) { + Msg.error(this, "Failed to rename variable", e); + } + finally { program.endTransaction(tx, true); } }); } catch (InterruptedException | InvocationTargetException e) { - Msg.error(this, "Failed to execute on Swing thread", e); - result.set("Error: " + e.getMessage()); + String errorMsg = "Failed to execute rename on Swing thread: " + e.getMessage(); + Msg.error(this, errorMsg, e); + return errorMsg; } - - return result.get(); + return successFlag.get() ? "Variable renamed" : "Failed to rename variable"; + } + + /** + * Copied from AbstractDecompilerAction.checkFullCommit, it's protected. + * Compare the given HighFunction's idea of the prototype with the Function's idea. + * Return true if there is a difference. If a specific symbol is being changed, + * it can be passed in to check whether or not the prototype is being affected. + * @param highSymbol (if not null) is the symbol being modified + * @param hfunction is the given HighFunction + * @return true if there is a difference (and a full commit is required) + */ + protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) { + if (highSymbol != null && !highSymbol.isParameter()) { + return false; + } + Function function = hfunction.getFunction(); + Parameter[] parameters = function.getParameters(); + LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap(); + int numParams = localSymbolMap.getNumParams(); + if (numParams != parameters.length) { + return true; + } + + for (int i = 0; i < numParams; i++) { + HighSymbol param = localSymbolMap.getParamSymbol(i); + if (param.getCategoryIndex() != i) { + return true; + } + VariableStorage storage = param.getStorage(); + // Don't compare using the equals method so that DynamicVariableStorage can match + if (0 != storage.compareTo(parameters[i].getVariableStorage())) { + return true; + } + } + + return false; } private String retypeVariable(String functionName, String varName, String dataTypeName) { @@ -830,12 +902,13 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { return; } - // Find the variable by name + // Find the variable by name - must match exactly and be in current scope HighSymbol targetSymbol = null; Iterator symbolIter = highFunction.getLocalSymbolMap().getSymbols(); while (symbolIter.hasNext()) { HighSymbol symbol = symbolIter.next(); - if (symbol.getName().equals(varName)) { + if (symbol.getName().equals(varName) && + symbol.getPCAddress().equals(function.getEntryPoint())) { targetSymbol = symbol; break; } @@ -1151,4 +1224,4 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { activeInstances.remove(port); super.dispose(); } -} \ No newline at end of file +} diff --git a/src/main/resources/Module.manifest b/src/main/resources/Module.manifest index 00f5a85..8be1e50 100644 --- a/src/main/resources/Module.manifest +++ b/src/main/resources/Module.manifest @@ -1,2 +1,3 @@ -GHIDRA_MODULE_NAME=GhydraMCP -GHIDRA_MODULE_DESC=A multi-headed REST interface for Ghidra for use with MCP agents. +Manifest-Version: 1.0 +GHIDRA_MODULE_NAME: GhydraMCP +GHIDRA_MODULE_DESC: A multi-headed REST interface for Ghidra for use with MCP agents.