diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..ec29081 --- /dev/null +++ b/pom.xml @@ -0,0 +1,145 @@ + + + 4.0.0 + com.lauriewired + GhidraMCP + jar + 1.0-SNAPSHOT + GhidraMCP + http://maven.apache.org + + + + + ghidra + Generic + 11.3.1 + system + ${project.basedir}/lib/Generic.jar + + + ghidra + SoftwareModeling + 11.3.1 + system + ${project.basedir}/lib/SoftwareModeling.jar + + + ghidra + Project + 11.3.1 + system + ${project.basedir}/lib/Project.jar + + + ghidra + Docking + 11.3.1 + system + ${project.basedir}/lib/Docking.jar + + + ghidra + Decompiler + 11.3.1 + system + ${project.basedir}/lib/Decompiler.jar + + + ghidra + Utility + 11.3.1 + system + ${project.basedir}/lib/Utility.jar + + + ghidra + Base + 11.3.1 + system + ${project.basedir}/lib/Base.jar + + + + + junit + junit + 3.8.1 + test + + + + + + + + maven-jar-plugin + 3.2.2 + + + src/main/resources/META-INF/MANIFEST.MF + + + GhidraMCP + + + **/App.class + + + ${project.build.directory} + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + + src/assembly/ghidra-extension.xml + + + + GhidraMCP-${project.version} + + + false + + + + + make-assembly + package + + single + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + + diff --git a/src/assembly/ghidra-extension.xml b/src/assembly/ghidra-extension.xml new file mode 100644 index 0000000..5941ca0 --- /dev/null +++ b/src/assembly/ghidra-extension.xml @@ -0,0 +1,40 @@ + + + + ghidra-extension + + + + zip + + + + false + + + + + src/main/resources + + extension.properties + Module.manifest + + GhidraMCP + + + + + ${project.build.directory} + + + GhidraMCP.jar + + GhidraMCP/lib + + + diff --git a/src/main/java/com/lauriewired/App.java b/src/main/java/com/lauriewired/App.java new file mode 100644 index 0000000..5155129 --- /dev/null +++ b/src/main/java/com/lauriewired/App.java @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000..323e133 --- /dev/null +++ b/src/main/java/com/lauriewired/GhidraMCPPlugin.java @@ -0,0 +1,288 @@ +package com.lauriewired; + +import ghidra.framework.plugintool.Plugin; +import ghidra.framework.plugintool.PluginTool; +import ghidra.program.model.listing.*; +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 com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +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, + category = PluginCategoryNames.ANALYSIS, + shortDescription = "HTTP server plugin", + description = "Starts an embedded HTTP server to expose program data." +) +public class GhidraMCPPlugin extends Plugin { + + private HttpServer server; + + public GhidraMCPPlugin(PluginTool tool) { + super(tool); + Msg.info(this, "✅ GhidraMCPPlugin loaded!"); + + try { + startServer(); + } catch (IOException e) { + Msg.error(this, "Failed to start HTTP server", e); + } + } + + private void startServer() throws IOException { + int port = 8080; + server = HttpServer.create(new InetSocketAddress(port), 0); + + server.createContext("/methods", exchange -> sendResponse(exchange, getAllFunctionNames())); + server.createContext("/classes", exchange -> sendResponse(exchange, getAllClassNames())); + server.createContext("/decompile", exchange -> { + String name = new String(exchange.getRequestBody().readAllBytes()); + 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"; + 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.setExecutor(null); + new Thread(() -> { + server.start(); + 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); + } + } + + 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() { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + StringBuilder sb = new StringBuilder(); + for (Function f : program.getFunctionManager().getFunctions(true)) { + sb.append(f.getName()).append("\n"); + } + return sb.toString(); + } + + private String getAllClassNames() { + 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(); + if (ns != null && !ns.isGlobal()) { + classNames.add(ns.getName()); + } + } + return String.join("\n", classNames); + } + + private String decompileFunctionByName(String name) { + Program program = getCurrentProgram(); + if (program == null) return "No program loaded"; + DecompInterface decomp = new DecompInterface(); + decomp.openProgram(program); + for (Function func : program.getFunctionManager().getFunctions(true)) { + if (func.getName().equals(name)) { + DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); + if (result != null && result.decompileCompleted()) { + return result.getDecompiledFunction().getC(); + } else return "Decompilation failed"; + } + } + return "Function not found"; + } + + private boolean renameFunction(String oldName, String newName) { + 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 { + for (Function func : program.getFunctionManager().getFunctions(true)) { + if (func.getName().equals(oldName)) { + func.setName(newName, SourceType.USER_DEFINED); + successFlag.set(true); + break; + } + } + } + catch (Exception e) { + Msg.error(this, "Error renaming function", e); + } + finally { + program.endTransaction(tx, successFlag.get()); + } + }); + } + catch (InterruptedException | InvocationTargetException e) { + Msg.error(this, "Failed to execute rename on Swing thread", e); + } + + return successFlag.get(); + } + + private void renameDataAtAddress(String addressStr, String newName) { + Program program = getCurrentProgram(); + if (program == null) return; + + try { + // Run in Swing EDT to ensure proper transaction handling + SwingUtilities.invokeAndWait(() -> { + int tx = program.startTransaction("Rename data"); + try { + Address addr = program.getAddressFactory().getAddress(addressStr); + Listing listing = program.getListing(); + Data data = listing.getDefinedDataAt(addr); + if (data != null) { + SymbolTable symTable = program.getSymbolTable(); + Symbol symbol = symTable.getPrimarySymbol(addr); + if (symbol != null) { + symbol.setName(newName, SourceType.USER_DEFINED); + } else { + symTable.createLabel(addr, newName, SourceType.USER_DEFINED); + } + } + } + catch (Exception e) { + Msg.error(this, "Rename data error", e); + } + finally { + program.endTransaction(tx, true); + } + }); + } + catch (InterruptedException | InvocationTargetException e) { + Msg.error(this, "Failed to execute rename data on Swing thread", e); + } + } + + 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(); + } + + 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())); + } + } + } + return sb.toString(); + } + + public Program getCurrentProgram() { + ProgramManager programManager = tool.getService(ProgramManager.class); + return programManager != null ? programManager.getCurrentProgram() : null; + } + + @Override + public void dispose() { + if (server != null) { + server.stop(0); + Msg.info(this, "🛑 HTTP server stopped."); + } + super.dispose(); + } +} diff --git a/src/main/resources/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 0000000..4145ab2 --- /dev/null +++ b/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,6 @@ +Manifest-Version: 1.0 +Plugin-Class: com.lauriewired.GhidraMCP +Plugin-Name: GhidraMCP +Plugin-Version: 1.0 +Plugin-Author: LaurieWired +Plugin-Description: A custom plugin by LaurieWired diff --git a/src/main/resources/Module.manifest b/src/main/resources/Module.manifest new file mode 100644 index 0000000..1aa8264 --- /dev/null +++ b/src/main/resources/Module.manifest @@ -0,0 +1,2 @@ +GHIDRA_MODULE_NAME=GhidraMCP +GHIDRA_MODULE_DESC=An HTTP server plugin for Ghidra diff --git a/src/main/resources/extension.properties b/src/main/resources/extension.properties new file mode 100644 index 0000000..6d09547 --- /dev/null +++ b/src/main/resources/extension.properties @@ -0,0 +1,6 @@ +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 diff --git a/src/test/java/com/lauriewired/AppTest.java b/src/test/java/com/lauriewired/AppTest.java new file mode 100644 index 0000000..77b1a97 --- /dev/null +++ b/src/test/java/com/lauriewired/AppTest.java @@ -0,0 +1,38 @@ +package com.lauriewired; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +}