feat: Add headless HTTP server and entrypoint scripts
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run
Some checks are pending
Build Ghidra Plugin / build (push) Waiting to run
- Add GhydraMCPServer.py with fixed strings endpoint (Jython compatible) - Fix strings endpoint to iterate through defined data instead of using DefinedDataIterator.definedStrings() which isn't accessible in Jython - Add entrypoint.sh for Docker container initialization
This commit is contained in:
parent
ee82f3b100
commit
88e1fe6ca8
348
docker/GhydraMCPServer.py
Normal file
348
docker/GhydraMCPServer.py
Normal file
@ -0,0 +1,348 @@
|
||||
# GhydraMCPServer.py - Headless Ghidra script for GhydraMCP HTTP API
|
||||
# Python/Jython scripts don't require OSGi bundle registration
|
||||
#
|
||||
# Usage: analyzeHeadless <project> <name> -import <binary> -postScript GhydraMCPServer.py [port]
|
||||
#
|
||||
#@category GhydraMCP
|
||||
#@keybinding
|
||||
#@menupath
|
||||
#@toolbar
|
||||
|
||||
from com.sun.net.httpserver import HttpServer, HttpHandler
|
||||
from java.net import InetSocketAddress
|
||||
from java.util.concurrent import Executors
|
||||
from java.io import OutputStream
|
||||
from ghidra.app.decompiler import DecompInterface
|
||||
from ghidra.program.model.listing import Function
|
||||
import json
|
||||
import threading
|
||||
|
||||
API_VERSION = 2 # Integer for MCP client compatibility (minimum expected: 2)
|
||||
API_VERSION_STRING = "2.1"
|
||||
DEFAULT_PORT = 8192
|
||||
|
||||
class GhydraMCPHandler(HttpHandler):
|
||||
def __init__(self, program, decompiler):
|
||||
self.program = program
|
||||
self.decompiler = decompiler
|
||||
|
||||
def handle(self, exchange):
|
||||
try:
|
||||
path = exchange.getRequestURI().getPath()
|
||||
method = exchange.getRequestMethod()
|
||||
|
||||
# Route to appropriate handler
|
||||
if path == "/" or path == "":
|
||||
response = self.handle_root()
|
||||
elif path == "/functions":
|
||||
response = self.handle_functions()
|
||||
elif path.endswith("/decompile"):
|
||||
# Handle /functions/{address}/decompile
|
||||
response = self.handle_decompile_by_path(path)
|
||||
elif path.startswith("/functions/"):
|
||||
response = self.handle_function_detail(path)
|
||||
elif path == "/strings" or path == "/data/strings":
|
||||
response = self.handle_strings()
|
||||
elif path == "/info":
|
||||
response = self.handle_info()
|
||||
elif path == "/decompile":
|
||||
response = self.handle_decompile(exchange)
|
||||
else:
|
||||
response = {"success": False, "error": "Not found", "path": path}
|
||||
|
||||
self.send_response(exchange, 200, response)
|
||||
except Exception as e:
|
||||
self.send_response(exchange, 500, {"success": False, "error": str(e)})
|
||||
|
||||
def send_response(self, exchange, code, data):
|
||||
response_bytes = json.dumps(data, indent=2).encode('utf-8')
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json")
|
||||
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", "*")
|
||||
exchange.sendResponseHeaders(code, len(response_bytes))
|
||||
os = exchange.getResponseBody()
|
||||
os.write(response_bytes)
|
||||
os.close()
|
||||
|
||||
def handle_root(self):
|
||||
return {
|
||||
"success": True,
|
||||
"api_version": API_VERSION,
|
||||
"message": "GhydraMCP API " + API_VERSION_STRING,
|
||||
"program": self.program.getName() if self.program else None,
|
||||
"endpoints": ["/", "/info", "/functions", "/functions/<name>", "/strings", "/decompile"],
|
||||
"_links": {
|
||||
"self": "/",
|
||||
"functions": "/functions",
|
||||
"strings": "/strings",
|
||||
"info": "/info"
|
||||
}
|
||||
}
|
||||
|
||||
def handle_info(self):
|
||||
if not self.program:
|
||||
return {"success": False, "error": "No program loaded"}
|
||||
return {
|
||||
"success": True,
|
||||
"name": self.program.getName(),
|
||||
"path": self.program.getExecutablePath(),
|
||||
"language": str(self.program.getLanguage().getLanguageID()),
|
||||
"processor": str(self.program.getLanguage().getProcessor()),
|
||||
"addressSize": self.program.getAddressFactory().getDefaultAddressSpace().getSize(),
|
||||
"imageBase": str(self.program.getImageBase()),
|
||||
}
|
||||
|
||||
def handle_functions(self):
|
||||
if not self.program:
|
||||
return {"success": False, "error": "No program loaded"}
|
||||
|
||||
functions = []
|
||||
fm = self.program.getFunctionManager()
|
||||
for func in fm.getFunctions(True): # True = forward iteration
|
||||
functions.append({
|
||||
"name": func.getName(),
|
||||
"address": str(func.getEntryPoint()),
|
||||
"signature": str(func.getSignature()),
|
||||
})
|
||||
if len(functions) >= 10000: # Higher limit for MCP client
|
||||
break
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"size": len(functions),
|
||||
"result": functions, # MCP client expects "result" key
|
||||
}
|
||||
|
||||
def handle_function_detail(self, path):
|
||||
if not self.program:
|
||||
return {"success": False, "error": "No program loaded"}
|
||||
|
||||
# Extract function name or address from path
|
||||
parts = path.split("/")
|
||||
if len(parts) < 3:
|
||||
return {"success": False, "error": "Invalid path"}
|
||||
|
||||
name_or_addr = parts[2]
|
||||
fm = self.program.getFunctionManager()
|
||||
|
||||
# Try to find by name first
|
||||
func = None
|
||||
for f in fm.getFunctions(True):
|
||||
if f.getName() == name_or_addr:
|
||||
func = f
|
||||
break
|
||||
|
||||
# If not found, try by address
|
||||
if not func:
|
||||
try:
|
||||
addr = self.program.getAddressFactory().getAddress(name_or_addr)
|
||||
func = fm.getFunctionAt(addr)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not func:
|
||||
return {"success": False, "error": "Function not found: " + name_or_addr}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"name": func.getName(),
|
||||
"address": str(func.getEntryPoint()),
|
||||
"signature": str(func.getSignature()),
|
||||
"body": str(func.getBody()),
|
||||
"callingConvention": func.getCallingConventionName(),
|
||||
"parameterCount": func.getParameterCount(),
|
||||
}
|
||||
|
||||
def handle_strings(self):
|
||||
if not self.program:
|
||||
return {"success": False, "error": "No program loaded"}
|
||||
|
||||
strings = []
|
||||
listing = self.program.getListing()
|
||||
|
||||
# Iterate through all defined data and filter for string types
|
||||
for data in listing.getDefinedData(True): # True = forward iteration
|
||||
try:
|
||||
dt = data.getDataType()
|
||||
if not dt:
|
||||
continue
|
||||
|
||||
# Check if data type is a string variant
|
||||
type_name = dt.getName().lower()
|
||||
if "string" in type_name or type_name in ("char", "wchar"):
|
||||
value = data.getValue()
|
||||
if value:
|
||||
str_val = str(value)
|
||||
if len(str_val) > 1: # Skip single chars
|
||||
strings.append({
|
||||
"address": str(data.getAddress()),
|
||||
"value": str_val[:200], # Truncate long strings
|
||||
"length": len(str_val)
|
||||
})
|
||||
if len(strings) >= 5000:
|
||||
break
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"size": len(strings),
|
||||
"result": strings, # MCP client expects "result" key
|
||||
}
|
||||
|
||||
def handle_decompile_by_path(self, path):
|
||||
"""Handle /functions/{address}/decompile or /functions/by-name/{name}/decompile"""
|
||||
if not self.program:
|
||||
return {"success": False, "error": "No program loaded"}
|
||||
|
||||
# Parse address or name from path
|
||||
# /functions/000496e8/decompile or /functions/by-name/main/decompile
|
||||
parts = path.split("/")
|
||||
|
||||
func = None
|
||||
fm = self.program.getFunctionManager()
|
||||
|
||||
if "by-name" in path and len(parts) >= 4:
|
||||
# /functions/by-name/{name}/decompile
|
||||
name = parts[3]
|
||||
for f in fm.getFunctions(True):
|
||||
if f.getName() == name:
|
||||
func = f
|
||||
break
|
||||
elif len(parts) >= 3:
|
||||
# /functions/{address}/decompile
|
||||
addr_str = parts[2]
|
||||
try:
|
||||
addr = self.program.getAddressFactory().getAddress(addr_str)
|
||||
func = fm.getFunctionAt(addr)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not func:
|
||||
return {"success": False, "error": "Function not found from path: " + path}
|
||||
|
||||
# Decompile
|
||||
try:
|
||||
result = self.decompiler.decompileFunction(func, 30, getMonitor())
|
||||
if result and result.decompileCompleted():
|
||||
code = result.getDecompiledFunction().getC()
|
||||
return {
|
||||
"success": True,
|
||||
"result": {
|
||||
"name": func.getName(),
|
||||
"address": str(func.getEntryPoint()),
|
||||
"decompiled_text": code,
|
||||
"ccode": code,
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {"success": False, "error": "Decompilation failed"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def handle_decompile(self, exchange):
|
||||
if not self.program:
|
||||
return {"success": False, "error": "No program loaded"}
|
||||
|
||||
# Get function name from query params
|
||||
query = exchange.getRequestURI().getQuery()
|
||||
if not query:
|
||||
return {"success": False, "error": "Missing 'name' or 'address' parameter"}
|
||||
|
||||
params = {}
|
||||
for part in query.split("&"):
|
||||
if "=" in part:
|
||||
k, v = part.split("=", 1)
|
||||
params[k] = v
|
||||
|
||||
name = params.get("name") or params.get("address")
|
||||
if not name:
|
||||
return {"success": False, "error": "Missing 'name' or 'address' parameter"}
|
||||
|
||||
# Find function
|
||||
fm = self.program.getFunctionManager()
|
||||
func = None
|
||||
for f in fm.getFunctions(True):
|
||||
if f.getName() == name:
|
||||
func = f
|
||||
break
|
||||
|
||||
if not func:
|
||||
try:
|
||||
addr = self.program.getAddressFactory().getAddress(name)
|
||||
func = fm.getFunctionAt(addr)
|
||||
except:
|
||||
pass
|
||||
|
||||
if not func:
|
||||
return {"success": False, "error": "Function not found: " + name}
|
||||
|
||||
# Decompile
|
||||
try:
|
||||
result = self.decompiler.decompileFunction(func, 30, getMonitor())
|
||||
if result and result.decompileCompleted():
|
||||
code = result.getDecompiledFunction().getC()
|
||||
return {
|
||||
"success": True,
|
||||
"name": func.getName(),
|
||||
"address": str(func.getEntryPoint()),
|
||||
"decompiled": code
|
||||
}
|
||||
else:
|
||||
return {"success": False, "error": "Decompilation failed"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def run_server(port, program, decompiler):
|
||||
"""Start the HTTP server"""
|
||||
server = HttpServer.create(InetSocketAddress(port), 0)
|
||||
server.createContext("/", GhydraMCPHandler(program, decompiler))
|
||||
server.setExecutor(Executors.newCachedThreadPool())
|
||||
server.start()
|
||||
println("GhydraMCP HTTP server started on port " + str(port))
|
||||
return server
|
||||
|
||||
|
||||
# Main script execution
|
||||
def main():
|
||||
port = DEFAULT_PORT
|
||||
|
||||
# Parse port from script arguments
|
||||
args = getScriptArgs()
|
||||
if args and len(args) > 0:
|
||||
try:
|
||||
port = int(args[0])
|
||||
except:
|
||||
println("Invalid port number, using default: " + str(DEFAULT_PORT))
|
||||
|
||||
# Initialize decompiler
|
||||
decompiler = DecompInterface()
|
||||
decompiler.openProgram(currentProgram)
|
||||
|
||||
println("=========================================")
|
||||
println(" GhydraMCP Headless HTTP Server")
|
||||
println("=========================================")
|
||||
println(" API Version: " + API_VERSION_STRING + " (compat: " + str(API_VERSION) + ")")
|
||||
println(" Port: " + str(port))
|
||||
println(" Program: " + (currentProgram.getName() if currentProgram else "None"))
|
||||
println(" Script: Python/Jython")
|
||||
println("=========================================")
|
||||
|
||||
server = run_server(port, currentProgram, decompiler)
|
||||
|
||||
println("")
|
||||
println("GhydraMCP Server running. Press Ctrl+C to stop.")
|
||||
println("API available at: http://localhost:" + str(port) + "/")
|
||||
|
||||
# Keep the script running
|
||||
import time
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
server.stop(0)
|
||||
println("Server stopped.")
|
||||
|
||||
|
||||
# Run the main function
|
||||
main()
|
||||
145
docker/entrypoint.sh
Executable file
145
docker/entrypoint.sh
Executable file
@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
# GhydraMCP Docker Entrypoint
|
||||
# Starts Ghidra in headless mode with HTTP API server
|
||||
|
||||
set -e
|
||||
|
||||
GHYDRA_MODE=${GHYDRA_MODE:-headless}
|
||||
GHYDRA_PORT=${GHYDRA_PORT:-8192}
|
||||
GHYDRA_MAXMEM=${GHYDRA_MAXMEM:-2G}
|
||||
GHIDRA_HOME=${GHIDRA_HOME:-/opt/ghidra}
|
||||
# User scripts directory - Python scripts don't need OSGi bundle registration
|
||||
SCRIPT_DIR=${SCRIPT_DIR:-/home/ghidra/ghidra_scripts}
|
||||
|
||||
# Project settings
|
||||
PROJECT_DIR=${PROJECT_DIR:-/projects}
|
||||
PROJECT_NAME=${PROJECT_NAME:-GhydraMCP}
|
||||
|
||||
echo "=============================================="
|
||||
echo " GhydraMCP Docker Container"
|
||||
echo "=============================================="
|
||||
echo " Mode: ${GHYDRA_MODE}"
|
||||
echo " Port: ${GHYDRA_PORT}"
|
||||
echo " Memory: ${GHYDRA_MAXMEM}"
|
||||
echo " Project: ${PROJECT_DIR}/${PROJECT_NAME}"
|
||||
echo "=============================================="
|
||||
|
||||
# Ensure directories exist
|
||||
mkdir -p "${PROJECT_DIR}"
|
||||
|
||||
# Handle different modes
|
||||
case "${GHYDRA_MODE}" in
|
||||
headless)
|
||||
# Headless mode: Import a binary and start HTTP server
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo ""
|
||||
echo "Usage: docker run ghydramcp:latest [binary_path] [options]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " # Analyze a binary mounted at /binaries/sample.exe"
|
||||
echo " docker run -p 8192:8192 -v ./samples:/binaries ghydramcp /binaries/sample.exe"
|
||||
echo ""
|
||||
echo " # With custom project name"
|
||||
echo " docker run -p 8192:8192 -v ./samples:/binaries -e PROJECT_NAME=malware ghydramcp /binaries/sample.exe"
|
||||
echo ""
|
||||
echo "Environment variables:"
|
||||
echo " GHYDRA_PORT - HTTP API port (default: 8192)"
|
||||
echo " GHYDRA_MAXMEM - Max JVM heap (default: 2G)"
|
||||
echo " PROJECT_NAME - Ghidra project name (default: GhydraMCP)"
|
||||
echo " PROJECT_DIR - Project directory (default: /projects)"
|
||||
echo ""
|
||||
echo "Starting in wait mode..."
|
||||
echo "Container will stay running for debugging or manual operation."
|
||||
echo "You can exec into this container to run analyzeHeadless manually."
|
||||
echo ""
|
||||
|
||||
# Keep container alive for debugging/manual operation
|
||||
tail -f /dev/null
|
||||
else
|
||||
BINARY_PATH="$1"
|
||||
shift
|
||||
|
||||
if [ ! -f "${BINARY_PATH}" ]; then
|
||||
echo "ERROR: Binary not found: ${BINARY_PATH}"
|
||||
echo "Make sure to mount the binary directory with -v /host/path:/binaries"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY_NAME=$(basename "${BINARY_PATH}")
|
||||
echo "Importing and analyzing: ${BINARY_NAME}"
|
||||
echo ""
|
||||
|
||||
# Build the analyzeHeadless command
|
||||
ANALYZE_CMD="${GHIDRA_HOME}/support/analyzeHeadless"
|
||||
ANALYZE_ARGS=(
|
||||
"${PROJECT_DIR}"
|
||||
"${PROJECT_NAME}"
|
||||
-import "${BINARY_PATH}"
|
||||
-max-cpu 2
|
||||
-scriptPath "${SCRIPT_DIR}"
|
||||
-postScript "GhydraMCPServer.py" "${GHYDRA_PORT}"
|
||||
)
|
||||
|
||||
# Add any extra arguments passed
|
||||
ANALYZE_ARGS+=("$@")
|
||||
|
||||
echo "Running: ${ANALYZE_CMD} ${ANALYZE_ARGS[*]}"
|
||||
echo ""
|
||||
|
||||
exec "${ANALYZE_CMD}" "${ANALYZE_ARGS[@]}"
|
||||
fi
|
||||
;;
|
||||
|
||||
server)
|
||||
# Server mode: Open existing project with HTTP server
|
||||
echo "Starting GhydraMCP server on existing project..."
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: docker run -e GHYDRA_MODE=server ghydramcp [program_name]"
|
||||
echo ""
|
||||
echo " program_name: Name of program in the project to open"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROGRAM_NAME="$1"
|
||||
shift
|
||||
|
||||
exec "${GHIDRA_HOME}/support/analyzeHeadless" \
|
||||
"${PROJECT_DIR}" "${PROJECT_NAME}" \
|
||||
-process "${PROGRAM_NAME}" \
|
||||
-noanalysis \
|
||||
-scriptPath "${SCRIPT_DIR}" \
|
||||
-postScript "GhydraMCPServer.py" "${GHYDRA_PORT}" \
|
||||
"$@"
|
||||
;;
|
||||
|
||||
analyze)
|
||||
# Analyze mode: Import and analyze, then exit (no HTTP server)
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: docker run -e GHYDRA_MODE=analyze ghydramcp [binary_path]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY_PATH="$1"
|
||||
shift
|
||||
|
||||
echo "Analyzing binary: ${BINARY_PATH}"
|
||||
exec "${GHIDRA_HOME}/support/analyzeHeadless" \
|
||||
"${PROJECT_DIR}" "${PROJECT_NAME}" \
|
||||
-import "${BINARY_PATH}" \
|
||||
-max-cpu 2 \
|
||||
"$@"
|
||||
;;
|
||||
|
||||
shell)
|
||||
# Interactive shell
|
||||
exec /bin/bash
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown mode: ${GHYDRA_MODE}"
|
||||
echo "Valid modes: headless, server, analyze, shell"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Loading…
x
Reference in New Issue
Block a user