Merge branch 'auto-register'
This commit is contained in:
commit
07c94b2324
1
.gitignore
vendored
1
.gitignore
vendored
@ -49,3 +49,4 @@ mvnw.cmd
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
|
||||
/__pycache__
|
||||
|
||||
58
README.md
58
README.md
@ -57,6 +57,8 @@ GhydraMCP combines a Ghidra plugin with an MCP server to provide a comprehensive
|
||||
- Run multiple Ghidra instances simultaneously
|
||||
- Analyze different binaries in parallel
|
||||
- Connect to specific instances using port numbers
|
||||
- Auto-discovery of running Ghidra instances
|
||||
- Instance metadata with project and file information
|
||||
|
||||
## Program Navigation
|
||||
|
||||
@ -86,6 +88,8 @@ First, download the latest [release](https://github.com/teal-bauer/GhydraMCP/rel
|
||||
> (HydraMCPPlugin) Plugin loaded on port 8193
|
||||
> (HydraMCPPlugin) HydraMCP HTTP server started on port 8193
|
||||
> ```
|
||||
>
|
||||
> GhydraMCP now includes auto-discovery of running Ghidra instances, so manually registering each instance is typically not necessary. The MCP bridge will automatically discover and register instances on startup and periodically check for new ones.
|
||||
|
||||
Video Installation Guide:
|
||||
|
||||
@ -116,6 +120,7 @@ Theoretically, any MCP client should work with GhydraMCP. Two examples are given
|
||||
- `list_instances`: List active Ghidra instances (no params)
|
||||
- `register_instance`: Register new instance (params: port, url)
|
||||
- `unregister_instance`: Remove instance (params: port)
|
||||
- `discover_instances`: Auto-discover running instances (params: host [optional])
|
||||
|
||||
**Example Usage**:
|
||||
```python
|
||||
@ -125,6 +130,10 @@ client.use_tool("ghydra", "decompile_function", {"name": "main"})
|
||||
# Instance management
|
||||
client.use_tool("ghydra", "register_instance", {"port": 8192, "url": "http://localhost:8192/"})
|
||||
client.use_tool("ghydra", "register_instance", {"port": 8193})
|
||||
|
||||
# Auto-discover instances
|
||||
client.use_tool("ghydra", "discover_instances") # Default host
|
||||
client.use_tool("ghydra", "discover_instances", {"host": "192.168.1.10"}) # Custom host
|
||||
```
|
||||
|
||||
## Client Setup
|
||||
@ -160,26 +169,53 @@ Below is an example of how an AI assistant might interact with multiple Ghidra i
|
||||
|
||||
**User:**
|
||||
```
|
||||
Register the following instances with the ghydra mcp tool:
|
||||
port 8192 = application.exe
|
||||
port 8193 = firmware.bin
|
||||
Check which Ghidra instances are currently running.
|
||||
```
|
||||
|
||||
**Assistant:**
|
||||
```
|
||||
I'll help you analyze these binaries. Let me register the instances with the ghydra mcp tool as requested.
|
||||
I'll check which Ghidra instances are currently running by discovering any active instances.
|
||||
|
||||
View result from register_instance from ghydra (local){
|
||||
`port`: 8192
|
||||
View result from discover_instances from ghydra (local)
|
||||
{
|
||||
"found": 2,
|
||||
"instances": [
|
||||
{
|
||||
"port": 8192,
|
||||
"url": "http://localhost:8192",
|
||||
"result": "Registered instance on port 8192 at http://localhost:8192"
|
||||
},
|
||||
{
|
||||
"port": 8193,
|
||||
"url": "http://localhost:8193",
|
||||
"result": "Registered instance on port 8193 at http://localhost:8193"
|
||||
}
|
||||
]
|
||||
}
|
||||
Registered instance on port 8192 at http://localhost:8192
|
||||
|
||||
View result from register_instance from ghydra (local){
|
||||
`port`: 8193
|
||||
View result from list_instances from ghydra (local)
|
||||
{
|
||||
"instances": [
|
||||
{
|
||||
"port": 8192,
|
||||
"url": "http://localhost:8192",
|
||||
"project": "TestProject",
|
||||
"file": "application.exe"
|
||||
},
|
||||
{
|
||||
"port": 8193,
|
||||
"url": "http://localhost:8193",
|
||||
"project": "FirmwareAnalysis",
|
||||
"file": "firmware.bin"
|
||||
}
|
||||
]
|
||||
}
|
||||
Registered instance on port 8193 at http://localhost:8193
|
||||
|
||||
Now that I've registered both instances, I'm ready to analyze the decompiled code.
|
||||
I found two active Ghidra instances:
|
||||
1. Port 8192: Analyzing "application.exe" in project "TestProject"
|
||||
2. Port 8193: Analyzing "firmware.bin" in project "FirmwareAnalysis"
|
||||
|
||||
I'm ready to analyze these binaries. What would you like me to examine?
|
||||
```
|
||||
|
||||
**User:**
|
||||
|
||||
@ -7,35 +7,38 @@
|
||||
# ///
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
import threading
|
||||
from typing import Dict
|
||||
from threading import Lock
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Track active Ghidra instances (port -> url)
|
||||
active_instances: Dict[int, str] = {}
|
||||
# Track active Ghidra instances (port -> info dict)
|
||||
active_instances: Dict[int, dict] = {}
|
||||
instances_lock = Lock()
|
||||
DEFAULT_GHIDRA_PORT = 8192
|
||||
DEFAULT_GHIDRA_HOST = "localhost"
|
||||
# Port ranges for scanning
|
||||
QUICK_DISCOVERY_RANGE = range(8192, 8202) # Limited range for interactive/triggered discovery (10 ports)
|
||||
FULL_DISCOVERY_RANGE = range(8192, 8212) # Wider range for background discovery (20 ports)
|
||||
|
||||
mcp = FastMCP("hydra-mcp")
|
||||
|
||||
# Get host from environment variable, command line, or use default
|
||||
ghidra_host = os.environ.get("GHIDRA_HYDRA_HOST", DEFAULT_GHIDRA_HOST)
|
||||
if len(sys.argv) > 1:
|
||||
ghidra_host = sys.argv[1]
|
||||
print(f"Using Ghidra host: {ghidra_host}")
|
||||
|
||||
def get_instance_url(port: int) -> str:
|
||||
"""Get URL for a Ghidra instance by port"""
|
||||
with instances_lock:
|
||||
if port in active_instances:
|
||||
return active_instances[port]
|
||||
return active_instances[port]["url"]
|
||||
|
||||
# Auto-register if not found but port is valid
|
||||
if 8192 <= port <= 65535:
|
||||
register_instance(port)
|
||||
return active_instances[port]
|
||||
if port in active_instances:
|
||||
return active_instances[port]["url"]
|
||||
|
||||
return f"http://{ghidra_host}:{port}"
|
||||
|
||||
@ -116,8 +119,13 @@ def list_instances() -> dict:
|
||||
with instances_lock:
|
||||
return {
|
||||
"instances": [
|
||||
{"port": port, "url": url}
|
||||
for port, url in active_instances.items()
|
||||
{
|
||||
"port": port,
|
||||
"url": info["url"],
|
||||
"project": info.get("project", ""),
|
||||
"file": info.get("file", "")
|
||||
}
|
||||
for port, info in active_instances.items()
|
||||
]
|
||||
}
|
||||
|
||||
@ -133,12 +141,41 @@ def register_instance(port: int, url: str = None) -> str:
|
||||
response = requests.get(test_url, timeout=2)
|
||||
if not response.ok:
|
||||
return f"Error: Instance at {url} is not responding properly"
|
||||
except Exception as e:
|
||||
return f"Error: Could not connect to instance at {url}: {str(e)}"
|
||||
|
||||
# Try to get project info
|
||||
project_info = {"url": url}
|
||||
|
||||
try:
|
||||
info_url = f"{url}/info"
|
||||
info_response = requests.get(info_url, timeout=2)
|
||||
if info_response.ok:
|
||||
try:
|
||||
# Parse JSON response
|
||||
info_data = info_response.json()
|
||||
|
||||
# Extract relevant information
|
||||
project_info["project"] = info_data.get("project", "Unknown")
|
||||
|
||||
# Handle file information which is nested
|
||||
file_info = info_data.get("file", {})
|
||||
if file_info:
|
||||
project_info["file"] = file_info.get("name", "")
|
||||
project_info["path"] = file_info.get("path", "")
|
||||
project_info["architecture"] = file_info.get("architecture", "")
|
||||
project_info["endian"] = file_info.get("endian", "")
|
||||
except ValueError:
|
||||
# Not valid JSON
|
||||
pass
|
||||
except Exception:
|
||||
# Non-critical, continue with registration even if project info fails
|
||||
pass
|
||||
|
||||
with instances_lock:
|
||||
active_instances[port] = url
|
||||
active_instances[port] = project_info
|
||||
|
||||
return f"Registered instance on port {port} at {url}"
|
||||
except Exception as e:
|
||||
return f"Error: Could not connect to instance at {url}: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def unregister_instance(port: int) -> str:
|
||||
@ -149,6 +186,40 @@ def unregister_instance(port: int) -> str:
|
||||
return f"Unregistered instance on port {port}"
|
||||
return f"No instance found on port {port}"
|
||||
|
||||
@mcp.tool()
|
||||
def discover_instances(host: str = None) -> dict:
|
||||
"""Auto-discover Ghidra instances by scanning ports (quick discovery with limited range)
|
||||
|
||||
Args:
|
||||
host: Optional host to scan (defaults to configured ghidra_host)
|
||||
"""
|
||||
return _discover_instances(QUICK_DISCOVERY_RANGE, host=host, timeout=0.5)
|
||||
|
||||
def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
|
||||
"""Internal function to discover Ghidra instances by scanning ports"""
|
||||
found_instances = []
|
||||
scan_host = host if host is not None else ghidra_host
|
||||
|
||||
for port in port_range:
|
||||
if port in active_instances:
|
||||
continue
|
||||
|
||||
url = f"http://{scan_host}:{port}"
|
||||
try:
|
||||
test_url = f"{url}/instances"
|
||||
response = requests.get(test_url, timeout=timeout) # Short timeout for scanning
|
||||
if response.ok:
|
||||
result = register_instance(port, url)
|
||||
found_instances.append({"port": port, "url": url, "result": result})
|
||||
except requests.exceptions.RequestException:
|
||||
# Instance not available, just continue
|
||||
continue
|
||||
|
||||
return {
|
||||
"found": len(found_instances),
|
||||
"instances": found_instances
|
||||
}
|
||||
|
||||
# Updated tool implementations with port parameter
|
||||
from urllib.parse import quote
|
||||
|
||||
@ -210,9 +281,49 @@ import os
|
||||
def handle_sigint(signum, frame):
|
||||
os._exit(0)
|
||||
|
||||
def periodic_discovery():
|
||||
"""Periodically discover new instances"""
|
||||
while True:
|
||||
try:
|
||||
# Use the full discovery range
|
||||
_discover_instances(FULL_DISCOVERY_RANGE, timeout=0.5)
|
||||
|
||||
# Also check if any existing instances are down
|
||||
with instances_lock:
|
||||
ports_to_remove = []
|
||||
for port, info in active_instances.items():
|
||||
url = info["url"]
|
||||
try:
|
||||
response = requests.get(f"{url}/instances", timeout=1)
|
||||
if not response.ok:
|
||||
ports_to_remove.append(port)
|
||||
except requests.exceptions.RequestException:
|
||||
ports_to_remove.append(port)
|
||||
|
||||
# Remove any instances that are down
|
||||
for port in ports_to_remove:
|
||||
del active_instances[port]
|
||||
print(f"Removed unreachable instance on port {port}")
|
||||
except Exception as e:
|
||||
print(f"Error in periodic discovery: {e}")
|
||||
|
||||
# Sleep for 30 seconds before next scan
|
||||
time.sleep(30)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Auto-register default instance
|
||||
register_instance(DEFAULT_GHIDRA_PORT, f"http://{ghidra_host}:{DEFAULT_GHIDRA_PORT}")
|
||||
|
||||
# Auto-discover other instances
|
||||
discover_instances()
|
||||
|
||||
# Start periodic discovery in background thread
|
||||
discovery_thread = threading.Thread(
|
||||
target=periodic_discovery,
|
||||
daemon=True,
|
||||
name="GhydraMCP-Discovery"
|
||||
)
|
||||
discovery_thread.start()
|
||||
|
||||
signal.signal(signal.SIGINT, handle_sigint)
|
||||
mcp.run()
|
||||
|
||||
7
pom.xml
7
pom.xml
@ -70,6 +70,13 @@
|
||||
<systemPath>${ghidra.jar.location}/Base.jar</systemPath>
|
||||
</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 -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
|
||||
@ -11,6 +11,8 @@ import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.services.ProgramManager;
|
||||
import ghidra.framework.model.Project;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.plugintool.PluginInfo;
|
||||
import ghidra.framework.plugintool.util.PluginStatus;
|
||||
import ghidra.util.Msg;
|
||||
@ -30,6 +32,9 @@ import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.*;
|
||||
|
||||
// For JSON response handling
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
@PluginInfo(
|
||||
status = PluginStatus.RELEASED,
|
||||
packageName = ghidra.app.DeveloperPluginPackage.NAME,
|
||||
@ -208,6 +213,24 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
sendResponse(exchange, sb.toString());
|
||||
});
|
||||
|
||||
// Info endpoints - both root and /info for flexibility
|
||||
server.createContext("/info", exchange -> {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
sendJsonResponse(exchange, getProjectInfo());
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
}
|
||||
});
|
||||
|
||||
// Root endpoint also returns project info
|
||||
server.createContext("/", exchange -> {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
sendJsonResponse(exchange, getProjectInfo());
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
}
|
||||
});
|
||||
|
||||
server.createContext("/registerInstance", exchange -> {
|
||||
Map<String, String> params = parsePostParams(exchange);
|
||||
int port = parseIntOrDefault(params.get("port"), 0);
|
||||
@ -551,6 +574,52 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
return pm != null ? pm.getCurrentProgram() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current project and open file in JSON format
|
||||
*/
|
||||
private JSONObject getProjectInfo() {
|
||||
JSONObject info = new JSONObject();
|
||||
Program program = getCurrentProgram();
|
||||
|
||||
// Get project information if available
|
||||
Project project = tool.getProject();
|
||||
if (project != null) {
|
||||
info.put("project", project.getName());
|
||||
} else {
|
||||
info.put("project", "Unknown");
|
||||
}
|
||||
|
||||
// Create file information object
|
||||
JSONObject fileInfo = new JSONObject();
|
||||
|
||||
// Get current file information if available
|
||||
if (program != null) {
|
||||
// Basic info
|
||||
fileInfo.put("name", program.getName());
|
||||
|
||||
// Try to get more detailed info
|
||||
DomainFile domainFile = program.getDomainFile();
|
||||
if (domainFile != null) {
|
||||
fileInfo.put("path", domainFile.getPathname());
|
||||
}
|
||||
|
||||
// Add any additional file info we might want
|
||||
fileInfo.put("architecture", program.getLanguage().getProcessor().toString());
|
||||
fileInfo.put("endian", program.getLanguage().isBigEndian() ? "big" : "little");
|
||||
|
||||
info.put("file", fileInfo);
|
||||
} else {
|
||||
info.put("file", null);
|
||||
info.put("status", "No file open");
|
||||
}
|
||||
|
||||
// Add server metadata
|
||||
info.put("port", port);
|
||||
info.put("isBaseInstance", isBaseInstance);
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
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");
|
||||
@ -560,6 +629,19 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a JSON response to the client
|
||||
*/
|
||||
private void sendJsonResponse(HttpExchange exchange, JSONObject json) throws IOException {
|
||||
String jsonString = json.toJSONString();
|
||||
byte[] bytes = jsonString.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
private int findAvailablePort() {
|
||||
int basePort = 8192;
|
||||
int maxAttempts = 10;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user