Expand functionality

Find variables, rename and retype them
Additionally merge changes from https://github.com/LaurieWired/GhidraMCP/pull/16 and https://github.com/LaurieWired/GhidraMCP/pull/18
This commit is contained in:
Teal Bauer 2025-04-02 18:50:59 +02:00
parent 1bfdf74554
commit 399c76b29a
5 changed files with 358 additions and 81 deletions

52
CHANGELOG.md Normal file
View File

@ -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

View File

@ -6,12 +6,14 @@
# ] # ]
# /// # ///
import os import os
import signal
import sys import sys
import time
import requests
import threading import threading
from typing import Dict import time
from threading import Lock from threading import Lock
from typing import Dict
import requests
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
# Track active Ghidra instances (port -> info dict) # 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 # Updated tool implementations with port parameter
from urllib.parse import quote from urllib.parse import quote
@mcp.tool() @mcp.tool()
def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
"""List all functions with pagination""" """List all functions with pagination"""
@ -278,30 +281,175 @@ def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: st
@mcp.tool() @mcp.tool()
def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "segments", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "symbols/imports", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "symbols/exports", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "namespaces", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list: 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}) return safe_get(port, "data", {"offset": offset, "limit": limit})
@mcp.tool() @mcp.tool()
def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", offset: int = 0, limit: int = 100) -> list: 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: if not query:
return ["Error: query string is required"] return ["Error: query string is required"]
return safe_get(port, "functions", {"query": query, "offset": offset, "limit": limit}) 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() @mcp.tool()
def list_variables(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100, search: str = "") -> list: def list_variables(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100, search: str = "") -> list:
"""List global variables with optional search""" """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) encoded_var = quote(name)
return safe_put(port, f"functions/{encoded_function}/variables/{encoded_var}", {"dataType": data_type}) 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): def handle_sigint(signum, frame):
os._exit(0) os._exit(0)

23
pom.xml
View File

@ -21,7 +21,14 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- Ghidra JARs as system-scoped dependencies for runtime --> <!-- JSON handling -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<!-- Ghidra JARs as system-scoped dependencies -->
<dependency> <dependency>
<groupId>ghidra</groupId> <groupId>ghidra</groupId>
<artifactId>Generic</artifactId> <artifactId>Generic</artifactId>
@ -72,13 +79,6 @@
<systemPath>${ghidra.jar.location}/Base.jar</systemPath> <systemPath>${ghidra.jar.location}/Base.jar</systemPath>
</dependency> </dependency>
<!-- JSON Simple for JSON handling -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<!-- Test dependencies --> <!-- Test dependencies -->
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
@ -265,7 +265,14 @@
<configuration> <configuration>
<failOnWarning>false</failOnWarning> <failOnWarning>false</failOnWarning>
<ignoredUnusedDeclaredDependencies> <ignoredUnusedDeclaredDependencies>
<ignoredUnusedDeclaredDependency>ghidra:Generic</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:SoftwareModeling</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Project</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Docking</ignoredUnusedDeclaredDependency> <ignoredUnusedDeclaredDependency>ghidra:Docking</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Decompiler</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Utility</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>ghidra:Base</ignoredUnusedDeclaredDependency>
<ignoredUnusedDeclaredDependency>junit:junit</ignoredUnusedDeclaredDependency>
</ignoredUnusedDeclaredDependencies> </ignoredUnusedDeclaredDependencies>
<ignoredSystemDependencies> <ignoredSystemDependencies>
<ignoredSystemDependency>ghidra:*</ignoredSystemDependency> <ignoredSystemDependency>ghidra:*</ignoredSystemDependency>

View File

@ -13,6 +13,8 @@ import ghidra.program.model.pcode.HighSymbol;
import ghidra.program.model.pcode.VarnodeAST; import ghidra.program.model.pcode.VarnodeAST;
import ghidra.program.model.pcode.HighFunction; import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.HighFunctionDBUtil; 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.program.model.symbol.*;
import ghidra.app.decompiler.DecompInterface; import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults; import ghidra.app.decompiler.DecompileResults;
@ -46,6 +48,23 @@ import java.util.concurrent.atomic.*;
// For JSON response handling // For JSON response handling
import org.json.simple.JSONObject; 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( @PluginInfo(
status = PluginStatus.RELEASED, status = PluginStatus.RELEASED,
packageName = ghidra.app.DeveloperPluginPackage.NAME, packageName = ghidra.app.DeveloperPluginPackage.NAME,
@ -421,7 +440,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
List<String> names = new ArrayList<>(); List<String> names = new ArrayList<>();
for (Function f : program.getFunctionManager().getFunctions(true)) { for (Function f : program.getFunctionManager().getFunctions(true)) {
names.add(f.getName()); names.add(f.getName() + " @ " + f.getEntryPoint());
} }
return paginateList(names, offset, limit); 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) { 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(); Program program = getCurrentProgram();
if (program == null) return "No program loaded"; if (program == null) return "No program loaded";
AtomicReference<String> result = new AtomicReference<>("Variable rename failed");
try {
SwingUtilities.invokeAndWait(() -> {
int tx = program.startTransaction("Rename variable via HTTP");
try {
Function function = findFunctionByName(program, functionName);
if (function == null) {
result.set("Function not found: " + functionName);
return;
}
// Initialize decompiler
DecompInterface decomp = new DecompInterface(); DecompInterface decomp = new DecompInterface();
decomp.openProgram(program); decomp.openProgram(program);
DecompileResults decompRes = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
if (decompRes == null || !decompRes.decompileCompleted()) { Function func = null;
result.set("Failed to decompile function: " + functionName); for (Function f : program.getFunctionManager().getFunctions(true)) {
return; if (f.getName().equals(functionName)) {
} func = f;
HighFunction highFunction = decompRes.getHighFunction();
if (highFunction == null) {
result.set("Failed to get high function");
return;
}
// Find the variable by name
HighSymbol targetSymbol = null;
Iterator<HighSymbol> symbolIter = highFunction.getLocalSymbolMap().getSymbols();
while (symbolIter.hasNext()) {
HighSymbol symbol = symbolIter.next();
if (symbol.getName().equals(oldName)) {
targetSymbol = symbol;
break; break;
} }
} }
if (targetSymbol == null) { if (func == null) {
result.set("Variable not found: " + oldName); return "Function not found";
return;
} }
// Rename the variable DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
HighFunctionDBUtil.updateDBVariable(targetSymbol, newName, targetSymbol.getDataType(), if (result == null || !result.decompileCompleted()) {
SourceType.USER_DEFINED); return "Decompilation failed";
}
result.set("Variable renamed from '" + oldName + "' to '" + newName + "'"); HighFunction highFunction = result.getHighFunction();
} catch (Exception e) { if (highFunction == null) {
Msg.error(this, "Error renaming variable", e); return "Decompilation failed (no high function)";
result.set("Error: " + e.getMessage()); }
} finally {
LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap();
if (localSymbolMap == null) {
return "Decompilation failed (no local symbol map)";
}
HighSymbol highSymbol = null;
Iterator<HighSymbol> 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");
try {
if (commitRequired) {
HighFunctionDBUtil.commitParamsToDatabase(highFunction, false,
ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource());
}
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); program.endTransaction(tx, true);
} }
}); });
} catch (InterruptedException | InvocationTargetException e) { } catch (InterruptedException | InvocationTargetException e) {
Msg.error(this, "Failed to execute on Swing thread", e); String errorMsg = "Failed to execute rename on Swing thread: " + e.getMessage();
result.set("Error: " + e.getMessage()); Msg.error(this, errorMsg, e);
return errorMsg;
}
return successFlag.get() ? "Variable renamed" : "Failed to rename variable";
} }
return result.get(); /**
* 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) { private String retypeVariable(String functionName, String varName, String dataTypeName) {
@ -830,12 +902,13 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
return; return;
} }
// Find the variable by name // Find the variable by name - must match exactly and be in current scope
HighSymbol targetSymbol = null; HighSymbol targetSymbol = null;
Iterator<HighSymbol> symbolIter = highFunction.getLocalSymbolMap().getSymbols(); Iterator<HighSymbol> symbolIter = highFunction.getLocalSymbolMap().getSymbols();
while (symbolIter.hasNext()) { while (symbolIter.hasNext()) {
HighSymbol symbol = symbolIter.next(); HighSymbol symbol = symbolIter.next();
if (symbol.getName().equals(varName)) { if (symbol.getName().equals(varName) &&
symbol.getPCAddress().equals(function.getEntryPoint())) {
targetSymbol = symbol; targetSymbol = symbol;
break; break;
} }

View File

@ -1,2 +1,3 @@
GHIDRA_MODULE_NAME=GhydraMCP Manifest-Version: 1.0
GHIDRA_MODULE_DESC=A multi-headed REST interface for Ghidra for use with MCP agents. GHIDRA_MODULE_NAME: GhydraMCP
GHIDRA_MODULE_DESC: A multi-headed REST interface for Ghidra for use with MCP agents.