Merge branch 'variable-mangling'
This commit is contained in:
commit
d4611377c8
52
CHANGELOG.md
Normal file
52
CHANGELOG.md
Normal file
@ -0,0 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Added docstrings for all @mcp.tool functions
|
||||
- Variable manipulation tools (rename/retype variables)
|
||||
- New endpoints for function variable management
|
||||
- Dynamic version output in API responses
|
||||
- Enhanced function analysis capabilities
|
||||
- Support for searching variables by name
|
||||
- New tools for working with function variables:
|
||||
- get_function_by_address
|
||||
- get_current_address
|
||||
- get_current_function
|
||||
- decompile_function_by_address
|
||||
- disassemble_function
|
||||
- set_decompiler_comment
|
||||
- set_disassembly_comment
|
||||
- rename_local_variable
|
||||
- rename_function_by_address
|
||||
- set_function_prototype
|
||||
- set_local_variable_type
|
||||
|
||||
### Changed
|
||||
- Improved version handling in build system
|
||||
- Reorganized imports in bridge_mcp_hydra.py
|
||||
- Updated MANIFEST.MF with more detailed description
|
||||
|
||||
## [1.2.0] - 2024-06-15
|
||||
|
||||
### Added
|
||||
- Enhanced function analysis capabilities
|
||||
- Additional variable manipulation tools
|
||||
- Support for multiple Ghidra instances
|
||||
|
||||
### Changed
|
||||
- Improved error handling in API calls
|
||||
- Optimized performance for large binaries
|
||||
|
||||
## [1.0.0] - 2024-03-15
|
||||
|
||||
### Added
|
||||
- Initial release of GhydraMCP bridge
|
||||
- Basic Ghidra instance management tools
|
||||
- Function analysis tools
|
||||
- Variable manipulation tools
|
||||
@ -6,12 +6,14 @@
|
||||
# ]
|
||||
# ///
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
import requests
|
||||
import threading
|
||||
from typing import Dict
|
||||
import time
|
||||
from threading import Lock
|
||||
from typing import Dict
|
||||
|
||||
import requests
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
|
||||
# Track active Ghidra instances (port -> info dict)
|
||||
@ -146,26 +148,54 @@ def register_instance(port: int, url: str = None) -> str:
|
||||
project_info = {"url": url}
|
||||
|
||||
try:
|
||||
info_url = f"{url}/info"
|
||||
info_response = requests.get(info_url, timeout=2)
|
||||
if info_response.ok:
|
||||
# Try the root endpoint first
|
||||
root_url = f"{url}/"
|
||||
print(f"Trying to get root info from {root_url}", file=sys.stderr)
|
||||
root_response = requests.get(root_url, timeout=1.5) # Short timeout for root
|
||||
|
||||
if root_response.ok:
|
||||
try:
|
||||
# Parse JSON response
|
||||
info_data = info_response.json()
|
||||
print(f"Got response from root: {root_response.text}", file=sys.stderr)
|
||||
root_data = root_response.json()
|
||||
|
||||
# Extract relevant information
|
||||
project_info["project"] = info_data.get("project", "Unknown")
|
||||
# Extract basic information from root
|
||||
if "project" in root_data and root_data["project"]:
|
||||
project_info["project"] = root_data["project"]
|
||||
if "file" in root_data and root_data["file"]:
|
||||
project_info["file"] = root_data["file"]
|
||||
|
||||
# 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
|
||||
print(f"Root data parsed: {project_info}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Error parsing root info: {e}", file=sys.stderr)
|
||||
else:
|
||||
print(f"Root endpoint returned {root_response.status_code}", file=sys.stderr)
|
||||
|
||||
# If we don't have project info yet, try the /info endpoint as a fallback
|
||||
if not project_info.get("project") and not project_info.get("file"):
|
||||
info_url = f"{url}/info"
|
||||
print(f"Trying fallback info from {info_url}", file=sys.stderr)
|
||||
|
||||
try:
|
||||
info_response = requests.get(info_url, timeout=2)
|
||||
if info_response.ok:
|
||||
try:
|
||||
info_data = info_response.json()
|
||||
# Extract relevant information
|
||||
if "project" in info_data and info_data["project"]:
|
||||
project_info["project"] = info_data["project"]
|
||||
|
||||
# Handle file information
|
||||
file_info = info_data.get("file", {})
|
||||
if isinstance(file_info, dict) and file_info.get("name"):
|
||||
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", "")
|
||||
print(f"Info data parsed: {project_info}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Error parsing info endpoint: {e}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
print(f"Error connecting to info endpoint: {e}", file=sys.stderr)
|
||||
except Exception:
|
||||
# Non-critical, continue with registration even if project info fails
|
||||
pass
|
||||
@ -223,6 +253,7 @@ def _discover_instances(port_range, host=None, timeout=0.5) -> dict:
|
||||
# Updated tool implementations with port parameter
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_functions(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||
"""List all functions with pagination"""
|
||||
@ -250,33 +281,211 @@ def update_data(port: int = DEFAULT_GHIDRA_PORT, address: str = "", new_name: st
|
||||
|
||||
@mcp.tool()
|
||||
def list_segments(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||
"""List all memory segments in the current program with pagination
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (default: 8192)
|
||||
offset: Pagination offset (default: 0)
|
||||
limit: Maximum number of segments to return (default: 100)
|
||||
|
||||
Returns:
|
||||
List of segment information strings
|
||||
"""
|
||||
return safe_get(port, "segments", {"offset": offset, "limit": limit})
|
||||
|
||||
@mcp.tool()
|
||||
def list_imports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||
"""List all imported symbols with pagination
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (default: 8192)
|
||||
offset: Pagination offset (default: 0)
|
||||
limit: Maximum number of imports to return (default: 100)
|
||||
|
||||
Returns:
|
||||
List of import information strings
|
||||
"""
|
||||
return safe_get(port, "symbols/imports", {"offset": offset, "limit": limit})
|
||||
|
||||
@mcp.tool()
|
||||
def list_exports(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||
"""List all exported symbols with pagination
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (default: 8192)
|
||||
offset: Pagination offset (default: 0)
|
||||
limit: Maximum number of exports to return (default: 100)
|
||||
|
||||
Returns:
|
||||
List of export information strings
|
||||
"""
|
||||
return safe_get(port, "symbols/exports", {"offset": offset, "limit": limit})
|
||||
|
||||
@mcp.tool()
|
||||
def list_namespaces(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||
"""List all namespaces in the current program with pagination
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (default: 8192)
|
||||
offset: Pagination offset (default: 0)
|
||||
limit: Maximum number of namespaces to return (default: 100)
|
||||
|
||||
Returns:
|
||||
List of namespace information strings
|
||||
"""
|
||||
return safe_get(port, "namespaces", {"offset": offset, "limit": limit})
|
||||
|
||||
@mcp.tool()
|
||||
def list_data_items(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100) -> list:
|
||||
"""List all defined data items with pagination
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (default: 8192)
|
||||
offset: Pagination offset (default: 0)
|
||||
limit: Maximum number of data items to return (default: 100)
|
||||
|
||||
Returns:
|
||||
List of data item information strings
|
||||
"""
|
||||
return safe_get(port, "data", {"offset": offset, "limit": limit})
|
||||
|
||||
@mcp.tool()
|
||||
def search_functions_by_name(port: int = DEFAULT_GHIDRA_PORT, query: str = "", offset: int = 0, limit: int = 100) -> list:
|
||||
"""Search for functions by name with pagination
|
||||
|
||||
Args:
|
||||
port: Ghidra instance port (default: 8192)
|
||||
query: Search string to match against function names
|
||||
offset: Pagination offset (default: 0)
|
||||
limit: Maximum number of functions to return (default: 100)
|
||||
|
||||
Returns:
|
||||
List of matching function information strings or error message if query is empty
|
||||
"""
|
||||
if not query:
|
||||
return ["Error: query string is required"]
|
||||
return safe_get(port, "functions", {"query": query, "offset": offset, "limit": limit})
|
||||
|
||||
# Handle graceful shutdown
|
||||
import signal
|
||||
import os
|
||||
@mcp.tool()
|
||||
def get_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str:
|
||||
"""
|
||||
Get a function by its address.
|
||||
"""
|
||||
return "\n".join(safe_get(port, "get_function_by_address", {"address": address}))
|
||||
|
||||
@mcp.tool()
|
||||
def get_current_address(port: int = DEFAULT_GHIDRA_PORT) -> str:
|
||||
"""
|
||||
Get the address currently selected by the user.
|
||||
"""
|
||||
return "\n".join(safe_get(port, "get_current_address"))
|
||||
|
||||
@mcp.tool()
|
||||
def get_current_function(port: int = DEFAULT_GHIDRA_PORT) -> str:
|
||||
"""
|
||||
Get the function currently selected by the user.
|
||||
"""
|
||||
return "\n".join(safe_get(port, "get_current_function"))
|
||||
|
||||
@mcp.tool()
|
||||
def list_functions(port: int = DEFAULT_GHIDRA_PORT) -> list:
|
||||
"""
|
||||
List all functions in the database.
|
||||
"""
|
||||
return safe_get(port, "list_functions")
|
||||
|
||||
@mcp.tool()
|
||||
def decompile_function_by_address(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> str:
|
||||
"""
|
||||
Decompile a function at the given address.
|
||||
"""
|
||||
return "\n".join(safe_get(port, "decompile_function", {"address": address}))
|
||||
|
||||
@mcp.tool()
|
||||
def disassemble_function(port: int = DEFAULT_GHIDRA_PORT, address: str = "") -> list:
|
||||
"""
|
||||
Get assembly code (address: instruction; comment) for a function.
|
||||
"""
|
||||
return safe_get(port, "disassemble_function", {"address": address})
|
||||
|
||||
@mcp.tool()
|
||||
def set_decompiler_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str:
|
||||
"""
|
||||
Set a comment for a given address in the function pseudocode.
|
||||
"""
|
||||
return safe_post(port, "set_decompiler_comment", {"address": address, "comment": comment})
|
||||
|
||||
@mcp.tool()
|
||||
def set_disassembly_comment(port: int = DEFAULT_GHIDRA_PORT, address: str = "", comment: str = "") -> str:
|
||||
"""
|
||||
Set a comment for a given address in the function disassembly.
|
||||
"""
|
||||
return safe_post(port, "set_disassembly_comment", {"address": address, "comment": comment})
|
||||
|
||||
@mcp.tool()
|
||||
def rename_local_variable(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", old_name: str = "", new_name: str = "") -> str:
|
||||
"""
|
||||
Rename a local variable in a function.
|
||||
"""
|
||||
return safe_post(port, "rename_local_variable", {"function_address": function_address, "old_name": old_name, "new_name": new_name})
|
||||
|
||||
@mcp.tool()
|
||||
def rename_function_by_address(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", new_name: str = "") -> str:
|
||||
"""
|
||||
Rename a function by its address.
|
||||
"""
|
||||
return safe_post(port, "rename_function_by_address", {"function_address": function_address, "new_name": new_name})
|
||||
|
||||
@mcp.tool()
|
||||
def set_function_prototype(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", prototype: str = "") -> str:
|
||||
"""
|
||||
Set a function's prototype.
|
||||
"""
|
||||
return safe_post(port, "set_function_prototype", {"function_address": function_address, "prototype": prototype})
|
||||
|
||||
@mcp.tool()
|
||||
def set_local_variable_type(port: int = DEFAULT_GHIDRA_PORT, function_address: str = "", variable_name: str = "", new_type: str = "") -> str:
|
||||
"""
|
||||
Set a local variable's type.
|
||||
"""
|
||||
return safe_post(port, "set_local_variable_type", {"function_address": function_address, "variable_name": variable_name, "new_type": new_type})
|
||||
|
||||
@mcp.tool()
|
||||
def list_variables(port: int = DEFAULT_GHIDRA_PORT, offset: int = 0, limit: int = 100, search: str = "") -> list:
|
||||
"""List global variables with optional search"""
|
||||
params = {"offset": offset, "limit": limit}
|
||||
if search:
|
||||
params["search"] = search
|
||||
return safe_get(port, "variables", params)
|
||||
|
||||
@mcp.tool()
|
||||
def list_function_variables(port: int = DEFAULT_GHIDRA_PORT, function: str = "") -> str:
|
||||
"""List variables in a specific function"""
|
||||
if not function:
|
||||
return "Error: function name is required"
|
||||
|
||||
encoded_name = quote(function)
|
||||
return safe_get(port, f"functions/{encoded_name}/variables", {})
|
||||
|
||||
@mcp.tool()
|
||||
def rename_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: str = "", new_name: str = "") -> str:
|
||||
"""Rename a variable in a function"""
|
||||
if not function or not name or not new_name:
|
||||
return "Error: function, name, and new_name parameters are required"
|
||||
|
||||
encoded_function = quote(function)
|
||||
encoded_var = quote(name)
|
||||
return safe_put(port, f"functions/{encoded_function}/variables/{encoded_var}", {"newName": new_name})
|
||||
|
||||
@mcp.tool()
|
||||
def retype_variable(port: int = DEFAULT_GHIDRA_PORT, function: str = "", name: str = "", data_type: str = "") -> str:
|
||||
"""Change the data type of a variable in a function"""
|
||||
if not function or not name or not data_type:
|
||||
return "Error: function, name, and data_type parameters are required"
|
||||
|
||||
encoded_function = quote(function)
|
||||
encoded_var = quote(name)
|
||||
return safe_put(port, f"functions/{encoded_function}/variables/{encoded_var}", {"dataType": data_type})
|
||||
|
||||
def handle_sigint(signum, frame):
|
||||
os._exit(0)
|
||||
|
||||
110
pom.xml
110
pom.xml
@ -6,7 +6,7 @@
|
||||
<groupId>eu.starsong.ghidra</groupId>
|
||||
<artifactId>GhydraMCP</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>11.3.1</version>
|
||||
<version>${revision}</version>
|
||||
<name>GhydraMCP</name>
|
||||
<url>https://github.com/teal-bauer/GhydraMCP</url>
|
||||
|
||||
@ -16,10 +16,19 @@
|
||||
<ghidra.jar.location>${project.basedir}/lib</ghidra.jar.location>
|
||||
<maven.deploy.skip>true</maven.deploy.skip>
|
||||
<maven.install.skip>true</maven.install.skip>
|
||||
<maven.build.timestamp.format>yyyyMMdd-HHmmss</maven.build.timestamp.format>
|
||||
<revision>dev-SNAPSHOT</revision>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Ghidra JARs as system-scoped dependencies for runtime -->
|
||||
<!-- JSON handling -->
|
||||
<dependency>
|
||||
<groupId>com.googlecode.json-simple</groupId>
|
||||
<artifactId>json-simple</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Ghidra JARs as system-scoped dependencies -->
|
||||
<dependency>
|
||||
<groupId>ghidra</groupId>
|
||||
<artifactId>Generic</artifactId>
|
||||
@ -70,13 +79,6 @@
|
||||
<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>
|
||||
@ -87,8 +89,24 @@
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<!-- Set Java version -->
|
||||
<!-- Resources plugin to handle filtering -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<configuration>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
@ -101,13 +119,74 @@
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Git Commit ID plugin to generate version from git -->
|
||||
<plugin>
|
||||
<groupId>io.github.git-commit-id</groupId>
|
||||
<artifactId>git-commit-id-maven-plugin</artifactId>
|
||||
<version>5.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>get-git-info</id>
|
||||
<phase>initialize</phase>
|
||||
<goals>
|
||||
<goal>revision</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<generateGitPropertiesFile>true</generateGitPropertiesFile>
|
||||
<generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
|
||||
<includeOnlyProperties>
|
||||
<includeOnlyProperty>git.commit.id.abbrev</includeOnlyProperty>
|
||||
<includeOnlyProperty>git.commit.time</includeOnlyProperty>
|
||||
<includeOnlyProperty>git.closest.tag.name</includeOnlyProperty>
|
||||
<includeOnlyProperty>git.build.version</includeOnlyProperty>
|
||||
</includeOnlyProperties>
|
||||
<commitIdGenerationMode>full</commitIdGenerationMode>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Set revision property from git info -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>set-revision-from-git</id>
|
||||
<phase>initialize</phase>
|
||||
<goals>
|
||||
<goal>regex-property</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<name>revision</name>
|
||||
<value>${git.commit.id.abbrev}-${maven.build.timestamp}</value>
|
||||
<regex>.*</regex>
|
||||
<replacement>$0</replacement>
|
||||
<failIfNoMatch>false</failIfNoMatch>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!-- Use custom MANIFEST.MF -->
|
||||
<plugin>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.2.2</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
|
||||
<manifest>
|
||||
<addDefaultImplementationEntries>false</addDefaultImplementationEntries>
|
||||
</manifest>
|
||||
<manifestEntries>
|
||||
<Implementation-Title>GhydraMCP</Implementation-Title>
|
||||
<Implementation-Version>${git.commit.id.abbrev}-${maven.build.timestamp}</Implementation-Version>
|
||||
<Plugin-Class>eu.starsong.ghidra.GhydraMCP</Plugin-Class>
|
||||
<Plugin-Name>GhydraMCP</Plugin-Name>
|
||||
<Plugin-Version>${git.commit.id.abbrev}-${maven.build.timestamp}</Plugin-Version>
|
||||
<Plugin-Author>LaurieWired, Teal Bauer</Plugin-Author>
|
||||
<Plugin-Description>Expose multiple Ghidra tools to MCP servers with variable management</Plugin-Description>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
<finalName>GhydraMCP</finalName>
|
||||
<excludes>
|
||||
@ -134,7 +213,7 @@
|
||||
<descriptors>
|
||||
<descriptor>src/assembly/ghidra-extension.xml</descriptor>
|
||||
</descriptors>
|
||||
<finalName>GhydraMCP-${project.version}</finalName>
|
||||
<finalName>GhydraMCP-${git.commit.id.abbrev}-${maven.build.timestamp}</finalName>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
</execution>
|
||||
@ -150,7 +229,7 @@
|
||||
<descriptors>
|
||||
<descriptor>src/assembly/complete-package.xml</descriptor>
|
||||
</descriptors>
|
||||
<finalName>GhydraMCP-Complete-${project.version}</finalName>
|
||||
<finalName>GhydraMCP-Complete-${git.commit.id.abbrev}-${maven.build.timestamp}</finalName>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
</configuration>
|
||||
</execution>
|
||||
@ -186,7 +265,14 @@
|
||||
<configuration>
|
||||
<failOnWarning>false</failOnWarning>
|
||||
<ignoredUnusedDeclaredDependencies>
|
||||
<ignoredUnusedDeclaredDependency>ghidra:Generic</ignoredUnusedDeclaredDependency>
|
||||
<ignoredUnusedDeclaredDependency>ghidra:SoftwareModeling</ignoredUnusedDeclaredDependency>
|
||||
<ignoredUnusedDeclaredDependency>ghidra:Project</ignoredUnusedDeclaredDependency>
|
||||
<ignoredUnusedDeclaredDependency>ghidra:Docking</ignoredUnusedDeclaredDependency>
|
||||
<ignoredUnusedDeclaredDependency>ghidra:Decompiler</ignoredUnusedDeclaredDependency>
|
||||
<ignoredUnusedDeclaredDependency>ghidra:Utility</ignoredUnusedDeclaredDependency>
|
||||
<ignoredUnusedDeclaredDependency>ghidra:Base</ignoredUnusedDeclaredDependency>
|
||||
<ignoredUnusedDeclaredDependency>junit:junit</ignoredUnusedDeclaredDependency>
|
||||
</ignoredUnusedDeclaredDependencies>
|
||||
<ignoredSystemDependencies>
|
||||
<ignoredSystemDependency>ghidra:*</ignoredSystemDependency>
|
||||
|
||||
@ -4,13 +4,26 @@ import ghidra.framework.plugintool.*;
|
||||
import ghidra.framework.main.ApplicationLevelPlugin;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.GlobalNamespace;
|
||||
import ghidra.program.model.data.DataType;
|
||||
import ghidra.program.model.data.DataTypeManager;
|
||||
import ghidra.program.model.listing.*;
|
||||
import ghidra.program.model.mem.MemoryBlock;
|
||||
import ghidra.program.model.pcode.HighVariable;
|
||||
import ghidra.program.model.pcode.HighSymbol;
|
||||
import ghidra.program.model.pcode.VarnodeAST;
|
||||
import ghidra.program.model.pcode.HighFunction;
|
||||
import ghidra.program.model.pcode.HighFunctionDBUtil;
|
||||
import ghidra.program.model.pcode.LocalSymbolMap;
|
||||
import ghidra.program.model.pcode.HighFunctionDBUtil.ReturnCommitOption;
|
||||
import ghidra.program.model.symbol.*;
|
||||
import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.app.decompiler.ClangNode;
|
||||
import ghidra.app.decompiler.ClangTokenGroup;
|
||||
import ghidra.app.decompiler.ClangVariableToken;
|
||||
import ghidra.app.plugin.PluginCategoryNames;
|
||||
import ghidra.app.services.ProgramManager;
|
||||
import ghidra.app.util.demangler.DemanglerUtil;
|
||||
import ghidra.framework.model.Project;
|
||||
import ghidra.framework.model.DomainFile;
|
||||
import ghidra.framework.plugintool.PluginInfo;
|
||||
@ -35,6 +48,23 @@ import java.util.concurrent.atomic.*;
|
||||
// For JSON response handling
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import ghidra.app.services.CodeViewerService;
|
||||
import ghidra.app.util.PseudoDisassembler;
|
||||
import ghidra.app.cmd.function.SetVariableNameCmd;
|
||||
import ghidra.program.model.symbol.SourceType;
|
||||
import ghidra.program.model.listing.LocalVariableImpl;
|
||||
import ghidra.program.model.listing.ParameterImpl;
|
||||
import ghidra.util.exception.DuplicateNameException;
|
||||
import ghidra.util.exception.InvalidInputException;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import ghidra.program.model.pcode.Varnode;
|
||||
import ghidra.program.model.data.PointerDataType;
|
||||
import ghidra.program.model.data.Undefined1DataType;
|
||||
import ghidra.program.model.listing.Variable;
|
||||
import ghidra.app.decompiler.component.DecompilerUtils;
|
||||
import ghidra.app.decompiler.ClangToken;
|
||||
|
||||
@PluginInfo(
|
||||
status = PluginStatus.RELEASED,
|
||||
packageName = ghidra.app.DeveloperPluginPackage.NAME,
|
||||
@ -105,25 +135,69 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
|
||||
server.createContext("/functions/", exchange -> {
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
String name = path.substring(path.lastIndexOf('/') + 1);
|
||||
|
||||
// Handle sub-paths: /functions/{name}
|
||||
// or /functions/{name}/variables
|
||||
String[] pathParts = path.split("/");
|
||||
|
||||
if (pathParts.length < 3) {
|
||||
exchange.sendResponseHeaders(400, -1); // Bad Request
|
||||
return;
|
||||
}
|
||||
|
||||
String functionName = pathParts[2];
|
||||
try {
|
||||
name = java.net.URLDecoder.decode(name, StandardCharsets.UTF_8.name());
|
||||
functionName = java.net.URLDecoder.decode(functionName, StandardCharsets.UTF_8.name());
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Failed to decode function name", e);
|
||||
exchange.sendResponseHeaders(400, -1); // Bad Request
|
||||
return;
|
||||
}
|
||||
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
sendResponse(exchange, decompileFunctionByName(name));
|
||||
} else if ("PUT".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> params = parsePostParams(exchange);
|
||||
String newName = params.get("newName");
|
||||
String response = renameFunction(name, newName)
|
||||
? "Renamed successfully" : "Rename failed";
|
||||
sendResponse(exchange, response);
|
||||
// Check if we're dealing with a variables request
|
||||
if (pathParts.length > 3 && "variables".equals(pathParts[3])) {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
// List all variables in function
|
||||
sendResponse(exchange, listVariablesInFunction(functionName));
|
||||
} else if ("PUT".equals(exchange.getRequestMethod()) && pathParts.length > 4) {
|
||||
// Handle operations on a specific variable
|
||||
String variableName = pathParts[4];
|
||||
try {
|
||||
variableName = java.net.URLDecoder.decode(variableName, StandardCharsets.UTF_8.name());
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Failed to decode variable name", e);
|
||||
exchange.sendResponseHeaders(400, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> params = parsePostParams(exchange);
|
||||
if (params.containsKey("newName")) {
|
||||
// Rename variable
|
||||
String result = renameVariable(functionName, variableName, params.get("newName"));
|
||||
sendResponse(exchange, result);
|
||||
} else if (params.containsKey("dataType")) {
|
||||
// Retype variable
|
||||
String result = retypeVariable(functionName, variableName, params.get("dataType"));
|
||||
sendResponse(exchange, result);
|
||||
} else {
|
||||
sendResponse(exchange, "Missing required parameter: newName or dataType");
|
||||
}
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
}
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
// Simple function operations
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
sendResponse(exchange, decompileFunctionByName(functionName));
|
||||
} else if ("PUT".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> params = parsePostParams(exchange);
|
||||
String newName = params.get("newName");
|
||||
String response = renameFunction(functionName, newName)
|
||||
? "Renamed successfully" : "Rename failed";
|
||||
sendResponse(exchange, response);
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -201,6 +275,24 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
}
|
||||
});
|
||||
|
||||
// Global variables endpoint
|
||||
server.createContext("/variables", exchange -> {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
Map<String, String> qparams = parseQueryParams(exchange);
|
||||
int offset = parseIntOrDefault(qparams.get("offset"), 0);
|
||||
int limit = parseIntOrDefault(qparams.get("limit"), 100);
|
||||
String search = qparams.get("search");
|
||||
|
||||
if (search != null && !search.isEmpty()) {
|
||||
sendResponse(exchange, searchVariables(search, offset, limit));
|
||||
} else {
|
||||
sendResponse(exchange, listGlobalVariables(offset, limit));
|
||||
}
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
}
|
||||
});
|
||||
|
||||
// Instance management endpoints
|
||||
server.createContext("/instances", exchange -> {
|
||||
@ -213,21 +305,99 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
sendResponse(exchange, sb.toString());
|
||||
});
|
||||
|
||||
// Info endpoints - both root and /info for flexibility
|
||||
// Super simple info endpoint with guaranteed response
|
||||
server.createContext("/info", exchange -> {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
sendJsonResponse(exchange, getProjectInfo());
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
try {
|
||||
String response = "{\n";
|
||||
response += "\"port\": " + port + ",\n";
|
||||
response += "\"isBaseInstance\": " + isBaseInstance + ",\n";
|
||||
|
||||
// Try to get program info if available
|
||||
Program program = getCurrentProgram();
|
||||
String programName = "\"\"";
|
||||
if (program != null) {
|
||||
programName = "\"" + program.getName() + "\"";
|
||||
}
|
||||
|
||||
// Try to get project info if available
|
||||
Project project = tool.getProject();
|
||||
String projectName = "\"\"";
|
||||
if (project != null) {
|
||||
projectName = "\"" + project.getName() + "\"";
|
||||
}
|
||||
|
||||
response += "\"project\": " + projectName + ",\n";
|
||||
response += "\"file\": " + programName + "\n";
|
||||
response += "}";
|
||||
|
||||
Msg.info(this, "Sending /info response: " + response);
|
||||
byte[] bytes = response.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);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error serving /info endpoint", e);
|
||||
try {
|
||||
String error = "{\"error\": \"Internal error\", \"port\": " + port + "}";
|
||||
byte[] bytes = error.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);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Msg.error(this, "Failed to send error response", ioe);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Root endpoint also returns project info
|
||||
// Super simple root endpoint - exact same as /info for consistency
|
||||
server.createContext("/", exchange -> {
|
||||
if ("GET".equals(exchange.getRequestMethod())) {
|
||||
sendJsonResponse(exchange, getProjectInfo());
|
||||
} else {
|
||||
exchange.sendResponseHeaders(405, -1); // Method Not Allowed
|
||||
try {
|
||||
String response = "{\n";
|
||||
response += "\"port\": " + port + ",\n";
|
||||
response += "\"isBaseInstance\": " + isBaseInstance + ",\n";
|
||||
|
||||
// Try to get program info if available
|
||||
Program program = getCurrentProgram();
|
||||
String programName = "\"\"";
|
||||
if (program != null) {
|
||||
programName = "\"" + program.getName() + "\"";
|
||||
}
|
||||
|
||||
// Try to get project info if available
|
||||
Project project = tool.getProject();
|
||||
String projectName = "\"\"";
|
||||
if (project != null) {
|
||||
projectName = "\"" + project.getName() + "\"";
|
||||
}
|
||||
|
||||
response += "\"project\": " + projectName + ",\n";
|
||||
response += "\"file\": " + programName + "\n";
|
||||
response += "}";
|
||||
|
||||
Msg.info(this, "Sending / response: " + response);
|
||||
byte[] bytes = response.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);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error serving / endpoint", e);
|
||||
try {
|
||||
String error = "{\"error\": \"Internal error\", \"port\": " + port + "}";
|
||||
byte[] bytes = error.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);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
Msg.error(this, "Failed to send error response", ioe);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -270,7 +440,7 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Function f : program.getFunctionManager().getFunctions(true)) {
|
||||
names.add(f.getName());
|
||||
names.add(f.getName() + " @ " + f.getEntryPoint());
|
||||
}
|
||||
return paginateList(names, offset, limit);
|
||||
}
|
||||
@ -485,6 +655,427 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
Msg.error(this, "Failed to execute rename data on Swing thread", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
// New variable handling methods
|
||||
// ----------------------------------------------------------------------------------
|
||||
|
||||
private String listVariablesInFunction(String functionName) {
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) return "No program loaded";
|
||||
|
||||
DecompInterface decomp = new DecompInterface();
|
||||
try {
|
||||
if (!decomp.openProgram(program)) {
|
||||
return "Failed to initialize decompiler";
|
||||
}
|
||||
|
||||
Function function = findFunctionByName(program, functionName);
|
||||
if (function == null) {
|
||||
return "Function not found: " + functionName;
|
||||
}
|
||||
|
||||
DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
|
||||
if (results == null || !results.decompileCompleted()) {
|
||||
return "Failed to decompile function: " + functionName;
|
||||
}
|
||||
|
||||
// Get high-level pcode representation for the function
|
||||
HighFunction highFunction = results.getHighFunction();
|
||||
if (highFunction == null) {
|
||||
return "Failed to get high function for: " + functionName;
|
||||
}
|
||||
|
||||
// Get local variables
|
||||
List<String> variables = new ArrayList<>();
|
||||
Iterator<HighSymbol> symbolIter = highFunction.getLocalSymbolMap().getSymbols();
|
||||
while (symbolIter.hasNext()) {
|
||||
HighSymbol symbol = symbolIter.next();
|
||||
if (symbol.getHighVariable() != null) {
|
||||
DataType dt = symbol.getDataType();
|
||||
String dtName = dt != null ? dt.getName() : "unknown";
|
||||
variables.add(String.format("%s: %s @ %s",
|
||||
symbol.getName(), dtName, symbol.getPCAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
// Get parameters
|
||||
List<String> parameters = new ArrayList<>();
|
||||
// In older Ghidra versions, we need to filter symbols to find parameters
|
||||
symbolIter = highFunction.getLocalSymbolMap().getSymbols();
|
||||
while (symbolIter.hasNext()) {
|
||||
HighSymbol symbol = symbolIter.next();
|
||||
if (symbol.isParameter()) {
|
||||
DataType dt = symbol.getDataType();
|
||||
String dtName = dt != null ? dt.getName() : "unknown";
|
||||
parameters.add(String.format("%s: %s (parameter)",
|
||||
symbol.getName(), dtName));
|
||||
}
|
||||
}
|
||||
|
||||
// Format the response
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Function: ").append(functionName).append("\n\n");
|
||||
|
||||
sb.append("Parameters:\n");
|
||||
if (parameters.isEmpty()) {
|
||||
sb.append(" none\n");
|
||||
} else {
|
||||
for (String param : parameters) {
|
||||
sb.append(" ").append(param).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\nLocal Variables:\n");
|
||||
if (variables.isEmpty()) {
|
||||
sb.append(" none\n");
|
||||
} else {
|
||||
for (String var : variables) {
|
||||
sb.append(" ").append(var).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
} finally {
|
||||
decomp.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private String renameVariable(String functionName, String oldName, String newName) {
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) return "No program loaded";
|
||||
|
||||
DecompInterface decomp = new DecompInterface();
|
||||
decomp.openProgram(program);
|
||||
|
||||
Function func = null;
|
||||
for (Function f : program.getFunctionManager().getFunctions(true)) {
|
||||
if (f.getName().equals(functionName)) {
|
||||
func = f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (func == null) {
|
||||
return "Function not found";
|
||||
}
|
||||
|
||||
DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor());
|
||||
if (result == null || !result.decompileCompleted()) {
|
||||
return "Decompilation failed";
|
||||
}
|
||||
|
||||
HighFunction highFunction = result.getHighFunction();
|
||||
if (highFunction == null) {
|
||||
return "Decompilation failed (no high function)";
|
||||
}
|
||||
|
||||
LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap();
|
||||
if (localSymbolMap == null) {
|
||||
return "Decompilation failed (no local symbol map)";
|
||||
}
|
||||
|
||||
HighSymbol highSymbol = null;
|
||||
Iterator<HighSymbol> symbols = localSymbolMap.getSymbols();
|
||||
while (symbols.hasNext()) {
|
||||
HighSymbol symbol = symbols.next();
|
||||
String symbolName = symbol.getName();
|
||||
|
||||
if (symbolName.equals(oldName)) {
|
||||
highSymbol = symbol;
|
||||
}
|
||||
if (symbolName.equals(newName)) {
|
||||
return "Error: A variable with name '" + newName + "' already exists in this function";
|
||||
}
|
||||
}
|
||||
|
||||
if (highSymbol == null) {
|
||||
return "Variable not found";
|
||||
}
|
||||
|
||||
boolean commitRequired = checkFullCommit(highSymbol, highFunction);
|
||||
|
||||
final HighSymbol finalHighSymbol = highSymbol;
|
||||
final Function finalFunction = func;
|
||||
AtomicBoolean successFlag = new AtomicBoolean(false);
|
||||
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
int tx = program.startTransaction("Rename variable");
|
||||
try {
|
||||
if (commitRequired) {
|
||||
HighFunctionDBUtil.commitParamsToDatabase(highFunction, false,
|
||||
ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource());
|
||||
}
|
||||
HighFunctionDBUtil.updateDBVariable(
|
||||
finalHighSymbol,
|
||||
newName,
|
||||
null,
|
||||
SourceType.USER_DEFINED
|
||||
);
|
||||
successFlag.set(true);
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Failed to rename variable", e);
|
||||
}
|
||||
finally {
|
||||
program.endTransaction(tx, true);
|
||||
}
|
||||
});
|
||||
} catch (InterruptedException | InvocationTargetException e) {
|
||||
String errorMsg = "Failed to execute rename on Swing thread: " + e.getMessage();
|
||||
Msg.error(this, errorMsg, e);
|
||||
return errorMsg;
|
||||
}
|
||||
return successFlag.get() ? "Variable renamed" : "Failed to rename variable";
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from AbstractDecompilerAction.checkFullCommit, it's protected.
|
||||
* Compare the given HighFunction's idea of the prototype with the Function's idea.
|
||||
* Return true if there is a difference. If a specific symbol is being changed,
|
||||
* it can be passed in to check whether or not the prototype is being affected.
|
||||
* @param highSymbol (if not null) is the symbol being modified
|
||||
* @param hfunction is the given HighFunction
|
||||
* @return true if there is a difference (and a full commit is required)
|
||||
*/
|
||||
protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) {
|
||||
if (highSymbol != null && !highSymbol.isParameter()) {
|
||||
return false;
|
||||
}
|
||||
Function function = hfunction.getFunction();
|
||||
Parameter[] parameters = function.getParameters();
|
||||
LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap();
|
||||
int numParams = localSymbolMap.getNumParams();
|
||||
if (numParams != parameters.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < numParams; i++) {
|
||||
HighSymbol param = localSymbolMap.getParamSymbol(i);
|
||||
if (param.getCategoryIndex() != i) {
|
||||
return true;
|
||||
}
|
||||
VariableStorage storage = param.getStorage();
|
||||
// Don't compare using the equals method so that DynamicVariableStorage can match
|
||||
if (0 != storage.compareTo(parameters[i].getVariableStorage())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String retypeVariable(String functionName, String varName, String dataTypeName) {
|
||||
if (varName == null || varName.isEmpty() || dataTypeName == null || dataTypeName.isEmpty()) {
|
||||
return "Both variable name and data type are required";
|
||||
}
|
||||
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) return "No program loaded";
|
||||
|
||||
AtomicReference<String> result = new AtomicReference<>("Variable retype failed");
|
||||
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
int tx = program.startTransaction("Retype variable via HTTP");
|
||||
try {
|
||||
Function function = findFunctionByName(program, functionName);
|
||||
if (function == null) {
|
||||
result.set("Function not found: " + functionName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize decompiler
|
||||
DecompInterface decomp = new DecompInterface();
|
||||
decomp.openProgram(program);
|
||||
DecompileResults decompRes = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
|
||||
|
||||
if (decompRes == null || !decompRes.decompileCompleted()) {
|
||||
result.set("Failed to decompile function: " + functionName);
|
||||
return;
|
||||
}
|
||||
|
||||
HighFunction highFunction = decompRes.getHighFunction();
|
||||
if (highFunction == null) {
|
||||
result.set("Failed to get high function");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the variable by name - must match exactly and be in current scope
|
||||
HighSymbol targetSymbol = null;
|
||||
Iterator<HighSymbol> symbolIter = highFunction.getLocalSymbolMap().getSymbols();
|
||||
while (symbolIter.hasNext()) {
|
||||
HighSymbol symbol = symbolIter.next();
|
||||
if (symbol.getName().equals(varName) &&
|
||||
symbol.getPCAddress().equals(function.getEntryPoint())) {
|
||||
targetSymbol = symbol;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetSymbol == null) {
|
||||
result.set("Variable not found: " + varName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the data type by name
|
||||
DataType dataType = findDataType(program, dataTypeName);
|
||||
if (dataType == null) {
|
||||
result.set("Data type not found: " + dataTypeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Retype the variable
|
||||
HighFunctionDBUtil.updateDBVariable(targetSymbol, targetSymbol.getName(), dataType,
|
||||
SourceType.USER_DEFINED);
|
||||
|
||||
result.set("Variable '" + varName + "' retyped to '" + dataTypeName + "'");
|
||||
} catch (Exception e) {
|
||||
Msg.error(this, "Error retyping variable", e);
|
||||
result.set("Error: " + e.getMessage());
|
||||
} finally {
|
||||
program.endTransaction(tx, true);
|
||||
}
|
||||
});
|
||||
} catch (InterruptedException | InvocationTargetException e) {
|
||||
Msg.error(this, "Failed to execute on Swing thread", e);
|
||||
result.set("Error: " + e.getMessage());
|
||||
}
|
||||
|
||||
return result.get();
|
||||
}
|
||||
|
||||
private String listGlobalVariables(int offset, int limit) {
|
||||
Program program = getCurrentProgram();
|
||||
if (program == null) return "No program loaded";
|
||||
|
||||
List<String> globalVars = new ArrayList<>();
|
||||
SymbolTable symbolTable = program.getSymbolTable();
|
||||
SymbolIterator it = symbolTable.getSymbolIterator();
|
||||
|
||||
while (it.hasNext()) {
|
||||
Symbol symbol = it.next();
|
||||
// Check for globals - look for symbols that are in global space and not functions
|
||||
if (symbol.isGlobal() &&
|
||||
symbol.getSymbolType() != SymbolType.FUNCTION &&
|
||||
symbol.getSymbolType() != SymbolType.LABEL) {
|
||||
globalVars.add(String.format("%s @ %s",
|
||||
symbol.getName(), symbol.getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(globalVars);
|
||||
return paginateList(globalVars, offset, limit);
|
||||
}
|
||||
|
||||
private String searchVariables(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<String> matchedVars = new ArrayList<>();
|
||||
|
||||
// Search global variables
|
||||
SymbolTable symbolTable = program.getSymbolTable();
|
||||
SymbolIterator it = symbolTable.getSymbolIterator();
|
||||
while (it.hasNext()) {
|
||||
Symbol symbol = it.next();
|
||||
if (symbol.isGlobal() &&
|
||||
symbol.getSymbolType() != SymbolType.FUNCTION &&
|
||||
symbol.getSymbolType() != SymbolType.LABEL &&
|
||||
symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) {
|
||||
matchedVars.add(String.format("%s @ %s (global)",
|
||||
symbol.getName(), symbol.getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
// Search local variables in functions
|
||||
DecompInterface decomp = new DecompInterface();
|
||||
try {
|
||||
if (decomp.openProgram(program)) {
|
||||
for (Function function : program.getFunctionManager().getFunctions(true)) {
|
||||
DecompileResults results = decomp.decompileFunction(function, 30, new ConsoleTaskMonitor());
|
||||
if (results != null && results.decompileCompleted()) {
|
||||
HighFunction highFunc = results.getHighFunction();
|
||||
if (highFunc != null) {
|
||||
// Check each local variable and parameter
|
||||
Iterator<HighSymbol> symbolIter = highFunc.getLocalSymbolMap().getSymbols();
|
||||
while (symbolIter.hasNext()) {
|
||||
HighSymbol symbol = symbolIter.next();
|
||||
if (symbol.getName().toLowerCase().contains(searchTerm.toLowerCase())) {
|
||||
if (symbol.isParameter()) {
|
||||
matchedVars.add(String.format("%s in %s (parameter)",
|
||||
symbol.getName(), function.getName()));
|
||||
} else {
|
||||
matchedVars.add(String.format("%s in %s @ %s (local)",
|
||||
symbol.getName(), function.getName(), symbol.getPCAddress()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
decomp.dispose();
|
||||
}
|
||||
|
||||
Collections.sort(matchedVars);
|
||||
|
||||
if (matchedVars.isEmpty()) {
|
||||
return "No variables matching '" + searchTerm + "'";
|
||||
}
|
||||
return paginateList(matchedVars, offset, limit);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
// Helper methods
|
||||
// ----------------------------------------------------------------------------------
|
||||
|
||||
private Function findFunctionByName(Program program, String name) {
|
||||
if (program == null || name == null || name.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Function function : program.getFunctionManager().getFunctions(true)) {
|
||||
if (function.getName().equals(name)) {
|
||||
return function;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private DataType findDataType(Program program, String name) {
|
||||
if (program == null || name == null || name.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DataTypeManager dtm = program.getDataTypeManager();
|
||||
|
||||
// First try direct lookup
|
||||
DataType dt = dtm.getDataType("/" + name);
|
||||
if (dt != null) {
|
||||
return dt;
|
||||
}
|
||||
|
||||
// Try built-in types by simple name
|
||||
dt = dtm.findDataType(name);
|
||||
if (dt != null) {
|
||||
return dt;
|
||||
}
|
||||
|
||||
// Try to find a matching type by name only
|
||||
Iterator<DataType> dtIter = dtm.getAllDataTypes();
|
||||
while (dtIter.hasNext()) {
|
||||
DataType type = dtIter.next();
|
||||
if (type.getName().equals(name)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
// Utility: parse query params, parse post params, pagination, etc.
|
||||
@ -569,56 +1160,32 @@ public class GhydraMCPPlugin extends Plugin implements ApplicationLevelPlugin {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public Program getCurrentProgram() {
|
||||
ProgramManager pm = tool.getService(ProgramManager.class);
|
||||
return pm != null ? pm.getCurrentProgram() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current project and open file in JSON format
|
||||
* Get the current program from the tool
|
||||
*/
|
||||
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");
|
||||
public Program getCurrentProgram() {
|
||||
if (tool == null) {
|
||||
Msg.debug(this, "Tool is null when trying to get current program");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
try {
|
||||
ProgramManager pm = tool.getService(ProgramManager.class);
|
||||
if (pm == null) {
|
||||
Msg.debug(this, "ProgramManager service is not available");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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");
|
||||
Program program = pm.getCurrentProgram();
|
||||
Msg.debug(this, "Got current program: " + (program != null ? program.getName() : "null"));
|
||||
return program;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Msg.error(this, "Error getting current program", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@ -629,18 +1196,6 @@ 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;
|
||||
|
||||
@ -3,4 +3,4 @@ Plugin-Class: eu.starsong.ghidra.GhydraMCP
|
||||
Plugin-Name: GhydraMCP
|
||||
Plugin-Version: 11.3.1
|
||||
Plugin-Author: LaurieWired, Teal Bauer
|
||||
Plugin-Description: Expose multiple Ghidra tools to MCP servers
|
||||
Plugin-Description: Expose multiple Ghidra tools to MCP servers with variable management
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
GHIDRA_MODULE_NAME=GhydraMCP
|
||||
GHIDRA_MODULE_DESC=A multi-headed REST interface for Ghidra for use with MCP agents.
|
||||
Manifest-Version: 1.0
|
||||
GHIDRA_MODULE_NAME: GhydraMCP
|
||||
GHIDRA_MODULE_DESC: A multi-headed REST interface for Ghidra for use with MCP agents.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user