diff --git a/bridge_mcp_hydra.py b/bridge_mcp_hydra.py index db0439f..cb36703 100644 --- a/bridge_mcp_hydra.py +++ b/bridge_mcp_hydra.py @@ -1616,6 +1616,34 @@ def rename_data(port: int = DEFAULT_GHIDRA_PORT, return simplify_response(response) +@mcp.tool() +def list_strings(port: int = DEFAULT_GHIDRA_PORT, + offset: int = 0, + limit: int = 2000, + filter: str = None) -> dict: + """List all defined strings in the binary with their memory addresses + + Args: + port: Ghidra instance port (default: 8192) + offset: Pagination offset (default: 0) + limit: Maximum strings to return (default: 2000) + filter: Optional string content filter + + Returns: + dict: List of string data with addresses, values, and metadata + """ + params = { + "offset": offset, + "limit": limit + } + + if filter: + params["filter"] = filter + + response = safe_get(port, "strings", params) + return simplify_response(response) + + @mcp.tool() def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", diff --git a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java index 4f428c7..1087c1f 100644 --- a/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java +++ b/src/main/java/eu/starsong/ghidra/GhydraMCPPlugin.java @@ -374,6 +374,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin { .addLink("functions", "/functions") .addLink("symbols", "/symbols") .addLink("data", "/data") + .addLink("strings", "/strings") .addLink("segments", "/segments") .addLink("memory", "/memory") .addLink("xrefs", "/xrefs") diff --git a/src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java b/src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java index f6b0079..4f9da3a 100644 --- a/src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java +++ b/src/main/java/eu/starsong/ghidra/endpoints/DataEndpoints.java @@ -84,6 +84,18 @@ package eu.starsong.ghidra.endpoints; sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); } }); + server.createContext("/strings", exchange -> { + try { + if ("GET".equals(exchange.getRequestMethod())) { + handleListStrings(exchange); + } else { + sendErrorResponse(exchange, 405, "Method Not Allowed"); + } + } catch (Exception e) { + Msg.error(this, "Error in /strings endpoint", e); + sendErrorResponse(exchange, 500, "Internal server error: " + e.getMessage()); + } + }); } public void handleData(HttpExchange exchange) throws IOException { @@ -1294,4 +1306,100 @@ package eu.starsong.ghidra.endpoints; } // Note: The handleUpdateData method is already defined earlier in this file at line 477 + + /** + * Handle request to list strings in the binary + * @param exchange The HTTP exchange + * @throws IOException If an I/O error occurs + */ + public void handleListStrings(HttpExchange exchange) throws IOException { + try { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 2000); + String filter = qparams.get("filter"); + + Program program = getCurrentProgram(); + if (program == null) { + sendErrorResponse(exchange, 400, "No program loaded", "NO_PROGRAM_LOADED"); + return; + } + + List> strings = new ArrayList<>(); + + for (MemoryBlock block : program.getMemory().getBlocks()) { + if (!block.isInitialized()) continue; + + DataIterator it = program.getListing().getDefinedData(block.getStart(), true); + while (it.hasNext()) { + Data data = it.next(); + if (!block.contains(data.getAddress())) continue; + + // Check if the data type is a string type + String dataTypeName = data.getDataType().getName().toLowerCase(); + boolean isString = dataTypeName.contains("string") || + dataTypeName.contains("unicode") || + (dataTypeName.contains("char") && data.getLength() > 1); // Array of chars + + if (isString) { + // Get the string value + String value = data.getDefaultValueRepresentation(); + if (value == null) value = ""; + + // Skip if it doesn't match the filter + if (filter != null && !filter.isEmpty() && !value.toLowerCase().contains(filter.toLowerCase())) { + continue; + } + + Map stringInfo = new HashMap<>(); + stringInfo.put("address", data.getAddress().toString()); + stringInfo.put("value", value); + stringInfo.put("length", data.getLength()); + stringInfo.put("type", data.getDataType().getName()); + + // If the data has a label/name, include it + String name = null; + Symbol symbol = program.getSymbolTable().getPrimarySymbol(data.getAddress()); + if (symbol != null) { + name = symbol.getName(); + } + stringInfo.put("name", name != null ? name : ""); + + // Add HATEOAS links + Map links = new HashMap<>(); + Map selfLink = new HashMap<>(); + selfLink.put("href", "/data/" + data.getAddress().toString()); + links.put("self", selfLink); + + Map memoryLink = new HashMap<>(); + memoryLink.put("href", "/memory?address=" + data.getAddress().toString()); + links.put("memory", memoryLink); + + stringInfo.put("_links", links); + + strings.add(stringInfo); + } + } + } + + // Build response with HATEOAS links + eu.starsong.ghidra.api.ResponseBuilder builder = new eu.starsong.ghidra.api.ResponseBuilder(exchange, port) + .success(true); + + // Apply pagination and get paginated items + List> paginated = applyPagination(strings, offset, limit, builder, "/strings"); + + // Set the paginated result + builder.result(paginated); + + // Add program link + builder.addLink("program", "/program"); + builder.addLink("data", "/data"); + + sendJsonResponse(exchange, builder.build(), 200); + } catch (Exception e) { + Msg.error(this, "Error listing strings", e); + sendErrorResponse(exchange, 500, "Error listing strings: " + e.getMessage(), "INTERNAL_ERROR"); + } + } }