diff --git a/bridge_mcp_ghidra.py b/bridge_mcp_ghidra.py index b7bbcab..bff8d9b 100644 --- a/bridge_mcp_ghidra.py +++ b/bridge_mcp_ghidra.py @@ -5,63 +5,121 @@ ghidra_server_url = "http://localhost:8080" mcp = FastMCP("ghidra-mcp") -@mcp.tool() -def list_methods() -> list: - response = requests.get(f"{ghidra_server_url}/methods") - return response.text.splitlines() if response.ok else [] +def safe_get(endpoint: str, params: dict = None) -> list: + """ + Perform a GET request. If 'params' is given, we convert it to a query string. + """ + 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() -def rename_method(method_address: str, new_name: str) -> str: - payload = {"method_address": method_address, "new_name": new_name} - response = requests.post(f"{ghidra_server_url}/rename", data=payload) - return response.text if response.ok else "Failed to rename method" +def list_methods(offset: int = 0, limit: int = 100) -> list: + """ + List all function names in the program with pagination. + """ + return safe_get("methods", {"offset": offset, "limit": limit}) @mcp.tool() -def list_classes() -> list: - response = requests.get(f"{ghidra_server_url}/classes") - return response.text.splitlines() if response.ok else [] +def list_classes(offset: int = 0, limit: int = 100) -> list: + """ + List all namespace/class names in the program with pagination. + """ + return safe_get("classes", {"offset": offset, "limit": limit}) @mcp.tool() 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() 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) - return response.text if response.ok else "Failed to rename function" + """ + Rename a function by its current name to a new user-defined name. + """ + return safe_post("renameFunction", {"oldName": old_name, "newName": new_name}) @mcp.tool() 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) - return response.text if response.ok else "Failed to rename data" + """ + Rename a data label at the specified address. + """ + return safe_post("renameData", {"address": address, "newName": new_name}) @mcp.tool() -def list_segments() -> list: - response = requests.get(f"{ghidra_server_url}/segments") - return response.text.splitlines() if response.ok else [] +def list_segments(offset: int = 0, limit: int = 100) -> list: + """ + List all memory segments in the program with pagination. + """ + return safe_get("segments", {"offset": offset, "limit": limit}) @mcp.tool() -def list_imports() -> list: - response = requests.get(f"{ghidra_server_url}/imports") - return response.text.splitlines() if response.ok else [] +def list_imports(offset: int = 0, limit: int = 100) -> list: + """ + List imported symbols in the program with pagination. + """ + return safe_get("imports", {"offset": offset, "limit": limit}) @mcp.tool() -def list_exports() -> list: - response = requests.get(f"{ghidra_server_url}/exports") - return response.text.splitlines() if response.ok else [] +def list_exports(offset: int = 0, limit: int = 100) -> list: + """ + List exported functions/symbols with pagination. + """ + return safe_get("exports", {"offset": offset, "limit": limit}) @mcp.tool() -def list_namespaces() -> list: - response = requests.get(f"{ghidra_server_url}/namespaces") - return response.text.splitlines() if response.ok else [] +def list_namespaces(offset: int = 0, limit: int = 100) -> list: + """ + List all non-global namespaces in the program with pagination. + """ + return safe_get("namespaces", {"offset": offset, "limit": limit}) @mcp.tool() -def list_data_items() -> list: - response = requests.get(f"{ghidra_server_url}/data") - return response.text.splitlines() if response.ok else [] +def list_data_items(offset: int = 0, limit: int = 100) -> list: + """ + 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__": mcp.run() diff --git a/src/main/java/com/lauriewired/App.java b/src/main/java/com/lauriewired/App.java deleted file mode 100644 index 5155129..0000000 --- a/src/main/java/com/lauriewired/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.lauriewired; - -/** - * Hello world! - * - */ -public class App -{ - public static void main( String[] args ) - { - System.out.println( "Hello World!" ); - } -} diff --git a/src/main/java/com/lauriewired/GhidraMCPPlugin.java b/src/main/java/com/lauriewired/GhidraMCPPlugin.java index 323e133..78aba8e 100644 --- a/src/main/java/com/lauriewired/GhidraMCPPlugin.java +++ b/src/main/java/com/lauriewired/GhidraMCPPlugin.java @@ -2,24 +2,24 @@ package com.lauriewired; import ghidra.framework.plugintool.Plugin; 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.mem.MemoryBlock; import ghidra.program.model.symbol.*; -import ghidra.program.model.address.*; -import ghidra.program.model.mem.*; import ghidra.app.decompiler.DecompInterface; 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.framework.plugintool.PluginInfo; 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.HttpServer; +import javax.swing.SwingUtilities; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; @@ -27,11 +27,10 @@ import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; -import javax.swing.SwingUtilities; @PluginInfo( status = PluginStatus.RELEASED, - packageName = DeveloperPluginPackage.NAME, + packageName = ghidra.app.DeveloperPluginPackage.NAME, category = PluginCategoryNames.ANALYSIS, shortDescription = "HTTP server plugin", description = "Starts an embedded HTTP server to expose program data." @@ -42,11 +41,11 @@ public class GhidraMCPPlugin extends Plugin { public GhidraMCPPlugin(PluginTool tool) { super(tool); - Msg.info(this, "✅ GhidraMCPPlugin loaded!"); - + Msg.info(this, "GhidraMCPPlugin loaded!"); try { startServer(); - } catch (IOException e) { + } + catch (IOException e) { Msg.error(this, "Failed to start HTTP server", e); } } @@ -55,68 +54,108 @@ public class GhidraMCPPlugin extends Plugin { int port = 8080; server = HttpServer.create(new InetSocketAddress(port), 0); - server.createContext("/methods", exchange -> sendResponse(exchange, getAllFunctionNames())); - server.createContext("/classes", exchange -> sendResponse(exchange, getAllClassNames())); + // Each listing endpoint uses offset & limit from query params: + server.createContext("/methods", exchange -> { + Map 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 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 -> { - String name = new String(exchange.getRequestBody().readAllBytes()); + String name = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); sendResponse(exchange, decompileFunctionByName(name)); }); + server.createContext("/renameFunction", exchange -> { Map params = parsePostParams(exchange); String response = renameFunction(params.get("oldName"), params.get("newName")) - ? "Renamed successfully" : "Rename failed"; + ? "Renamed successfully" : "Rename failed"; sendResponse(exchange, response); }); + server.createContext("/renameData", exchange -> { Map params = parsePostParams(exchange); renameDataAtAddress(params.get("address"), params.get("newName")); sendResponse(exchange, "Rename data attempted"); }); - server.createContext("/segments", exchange -> sendResponse(exchange, listSegments())); - server.createContext("/imports", exchange -> sendResponse(exchange, listImports())); - server.createContext("/exports", exchange -> sendResponse(exchange, listExports())); - server.createContext("/namespaces", exchange -> sendResponse(exchange, listNamespaces())); - server.createContext("/data", exchange -> sendResponse(exchange, listDefinedData())); + + server.createContext("/segments", exchange -> { + Map qparams = parseQueryParams(exchange); + int offset = parseIntOrDefault(qparams.get("offset"), 0); + int limit = parseIntOrDefault(qparams.get("limit"), 100); + sendResponse(exchange, listSegments(offset, limit)); + }); + + server.createContext("/imports", exchange -> { + Map 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 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 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 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 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); new Thread(() -> { 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(); } - 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); - } - } + // ---------------------------------------------------------------------------------- + // Pagination-aware listing methods + // ---------------------------------------------------------------------------------- - private Map parsePostParams(HttpExchange exchange) throws IOException { - String body = new String(exchange.getRequestBody().readAllBytes()); - Map 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() { + private String getAllFunctionNames(int offset, int limit) { Program program = getCurrentProgram(); if (program == null) return "No program loaded"; - StringBuilder sb = new StringBuilder(); + + List names = new ArrayList<>(); 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(); if (program == null) return "No program loaded"; + Set classNames = new HashSet<>(); for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { Namespace ns = symbol.getParentNamespace(); @@ -124,9 +163,117 @@ public class GhidraMCPPlugin extends Plugin { classNames.add(ns.getName()); } } - return String.join("\n", classNames); + // Convert set to list for pagination + List 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 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 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 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 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 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 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 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) { Program program = getCurrentProgram(); if (program == null) return "No program loaded"; @@ -134,10 +281,13 @@ public class GhidraMCPPlugin extends Plugin { decomp.openProgram(program); for (Function func : program.getFunctionManager().getFunctions(true)) { 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()) { return result.getDecompiledFunction().getC(); - } else return "Decompilation failed"; + } else { + return "Decompilation failed"; + } } } return "Function not found"; @@ -147,11 +297,8 @@ public class GhidraMCPPlugin extends Plugin { Program program = getCurrentProgram(); if (program == null) return false; - // Use AtomicBoolean to capture the result from inside the Task AtomicBoolean successFlag = new AtomicBoolean(false); - try { - // Run in Swing EDT to ensure proper transaction handling SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Rename function via HTTP"); try { @@ -170,11 +317,10 @@ public class GhidraMCPPlugin extends Plugin { program.endTransaction(tx, successFlag.get()); } }); - } + } catch (InterruptedException | InvocationTargetException e) { Msg.error(this, "Failed to execute rename on Swing thread", e); } - return successFlag.get(); } @@ -183,7 +329,6 @@ public class GhidraMCPPlugin extends Plugin { if (program == null) return; try { - // Run in Swing EDT to ensure proper transaction handling SwingUtilities.invokeAndWait(() -> { int tx = program.startTransaction("Rename data"); try { @@ -213,75 +358,108 @@ public class GhidraMCPPlugin extends Plugin { } } - private String listSegments() { - Program program = getCurrentProgram(); - 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(); - } + // ---------------------------------------------------------------------------------- + // Utility: parse query params, parse post params, pagination, etc. + // ---------------------------------------------------------------------------------- - private String listImports() { - Program program = getCurrentProgram(); - StringBuilder sb = new StringBuilder(); - for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) { - sb.append(symbol.getName()).append(" -> ").append(symbol.getAddress()).append("\n"); - } - return sb.toString(); - } - - private String listExports() { - Program program = getCurrentProgram(); - StringBuilder sb = new StringBuilder(); - 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 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())); + /** + * Parse query parameters from the URL, e.g. ?offset=10&limit=100 + */ + private Map parseQueryParams(HttpExchange exchange) { + Map result = new HashMap<>(); + String query = exchange.getRequestURI().getQuery(); // e.g. offset=10&limit=100 + if (query != null) { + String[] pairs = query.split("&"); + for (String p : pairs) { + String[] kv = p.split("="); + if (kv.length == 2) { + result.put(kv[0], kv[1]); } } } + return result; + } + + /** + * Parse post body form params, e.g. oldName=foo&newName=bar + */ + private Map parsePostParams(HttpExchange exchange) throws IOException { + byte[] body = exchange.getRequestBody().readAllBytes(); + String bodyStr = new String(body, StandardCharsets.UTF_8); + Map 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 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 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(); } public Program getCurrentProgram() { - ProgramManager programManager = tool.getService(ProgramManager.class); - return programManager != null ? programManager.getCurrentProgram() : null; + ProgramManager pm = tool.getService(ProgramManager.class); + 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 public void dispose() { if (server != null) { server.stop(0); - Msg.info(this, "🛑 HTTP server stopped."); + Msg.info(this, "HTTP server stopped."); } super.dispose(); } diff --git a/src/main/resources/extension.properties b/src/main/resources/extension.properties index 6d09547..b0c5ea9 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.2 -ghidraVersion=11.2 \ No newline at end of file +version=11.3.1 +ghidraVersion=11.3.1 \ No newline at end of file