├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── bridge_mcp_ghidra.py ├── images ├── enable_plugin.png └── new_plugins_found.png ├── lib └── .gitignore ├── pom.xml ├── requirements.txt └── src ├── assembly └── ghidra-extension.xml ├── main ├── java │ └── com │ │ └── lauriewired │ │ └── GhidraMCPPlugin.java └── resources │ ├── META-INF │ └── MANIFEST.MF │ ├── Module.manifest │ └── extension.properties └── test └── java └── com └── lauriewired └── AppTest.java /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build with Maven 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | env: 13 | GHIDRA_VERSION: 11.3.1 14 | GHIDRA_DATE: 20250219 15 | GHIDRA_LIBS: >- 16 | Features/Base/lib/Base.jar 17 | Features/Decompiler/lib/Decompiler.jar 18 | Framework/Docking/lib/Docking.jar 19 | Framework/Generic/lib/Generic.jar 20 | Framework/Project/lib/Project.jar 21 | Framework/SoftwareModeling/lib/SoftwareModeling.jar 22 | Framework/Utility/lib/Utility.jar 23 | Framework/Gui/lib/Gui.jar 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up JDK 21 28 | uses: actions/setup-java@v4 29 | with: 30 | java-version: '21' 31 | distribution: 'temurin' 32 | cache: maven 33 | 34 | - name: Download Ghidra 35 | run: | 36 | wget --no-verbose -O ghidra.zip https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${{ env.GHIDRA_VERSION }}_build/ghidra_${{ env.GHIDRA_VERSION }}_PUBLIC_${{ env.GHIDRA_DATE }}.zip 37 | 7z x -bd ghidra.zip 38 | 39 | - name: Copy Ghidra libs 40 | run: | 41 | mkdir -p ./lib 42 | for libfile in ${{ env.GHIDRA_LIBS }} 43 | do echo "Copying ${libfile} to lib/" 44 | cp ghidra_${{ env.GHIDRA_VERSION }}_PUBLIC/Ghidra/${libfile} ./lib/ 45 | done 46 | 47 | - name: Build with Maven 48 | run: mvn clean package assembly:single 49 | 50 | - name: Assemble release directory 51 | run: | 52 | mkdir release 53 | cp target/GhidraMCP-*-SNAPSHOT.zip release/ 54 | cp bridge_mcp_ghidra.py release/ 55 | 56 | - name: Upload artifact 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: GhidraMCP-artifact 60 | path: | 61 | release/* 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Maven target directory 2 | /target/ 3 | 4 | # Compiled class files 5 | *.class 6 | 7 | # Logs 8 | *.log 9 | 10 | # IDE files 11 | # IntelliJ 12 | .idea/ 13 | *.iml 14 | *.iws 15 | out/ 16 | 17 | # Eclipse 18 | .project 19 | .classpath 20 | .settings/ 21 | bin/ 22 | 23 | # VS Code 24 | .vscode/ 25 | 26 | # macOS 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # macOS metadata files 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Maven Wrapper 39 | .mvn/ 40 | !/.mvn/wrapper/maven-wrapper.jar 41 | mvnw 42 | mvnw.cmd 43 | 44 | # Environment files 45 | .env 46 | .env.* 47 | 48 | # Java crash logs (if any) 49 | hs_err_pid* 50 | replay_pid* 51 | 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) 2 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/LaurieWired/GhidraMCP)](https://github.com/LaurieWired/GhidraMCP/releases) 3 | [![GitHub stars](https://img.shields.io/github/stars/LaurieWired/GhidraMCP)](https://github.com/LaurieWired/GhidraMCP/stargazers) 4 | [![GitHub forks](https://img.shields.io/github/forks/LaurieWired/GhidraMCP)](https://github.com/LaurieWired/GhidraMCP/network/members) 5 | [![GitHub contributors](https://img.shields.io/github/contributors/LaurieWired/GhidraMCP)](https://github.com/LaurieWired/GhidraMCP/graphs/contributors) 6 | [![Follow @lauriewired](https://img.shields.io/twitter/follow/lauriewired?style=social)](https://twitter.com/lauriewired) 7 | 8 | ![ghidra_MCP_logo](https://github.com/user-attachments/assets/4986d702-be3f-4697-acce-aea55cd79ad3) 9 | 10 | 11 | # ghidraMCP 12 | ghidraMCP is an Model Context Protocol server for allowing LLMs to autonomously reverse engineer applications. It exposes numerous tools from core Ghidra functionality to MCP clients. 13 | 14 | https://github.com/user-attachments/assets/36080514-f227-44bd-af84-78e29ee1d7f9 15 | 16 | 17 | # Features 18 | MCP Server + Ghidra Plugin 19 | 20 | - Decompile and analyze binaries in Ghidra 21 | - Automatically rename methods and data 22 | - List methods, classes, imports, and exports 23 | 24 | # Installation 25 | 26 | ## Prerequisites 27 | - Install [Ghidra](https://ghidra-sre.org) 28 | - Python3 29 | - MCP [SDK](https://github.com/modelcontextprotocol/python-sdk) 30 | 31 | ## Ghidra 32 | First, download the latest [release](https://github.com/LaurieWired/GhidraMCP/releases) from this repository. This contains the Ghidra plugin and Python MCP client. Then, you can directly import the plugin into Ghidra. 33 | 34 | 1. Run Ghidra 35 | 2. Select `File` -> `Install Extensions` 36 | 3. Click the `+` button 37 | 4. Select the `GhidraMCP-1-2.zip` (or your chosen version) from the downloaded release 38 | 5. Restart Ghidra 39 | 6. Make sure the GhidraMCPPlugin is enabled in `File` -> `Configure` -> `Developer` 40 | 7. *Optional*: Configure the port in Ghidra with `Edit` -> `Tool Options` -> `GhidraMCP HTTP Server` 41 | 42 | Video Installation Guide: 43 | 44 | 45 | https://github.com/user-attachments/assets/75f0c176-6da1-48dc-ad96-c182eb4648c3 46 | 47 | 48 | 49 | ## MCP Clients 50 | 51 | Theoretically, any MCP client should work with ghidraMCP. Three examples are given below. 52 | 53 | ## Example 1: Claude Desktop 54 | To set up Claude Desktop as a Ghidra MCP client, go to `Claude` -> `Settings` -> `Developer` -> `Edit Config` -> `claude_desktop_config.json` and add the following: 55 | 56 | ```json 57 | { 58 | "mcpServers": { 59 | "ghidra": { 60 | "command": "python", 61 | "args": [ 62 | "/ABSOLUTE_PATH_TO/bridge_mcp_ghidra.py", 63 | "--ghidra-server", 64 | "http://127.0.0.1:8080/" 65 | ] 66 | } 67 | } 68 | } 69 | ``` 70 | 71 | Alternatively, edit this file directly: 72 | ``` 73 | /Users/YOUR_USER/Library/Application Support/Claude/claude_desktop_config.json 74 | ``` 75 | 76 | The server IP and port are configurable and should be set to point to the target Ghidra instance. If not set, both will default to localhost:8080. 77 | 78 | ## Example 2: Cline 79 | To use GhidraMCP with [Cline](https://cline.bot), this requires manually running the MCP server as well. First run the following command: 80 | 81 | ``` 82 | python bridge_mcp_ghidra.py --transport sse --mcp-host 127.0.0.1 --mcp-port 8081 --ghidra-server http://127.0.0.1:8080/ 83 | ``` 84 | 85 | The only *required* argument is the transport. If all other arguments are unspecified, they will default to the above. Once the MCP server is running, open up Cline and select `MCP Servers` at the top. 86 | 87 | ![Cline select](https://github.com/user-attachments/assets/88e1f336-4729-46ee-9b81-53271e9c0ce0) 88 | 89 | Then select `Remote Servers` and add the following, ensuring that the url matches the MCP host and port: 90 | 91 | 1. Server Name: GhidraMCP 92 | 2. Server URL: `http://127.0.0.1:8081/sse` 93 | 94 | ## Example 3: 5ire 95 | Another MCP client that supports multiple models on the backend is [5ire](https://github.com/nanbingxyz/5ire). To set up GhidraMCP, open 5ire and go to `Tools` -> `New` and set the following configurations: 96 | 97 | 1. Tool Key: ghidra 98 | 2. Name: GhidraMCP 99 | 3. Command: `python /ABSOLUTE_PATH_TO/bridge_mcp_ghidra.py` 100 | 101 | # Building from Source 102 | 1. Copy the following files from your Ghidra directory to this project's `lib/` directory: 103 | - `Ghidra/Features/Base/lib/Base.jar` 104 | - `Ghidra/Features/Decompiler/lib/Decompiler.jar` 105 | - `Ghidra/Framework/Docking/lib/Docking.jar` 106 | - `Ghidra/Framework/Generic/lib/Generic.jar` 107 | - `Ghidra/Framework/Project/lib/Project.jar` 108 | - `Ghidra/Framework/SoftwareModeling/lib/SoftwareModeling.jar` 109 | - `Ghidra/Framework/Utility/lib/Utility.jar` 110 | - `Ghidra/Framework/Gui/lib/Gui.jar` 111 | 2. Build with Maven by running: 112 | 113 | `mvn clean package assembly:single` 114 | 115 | The generated zip file includes the built Ghidra plugin and its resources. These files are required for Ghidra to recognize the new extension. 116 | 117 | - lib/GhidraMCP.jar 118 | - extensions.properties 119 | - Module.manifest 120 | -------------------------------------------------------------------------------- /bridge_mcp_ghidra.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # requires-python = ">=3.10" 3 | # dependencies = [ 4 | # "requests>=2,<3", 5 | # "mcp>=1.2.0,<2", 6 | # ] 7 | # /// 8 | 9 | import sys 10 | import requests 11 | import argparse 12 | import logging 13 | from urllib.parse import urljoin 14 | 15 | from mcp.server.fastmcp import FastMCP 16 | 17 | DEFAULT_GHIDRA_SERVER = "http://127.0.0.1:8080/" 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | mcp = FastMCP("ghidra-mcp") 22 | 23 | # Initialize ghidra_server_url with default value 24 | ghidra_server_url = DEFAULT_GHIDRA_SERVER 25 | 26 | def safe_get(endpoint: str, params: dict = None) -> list: 27 | """ 28 | Perform a GET request with optional query parameters. 29 | """ 30 | if params is None: 31 | params = {} 32 | 33 | url = urljoin(ghidra_server_url, endpoint) 34 | 35 | try: 36 | response = requests.get(url, params=params, timeout=5) 37 | response.encoding = 'utf-8' 38 | if response.ok: 39 | return response.text.splitlines() 40 | else: 41 | return [f"Error {response.status_code}: {response.text.strip()}"] 42 | except Exception as e: 43 | return [f"Request failed: {str(e)}"] 44 | 45 | def safe_post(endpoint: str, data: dict | str) -> str: 46 | try: 47 | url = urljoin(ghidra_server_url, endpoint) 48 | if isinstance(data, dict): 49 | response = requests.post(url, data=data, timeout=5) 50 | else: 51 | response = requests.post(url, data=data.encode("utf-8"), timeout=5) 52 | response.encoding = 'utf-8' 53 | if response.ok: 54 | return response.text.strip() 55 | else: 56 | return f"Error {response.status_code}: {response.text.strip()}" 57 | except Exception as e: 58 | return f"Request failed: {str(e)}" 59 | 60 | @mcp.tool() 61 | def list_methods(offset: int = 0, limit: int = 100) -> list: 62 | """ 63 | List all function names in the program with pagination. 64 | """ 65 | return safe_get("methods", {"offset": offset, "limit": limit}) 66 | 67 | @mcp.tool() 68 | def list_classes(offset: int = 0, limit: int = 100) -> list: 69 | """ 70 | List all namespace/class names in the program with pagination. 71 | """ 72 | return safe_get("classes", {"offset": offset, "limit": limit}) 73 | 74 | @mcp.tool() 75 | def decompile_function(name: str) -> str: 76 | """ 77 | Decompile a specific function by name and return the decompiled C code. 78 | """ 79 | return safe_post("decompile", name) 80 | 81 | @mcp.tool() 82 | def rename_function(old_name: str, new_name: str) -> str: 83 | """ 84 | Rename a function by its current name to a new user-defined name. 85 | """ 86 | return safe_post("renameFunction", {"oldName": old_name, "newName": new_name}) 87 | 88 | @mcp.tool() 89 | def rename_data(address: str, new_name: str) -> str: 90 | """ 91 | Rename a data label at the specified address. 92 | """ 93 | return safe_post("renameData", {"address": address, "newName": new_name}) 94 | 95 | @mcp.tool() 96 | def list_segments(offset: int = 0, limit: int = 100) -> list: 97 | """ 98 | List all memory segments in the program with pagination. 99 | """ 100 | return safe_get("segments", {"offset": offset, "limit": limit}) 101 | 102 | @mcp.tool() 103 | def list_imports(offset: int = 0, limit: int = 100) -> list: 104 | """ 105 | List imported symbols in the program with pagination. 106 | """ 107 | return safe_get("imports", {"offset": offset, "limit": limit}) 108 | 109 | @mcp.tool() 110 | def list_exports(offset: int = 0, limit: int = 100) -> list: 111 | """ 112 | List exported functions/symbols with pagination. 113 | """ 114 | return safe_get("exports", {"offset": offset, "limit": limit}) 115 | 116 | @mcp.tool() 117 | def list_namespaces(offset: int = 0, limit: int = 100) -> list: 118 | """ 119 | List all non-global namespaces in the program with pagination. 120 | """ 121 | return safe_get("namespaces", {"offset": offset, "limit": limit}) 122 | 123 | @mcp.tool() 124 | def list_data_items(offset: int = 0, limit: int = 100) -> list: 125 | """ 126 | List defined data labels and their values with pagination. 127 | """ 128 | return safe_get("data", {"offset": offset, "limit": limit}) 129 | 130 | @mcp.tool() 131 | def search_functions_by_name(query: str, offset: int = 0, limit: int = 100) -> list: 132 | """ 133 | Search for functions whose name contains the given substring. 134 | """ 135 | if not query: 136 | return ["Error: query string is required"] 137 | return safe_get("searchFunctions", {"query": query, "offset": offset, "limit": limit}) 138 | 139 | @mcp.tool() 140 | def rename_variable(function_name: str, old_name: str, new_name: str) -> str: 141 | """ 142 | Rename a local variable within a function. 143 | """ 144 | return safe_post("renameVariable", { 145 | "functionName": function_name, 146 | "oldName": old_name, 147 | "newName": new_name 148 | }) 149 | 150 | @mcp.tool() 151 | def get_function_by_address(address: str) -> str: 152 | """ 153 | Get a function by its address. 154 | """ 155 | return "\n".join(safe_get("get_function_by_address", {"address": address})) 156 | 157 | @mcp.tool() 158 | def get_current_address() -> str: 159 | """ 160 | Get the address currently selected by the user. 161 | """ 162 | return "\n".join(safe_get("get_current_address")) 163 | 164 | @mcp.tool() 165 | def get_current_function() -> str: 166 | """ 167 | Get the function currently selected by the user. 168 | """ 169 | return "\n".join(safe_get("get_current_function")) 170 | 171 | @mcp.tool() 172 | def list_functions() -> list: 173 | """ 174 | List all functions in the database. 175 | """ 176 | return safe_get("list_functions") 177 | 178 | @mcp.tool() 179 | def decompile_function_by_address(address: str) -> str: 180 | """ 181 | Decompile a function at the given address. 182 | """ 183 | return "\n".join(safe_get("decompile_function", {"address": address})) 184 | 185 | @mcp.tool() 186 | def disassemble_function(address: str) -> list: 187 | """ 188 | Get assembly code (address: instruction; comment) for a function. 189 | """ 190 | return safe_get("disassemble_function", {"address": address}) 191 | 192 | @mcp.tool() 193 | def set_decompiler_comment(address: str, comment: str) -> str: 194 | """ 195 | Set a comment for a given address in the function pseudocode. 196 | """ 197 | return safe_post("set_decompiler_comment", {"address": address, "comment": comment}) 198 | 199 | @mcp.tool() 200 | def set_disassembly_comment(address: str, comment: str) -> str: 201 | """ 202 | Set a comment for a given address in the function disassembly. 203 | """ 204 | return safe_post("set_disassembly_comment", {"address": address, "comment": comment}) 205 | 206 | @mcp.tool() 207 | def rename_function_by_address(function_address: str, new_name: str) -> str: 208 | """ 209 | Rename a function by its address. 210 | """ 211 | return safe_post("rename_function_by_address", {"function_address": function_address, "new_name": new_name}) 212 | 213 | @mcp.tool() 214 | def set_function_prototype(function_address: str, prototype: str) -> str: 215 | """ 216 | Set a function's prototype. 217 | """ 218 | return safe_post("set_function_prototype", {"function_address": function_address, "prototype": prototype}) 219 | 220 | @mcp.tool() 221 | def set_local_variable_type(function_address: str, variable_name: str, new_type: str) -> str: 222 | """ 223 | Set a local variable's type. 224 | """ 225 | return safe_post("set_local_variable_type", {"function_address": function_address, "variable_name": variable_name, "new_type": new_type}) 226 | 227 | @mcp.tool() 228 | def get_xrefs_to(address: str, offset: int = 0, limit: int = 100) -> list: 229 | """ 230 | Get all references to the specified address (xref to). 231 | 232 | Args: 233 | address: Target address in hex format (e.g. "0x1400010a0") 234 | offset: Pagination offset (default: 0) 235 | limit: Maximum number of references to return (default: 100) 236 | 237 | Returns: 238 | List of references to the specified address 239 | """ 240 | return safe_get("xrefs_to", {"address": address, "offset": offset, "limit": limit}) 241 | 242 | @mcp.tool() 243 | def get_xrefs_from(address: str, offset: int = 0, limit: int = 100) -> list: 244 | """ 245 | Get all references from the specified address (xref from). 246 | 247 | Args: 248 | address: Source address in hex format (e.g. "0x1400010a0") 249 | offset: Pagination offset (default: 0) 250 | limit: Maximum number of references to return (default: 100) 251 | 252 | Returns: 253 | List of references from the specified address 254 | """ 255 | return safe_get("xrefs_from", {"address": address, "offset": offset, "limit": limit}) 256 | 257 | @mcp.tool() 258 | def get_function_xrefs(name: str, offset: int = 0, limit: int = 100) -> list: 259 | """ 260 | Get all references to the specified function by name. 261 | 262 | Args: 263 | name: Function name to search for 264 | offset: Pagination offset (default: 0) 265 | limit: Maximum number of references to return (default: 100) 266 | 267 | Returns: 268 | List of references to the specified function 269 | """ 270 | return safe_get("function_xrefs", {"name": name, "offset": offset, "limit": limit}) 271 | 272 | @mcp.tool() 273 | def list_strings(offset: int = 0, limit: int = 2000, filter: str = None) -> list: 274 | """ 275 | List all defined strings in the program with their addresses. 276 | 277 | Args: 278 | offset: Pagination offset (default: 0) 279 | limit: Maximum number of strings to return (default: 2000) 280 | filter: Optional filter to match within string content 281 | 282 | Returns: 283 | List of strings with their addresses 284 | """ 285 | params = {"offset": offset, "limit": limit} 286 | if filter: 287 | params["filter"] = filter 288 | return safe_get("strings", params) 289 | 290 | def main(): 291 | parser = argparse.ArgumentParser(description="MCP server for Ghidra") 292 | parser.add_argument("--ghidra-server", type=str, default=DEFAULT_GHIDRA_SERVER, 293 | help=f"Ghidra server URL, default: {DEFAULT_GHIDRA_SERVER}") 294 | parser.add_argument("--mcp-host", type=str, default="127.0.0.1", 295 | help="Host to run MCP server on (only used for sse), default: 127.0.0.1") 296 | parser.add_argument("--mcp-port", type=int, 297 | help="Port to run MCP server on (only used for sse), default: 8081") 298 | parser.add_argument("--transport", type=str, default="stdio", choices=["stdio", "sse"], 299 | help="Transport protocol for MCP, default: stdio") 300 | args = parser.parse_args() 301 | 302 | # Use the global variable to ensure it's properly updated 303 | global ghidra_server_url 304 | if args.ghidra_server: 305 | ghidra_server_url = args.ghidra_server 306 | 307 | if args.transport == "sse": 308 | try: 309 | # Set up logging 310 | log_level = logging.INFO 311 | logging.basicConfig(level=log_level) 312 | logging.getLogger().setLevel(log_level) 313 | 314 | # Configure MCP settings 315 | mcp.settings.log_level = "INFO" 316 | if args.mcp_host: 317 | mcp.settings.host = args.mcp_host 318 | else: 319 | mcp.settings.host = "127.0.0.1" 320 | 321 | if args.mcp_port: 322 | mcp.settings.port = args.mcp_port 323 | else: 324 | mcp.settings.port = 8081 325 | 326 | logger.info(f"Connecting to Ghidra server at {ghidra_server_url}") 327 | logger.info(f"Starting MCP server on http://{mcp.settings.host}:{mcp.settings.port}/sse") 328 | logger.info(f"Using transport: {args.transport}") 329 | 330 | mcp.run(transport="sse") 331 | except KeyboardInterrupt: 332 | logger.info("Server stopped by user") 333 | else: 334 | mcp.run() 335 | 336 | if __name__ == "__main__": 337 | main() 338 | 339 | -------------------------------------------------------------------------------- /images/enable_plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/GhidraMCP/3b876ddbf761a9b41c6c4c42697e8fb5a3cedcdb/images/enable_plugin.png -------------------------------------------------------------------------------- /images/new_plugins_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LaurieWired/GhidraMCP/3b876ddbf761a9b41c6c4c42697e8fb5a3cedcdb/images/new_plugins_found.png -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 4.0.0 6 | com.lauriewired 7 | GhidraMCP 8 | jar 9 | 1.0-SNAPSHOT 10 | GhidraMCP 11 | http://maven.apache.org 12 | 13 | 14 | 15 | 16 | ghidra 17 | Generic 18 | 11.3.1 19 | system 20 | ${project.basedir}/lib/Generic.jar 21 | 22 | 23 | ghidra 24 | SoftwareModeling 25 | 11.3.1 26 | system 27 | ${project.basedir}/lib/SoftwareModeling.jar 28 | 29 | 30 | ghidra 31 | Project 32 | 11.3.1 33 | system 34 | ${project.basedir}/lib/Project.jar 35 | 36 | 37 | ghidra 38 | Docking 39 | 11.3.1 40 | system 41 | ${project.basedir}/lib/Docking.jar 42 | 43 | 44 | ghidra 45 | Decompiler 46 | 11.3.1 47 | system 48 | ${project.basedir}/lib/Decompiler.jar 49 | 50 | 51 | ghidra 52 | Utility 53 | 11.3.1 54 | system 55 | ${project.basedir}/lib/Utility.jar 56 | 57 | 58 | ghidra 59 | Base 60 | 11.3.1 61 | system 62 | ${project.basedir}/lib/Base.jar 63 | 64 | 65 | ghidra 66 | Gui 67 | 11.3.1 68 | system 69 | ${project.basedir}/lib/Gui.jar 70 | 71 | 72 | 73 | 74 | junit 75 | junit 76 | 3.8.1 77 | test 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | maven-jar-plugin 86 | 3.2.2 87 | 88 | 89 | src/main/resources/META-INF/MANIFEST.MF 90 | 91 | 92 | GhidraMCP 93 | 94 | 95 | **/App.class 96 | 97 | 98 | ${project.build.directory} 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-assembly-plugin 106 | 3.3.0 107 | 108 | 109 | 110 | src/assembly/ghidra-extension.xml 111 | 112 | 113 | 114 | GhidraMCP-${project.version} 115 | 116 | 117 | false 118 | 119 | 120 | 121 | 122 | make-assembly 123 | package 124 | 125 | single 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-dependency-plugin 135 | 3.1.2 136 | 137 | 138 | copy-dependencies 139 | prepare-package 140 | 141 | copy-dependencies 142 | 143 | 144 | ${project.build.directory}/lib 145 | runtime 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mcp==1.5.0 2 | requests==2.32.3 3 | -------------------------------------------------------------------------------- /src/assembly/ghidra-extension.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | ghidra-extension 9 | 10 | 11 | 12 | zip 13 | 14 | 15 | 16 | false 17 | 18 | 19 | 21 | 22 | src/main/resources 23 | 24 | extension.properties 25 | Module.manifest 26 | 27 | GhidraMCP 28 | 29 | 30 | 31 | 32 | ${project.build.directory} 33 | 34 | 35 | GhidraMCP.jar 36 | 37 | GhidraMCP/lib 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/main/java/com/lauriewired/GhidraMCPPlugin.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired; 2 | 3 | import ghidra.framework.plugintool.Plugin; 4 | import ghidra.framework.plugintool.PluginTool; 5 | import ghidra.program.model.address.Address; 6 | import ghidra.program.model.address.GlobalNamespace; 7 | import ghidra.program.model.listing.*; 8 | import ghidra.program.model.mem.MemoryBlock; 9 | import ghidra.program.model.symbol.*; 10 | import ghidra.program.model.symbol.ReferenceManager; 11 | import ghidra.program.model.symbol.Reference; 12 | import ghidra.program.model.symbol.ReferenceIterator; 13 | import ghidra.program.model.symbol.RefType; 14 | import ghidra.program.model.pcode.HighFunction; 15 | import ghidra.program.model.pcode.HighSymbol; 16 | import ghidra.program.model.pcode.LocalSymbolMap; 17 | import ghidra.program.model.pcode.HighFunctionDBUtil; 18 | import ghidra.program.model.pcode.HighFunctionDBUtil.ReturnCommitOption; 19 | import ghidra.app.decompiler.DecompInterface; 20 | import ghidra.app.decompiler.DecompileResults; 21 | import ghidra.app.plugin.PluginCategoryNames; 22 | import ghidra.app.services.CodeViewerService; 23 | import ghidra.app.services.ProgramManager; 24 | import ghidra.app.util.PseudoDisassembler; 25 | import ghidra.app.cmd.function.SetVariableNameCmd; 26 | import ghidra.program.model.symbol.SourceType; 27 | import ghidra.program.model.listing.LocalVariableImpl; 28 | import ghidra.program.model.listing.ParameterImpl; 29 | import ghidra.util.exception.DuplicateNameException; 30 | import ghidra.util.exception.InvalidInputException; 31 | import ghidra.framework.plugintool.PluginInfo; 32 | import ghidra.framework.plugintool.util.PluginStatus; 33 | import ghidra.program.util.ProgramLocation; 34 | import ghidra.util.Msg; 35 | import ghidra.util.task.ConsoleTaskMonitor; 36 | import ghidra.util.task.TaskMonitor; 37 | import ghidra.program.model.pcode.HighVariable; 38 | import ghidra.program.model.pcode.Varnode; 39 | import ghidra.program.model.data.DataType; 40 | import ghidra.program.model.data.DataTypeManager; 41 | import ghidra.program.model.data.PointerDataType; 42 | import ghidra.program.model.data.Undefined1DataType; 43 | import ghidra.program.model.listing.Variable; 44 | import ghidra.app.decompiler.component.DecompilerUtils; 45 | import ghidra.app.decompiler.ClangToken; 46 | import ghidra.framework.options.Options; 47 | 48 | import com.sun.net.httpserver.HttpExchange; 49 | import com.sun.net.httpserver.HttpServer; 50 | 51 | import javax.swing.SwingUtilities; 52 | import java.io.IOException; 53 | import java.io.OutputStream; 54 | import java.lang.reflect.InvocationTargetException; 55 | import java.net.InetSocketAddress; 56 | import java.net.URLDecoder; 57 | import java.nio.charset.StandardCharsets; 58 | import java.util.*; 59 | import java.util.concurrent.atomic.AtomicBoolean; 60 | 61 | @PluginInfo( 62 | status = PluginStatus.RELEASED, 63 | packageName = ghidra.app.DeveloperPluginPackage.NAME, 64 | category = PluginCategoryNames.ANALYSIS, 65 | shortDescription = "HTTP server plugin", 66 | description = "Starts an embedded HTTP server to expose program data. Port configurable via Tool Options." 67 | ) 68 | public class GhidraMCPPlugin extends Plugin { 69 | 70 | private HttpServer server; 71 | private static final String OPTION_CATEGORY_NAME = "GhidraMCP HTTP Server"; 72 | private static final String PORT_OPTION_NAME = "Server Port"; 73 | private static final int DEFAULT_PORT = 8080; 74 | 75 | public GhidraMCPPlugin(PluginTool tool) { 76 | super(tool); 77 | Msg.info(this, "GhidraMCPPlugin loading..."); 78 | 79 | // Register the configuration option 80 | Options options = tool.getOptions(OPTION_CATEGORY_NAME); 81 | options.registerOption(PORT_OPTION_NAME, DEFAULT_PORT, 82 | null, // No help location for now 83 | "The network port number the embedded HTTP server will listen on. " + 84 | "Requires Ghidra restart or plugin reload to take effect after changing."); 85 | 86 | try { 87 | startServer(); 88 | } 89 | catch (IOException e) { 90 | Msg.error(this, "Failed to start HTTP server", e); 91 | } 92 | Msg.info(this, "GhidraMCPPlugin loaded!"); 93 | } 94 | 95 | private void startServer() throws IOException { 96 | // Read the configured port 97 | Options options = tool.getOptions(OPTION_CATEGORY_NAME); 98 | int port = options.getInt(PORT_OPTION_NAME, DEFAULT_PORT); 99 | 100 | // Stop existing server if running (e.g., if plugin is reloaded) 101 | if (server != null) { 102 | Msg.info(this, "Stopping existing HTTP server before starting new one."); 103 | server.stop(0); 104 | server = null; 105 | } 106 | 107 | server = HttpServer.create(new InetSocketAddress(port), 0); 108 | 109 | // Each listing endpoint uses offset & limit from query params: 110 | server.createContext("/methods", exchange -> { 111 | Map qparams = parseQueryParams(exchange); 112 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 113 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 114 | sendResponse(exchange, getAllFunctionNames(offset, limit)); 115 | }); 116 | 117 | server.createContext("/classes", exchange -> { 118 | Map qparams = parseQueryParams(exchange); 119 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 120 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 121 | sendResponse(exchange, getAllClassNames(offset, limit)); 122 | }); 123 | 124 | server.createContext("/decompile", exchange -> { 125 | String name = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); 126 | sendResponse(exchange, decompileFunctionByName(name)); 127 | }); 128 | 129 | server.createContext("/renameFunction", exchange -> { 130 | Map params = parsePostParams(exchange); 131 | String response = renameFunction(params.get("oldName"), params.get("newName")) 132 | ? "Renamed successfully" : "Rename failed"; 133 | sendResponse(exchange, response); 134 | }); 135 | 136 | server.createContext("/renameData", exchange -> { 137 | Map params = parsePostParams(exchange); 138 | renameDataAtAddress(params.get("address"), params.get("newName")); 139 | sendResponse(exchange, "Rename data attempted"); 140 | }); 141 | 142 | server.createContext("/renameVariable", exchange -> { 143 | Map params = parsePostParams(exchange); 144 | String functionName = params.get("functionName"); 145 | String oldName = params.get("oldName"); 146 | String newName = params.get("newName"); 147 | String result = renameVariableInFunction(functionName, oldName, newName); 148 | sendResponse(exchange, result); 149 | }); 150 | 151 | server.createContext("/segments", exchange -> { 152 | Map qparams = parseQueryParams(exchange); 153 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 154 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 155 | sendResponse(exchange, listSegments(offset, limit)); 156 | }); 157 | 158 | server.createContext("/imports", exchange -> { 159 | Map qparams = parseQueryParams(exchange); 160 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 161 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 162 | sendResponse(exchange, listImports(offset, limit)); 163 | }); 164 | 165 | server.createContext("/exports", exchange -> { 166 | Map qparams = parseQueryParams(exchange); 167 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 168 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 169 | sendResponse(exchange, listExports(offset, limit)); 170 | }); 171 | 172 | server.createContext("/namespaces", exchange -> { 173 | Map qparams = parseQueryParams(exchange); 174 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 175 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 176 | sendResponse(exchange, listNamespaces(offset, limit)); 177 | }); 178 | 179 | server.createContext("/data", exchange -> { 180 | Map qparams = parseQueryParams(exchange); 181 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 182 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 183 | sendResponse(exchange, listDefinedData(offset, limit)); 184 | }); 185 | 186 | server.createContext("/searchFunctions", exchange -> { 187 | Map qparams = parseQueryParams(exchange); 188 | String searchTerm = qparams.get("query"); 189 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 190 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 191 | sendResponse(exchange, searchFunctionsByName(searchTerm, offset, limit)); 192 | }); 193 | 194 | // New API endpoints based on requirements 195 | 196 | server.createContext("/get_function_by_address", exchange -> { 197 | Map qparams = parseQueryParams(exchange); 198 | String address = qparams.get("address"); 199 | sendResponse(exchange, getFunctionByAddress(address)); 200 | }); 201 | 202 | server.createContext("/get_current_address", exchange -> { 203 | sendResponse(exchange, getCurrentAddress()); 204 | }); 205 | 206 | server.createContext("/get_current_function", exchange -> { 207 | sendResponse(exchange, getCurrentFunction()); 208 | }); 209 | 210 | server.createContext("/list_functions", exchange -> { 211 | sendResponse(exchange, listFunctions()); 212 | }); 213 | 214 | server.createContext("/decompile_function", exchange -> { 215 | Map qparams = parseQueryParams(exchange); 216 | String address = qparams.get("address"); 217 | sendResponse(exchange, decompileFunctionByAddress(address)); 218 | }); 219 | 220 | server.createContext("/disassemble_function", exchange -> { 221 | Map qparams = parseQueryParams(exchange); 222 | String address = qparams.get("address"); 223 | sendResponse(exchange, disassembleFunction(address)); 224 | }); 225 | 226 | server.createContext("/set_decompiler_comment", exchange -> { 227 | Map params = parsePostParams(exchange); 228 | String address = params.get("address"); 229 | String comment = params.get("comment"); 230 | boolean success = setDecompilerComment(address, comment); 231 | sendResponse(exchange, success ? "Comment set successfully" : "Failed to set comment"); 232 | }); 233 | 234 | server.createContext("/set_disassembly_comment", exchange -> { 235 | Map params = parsePostParams(exchange); 236 | String address = params.get("address"); 237 | String comment = params.get("comment"); 238 | boolean success = setDisassemblyComment(address, comment); 239 | sendResponse(exchange, success ? "Comment set successfully" : "Failed to set comment"); 240 | }); 241 | 242 | server.createContext("/rename_function_by_address", exchange -> { 243 | Map params = parsePostParams(exchange); 244 | String functionAddress = params.get("function_address"); 245 | String newName = params.get("new_name"); 246 | boolean success = renameFunctionByAddress(functionAddress, newName); 247 | sendResponse(exchange, success ? "Function renamed successfully" : "Failed to rename function"); 248 | }); 249 | 250 | server.createContext("/set_function_prototype", exchange -> { 251 | Map params = parsePostParams(exchange); 252 | String functionAddress = params.get("function_address"); 253 | String prototype = params.get("prototype"); 254 | 255 | // Call the set prototype function and get detailed result 256 | PrototypeResult result = setFunctionPrototype(functionAddress, prototype); 257 | 258 | if (result.isSuccess()) { 259 | // Even with successful operations, include any warning messages for debugging 260 | String successMsg = "Function prototype set successfully"; 261 | if (!result.getErrorMessage().isEmpty()) { 262 | successMsg += "\n\nWarnings/Debug Info:\n" + result.getErrorMessage(); 263 | } 264 | sendResponse(exchange, successMsg); 265 | } else { 266 | // Return the detailed error message to the client 267 | sendResponse(exchange, "Failed to set function prototype: " + result.getErrorMessage()); 268 | } 269 | }); 270 | 271 | server.createContext("/set_local_variable_type", exchange -> { 272 | Map params = parsePostParams(exchange); 273 | String functionAddress = params.get("function_address"); 274 | String variableName = params.get("variable_name"); 275 | String newType = params.get("new_type"); 276 | 277 | // Capture detailed information about setting the type 278 | StringBuilder responseMsg = new StringBuilder(); 279 | responseMsg.append("Setting variable type: ").append(variableName) 280 | .append(" to ").append(newType) 281 | .append(" in function at ").append(functionAddress).append("\n\n"); 282 | 283 | // Attempt to find the data type in various categories 284 | Program program = getCurrentProgram(); 285 | if (program != null) { 286 | DataTypeManager dtm = program.getDataTypeManager(); 287 | DataType directType = findDataTypeByNameInAllCategories(dtm, newType); 288 | if (directType != null) { 289 | responseMsg.append("Found type: ").append(directType.getPathName()).append("\n"); 290 | } else if (newType.startsWith("P") && newType.length() > 1) { 291 | String baseTypeName = newType.substring(1); 292 | DataType baseType = findDataTypeByNameInAllCategories(dtm, baseTypeName); 293 | if (baseType != null) { 294 | responseMsg.append("Found base type for pointer: ").append(baseType.getPathName()).append("\n"); 295 | } else { 296 | responseMsg.append("Base type not found for pointer: ").append(baseTypeName).append("\n"); 297 | } 298 | } else { 299 | responseMsg.append("Type not found directly: ").append(newType).append("\n"); 300 | } 301 | } 302 | 303 | // Try to set the type 304 | boolean success = setLocalVariableType(functionAddress, variableName, newType); 305 | 306 | String successMsg = success ? "Variable type set successfully" : "Failed to set variable type"; 307 | responseMsg.append("\nResult: ").append(successMsg); 308 | 309 | sendResponse(exchange, responseMsg.toString()); 310 | }); 311 | 312 | server.createContext("/xrefs_to", exchange -> { 313 | Map qparams = parseQueryParams(exchange); 314 | String address = qparams.get("address"); 315 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 316 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 317 | sendResponse(exchange, getXrefsTo(address, offset, limit)); 318 | }); 319 | 320 | server.createContext("/xrefs_from", exchange -> { 321 | Map qparams = parseQueryParams(exchange); 322 | String address = qparams.get("address"); 323 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 324 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 325 | sendResponse(exchange, getXrefsFrom(address, offset, limit)); 326 | }); 327 | 328 | server.createContext("/function_xrefs", exchange -> { 329 | Map qparams = parseQueryParams(exchange); 330 | String name = qparams.get("name"); 331 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 332 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 333 | sendResponse(exchange, getFunctionXrefs(name, offset, limit)); 334 | }); 335 | 336 | server.createContext("/strings", exchange -> { 337 | Map qparams = parseQueryParams(exchange); 338 | int offset = parseIntOrDefault(qparams.get("offset"), 0); 339 | int limit = parseIntOrDefault(qparams.get("limit"), 100); 340 | String filter = qparams.get("filter"); 341 | sendResponse(exchange, listDefinedStrings(offset, limit, filter)); 342 | }); 343 | 344 | server.setExecutor(null); 345 | new Thread(() -> { 346 | try { 347 | server.start(); 348 | Msg.info(this, "GhidraMCP HTTP server started on port " + port); 349 | } catch (Exception e) { 350 | Msg.error(this, "Failed to start HTTP server on port " + port + ". Port might be in use.", e); 351 | server = null; // Ensure server isn't considered running 352 | } 353 | }, "GhidraMCP-HTTP-Server").start(); 354 | } 355 | 356 | // ---------------------------------------------------------------------------------- 357 | // Pagination-aware listing methods 358 | // ---------------------------------------------------------------------------------- 359 | 360 | private String getAllFunctionNames(int offset, int limit) { 361 | Program program = getCurrentProgram(); 362 | if (program == null) return "No program loaded"; 363 | 364 | List names = new ArrayList<>(); 365 | for (Function f : program.getFunctionManager().getFunctions(true)) { 366 | names.add(f.getName()); 367 | } 368 | return paginateList(names, offset, limit); 369 | } 370 | 371 | private String getAllClassNames(int offset, int limit) { 372 | Program program = getCurrentProgram(); 373 | if (program == null) return "No program loaded"; 374 | 375 | Set classNames = new HashSet<>(); 376 | for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { 377 | Namespace ns = symbol.getParentNamespace(); 378 | if (ns != null && !ns.isGlobal()) { 379 | classNames.add(ns.getName()); 380 | } 381 | } 382 | // Convert set to list for pagination 383 | List sorted = new ArrayList<>(classNames); 384 | Collections.sort(sorted); 385 | return paginateList(sorted, offset, limit); 386 | } 387 | 388 | private String listSegments(int offset, int limit) { 389 | Program program = getCurrentProgram(); 390 | if (program == null) return "No program loaded"; 391 | 392 | List lines = new ArrayList<>(); 393 | for (MemoryBlock block : program.getMemory().getBlocks()) { 394 | lines.add(String.format("%s: %s - %s", block.getName(), block.getStart(), block.getEnd())); 395 | } 396 | return paginateList(lines, offset, limit); 397 | } 398 | 399 | private String listImports(int offset, int limit) { 400 | Program program = getCurrentProgram(); 401 | if (program == null) return "No program loaded"; 402 | 403 | List lines = new ArrayList<>(); 404 | for (Symbol symbol : program.getSymbolTable().getExternalSymbols()) { 405 | lines.add(symbol.getName() + " -> " + symbol.getAddress()); 406 | } 407 | return paginateList(lines, offset, limit); 408 | } 409 | 410 | private String listExports(int offset, int limit) { 411 | Program program = getCurrentProgram(); 412 | if (program == null) return "No program loaded"; 413 | 414 | SymbolTable table = program.getSymbolTable(); 415 | SymbolIterator it = table.getAllSymbols(true); 416 | 417 | List lines = new ArrayList<>(); 418 | while (it.hasNext()) { 419 | Symbol s = it.next(); 420 | // On older Ghidra, "export" is recognized via isExternalEntryPoint() 421 | if (s.isExternalEntryPoint()) { 422 | lines.add(s.getName() + " -> " + s.getAddress()); 423 | } 424 | } 425 | return paginateList(lines, offset, limit); 426 | } 427 | 428 | private String listNamespaces(int offset, int limit) { 429 | Program program = getCurrentProgram(); 430 | if (program == null) return "No program loaded"; 431 | 432 | Set namespaces = new HashSet<>(); 433 | for (Symbol symbol : program.getSymbolTable().getAllSymbols(true)) { 434 | Namespace ns = symbol.getParentNamespace(); 435 | if (ns != null && !(ns instanceof GlobalNamespace)) { 436 | namespaces.add(ns.getName()); 437 | } 438 | } 439 | List sorted = new ArrayList<>(namespaces); 440 | Collections.sort(sorted); 441 | return paginateList(sorted, offset, limit); 442 | } 443 | 444 | private String listDefinedData(int offset, int limit) { 445 | Program program = getCurrentProgram(); 446 | if (program == null) return "No program loaded"; 447 | 448 | List lines = new ArrayList<>(); 449 | for (MemoryBlock block : program.getMemory().getBlocks()) { 450 | DataIterator it = program.getListing().getDefinedData(block.getStart(), true); 451 | while (it.hasNext()) { 452 | Data data = it.next(); 453 | if (block.contains(data.getAddress())) { 454 | String label = data.getLabel() != null ? data.getLabel() : "(unnamed)"; 455 | String valRepr = data.getDefaultValueRepresentation(); 456 | lines.add(String.format("%s: %s = %s", 457 | data.getAddress(), 458 | escapeNonAscii(label), 459 | escapeNonAscii(valRepr) 460 | )); 461 | } 462 | } 463 | } 464 | return paginateList(lines, offset, limit); 465 | } 466 | 467 | private String searchFunctionsByName(String searchTerm, int offset, int limit) { 468 | Program program = getCurrentProgram(); 469 | if (program == null) return "No program loaded"; 470 | if (searchTerm == null || searchTerm.isEmpty()) return "Search term is required"; 471 | 472 | List matches = new ArrayList<>(); 473 | for (Function func : program.getFunctionManager().getFunctions(true)) { 474 | String name = func.getName(); 475 | // simple substring match 476 | if (name.toLowerCase().contains(searchTerm.toLowerCase())) { 477 | matches.add(String.format("%s @ %s", name, func.getEntryPoint())); 478 | } 479 | } 480 | 481 | Collections.sort(matches); 482 | 483 | if (matches.isEmpty()) { 484 | return "No functions matching '" + searchTerm + "'"; 485 | } 486 | return paginateList(matches, offset, limit); 487 | } 488 | 489 | // ---------------------------------------------------------------------------------- 490 | // Logic for rename, decompile, etc. 491 | // ---------------------------------------------------------------------------------- 492 | 493 | private String decompileFunctionByName(String name) { 494 | Program program = getCurrentProgram(); 495 | if (program == null) return "No program loaded"; 496 | DecompInterface decomp = new DecompInterface(); 497 | decomp.openProgram(program); 498 | for (Function func : program.getFunctionManager().getFunctions(true)) { 499 | if (func.getName().equals(name)) { 500 | DecompileResults result = 501 | decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); 502 | if (result != null && result.decompileCompleted()) { 503 | return result.getDecompiledFunction().getC(); 504 | } else { 505 | return "Decompilation failed"; 506 | } 507 | } 508 | } 509 | return "Function not found"; 510 | } 511 | 512 | private boolean renameFunction(String oldName, String newName) { 513 | Program program = getCurrentProgram(); 514 | if (program == null) return false; 515 | 516 | AtomicBoolean successFlag = new AtomicBoolean(false); 517 | try { 518 | SwingUtilities.invokeAndWait(() -> { 519 | int tx = program.startTransaction("Rename function via HTTP"); 520 | try { 521 | for (Function func : program.getFunctionManager().getFunctions(true)) { 522 | if (func.getName().equals(oldName)) { 523 | func.setName(newName, SourceType.USER_DEFINED); 524 | successFlag.set(true); 525 | break; 526 | } 527 | } 528 | } 529 | catch (Exception e) { 530 | Msg.error(this, "Error renaming function", e); 531 | } 532 | finally { 533 | program.endTransaction(tx, successFlag.get()); 534 | } 535 | }); 536 | } 537 | catch (InterruptedException | InvocationTargetException e) { 538 | Msg.error(this, "Failed to execute rename on Swing thread", e); 539 | } 540 | return successFlag.get(); 541 | } 542 | 543 | private void renameDataAtAddress(String addressStr, String newName) { 544 | Program program = getCurrentProgram(); 545 | if (program == null) return; 546 | 547 | try { 548 | SwingUtilities.invokeAndWait(() -> { 549 | int tx = program.startTransaction("Rename data"); 550 | try { 551 | Address addr = program.getAddressFactory().getAddress(addressStr); 552 | Listing listing = program.getListing(); 553 | Data data = listing.getDefinedDataAt(addr); 554 | if (data != null) { 555 | SymbolTable symTable = program.getSymbolTable(); 556 | Symbol symbol = symTable.getPrimarySymbol(addr); 557 | if (symbol != null) { 558 | symbol.setName(newName, SourceType.USER_DEFINED); 559 | } else { 560 | symTable.createLabel(addr, newName, SourceType.USER_DEFINED); 561 | } 562 | } 563 | } 564 | catch (Exception e) { 565 | Msg.error(this, "Rename data error", e); 566 | } 567 | finally { 568 | program.endTransaction(tx, true); 569 | } 570 | }); 571 | } 572 | catch (InterruptedException | InvocationTargetException e) { 573 | Msg.error(this, "Failed to execute rename data on Swing thread", e); 574 | } 575 | } 576 | 577 | private String renameVariableInFunction(String functionName, String oldVarName, String newVarName) { 578 | Program program = getCurrentProgram(); 579 | if (program == null) return "No program loaded"; 580 | 581 | DecompInterface decomp = new DecompInterface(); 582 | decomp.openProgram(program); 583 | 584 | Function func = null; 585 | for (Function f : program.getFunctionManager().getFunctions(true)) { 586 | if (f.getName().equals(functionName)) { 587 | func = f; 588 | break; 589 | } 590 | } 591 | 592 | if (func == null) { 593 | return "Function not found"; 594 | } 595 | 596 | DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); 597 | if (result == null || !result.decompileCompleted()) { 598 | return "Decompilation failed"; 599 | } 600 | 601 | HighFunction highFunction = result.getHighFunction(); 602 | if (highFunction == null) { 603 | return "Decompilation failed (no high function)"; 604 | } 605 | 606 | LocalSymbolMap localSymbolMap = highFunction.getLocalSymbolMap(); 607 | if (localSymbolMap == null) { 608 | return "Decompilation failed (no local symbol map)"; 609 | } 610 | 611 | HighSymbol highSymbol = null; 612 | Iterator symbols = localSymbolMap.getSymbols(); 613 | while (symbols.hasNext()) { 614 | HighSymbol symbol = symbols.next(); 615 | String symbolName = symbol.getName(); 616 | 617 | if (symbolName.equals(oldVarName)) { 618 | highSymbol = symbol; 619 | } 620 | if (symbolName.equals(newVarName)) { 621 | return "Error: A variable with name '" + newVarName + "' already exists in this function"; 622 | } 623 | } 624 | 625 | if (highSymbol == null) { 626 | return "Variable not found"; 627 | } 628 | 629 | boolean commitRequired = checkFullCommit(highSymbol, highFunction); 630 | 631 | final HighSymbol finalHighSymbol = highSymbol; 632 | final Function finalFunction = func; 633 | AtomicBoolean successFlag = new AtomicBoolean(false); 634 | 635 | try { 636 | SwingUtilities.invokeAndWait(() -> { 637 | int tx = program.startTransaction("Rename variable"); 638 | try { 639 | if (commitRequired) { 640 | HighFunctionDBUtil.commitParamsToDatabase(highFunction, false, 641 | ReturnCommitOption.NO_COMMIT, finalFunction.getSignatureSource()); 642 | } 643 | HighFunctionDBUtil.updateDBVariable( 644 | finalHighSymbol, 645 | newVarName, 646 | null, 647 | SourceType.USER_DEFINED 648 | ); 649 | successFlag.set(true); 650 | } 651 | catch (Exception e) { 652 | Msg.error(this, "Failed to rename variable", e); 653 | } 654 | finally { 655 | program.endTransaction(tx, true); 656 | } 657 | }); 658 | } catch (InterruptedException | InvocationTargetException e) { 659 | String errorMsg = "Failed to execute rename on Swing thread: " + e.getMessage(); 660 | Msg.error(this, errorMsg, e); 661 | return errorMsg; 662 | } 663 | return successFlag.get() ? "Variable renamed" : "Failed to rename variable"; 664 | } 665 | 666 | /** 667 | * Copied from AbstractDecompilerAction.checkFullCommit, it's protected. 668 | * Compare the given HighFunction's idea of the prototype with the Function's idea. 669 | * Return true if there is a difference. If a specific symbol is being changed, 670 | * it can be passed in to check whether or not the prototype is being affected. 671 | * @param highSymbol (if not null) is the symbol being modified 672 | * @param hfunction is the given HighFunction 673 | * @return true if there is a difference (and a full commit is required) 674 | */ 675 | protected static boolean checkFullCommit(HighSymbol highSymbol, HighFunction hfunction) { 676 | if (highSymbol != null && !highSymbol.isParameter()) { 677 | return false; 678 | } 679 | Function function = hfunction.getFunction(); 680 | Parameter[] parameters = function.getParameters(); 681 | LocalSymbolMap localSymbolMap = hfunction.getLocalSymbolMap(); 682 | int numParams = localSymbolMap.getNumParams(); 683 | if (numParams != parameters.length) { 684 | return true; 685 | } 686 | 687 | for (int i = 0; i < numParams; i++) { 688 | HighSymbol param = localSymbolMap.getParamSymbol(i); 689 | if (param.getCategoryIndex() != i) { 690 | return true; 691 | } 692 | VariableStorage storage = param.getStorage(); 693 | // Don't compare using the equals method so that DynamicVariableStorage can match 694 | if (0 != storage.compareTo(parameters[i].getVariableStorage())) { 695 | return true; 696 | } 697 | } 698 | 699 | return false; 700 | } 701 | 702 | // ---------------------------------------------------------------------------------- 703 | // New methods to implement the new functionalities 704 | // ---------------------------------------------------------------------------------- 705 | 706 | /** 707 | * Get function by address 708 | */ 709 | private String getFunctionByAddress(String addressStr) { 710 | Program program = getCurrentProgram(); 711 | if (program == null) return "No program loaded"; 712 | if (addressStr == null || addressStr.isEmpty()) return "Address is required"; 713 | 714 | try { 715 | Address addr = program.getAddressFactory().getAddress(addressStr); 716 | Function func = program.getFunctionManager().getFunctionAt(addr); 717 | 718 | if (func == null) return "No function found at address " + addressStr; 719 | 720 | return String.format("Function: %s at %s\nSignature: %s\nEntry: %s\nBody: %s - %s", 721 | func.getName(), 722 | func.getEntryPoint(), 723 | func.getSignature(), 724 | func.getEntryPoint(), 725 | func.getBody().getMinAddress(), 726 | func.getBody().getMaxAddress()); 727 | } catch (Exception e) { 728 | return "Error getting function: " + e.getMessage(); 729 | } 730 | } 731 | 732 | /** 733 | * Get current address selected in Ghidra GUI 734 | */ 735 | private String getCurrentAddress() { 736 | CodeViewerService service = tool.getService(CodeViewerService.class); 737 | if (service == null) return "Code viewer service not available"; 738 | 739 | ProgramLocation location = service.getCurrentLocation(); 740 | return (location != null) ? location.getAddress().toString() : "No current location"; 741 | } 742 | 743 | /** 744 | * Get current function selected in Ghidra GUI 745 | */ 746 | private String getCurrentFunction() { 747 | CodeViewerService service = tool.getService(CodeViewerService.class); 748 | if (service == null) return "Code viewer service not available"; 749 | 750 | ProgramLocation location = service.getCurrentLocation(); 751 | if (location == null) return "No current location"; 752 | 753 | Program program = getCurrentProgram(); 754 | if (program == null) return "No program loaded"; 755 | 756 | Function func = program.getFunctionManager().getFunctionContaining(location.getAddress()); 757 | if (func == null) return "No function at current location: " + location.getAddress(); 758 | 759 | return String.format("Function: %s at %s\nSignature: %s", 760 | func.getName(), 761 | func.getEntryPoint(), 762 | func.getSignature()); 763 | } 764 | 765 | /** 766 | * List all functions in the database 767 | */ 768 | private String listFunctions() { 769 | Program program = getCurrentProgram(); 770 | if (program == null) return "No program loaded"; 771 | 772 | StringBuilder result = new StringBuilder(); 773 | for (Function func : program.getFunctionManager().getFunctions(true)) { 774 | result.append(String.format("%s at %s\n", 775 | func.getName(), 776 | func.getEntryPoint())); 777 | } 778 | 779 | return result.toString(); 780 | } 781 | 782 | /** 783 | * Gets a function at the given address or containing the address 784 | * @return the function or null if not found 785 | */ 786 | private Function getFunctionForAddress(Program program, Address addr) { 787 | Function func = program.getFunctionManager().getFunctionAt(addr); 788 | if (func == null) { 789 | func = program.getFunctionManager().getFunctionContaining(addr); 790 | } 791 | return func; 792 | } 793 | 794 | /** 795 | * Decompile a function at the given address 796 | */ 797 | private String decompileFunctionByAddress(String addressStr) { 798 | Program program = getCurrentProgram(); 799 | if (program == null) return "No program loaded"; 800 | if (addressStr == null || addressStr.isEmpty()) return "Address is required"; 801 | 802 | try { 803 | Address addr = program.getAddressFactory().getAddress(addressStr); 804 | Function func = getFunctionForAddress(program, addr); 805 | if (func == null) return "No function found at or containing address " + addressStr; 806 | 807 | DecompInterface decomp = new DecompInterface(); 808 | decomp.openProgram(program); 809 | DecompileResults result = decomp.decompileFunction(func, 30, new ConsoleTaskMonitor()); 810 | 811 | return (result != null && result.decompileCompleted()) 812 | ? result.getDecompiledFunction().getC() 813 | : "Decompilation failed"; 814 | } catch (Exception e) { 815 | return "Error decompiling function: " + e.getMessage(); 816 | } 817 | } 818 | 819 | /** 820 | * Get assembly code for a function 821 | */ 822 | private String disassembleFunction(String addressStr) { 823 | Program program = getCurrentProgram(); 824 | if (program == null) return "No program loaded"; 825 | if (addressStr == null || addressStr.isEmpty()) return "Address is required"; 826 | 827 | try { 828 | Address addr = program.getAddressFactory().getAddress(addressStr); 829 | Function func = getFunctionForAddress(program, addr); 830 | if (func == null) return "No function found at or containing address " + addressStr; 831 | 832 | StringBuilder result = new StringBuilder(); 833 | Listing listing = program.getListing(); 834 | Address start = func.getEntryPoint(); 835 | Address end = func.getBody().getMaxAddress(); 836 | 837 | InstructionIterator instructions = listing.getInstructions(start, true); 838 | while (instructions.hasNext()) { 839 | Instruction instr = instructions.next(); 840 | if (instr.getAddress().compareTo(end) > 0) { 841 | break; // Stop if we've gone past the end of the function 842 | } 843 | String comment = listing.getComment(CodeUnit.EOL_COMMENT, instr.getAddress()); 844 | comment = (comment != null) ? "; " + comment : ""; 845 | 846 | result.append(String.format("%s: %s %s\n", 847 | instr.getAddress(), 848 | instr.toString(), 849 | comment)); 850 | } 851 | 852 | return result.toString(); 853 | } catch (Exception e) { 854 | return "Error disassembling function: " + e.getMessage(); 855 | } 856 | } 857 | 858 | /** 859 | * Set a comment using the specified comment type (PRE_COMMENT or EOL_COMMENT) 860 | */ 861 | private boolean setCommentAtAddress(String addressStr, String comment, int commentType, String transactionName) { 862 | Program program = getCurrentProgram(); 863 | if (program == null) return false; 864 | if (addressStr == null || addressStr.isEmpty() || comment == null) return false; 865 | 866 | AtomicBoolean success = new AtomicBoolean(false); 867 | 868 | try { 869 | SwingUtilities.invokeAndWait(() -> { 870 | int tx = program.startTransaction(transactionName); 871 | try { 872 | Address addr = program.getAddressFactory().getAddress(addressStr); 873 | program.getListing().setComment(addr, commentType, comment); 874 | success.set(true); 875 | } catch (Exception e) { 876 | Msg.error(this, "Error setting " + transactionName.toLowerCase(), e); 877 | } finally { 878 | program.endTransaction(tx, success.get()); 879 | } 880 | }); 881 | } catch (InterruptedException | InvocationTargetException e) { 882 | Msg.error(this, "Failed to execute " + transactionName.toLowerCase() + " on Swing thread", e); 883 | } 884 | 885 | return success.get(); 886 | } 887 | 888 | /** 889 | * Set a comment for a given address in the function pseudocode 890 | */ 891 | private boolean setDecompilerComment(String addressStr, String comment) { 892 | return setCommentAtAddress(addressStr, comment, CodeUnit.PRE_COMMENT, "Set decompiler comment"); 893 | } 894 | 895 | /** 896 | * Set a comment for a given address in the function disassembly 897 | */ 898 | private boolean setDisassemblyComment(String addressStr, String comment) { 899 | return setCommentAtAddress(addressStr, comment, CodeUnit.EOL_COMMENT, "Set disassembly comment"); 900 | } 901 | 902 | /** 903 | * Class to hold the result of a prototype setting operation 904 | */ 905 | private static class PrototypeResult { 906 | private final boolean success; 907 | private final String errorMessage; 908 | 909 | public PrototypeResult(boolean success, String errorMessage) { 910 | this.success = success; 911 | this.errorMessage = errorMessage; 912 | } 913 | 914 | public boolean isSuccess() { 915 | return success; 916 | } 917 | 918 | public String getErrorMessage() { 919 | return errorMessage; 920 | } 921 | } 922 | 923 | /** 924 | * Rename a function by its address 925 | */ 926 | private boolean renameFunctionByAddress(String functionAddrStr, String newName) { 927 | Program program = getCurrentProgram(); 928 | if (program == null) return false; 929 | if (functionAddrStr == null || functionAddrStr.isEmpty() || 930 | newName == null || newName.isEmpty()) { 931 | return false; 932 | } 933 | 934 | AtomicBoolean success = new AtomicBoolean(false); 935 | 936 | try { 937 | SwingUtilities.invokeAndWait(() -> { 938 | performFunctionRename(program, functionAddrStr, newName, success); 939 | }); 940 | } catch (InterruptedException | InvocationTargetException e) { 941 | Msg.error(this, "Failed to execute rename function on Swing thread", e); 942 | } 943 | 944 | return success.get(); 945 | } 946 | 947 | /** 948 | * Helper method to perform the actual function rename within a transaction 949 | */ 950 | private void performFunctionRename(Program program, String functionAddrStr, String newName, AtomicBoolean success) { 951 | int tx = program.startTransaction("Rename function by address"); 952 | try { 953 | Address addr = program.getAddressFactory().getAddress(functionAddrStr); 954 | Function func = getFunctionForAddress(program, addr); 955 | 956 | if (func == null) { 957 | Msg.error(this, "Could not find function at address: " + functionAddrStr); 958 | return; 959 | } 960 | 961 | func.setName(newName, SourceType.USER_DEFINED); 962 | success.set(true); 963 | } catch (Exception e) { 964 | Msg.error(this, "Error renaming function by address", e); 965 | } finally { 966 | program.endTransaction(tx, success.get()); 967 | } 968 | } 969 | 970 | /** 971 | * Set a function's prototype with proper error handling using ApplyFunctionSignatureCmd 972 | */ 973 | private PrototypeResult setFunctionPrototype(String functionAddrStr, String prototype) { 974 | // Input validation 975 | Program program = getCurrentProgram(); 976 | if (program == null) return new PrototypeResult(false, "No program loaded"); 977 | if (functionAddrStr == null || functionAddrStr.isEmpty()) { 978 | return new PrototypeResult(false, "Function address is required"); 979 | } 980 | if (prototype == null || prototype.isEmpty()) { 981 | return new PrototypeResult(false, "Function prototype is required"); 982 | } 983 | 984 | final StringBuilder errorMessage = new StringBuilder(); 985 | final AtomicBoolean success = new AtomicBoolean(false); 986 | 987 | try { 988 | SwingUtilities.invokeAndWait(() -> 989 | applyFunctionPrototype(program, functionAddrStr, prototype, success, errorMessage)); 990 | } catch (InterruptedException | InvocationTargetException e) { 991 | String msg = "Failed to set function prototype on Swing thread: " + e.getMessage(); 992 | errorMessage.append(msg); 993 | Msg.error(this, msg, e); 994 | } 995 | 996 | return new PrototypeResult(success.get(), errorMessage.toString()); 997 | } 998 | 999 | /** 1000 | * Helper method that applies the function prototype within a transaction 1001 | */ 1002 | private void applyFunctionPrototype(Program program, String functionAddrStr, String prototype, 1003 | AtomicBoolean success, StringBuilder errorMessage) { 1004 | try { 1005 | // Get the address and function 1006 | Address addr = program.getAddressFactory().getAddress(functionAddrStr); 1007 | Function func = getFunctionForAddress(program, addr); 1008 | 1009 | if (func == null) { 1010 | String msg = "Could not find function at address: " + functionAddrStr; 1011 | errorMessage.append(msg); 1012 | Msg.error(this, msg); 1013 | return; 1014 | } 1015 | 1016 | Msg.info(this, "Setting prototype for function " + func.getName() + ": " + prototype); 1017 | 1018 | // Store original prototype as a comment for reference 1019 | addPrototypeComment(program, func, prototype); 1020 | 1021 | // Use ApplyFunctionSignatureCmd to parse and apply the signature 1022 | parseFunctionSignatureAndApply(program, addr, prototype, success, errorMessage); 1023 | 1024 | } catch (Exception e) { 1025 | String msg = "Error setting function prototype: " + e.getMessage(); 1026 | errorMessage.append(msg); 1027 | Msg.error(this, msg, e); 1028 | } 1029 | } 1030 | 1031 | /** 1032 | * Add a comment showing the prototype being set 1033 | */ 1034 | private void addPrototypeComment(Program program, Function func, String prototype) { 1035 | int txComment = program.startTransaction("Add prototype comment"); 1036 | try { 1037 | program.getListing().setComment( 1038 | func.getEntryPoint(), 1039 | CodeUnit.PLATE_COMMENT, 1040 | "Setting prototype: " + prototype 1041 | ); 1042 | } finally { 1043 | program.endTransaction(txComment, true); 1044 | } 1045 | } 1046 | 1047 | /** 1048 | * Parse and apply the function signature with error handling 1049 | */ 1050 | private void parseFunctionSignatureAndApply(Program program, Address addr, String prototype, 1051 | AtomicBoolean success, StringBuilder errorMessage) { 1052 | // Use ApplyFunctionSignatureCmd to parse and apply the signature 1053 | int txProto = program.startTransaction("Set function prototype"); 1054 | try { 1055 | // Get data type manager 1056 | DataTypeManager dtm = program.getDataTypeManager(); 1057 | 1058 | // Get data type manager service 1059 | ghidra.app.services.DataTypeManagerService dtms = 1060 | tool.getService(ghidra.app.services.DataTypeManagerService.class); 1061 | 1062 | // Create function signature parser 1063 | ghidra.app.util.parser.FunctionSignatureParser parser = 1064 | new ghidra.app.util.parser.FunctionSignatureParser(dtm, dtms); 1065 | 1066 | // Parse the prototype into a function signature 1067 | ghidra.program.model.data.FunctionDefinitionDataType sig = parser.parse(null, prototype); 1068 | 1069 | if (sig == null) { 1070 | String msg = "Failed to parse function prototype"; 1071 | errorMessage.append(msg); 1072 | Msg.error(this, msg); 1073 | return; 1074 | } 1075 | 1076 | // Create and apply the command 1077 | ghidra.app.cmd.function.ApplyFunctionSignatureCmd cmd = 1078 | new ghidra.app.cmd.function.ApplyFunctionSignatureCmd( 1079 | addr, sig, SourceType.USER_DEFINED); 1080 | 1081 | // Apply the command to the program 1082 | boolean cmdResult = cmd.applyTo(program, new ConsoleTaskMonitor()); 1083 | 1084 | if (cmdResult) { 1085 | success.set(true); 1086 | Msg.info(this, "Successfully applied function signature"); 1087 | } else { 1088 | String msg = "Command failed: " + cmd.getStatusMsg(); 1089 | errorMessage.append(msg); 1090 | Msg.error(this, msg); 1091 | } 1092 | } catch (Exception e) { 1093 | String msg = "Error applying function signature: " + e.getMessage(); 1094 | errorMessage.append(msg); 1095 | Msg.error(this, msg, e); 1096 | } finally { 1097 | program.endTransaction(txProto, success.get()); 1098 | } 1099 | } 1100 | 1101 | /** 1102 | * Set a local variable's type using HighFunctionDBUtil.updateDBVariable 1103 | */ 1104 | private boolean setLocalVariableType(String functionAddrStr, String variableName, String newType) { 1105 | // Input validation 1106 | Program program = getCurrentProgram(); 1107 | if (program == null) return false; 1108 | if (functionAddrStr == null || functionAddrStr.isEmpty() || 1109 | variableName == null || variableName.isEmpty() || 1110 | newType == null || newType.isEmpty()) { 1111 | return false; 1112 | } 1113 | 1114 | AtomicBoolean success = new AtomicBoolean(false); 1115 | 1116 | try { 1117 | SwingUtilities.invokeAndWait(() -> 1118 | applyVariableType(program, functionAddrStr, variableName, newType, success)); 1119 | } catch (InterruptedException | InvocationTargetException e) { 1120 | Msg.error(this, "Failed to execute set variable type on Swing thread", e); 1121 | } 1122 | 1123 | return success.get(); 1124 | } 1125 | 1126 | /** 1127 | * Helper method that performs the actual variable type change 1128 | */ 1129 | private void applyVariableType(Program program, String functionAddrStr, 1130 | String variableName, String newType, AtomicBoolean success) { 1131 | try { 1132 | // Find the function 1133 | Address addr = program.getAddressFactory().getAddress(functionAddrStr); 1134 | Function func = getFunctionForAddress(program, addr); 1135 | 1136 | if (func == null) { 1137 | Msg.error(this, "Could not find function at address: " + functionAddrStr); 1138 | return; 1139 | } 1140 | 1141 | DecompileResults results = decompileFunction(func, program); 1142 | if (results == null || !results.decompileCompleted()) { 1143 | return; 1144 | } 1145 | 1146 | ghidra.program.model.pcode.HighFunction highFunction = results.getHighFunction(); 1147 | if (highFunction == null) { 1148 | Msg.error(this, "No high function available"); 1149 | return; 1150 | } 1151 | 1152 | // Find the symbol by name 1153 | HighSymbol symbol = findSymbolByName(highFunction, variableName); 1154 | if (symbol == null) { 1155 | Msg.error(this, "Could not find variable '" + variableName + "' in decompiled function"); 1156 | return; 1157 | } 1158 | 1159 | // Get high variable 1160 | HighVariable highVar = symbol.getHighVariable(); 1161 | if (highVar == null) { 1162 | Msg.error(this, "No HighVariable found for symbol: " + variableName); 1163 | return; 1164 | } 1165 | 1166 | Msg.info(this, "Found high variable for: " + variableName + 1167 | " with current type " + highVar.getDataType().getName()); 1168 | 1169 | // Find the data type 1170 | DataTypeManager dtm = program.getDataTypeManager(); 1171 | DataType dataType = resolveDataType(dtm, newType); 1172 | 1173 | if (dataType == null) { 1174 | Msg.error(this, "Could not resolve data type: " + newType); 1175 | return; 1176 | } 1177 | 1178 | Msg.info(this, "Using data type: " + dataType.getName() + " for variable " + variableName); 1179 | 1180 | // Apply the type change in a transaction 1181 | updateVariableType(program, symbol, dataType, success); 1182 | 1183 | } catch (Exception e) { 1184 | Msg.error(this, "Error setting variable type: " + e.getMessage()); 1185 | } 1186 | } 1187 | 1188 | /** 1189 | * Find a high symbol by name in the given high function 1190 | */ 1191 | private HighSymbol findSymbolByName(ghidra.program.model.pcode.HighFunction highFunction, String variableName) { 1192 | Iterator symbols = highFunction.getLocalSymbolMap().getSymbols(); 1193 | while (symbols.hasNext()) { 1194 | HighSymbol s = symbols.next(); 1195 | if (s.getName().equals(variableName)) { 1196 | return s; 1197 | } 1198 | } 1199 | return null; 1200 | } 1201 | 1202 | /** 1203 | * Decompile a function and return the results 1204 | */ 1205 | private DecompileResults decompileFunction(Function func, Program program) { 1206 | // Set up decompiler for accessing the decompiled function 1207 | DecompInterface decomp = new DecompInterface(); 1208 | decomp.openProgram(program); 1209 | decomp.setSimplificationStyle("decompile"); // Full decompilation 1210 | 1211 | // Decompile the function 1212 | DecompileResults results = decomp.decompileFunction(func, 60, new ConsoleTaskMonitor()); 1213 | 1214 | if (!results.decompileCompleted()) { 1215 | Msg.error(this, "Could not decompile function: " + results.getErrorMessage()); 1216 | return null; 1217 | } 1218 | 1219 | return results; 1220 | } 1221 | 1222 | /** 1223 | * Apply the type update in a transaction 1224 | */ 1225 | private void updateVariableType(Program program, HighSymbol symbol, DataType dataType, AtomicBoolean success) { 1226 | int tx = program.startTransaction("Set variable type"); 1227 | try { 1228 | // Use HighFunctionDBUtil to update the variable with the new type 1229 | HighFunctionDBUtil.updateDBVariable( 1230 | symbol, // The high symbol to modify 1231 | symbol.getName(), // Keep original name 1232 | dataType, // The new data type 1233 | SourceType.USER_DEFINED // Mark as user-defined 1234 | ); 1235 | 1236 | success.set(true); 1237 | Msg.info(this, "Successfully set variable type using HighFunctionDBUtil"); 1238 | } catch (Exception e) { 1239 | Msg.error(this, "Error setting variable type: " + e.getMessage()); 1240 | } finally { 1241 | program.endTransaction(tx, success.get()); 1242 | } 1243 | } 1244 | 1245 | /** 1246 | * Get all references to a specific address (xref to) 1247 | */ 1248 | private String getXrefsTo(String addressStr, int offset, int limit) { 1249 | Program program = getCurrentProgram(); 1250 | if (program == null) return "No program loaded"; 1251 | if (addressStr == null || addressStr.isEmpty()) return "Address is required"; 1252 | 1253 | try { 1254 | Address addr = program.getAddressFactory().getAddress(addressStr); 1255 | ReferenceManager refManager = program.getReferenceManager(); 1256 | 1257 | ReferenceIterator refIter = refManager.getReferencesTo(addr); 1258 | 1259 | List refs = new ArrayList<>(); 1260 | while (refIter.hasNext()) { 1261 | Reference ref = refIter.next(); 1262 | Address fromAddr = ref.getFromAddress(); 1263 | RefType refType = ref.getReferenceType(); 1264 | 1265 | Function fromFunc = program.getFunctionManager().getFunctionContaining(fromAddr); 1266 | String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : ""; 1267 | 1268 | refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName())); 1269 | } 1270 | 1271 | return paginateList(refs, offset, limit); 1272 | } catch (Exception e) { 1273 | return "Error getting references to address: " + e.getMessage(); 1274 | } 1275 | } 1276 | 1277 | /** 1278 | * Get all references from a specific address (xref from) 1279 | */ 1280 | private String getXrefsFrom(String addressStr, int offset, int limit) { 1281 | Program program = getCurrentProgram(); 1282 | if (program == null) return "No program loaded"; 1283 | if (addressStr == null || addressStr.isEmpty()) return "Address is required"; 1284 | 1285 | try { 1286 | Address addr = program.getAddressFactory().getAddress(addressStr); 1287 | ReferenceManager refManager = program.getReferenceManager(); 1288 | 1289 | Reference[] references = refManager.getReferencesFrom(addr); 1290 | 1291 | List refs = new ArrayList<>(); 1292 | for (Reference ref : references) { 1293 | Address toAddr = ref.getToAddress(); 1294 | RefType refType = ref.getReferenceType(); 1295 | 1296 | String targetInfo = ""; 1297 | Function toFunc = program.getFunctionManager().getFunctionAt(toAddr); 1298 | if (toFunc != null) { 1299 | targetInfo = " to function " + toFunc.getName(); 1300 | } else { 1301 | Data data = program.getListing().getDataAt(toAddr); 1302 | if (data != null) { 1303 | targetInfo = " to data " + (data.getLabel() != null ? data.getLabel() : data.getPathName()); 1304 | } 1305 | } 1306 | 1307 | refs.add(String.format("To %s%s [%s]", toAddr, targetInfo, refType.getName())); 1308 | } 1309 | 1310 | return paginateList(refs, offset, limit); 1311 | } catch (Exception e) { 1312 | return "Error getting references from address: " + e.getMessage(); 1313 | } 1314 | } 1315 | 1316 | /** 1317 | * Get all references to a specific function by name 1318 | */ 1319 | private String getFunctionXrefs(String functionName, int offset, int limit) { 1320 | Program program = getCurrentProgram(); 1321 | if (program == null) return "No program loaded"; 1322 | if (functionName == null || functionName.isEmpty()) return "Function name is required"; 1323 | 1324 | try { 1325 | List refs = new ArrayList<>(); 1326 | FunctionManager funcManager = program.getFunctionManager(); 1327 | for (Function function : funcManager.getFunctions(true)) { 1328 | if (function.getName().equals(functionName)) { 1329 | Address entryPoint = function.getEntryPoint(); 1330 | ReferenceIterator refIter = program.getReferenceManager().getReferencesTo(entryPoint); 1331 | 1332 | while (refIter.hasNext()) { 1333 | Reference ref = refIter.next(); 1334 | Address fromAddr = ref.getFromAddress(); 1335 | RefType refType = ref.getReferenceType(); 1336 | 1337 | Function fromFunc = funcManager.getFunctionContaining(fromAddr); 1338 | String funcInfo = (fromFunc != null) ? " in " + fromFunc.getName() : ""; 1339 | 1340 | refs.add(String.format("From %s%s [%s]", fromAddr, funcInfo, refType.getName())); 1341 | } 1342 | } 1343 | } 1344 | 1345 | if (refs.isEmpty()) { 1346 | return "No references found to function: " + functionName; 1347 | } 1348 | 1349 | return paginateList(refs, offset, limit); 1350 | } catch (Exception e) { 1351 | return "Error getting function references: " + e.getMessage(); 1352 | } 1353 | } 1354 | 1355 | /** 1356 | * List all defined strings in the program with their addresses 1357 | */ 1358 | private String listDefinedStrings(int offset, int limit, String filter) { 1359 | Program program = getCurrentProgram(); 1360 | if (program == null) return "No program loaded"; 1361 | 1362 | List lines = new ArrayList<>(); 1363 | DataIterator dataIt = program.getListing().getDefinedData(true); 1364 | 1365 | while (dataIt.hasNext()) { 1366 | Data data = dataIt.next(); 1367 | 1368 | if (data != null && isStringData(data)) { 1369 | String value = data.getValue() != null ? data.getValue().toString() : ""; 1370 | 1371 | if (filter == null || value.toLowerCase().contains(filter.toLowerCase())) { 1372 | String escapedValue = escapeString(value); 1373 | lines.add(String.format("%s: \"%s\"", data.getAddress(), escapedValue)); 1374 | } 1375 | } 1376 | } 1377 | 1378 | return paginateList(lines, offset, limit); 1379 | } 1380 | 1381 | /** 1382 | * Check if the given data is a string type 1383 | */ 1384 | private boolean isStringData(Data data) { 1385 | if (data == null) return false; 1386 | 1387 | DataType dt = data.getDataType(); 1388 | String typeName = dt.getName().toLowerCase(); 1389 | return typeName.contains("string") || typeName.contains("char") || typeName.equals("unicode"); 1390 | } 1391 | 1392 | /** 1393 | * Escape special characters in a string for display 1394 | */ 1395 | private String escapeString(String input) { 1396 | if (input == null) return ""; 1397 | 1398 | StringBuilder sb = new StringBuilder(); 1399 | for (int i = 0; i < input.length(); i++) { 1400 | char c = input.charAt(i); 1401 | if (c >= 32 && c < 127) { 1402 | sb.append(c); 1403 | } else if (c == '\n') { 1404 | sb.append("\\n"); 1405 | } else if (c == '\r') { 1406 | sb.append("\\r"); 1407 | } else if (c == '\t') { 1408 | sb.append("\\t"); 1409 | } else { 1410 | sb.append(String.format("\\x%02x", (int)c & 0xFF)); 1411 | } 1412 | } 1413 | return sb.toString(); 1414 | } 1415 | 1416 | /** 1417 | * Resolves a data type by name, handling common types and pointer types 1418 | * @param dtm The data type manager 1419 | * @param typeName The type name to resolve 1420 | * @return The resolved DataType, or null if not found 1421 | */ 1422 | private DataType resolveDataType(DataTypeManager dtm, String typeName) { 1423 | // First try to find exact match in all categories 1424 | DataType dataType = findDataTypeByNameInAllCategories(dtm, typeName); 1425 | if (dataType != null) { 1426 | Msg.info(this, "Found exact data type match: " + dataType.getPathName()); 1427 | return dataType; 1428 | } 1429 | 1430 | // Check for Windows-style pointer types (PXXX) 1431 | if (typeName.startsWith("P") && typeName.length() > 1) { 1432 | String baseTypeName = typeName.substring(1); 1433 | 1434 | // Special case for PVOID 1435 | if (baseTypeName.equals("VOID")) { 1436 | return new PointerDataType(dtm.getDataType("/void")); 1437 | } 1438 | 1439 | // Try to find the base type 1440 | DataType baseType = findDataTypeByNameInAllCategories(dtm, baseTypeName); 1441 | if (baseType != null) { 1442 | return new PointerDataType(baseType); 1443 | } 1444 | 1445 | Msg.warn(this, "Base type not found for " + typeName + ", defaulting to void*"); 1446 | return new PointerDataType(dtm.getDataType("/void")); 1447 | } 1448 | 1449 | // Handle common built-in types 1450 | switch (typeName.toLowerCase()) { 1451 | case "int": 1452 | case "long": 1453 | return dtm.getDataType("/int"); 1454 | case "uint": 1455 | case "unsigned int": 1456 | case "unsigned long": 1457 | case "dword": 1458 | return dtm.getDataType("/uint"); 1459 | case "short": 1460 | return dtm.getDataType("/short"); 1461 | case "ushort": 1462 | case "unsigned short": 1463 | case "word": 1464 | return dtm.getDataType("/ushort"); 1465 | case "char": 1466 | case "byte": 1467 | return dtm.getDataType("/char"); 1468 | case "uchar": 1469 | case "unsigned char": 1470 | return dtm.getDataType("/uchar"); 1471 | case "longlong": 1472 | case "__int64": 1473 | return dtm.getDataType("/longlong"); 1474 | case "ulonglong": 1475 | case "unsigned __int64": 1476 | return dtm.getDataType("/ulonglong"); 1477 | case "bool": 1478 | case "boolean": 1479 | return dtm.getDataType("/bool"); 1480 | case "void": 1481 | return dtm.getDataType("/void"); 1482 | default: 1483 | // Try as a direct path 1484 | DataType directType = dtm.getDataType("/" + typeName); 1485 | if (directType != null) { 1486 | return directType; 1487 | } 1488 | 1489 | // Fallback to int if we couldn't find it 1490 | Msg.warn(this, "Unknown type: " + typeName + ", defaulting to int"); 1491 | return dtm.getDataType("/int"); 1492 | } 1493 | } 1494 | 1495 | /** 1496 | * Find a data type by name in all categories/folders of the data type manager 1497 | * This searches through all categories rather than just the root 1498 | */ 1499 | private DataType findDataTypeByNameInAllCategories(DataTypeManager dtm, String typeName) { 1500 | // Try exact match first 1501 | DataType result = searchByNameInAllCategories(dtm, typeName); 1502 | if (result != null) { 1503 | return result; 1504 | } 1505 | 1506 | // Try lowercase 1507 | return searchByNameInAllCategories(dtm, typeName.toLowerCase()); 1508 | } 1509 | 1510 | /** 1511 | * Helper method to search for a data type by name in all categories 1512 | */ 1513 | private DataType searchByNameInAllCategories(DataTypeManager dtm, String name) { 1514 | // Get all data types from the manager 1515 | Iterator allTypes = dtm.getAllDataTypes(); 1516 | while (allTypes.hasNext()) { 1517 | DataType dt = allTypes.next(); 1518 | // Check if the name matches exactly (case-sensitive) 1519 | if (dt.getName().equals(name)) { 1520 | return dt; 1521 | } 1522 | // For case-insensitive, we want an exact match except for case 1523 | if (dt.getName().equalsIgnoreCase(name)) { 1524 | return dt; 1525 | } 1526 | } 1527 | return null; 1528 | } 1529 | 1530 | // ---------------------------------------------------------------------------------- 1531 | // Utility: parse query params, parse post params, pagination, etc. 1532 | // ---------------------------------------------------------------------------------- 1533 | 1534 | /** 1535 | * Parse query parameters from the URL, e.g. ?offset=10&limit=100 1536 | */ 1537 | private Map parseQueryParams(HttpExchange exchange) { 1538 | Map result = new HashMap<>(); 1539 | String query = exchange.getRequestURI().getQuery(); // e.g. offset=10&limit=100 1540 | if (query != null) { 1541 | String[] pairs = query.split("&"); 1542 | for (String p : pairs) { 1543 | String[] kv = p.split("="); 1544 | if (kv.length == 2) { 1545 | // URL decode parameter values 1546 | try { 1547 | String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); 1548 | String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8); 1549 | result.put(key, value); 1550 | } catch (Exception e) { 1551 | Msg.error(this, "Error decoding URL parameter", e); 1552 | } 1553 | } 1554 | } 1555 | } 1556 | return result; 1557 | } 1558 | 1559 | /** 1560 | * Parse post body form params, e.g. oldName=foo&newName=bar 1561 | */ 1562 | private Map parsePostParams(HttpExchange exchange) throws IOException { 1563 | byte[] body = exchange.getRequestBody().readAllBytes(); 1564 | String bodyStr = new String(body, StandardCharsets.UTF_8); 1565 | Map params = new HashMap<>(); 1566 | for (String pair : bodyStr.split("&")) { 1567 | String[] kv = pair.split("="); 1568 | if (kv.length == 2) { 1569 | // URL decode parameter values 1570 | try { 1571 | String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); 1572 | String value = URLDecoder.decode(kv[1], StandardCharsets.UTF_8); 1573 | params.put(key, value); 1574 | } catch (Exception e) { 1575 | Msg.error(this, "Error decoding URL parameter", e); 1576 | } 1577 | } 1578 | } 1579 | return params; 1580 | } 1581 | 1582 | /** 1583 | * Convert a list of strings into one big newline-delimited string, applying offset & limit. 1584 | */ 1585 | private String paginateList(List items, int offset, int limit) { 1586 | int start = Math.max(0, offset); 1587 | int end = Math.min(items.size(), offset + limit); 1588 | 1589 | if (start >= items.size()) { 1590 | return ""; // no items in range 1591 | } 1592 | List sub = items.subList(start, end); 1593 | return String.join("\n", sub); 1594 | } 1595 | 1596 | /** 1597 | * Parse an integer from a string, or return defaultValue if null/invalid. 1598 | */ 1599 | private int parseIntOrDefault(String val, int defaultValue) { 1600 | if (val == null) return defaultValue; 1601 | try { 1602 | return Integer.parseInt(val); 1603 | } 1604 | catch (NumberFormatException e) { 1605 | return defaultValue; 1606 | } 1607 | } 1608 | 1609 | /** 1610 | * Escape non-ASCII chars to avoid potential decode issues. 1611 | */ 1612 | private String escapeNonAscii(String input) { 1613 | if (input == null) return ""; 1614 | StringBuilder sb = new StringBuilder(); 1615 | for (char c : input.toCharArray()) { 1616 | if (c >= 32 && c < 127) { 1617 | sb.append(c); 1618 | } 1619 | else { 1620 | sb.append("\\x"); 1621 | sb.append(Integer.toHexString(c & 0xFF)); 1622 | } 1623 | } 1624 | return sb.toString(); 1625 | } 1626 | 1627 | public Program getCurrentProgram() { 1628 | ProgramManager pm = tool.getService(ProgramManager.class); 1629 | return pm != null ? pm.getCurrentProgram() : null; 1630 | } 1631 | 1632 | private void sendResponse(HttpExchange exchange, String response) throws IOException { 1633 | byte[] bytes = response.getBytes(StandardCharsets.UTF_8); 1634 | exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8"); 1635 | exchange.sendResponseHeaders(200, bytes.length); 1636 | try (OutputStream os = exchange.getResponseBody()) { 1637 | os.write(bytes); 1638 | } 1639 | } 1640 | 1641 | @Override 1642 | public void dispose() { 1643 | if (server != null) { 1644 | Msg.info(this, "Stopping GhidraMCP HTTP server..."); 1645 | server.stop(1); // Stop with a small delay (e.g., 1 second) for connections to finish 1646 | server = null; // Nullify the reference 1647 | Msg.info(this, "GhidraMCP HTTP server stopped."); 1648 | } 1649 | super.dispose(); 1650 | } 1651 | } 1652 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Plugin-Class: com.lauriewired.GhidraMCP 3 | Plugin-Name: GhidraMCP 4 | Plugin-Version: 1.0 5 | Plugin-Author: LaurieWired 6 | Plugin-Description: A custom plugin by LaurieWired 7 | -------------------------------------------------------------------------------- /src/main/resources/Module.manifest: -------------------------------------------------------------------------------- 1 | GHIDRA_MODULE_NAME=GhidraMCP 2 | GHIDRA_MODULE_DESC=An HTTP server plugin for Ghidra 3 | -------------------------------------------------------------------------------- /src/main/resources/extension.properties: -------------------------------------------------------------------------------- 1 | name=GhidraMCP 2 | description=A plugin that runs an embedded HTTP server to expose program data. 3 | author=LaurieWired 4 | createdOn=2025-03-22 5 | version=11.3.1 6 | ghidraVersion=11.3.1 -------------------------------------------------------------------------------- /src/test/java/com/lauriewired/AppTest.java: -------------------------------------------------------------------------------- 1 | package com.lauriewired; 2 | 3 | import junit.framework.Test; 4 | import junit.framework.TestCase; 5 | import junit.framework.TestSuite; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class AppTest 11 | extends TestCase 12 | { 13 | /** 14 | * Create the test case 15 | * 16 | * @param testName name of the test case 17 | */ 18 | public AppTest( String testName ) 19 | { 20 | super( testName ); 21 | } 22 | 23 | /** 24 | * @return the suite of tests being tested 25 | */ 26 | public static Test suite() 27 | { 28 | return new TestSuite( AppTest.class ); 29 | } 30 | 31 | /** 32 | * Rigourous Test :-) 33 | */ 34 | public void testApp() 35 | { 36 | assertTrue( true ); 37 | } 38 | } 39 | --------------------------------------------------------------------------------