Fix bugs in renaming functions and listing data
This commit is contained in:
parent
30ec90e650
commit
bc3579d475
@ -5,63 +5,121 @@ ghidra_server_url = "http://localhost:8080"
|
|||||||
|
|
||||||
mcp = FastMCP("ghidra-mcp")
|
mcp = FastMCP("ghidra-mcp")
|
||||||
|
|
||||||
@mcp.tool()
|
def safe_get(endpoint: str, params: dict = None) -> list:
|
||||||
def list_methods() -> list:
|
"""
|
||||||
response = requests.get(f"{ghidra_server_url}/methods")
|
Perform a GET request. If 'params' is given, we convert it to a query string.
|
||||||
return response.text.splitlines() if response.ok else []
|
"""
|
||||||
|
if params is None:
|
||||||
|
params = {}
|
||||||
|
qs = [f"{k}={v}" for k, v in params.items()]
|
||||||
|
query_string = "&".join(qs)
|
||||||
|
url = f"{ghidra_server_url}/{endpoint}"
|
||||||
|
if query_string:
|
||||||
|
url += "?" + query_string
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
response.encoding = 'utf-8'
|
||||||
|
if response.ok:
|
||||||
|
return response.text.splitlines()
|
||||||
|
else:
|
||||||
|
return [f"Error {response.status_code}: {response.text.strip()}"]
|
||||||
|
except Exception as e:
|
||||||
|
return [f"Request failed: {str(e)}"]
|
||||||
|
|
||||||
|
def safe_post(endpoint: str, data: dict | str) -> str:
|
||||||
|
try:
|
||||||
|
if isinstance(data, dict):
|
||||||
|
response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data, timeout=5)
|
||||||
|
else:
|
||||||
|
response = requests.post(f"{ghidra_server_url}/{endpoint}", data=data.encode("utf-8"), timeout=5)
|
||||||
|
response.encoding = 'utf-8'
|
||||||
|
if response.ok:
|
||||||
|
return response.text.strip()
|
||||||
|
else:
|
||||||
|
return f"Error {response.status_code}: {response.text.strip()}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"Request failed: {str(e)}"
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def rename_method(method_address: str, new_name: str) -> str:
|
def list_methods(offset: int = 0, limit: int = 100) -> list:
|
||||||
payload = {"method_address": method_address, "new_name": new_name}
|
"""
|
||||||
response = requests.post(f"{ghidra_server_url}/rename", data=payload)
|
List all function names in the program with pagination.
|
||||||
return response.text if response.ok else "Failed to rename method"
|
"""
|
||||||
|
return safe_get("methods", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_classes() -> list:
|
def list_classes(offset: int = 0, limit: int = 100) -> list:
|
||||||
response = requests.get(f"{ghidra_server_url}/classes")
|
"""
|
||||||
return response.text.splitlines() if response.ok else []
|
List all namespace/class names in the program with pagination.
|
||||||
|
"""
|
||||||
|
return safe_get("classes", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def decompile_function(name: str) -> str:
|
def decompile_function(name: str) -> str:
|
||||||
response = requests.post(f"{ghidra_server_url}/decompile", data=name)
|
"""
|
||||||
return response.text if response.ok else "Failed to decompile function"
|
Decompile a specific function by name and return the decompiled C code.
|
||||||
|
"""
|
||||||
|
return safe_post("decompile", name)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def rename_function(old_name: str, new_name: str) -> str:
|
def rename_function(old_name: str, new_name: str) -> str:
|
||||||
payload = {"oldName": old_name, "newName": new_name}
|
"""
|
||||||
response = requests.post(f"{ghidra_server_url}/renameFunction", data=payload)
|
Rename a function by its current name to a new user-defined name.
|
||||||
return response.text if response.ok else "Failed to rename function"
|
"""
|
||||||
|
return safe_post("renameFunction", {"oldName": old_name, "newName": new_name})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def rename_data(address: str, new_name: str) -> str:
|
def rename_data(address: str, new_name: str) -> str:
|
||||||
payload = {"address": address, "newName": new_name}
|
"""
|
||||||
response = requests.post(f"{ghidra_server_url}/renameData", data=payload)
|
Rename a data label at the specified address.
|
||||||
return response.text if response.ok else "Failed to rename data"
|
"""
|
||||||
|
return safe_post("renameData", {"address": address, "newName": new_name})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_segments() -> list:
|
def list_segments(offset: int = 0, limit: int = 100) -> list:
|
||||||
response = requests.get(f"{ghidra_server_url}/segments")
|
"""
|
||||||
return response.text.splitlines() if response.ok else []
|
List all memory segments in the program with pagination.
|
||||||
|
"""
|
||||||
|
return safe_get("segments", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_imports() -> list:
|
def list_imports(offset: int = 0, limit: int = 100) -> list:
|
||||||
response = requests.get(f"{ghidra_server_url}/imports")
|
"""
|
||||||
return response.text.splitlines() if response.ok else []
|
List imported symbols in the program with pagination.
|
||||||
|
"""
|
||||||
|
return safe_get("imports", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_exports() -> list:
|
def list_exports(offset: int = 0, limit: int = 100) -> list:
|
||||||
response = requests.get(f"{ghidra_server_url}/exports")
|
"""
|
||||||
return response.text.splitlines() if response.ok else []
|
List exported functions/symbols with pagination.
|
||||||
|
"""
|
||||||
|
return safe_get("exports", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_namespaces() -> list:
|
def list_namespaces(offset: int = 0, limit: int = 100) -> list:
|
||||||
response = requests.get(f"{ghidra_server_url}/namespaces")
|
"""
|
||||||
return response.text.splitlines() if response.ok else []
|
List all non-global namespaces in the program with pagination.
|
||||||
|
"""
|
||||||
|
return safe_get("namespaces", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def list_data_items() -> list:
|
def list_data_items(offset: int = 0, limit: int = 100) -> list:
|
||||||
response = requests.get(f"{ghidra_server_url}/data")
|
"""
|
||||||
return response.text.splitlines() if response.ok else []
|
List defined data labels and their values with pagination.
|
||||||
|
"""
|
||||||
|
return safe_get("data", {"offset": offset, "limit": limit})
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def search_functions_by_name(query: str, offset: int = 0, limit: int = 100) -> list:
|
||||||
|
"""
|
||||||
|
Search for functions whose name contains the given substring.
|
||||||
|
"""
|
||||||
|
if not query:
|
||||||
|
return ["Error: query string is required"]
|
||||||
|
return safe_get("searchFunctions", {"query": query, "offset": offset, "limit": limit})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mcp.run()
|
mcp.run()
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
package com.lauriewired;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hello world!
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class App
|
|
||||||
{
|
|
||||||
public static void main( String[] args )
|
|
||||||
{
|
|
||||||
System.out.println( "Hello World!" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,24 +2,24 @@ package com.lauriewired;
|
|||||||
|
|
||||||
import ghidra.framework.plugintool.Plugin;
|
import ghidra.framework.plugintool.Plugin;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
import ghidra.framework.plugintool.PluginTool;
|
||||||
|
import ghidra.program.model.address.Address;
|
||||||
|
import ghidra.program.model.address.GlobalNamespace;
|
||||||
import ghidra.program.model.listing.*;
|
import ghidra.program.model.listing.*;
|
||||||
|
import ghidra.program.model.mem.MemoryBlock;
|
||||||
import ghidra.program.model.symbol.*;
|
import ghidra.program.model.symbol.*;
|
||||||
import ghidra.program.model.address.*;
|
|
||||||
import ghidra.program.model.mem.*;
|
|
||||||
import ghidra.app.decompiler.DecompInterface;
|
import ghidra.app.decompiler.DecompInterface;
|
||||||
import ghidra.app.decompiler.DecompileResults;
|
import ghidra.app.decompiler.DecompileResults;
|
||||||
import ghidra.util.task.ConsoleTaskMonitor;
|
|
||||||
import ghidra.util.Msg;
|
|
||||||
|
|
||||||
import ghidra.framework.plugintool.util.PluginStatus;
|
|
||||||
import ghidra.app.DeveloperPluginPackage;
|
|
||||||
import ghidra.app.plugin.PluginCategoryNames;
|
import ghidra.app.plugin.PluginCategoryNames;
|
||||||
import ghidra.framework.plugintool.PluginInfo;
|
|
||||||
import ghidra.app.services.ProgramManager;
|
import ghidra.app.services.ProgramManager;
|
||||||
|
import ghidra.framework.plugintool.PluginInfo;
|
||||||
|
import ghidra.framework.plugintool.util.PluginStatus;
|
||||||
|
import ghidra.util.Msg;
|
||||||
|
import ghidra.util.task.ConsoleTaskMonitor;
|
||||||
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
|
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
@ -27,11 +27,10 @@ import java.net.InetSocketAddress;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import javax.swing.SwingUtilities;
|
|
||||||
|
|
||||||
@PluginInfo(
|
@PluginInfo(
|
||||||
status = PluginStatus.RELEASED,
|
status = PluginStatus.RELEASED,
|
||||||
packageName = DeveloperPluginPackage.NAME,
|
packageName = ghidra.app.DeveloperPluginPackage.NAME,
|
||||||
category = PluginCategoryNames.ANALYSIS,
|
category = PluginCategoryNames.ANALYSIS,
|
||||||
shortDescription = "HTTP server plugin",
|
shortDescription = "HTTP server plugin",
|
||||||
description = "Starts an embedded HTTP server to expose program data."
|
description = "Starts an embedded HTTP server to expose program data."
|
||||||
@ -42,11 +41,11 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
|
|
||||||
public GhidraMCPPlugin(PluginTool tool) {
|
public GhidraMCPPlugin(PluginTool tool) {
|
||||||
super(tool);
|
super(tool);
|
||||||
Msg.info(this, "✅ GhidraMCPPlugin loaded!");
|
Msg.info(this, "GhidraMCPPlugin loaded!");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startServer();
|
startServer();
|
||||||
} catch (IOException e) {
|
}
|
||||||
|
catch (IOException e) {
|
||||||
Msg.error(this, "Failed to start HTTP server", e);
|
Msg.error(this, "Failed to start HTTP server", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,68 +54,108 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
int port = 8080;
|
int port = 8080;
|
||||||
server = HttpServer.create(new InetSocketAddress(port), 0);
|
server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||||
|
|
||||||
server.createContext("/methods", exchange -> sendResponse(exchange, getAllFunctionNames()));
|
// Each listing endpoint uses offset & limit from query params:
|
||||||
server.createContext("/classes", exchange -> sendResponse(exchange, getAllClassNames()));
|
server.createContext("/methods", exchange -> {
|
||||||
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, getAllFunctionNames(offset, limit));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.createContext("/classes", exchange -> {
|
||||||
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, getAllClassNames(offset, limit));
|
||||||
|
});
|
||||||
|
|
||||||
server.createContext("/decompile", exchange -> {
|
server.createContext("/decompile", exchange -> {
|
||||||
String name = new String(exchange.getRequestBody().readAllBytes());
|
String name = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
sendResponse(exchange, decompileFunctionByName(name));
|
sendResponse(exchange, decompileFunctionByName(name));
|
||||||
});
|
});
|
||||||
|
|
||||||
server.createContext("/renameFunction", exchange -> {
|
server.createContext("/renameFunction", exchange -> {
|
||||||
Map<String, String> params = parsePostParams(exchange);
|
Map<String, String> params = parsePostParams(exchange);
|
||||||
String response = renameFunction(params.get("oldName"), params.get("newName"))
|
String response = renameFunction(params.get("oldName"), params.get("newName"))
|
||||||
? "Renamed successfully" : "Rename failed";
|
? "Renamed successfully" : "Rename failed";
|
||||||
sendResponse(exchange, response);
|
sendResponse(exchange, response);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.createContext("/renameData", exchange -> {
|
server.createContext("/renameData", exchange -> {
|
||||||
Map<String, String> params = parsePostParams(exchange);
|
Map<String, String> params = parsePostParams(exchange);
|
||||||
renameDataAtAddress(params.get("address"), params.get("newName"));
|
renameDataAtAddress(params.get("address"), params.get("newName"));
|
||||||
sendResponse(exchange, "Rename data attempted");
|
sendResponse(exchange, "Rename data attempted");
|
||||||
});
|
});
|
||||||
server.createContext("/segments", exchange -> sendResponse(exchange, listSegments()));
|
|
||||||
server.createContext("/imports", exchange -> sendResponse(exchange, listImports()));
|
server.createContext("/segments", exchange -> {
|
||||||
server.createContext("/exports", exchange -> sendResponse(exchange, listExports()));
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
server.createContext("/namespaces", exchange -> sendResponse(exchange, listNamespaces()));
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
server.createContext("/data", exchange -> sendResponse(exchange, listDefinedData()));
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listSegments(offset, limit));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.createContext("/imports", exchange -> {
|
||||||
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listImports(offset, limit));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.createContext("/exports", exchange -> {
|
||||||
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listExports(offset, limit));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.createContext("/namespaces", exchange -> {
|
||||||
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listNamespaces(offset, limit));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.createContext("/data", exchange -> {
|
||||||
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, listDefinedData(offset, limit));
|
||||||
|
});
|
||||||
|
|
||||||
|
server.createContext("/searchFunctions", exchange -> {
|
||||||
|
Map<String, String> qparams = parseQueryParams(exchange);
|
||||||
|
String searchTerm = qparams.get("query");
|
||||||
|
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||||
|
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||||
|
sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit));
|
||||||
|
});
|
||||||
|
|
||||||
server.setExecutor(null);
|
server.setExecutor(null);
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
server.start();
|
server.start();
|
||||||
Msg.info(this, "🌐 GhidraMCP HTTP server started on port " + port);
|
Msg.info(this, "GhidraMCP HTTP server started on port " + port);
|
||||||
}, "GhidraMCP-HTTP-Server").start();
|
}, "GhidraMCP-HTTP-Server").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendResponse(HttpExchange exchange, String response) throws IOException {
|
// ----------------------------------------------------------------------------------
|
||||||
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
|
// Pagination-aware listing methods
|
||||||
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
|
// ----------------------------------------------------------------------------------
|
||||||
exchange.sendResponseHeaders(200, bytes.length);
|
|
||||||
try (OutputStream os = exchange.getResponseBody()) {
|
|
||||||
os.write(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> parsePostParams(HttpExchange exchange) throws IOException {
|
private String getAllFunctionNames(int offset, int limit) {
|
||||||
String body = new String(exchange.getRequestBody().readAllBytes());
|
|
||||||
Map<String, String> params = new HashMap<>();
|
|
||||||
for (String pair : body.split("&")) {
|
|
||||||
String[] kv = pair.split("=");
|
|
||||||
if (kv.length == 2) params.put(kv[0], kv[1]);
|
|
||||||
}
|
|
||||||
return params;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getAllFunctionNames() {
|
|
||||||
Program program = getCurrentProgram();
|
Program program = getCurrentProgram();
|
||||||
if (program == null) return "No program loaded";
|
if (program == null) return "No program loaded";
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
|
List<String> names = new ArrayList<>();
|
||||||
for (Function f : program.getFunctionManager().getFunctions(true)) {
|
for (Function f : program.getFunctionManager().getFunctions(true)) {
|
||||||
sb.append(f.getName()).append("\n");
|
names.add(f.getName());
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return paginateList(names, offset, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getAllClassNames() {
|
private String getAllClassNames(int offset, int limit) {
|
||||||
Program program = getCurrentProgram();
|
Program program = getCurrentProgram();
|
||||||
if (program == null) return "No program loaded";
|
if (program == null) return "No program loaded";
|
||||||
|
|
||||||
Set<String> classNames = new HashSet<>();
|
Set<String> classNames = new HashSet<>();
|
||||||
for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) {
|
for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) {
|
||||||
Namespace ns = symbol.getParentNamespace();
|
Namespace ns = symbol.getParentNamespace();
|
||||||
@ -124,9 +163,117 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
classNames.add(ns.getName());
|
classNames.add(ns.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return String.join("\n", classNames);
|
// Convert set to list for pagination
|
||||||
|
List<String> sorted = new ArrayList<>(classNames);
|
||||||
|
Collections.sort(sorted);
|
||||||
|
return paginateList(sorted, offset, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String listSegments(int offset, int limit) {
|
||||||
|
Program program = getCurrentProgram();
|
||||||
|
if (program == null) return "No program loaded";
|
||||||
|
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
for (MemoryBlock block : program.getMemory().getBlocks()) {
|
||||||
|
lines.add(String.format("%s: %s - %s", block.getName(), block.getStart(), block.getEnd()));
|
||||||
|
}
|
||||||
|
return paginateList(lines, offset, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String listImports(int offset, int limit) {
|
||||||
|
Program program = getCurrentProgram();
|
||||||
|
if (program == null) return "No program loaded";
|
||||||
|
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) {
|
||||||
|
lines.add(symbol.getName() + " -> " + symbol.getAddress());
|
||||||
|
}
|
||||||
|
return paginateList(lines, offset, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String listExports(int offset, int limit) {
|
||||||
|
Program program = getCurrentProgram();
|
||||||
|
if (program == null) return "No program loaded";
|
||||||
|
|
||||||
|
SymbolTable table = program.getSymbolTable();
|
||||||
|
SymbolIterator it = table.getAllSymbols(true);
|
||||||
|
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Symbol s = it.next();
|
||||||
|
// On older Ghidra, "export" is recognized via isExternalEntryPoint()
|
||||||
|
if (s.isExternalEntryPoint()) {
|
||||||
|
lines.add(s.getName() + " -> " + s.getAddress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paginateList(lines, offset, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String listNamespaces(int offset, int limit) {
|
||||||
|
Program program = getCurrentProgram();
|
||||||
|
if (program == null) return "No program loaded";
|
||||||
|
|
||||||
|
Set<String> namespaces = new HashSet<>();
|
||||||
|
for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) {
|
||||||
|
Namespace ns = symbol.getParentNamespace();
|
||||||
|
if (ns != null && !(ns instanceof GlobalNamespace)) {
|
||||||
|
namespaces.add(ns.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> sorted = new ArrayList<>(namespaces);
|
||||||
|
Collections.sort(sorted);
|
||||||
|
return paginateList(sorted, offset, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String listDefinedData(int offset, int limit) {
|
||||||
|
Program program = getCurrentProgram();
|
||||||
|
if (program == null) return "No program loaded";
|
||||||
|
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
for (MemoryBlock block : program.getMemory().getBlocks()) {
|
||||||
|
DataIterator it = program.getListing().getDefinedData(block.getStart(), true);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
Data data = it.next();
|
||||||
|
if (block.contains(data.getAddress())) {
|
||||||
|
String label = data.getLabel() != null ? data.getLabel() : "(unnamed)";
|
||||||
|
String valRepr = data.getDefaultValueRepresentation();
|
||||||
|
lines.add(String.format("%s: %s = %s",
|
||||||
|
data.getAddress(),
|
||||||
|
escapeNonAscii(label),
|
||||||
|
escapeNonAscii(valRepr)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paginateList(lines, offset, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String searchFunctionsByName(String searchTerm, int offset, int limit) {
|
||||||
|
Program program = getCurrentProgram();
|
||||||
|
if (program == null) return "No program loaded";
|
||||||
|
if (searchTerm == null || searchTerm.isEmpty()) return "Search term is required";
|
||||||
|
|
||||||
|
List<String> matches = new ArrayList<>();
|
||||||
|
for (Function func : program.getFunctionManager().getFunctions(true)) {
|
||||||
|
String name = func.getName();
|
||||||
|
// simple substring match
|
||||||
|
if (name.toLowerCase().contains(searchTerm.toLowerCase())) {
|
||||||
|
matches.add(String.format("%s @ %s", name, func.getEntryPoint()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(matches);
|
||||||
|
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
return "No functions matching '" + searchTerm + "'";
|
||||||
|
}
|
||||||
|
return paginateList(matches, offset, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------------
|
||||||
|
// Logic for rename, decompile, etc.
|
||||||
|
// ----------------------------------------------------------------------------------
|
||||||
|
|
||||||
private String decompileFunctionByName(String name) {
|
private String decompileFunctionByName(String name) {
|
||||||
Program program = getCurrentProgram();
|
Program program = getCurrentProgram();
|
||||||
if (program == null) return "No program loaded";
|
if (program == null) return "No program loaded";
|
||||||
@ -134,10 +281,13 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
decomp.openProgram(program);
|
decomp.openProgram(program);
|
||||||
for (Function func : program.getFunctionManager().getFunctions(true)) {
|
for (Function func : program.getFunctionManager().getFunctions(true)) {
|
||||||
if (func.getName().equals(name)) {
|
if (func.getName().equals(name)) {
|
||||||
DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
|
DecompileResults result =
|
||||||
|
decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
|
||||||
if (result != null && result.decompileCompleted()) {
|
if (result != null && result.decompileCompleted()) {
|
||||||
return result.getDecompiledFunction().getC();
|
return result.getDecompiledFunction().getC();
|
||||||
} else return "Decompilation failed";
|
} else {
|
||||||
|
return "Decompilation failed";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "Function not found";
|
return "Function not found";
|
||||||
@ -147,11 +297,8 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
Program program = getCurrentProgram();
|
Program program = getCurrentProgram();
|
||||||
if (program == null) return false;
|
if (program == null) return false;
|
||||||
|
|
||||||
// Use AtomicBoolean to capture the result from inside the Task
|
|
||||||
AtomicBoolean successFlag = new AtomicBoolean(false);
|
AtomicBoolean successFlag = new AtomicBoolean(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Run in Swing EDT to ensure proper transaction handling
|
|
||||||
SwingUtilities.invokeAndWait(() -> {
|
SwingUtilities.invokeAndWait(() -> {
|
||||||
int tx = program.startTransaction("Rename function via HTTP");
|
int tx = program.startTransaction("Rename function via HTTP");
|
||||||
try {
|
try {
|
||||||
@ -170,11 +317,10 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
program.endTransaction(tx, successFlag.get());
|
program.endTransaction(tx, successFlag.get());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (InterruptedException | InvocationTargetException e) {
|
catch (InterruptedException | InvocationTargetException e) {
|
||||||
Msg.error(this, "Failed to execute rename on Swing thread", e);
|
Msg.error(this, "Failed to execute rename on Swing thread", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return successFlag.get();
|
return successFlag.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +329,6 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
if (program == null) return;
|
if (program == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Run in Swing EDT to ensure proper transaction handling
|
|
||||||
SwingUtilities.invokeAndWait(() -> {
|
SwingUtilities.invokeAndWait(() -> {
|
||||||
int tx = program.startTransaction("Rename data");
|
int tx = program.startTransaction("Rename data");
|
||||||
try {
|
try {
|
||||||
@ -213,75 +358,108 @@ public class GhidraMCPPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String listSegments() {
|
// ----------------------------------------------------------------------------------
|
||||||
Program program = getCurrentProgram();
|
// Utility: parse query params, parse post params, pagination, etc.
|
||||||
StringBuilder sb = new StringBuilder();
|
// ----------------------------------------------------------------------------------
|
||||||
for (MemoryBlock block : program.getMemory().getBlocks()) {
|
|
||||||
sb.append(String.format("%s: %s - %s\n", block.getName(), block.getStart(), block.getEnd()));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String listImports() {
|
/**
|
||||||
Program program = getCurrentProgram();
|
* Parse query parameters from the URL, e.g. ?offset=10&limit=100
|
||||||
StringBuilder sb = new StringBuilder();
|
*/
|
||||||
for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) {
|
private Map<String, String> parseQueryParams(HttpExchange exchange) {
|
||||||
sb.append(symbol.getName()).append(" -> ").append(symbol.getAddress()).append("\n");
|
Map<String, String> result = new HashMap<>();
|
||||||
}
|
String query = exchange.getRequestURI().getQuery(); // e.g. offset=10&limit=100
|
||||||
return sb.toString();
|
if (query != null) {
|
||||||
}
|
String[] pairs = query.split("&");
|
||||||
|
for (String p : pairs) {
|
||||||
private String listExports() {
|
String[] kv = p.split("=");
|
||||||
Program program = getCurrentProgram();
|
if (kv.length == 2) {
|
||||||
StringBuilder sb = new StringBuilder();
|
result.put(kv[0], kv[1]);
|
||||||
for (Function func : program.getFunctionManager().getFunctions(true)) {
|
|
||||||
if (func.isExternal()) {
|
|
||||||
sb.append(func.getName()).append(" -> ").append(func.getEntryPoint()).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String listNamespaces() {
|
|
||||||
Program program = getCurrentProgram();
|
|
||||||
Set<String> namespaces = new HashSet<>();
|
|
||||||
for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) {
|
|
||||||
Namespace ns = symbol.getParentNamespace();
|
|
||||||
if (ns != null && !(ns instanceof GlobalNamespace)) {
|
|
||||||
namespaces.add(ns.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return String.join("\n", namespaces);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String listDefinedData() {
|
|
||||||
Program program = getCurrentProgram();
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (MemoryBlock block : program.getMemory().getBlocks()) {
|
|
||||||
DataIterator it = program.getListing().getDefinedData(block.getStart(), true);
|
|
||||||
while (it.hasNext()) {
|
|
||||||
Data data = it.next();
|
|
||||||
if (block.contains(data.getAddress())) {
|
|
||||||
sb.append(String.format("%s: %s = %s\n",
|
|
||||||
data.getAddress(),
|
|
||||||
data.getLabel() != null ? data.getLabel() : "(unnamed)",
|
|
||||||
data.getDefaultValueRepresentation()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse post body form params, e.g. oldName=foo&newName=bar
|
||||||
|
*/
|
||||||
|
private Map<String, String> parsePostParams(HttpExchange exchange) throws IOException {
|
||||||
|
byte[] body = exchange.getRequestBody().readAllBytes();
|
||||||
|
String bodyStr = new String(body, StandardCharsets.UTF_8);
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
for (String pair : bodyStr.split("&")) {
|
||||||
|
String[] kv = pair.split("=");
|
||||||
|
if (kv.length == 2) {
|
||||||
|
params.put(kv[0], kv[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a list of strings into one big newline-delimited string, applying offset & limit.
|
||||||
|
*/
|
||||||
|
private String paginateList(List<String> items, int offset, int limit) {
|
||||||
|
int start = Math.max(0, offset);
|
||||||
|
int end = Math.min(items.size(), offset + limit);
|
||||||
|
|
||||||
|
if (start >= items.size()) {
|
||||||
|
return ""; // no items in range
|
||||||
|
}
|
||||||
|
List<String> sub = items.subList(start, end);
|
||||||
|
return String.join("\n", sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an integer from a string, or return defaultValue if null/invalid.
|
||||||
|
*/
|
||||||
|
private int parseIntOrDefault(String val, int defaultValue) {
|
||||||
|
if (val == null) return defaultValue;
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(val);
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape non-ASCII chars to avoid potential decode issues.
|
||||||
|
*/
|
||||||
|
private String escapeNonAscii(String input) {
|
||||||
|
if (input == null) return "";
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (char c : input.toCharArray()) {
|
||||||
|
if (c >= 32 && c < 127) {
|
||||||
|
sb.append(c);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sb.append("\\x");
|
||||||
|
sb.append(Integer.toHexString(c & 0xFF));
|
||||||
|
}
|
||||||
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Program getCurrentProgram() {
|
public Program getCurrentProgram() {
|
||||||
ProgramManager programManager = tool.getService(ProgramManager.class);
|
ProgramManager pm = tool.getService(ProgramManager.class);
|
||||||
return programManager != null ? programManager.getCurrentProgram() : null;
|
return pm != null ? pm.getCurrentProgram() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendResponse(HttpExchange exchange, String response) throws IOException {
|
||||||
|
byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
|
||||||
|
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
|
||||||
|
exchange.sendResponseHeaders(200, bytes.length);
|
||||||
|
try (OutputStream os = exchange.getResponseBody()) {
|
||||||
|
os.write(bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose() {
|
public void dispose() {
|
||||||
if (server != null) {
|
if (server != null) {
|
||||||
server.stop(0);
|
server.stop(0);
|
||||||
Msg.info(this, "🛑 HTTP server stopped.");
|
Msg.info(this, "HTTP server stopped.");
|
||||||
}
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,5 +2,5 @@ name=GhidraMCP
|
|||||||
description=A plugin that runs an embedded HTTP server to expose program data.
|
description=A plugin that runs an embedded HTTP server to expose program data.
|
||||||
author=LaurieWired
|
author=LaurieWired
|
||||||
createdOn=2025-03-22
|
createdOn=2025-03-22
|
||||||
version=11.2
|
version=11.3.1
|
||||||
ghidraVersion=11.2
|
ghidraVersion=11.3.1
|
||||||
Loading…
x
Reference in New Issue
Block a user