├── .gitignore ├── image.png ├── tsconfig.json ├── package.json ├── LICENSE ├── README.md ├── idaremoteclient.ts ├── index.ts └── ida_remote_server.py /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | logs 3 | dist 4 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fdrechsler/mcp-server-idapro/HEAD/image.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "outDir": "./dist", 12 | "rootDir": "." 13 | }, 14 | "include": [ 15 | "src/**/*", 16 | "./**/*.ts" 17 | , "test.cjs" ], 18 | "exclude": [ 19 | "node_modules" 20 | ] 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ida-server", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "author": "", 6 | "license": "ISC", 7 | "description": "", 8 | "dependencies": { 9 | "@modelcontextprotocol/sdk": "*", 10 | "diff": "^5.1.0", 11 | "glob": "^10.3.10", 12 | "minimatch": "^10.0.1", 13 | "mongodb": "^6.15.0", 14 | "zod-to-json-schema": "^3.23.5" 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "build": "tsc && shx chmod +x dist/*.js" 21 | }, 22 | "devDependencies": { 23 | "@types/diff": "^5.0.9", 24 | "@types/minimatch": "^5.1.2", 25 | "@types/node": "^22", 26 | "shx": "^0.3.4", 27 | "typescript": "^5.3.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Florian Drechsler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IDA Pro MCP Server 2 | 3 | A Model Context Protocol (MCP) server that enables AI assistants to interact with IDA Pro for reverse engineering and binary analysis tasks. 4 | 5 | 6 | IDA Pro Server MCP server 7 | 8 | 9 | ## Overview 10 | 11 | This project provides a bridge between AI assistants and IDA Pro, a popular disassembler and debugger used for reverse engineering software. It consists of three main components: 12 | 13 | 1. **IDA Pro Remote Control Plugin** (`ida_remote_server.py`): An IDA Pro plugin that creates an HTTP server to remotely control IDA Pro functions. 14 | 2. **IDA Remote Client** (`idaremoteclient.ts`): A TypeScript client for interacting with the IDA Pro Remote Control Server. 15 | 3. **MCP Server** (`index.ts`): A Model Context Protocol server that exposes IDA Pro functionality to AI assistants. 16 | 17 | ## Features 18 | 19 | - Execute Python scripts in IDA Pro from AI assistants 20 | - Retrieve information about binaries: 21 | - Strings 22 | - Imports 23 | - Exports 24 | - Functions 25 | - Advanced binary analysis capabilities: 26 | - Search for immediate values in instructions 27 | - Search for text strings in the binary 28 | - Search for specific byte sequences 29 | - Get disassembly for address ranges 30 | - Automate IDA Pro operations through a standardized interface 31 | - Secure communication between components 32 | 33 | ## Prerequisites 34 | 35 | - IDA Pro 8.3 or later 36 | - Node.js 18 or later 37 | - TypeScript 38 | 39 | ### Example usage ida_remote_server.py 40 | 41 | ```bash 42 | curl -X POST -H "Content-Type: application/json" -d '{"script":"print(\"Script initialization...\")"}' http://127.0.0.1:9045/api/execute 43 | {"success": true, "output": "Script initialization...\n"} 44 | ``` 45 | 46 | ### Example usage MCP Server 47 | 48 | ![Roo Output](/image.png) 49 | 50 | ## Installation 51 | 52 | ### 1. Install the IDA Pro Remote Control Plugin 53 | 54 | 1. Copy `ida_remote_server.py` to your IDA Pro plugins directory: 55 | - Windows: `%PROGRAMFILES%\IDA Pro\plugins` 56 | - macOS: `/Applications/IDA Pro.app/Contents/MacOS/plugins` 57 | - Linux: `/opt/idapro/plugins` 58 | 59 | 2. Start IDA Pro and open a binary file. 60 | 61 | 3. The plugin will automatically start an HTTP server on `127.0.0.1:9045`. 62 | 63 | ### 2. Install the MCP Server 64 | 65 | 1. Clone this repository: 66 | ```bash 67 | git clone 68 | cd ida-server 69 | ``` 70 | 71 | 2. Install dependencies: 72 | ```bash 73 | npm install 74 | ``` 75 | 76 | 3. Build the project: 77 | ```bash 78 | npm run build 79 | ``` 80 | 81 | 4. Configure the MCP server in your AI assistant's MCP settings file: 82 | ```json 83 | { 84 | "mcpServers": { 85 | "ida-pro": { 86 | "command": "node", 87 | "args": ["path/to/ida-server/dist/index.js"], 88 | "env": {} 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | ## Usage 95 | 96 | Once installed and configured, the MCP server provides the following tool to AI assistants: 97 | 98 | ### run_ida_command 99 | 100 | Executes an IDA Pro Python script. 101 | 102 | **Parameters:** 103 | - `scriptPath` (required): Absolute path to the script file to execute 104 | - `outputPath` (optional): Absolute path to save the script's output to 105 | 106 | **Example:** 107 | 108 | ```python 109 | # Example IDA Pro script (save as /path/to/script.py) 110 | import idautils 111 | 112 | # Count functions 113 | function_count = len(list(idautils.Functions())) 114 | print(f"Binary has {function_count} functions") 115 | 116 | # Get the first 5 function names 117 | functions = list(idautils.Functions())[:5] 118 | for func_ea in functions: 119 | print(f"Function: {ida_name.get_ea_name(func_ea)} at {hex(func_ea)}") 120 | 121 | # Return data 122 | return_value = function_count 123 | ``` 124 | 125 | The AI assistant can then use this script with: 126 | 127 | ``` 128 | 129 | ida-pro 130 | run_ida_command 131 | 132 | { 133 | "scriptPath": "/path/to/script.py" 134 | } 135 | 136 | 137 | ``` 138 | 139 | ### search_immediate_value 140 | 141 | Searches for immediate values in the binary's instructions. 142 | 143 | **Parameters:** 144 | - `value` (required): Value to search for (number or string) 145 | - `radix` (optional): Radix for number conversion (default: 16) 146 | - `startAddress` (optional): Start address for search 147 | - `endAddress` (optional): End address for search 148 | 149 | **Example:** 150 | 151 | ``` 152 | 153 | ida-pro 154 | search_immediate_value 155 | 156 | { 157 | "value": "42", 158 | "radix": 10 159 | } 160 | 161 | 162 | ``` 163 | 164 | ### search_text 165 | 166 | Searches for text strings in the binary. 167 | 168 | **Parameters:** 169 | - `text` (required): Text to search for 170 | - `caseSensitive` (optional): Whether the search is case sensitive (default: false) 171 | - `startAddress` (optional): Start address for search 172 | - `endAddress` (optional): End address for search 173 | 174 | **Example:** 175 | 176 | ``` 177 | 178 | ida-pro 179 | search_text 180 | 181 | { 182 | "text": "password", 183 | "caseSensitive": false 184 | } 185 | 186 | 187 | ``` 188 | 189 | ### search_byte_sequence 190 | 191 | Searches for a specific byte sequence in the binary. 192 | 193 | **Parameters:** 194 | - `bytes` (required): Byte sequence to search for (e.g., "90 90 90" for three NOPs) 195 | - `startAddress` (optional): Start address for search 196 | - `endAddress` (optional): End address for search 197 | 198 | **Example:** 199 | 200 | ``` 201 | 202 | ida-pro 203 | search_byte_sequence 204 | 205 | { 206 | "bytes": "90 90 90" 207 | } 208 | 209 | 210 | ``` 211 | 212 | ### get_disassembly 213 | 214 | Gets disassembly for an address range. 215 | 216 | **Parameters:** 217 | - `startAddress` (required): Start address for disassembly 218 | - `endAddress` (optional): End address for disassembly 219 | - `count` (optional): Number of instructions to disassemble 220 | 221 | **Example:** 222 | 223 | ``` 224 | 225 | ida-pro 226 | get_disassembly 227 | 228 | { 229 | "startAddress": "0x401000", 230 | "count": 10 231 | } 232 | 233 | 234 | ``` 235 | 236 | ### get_functions 237 | 238 | Gets the list of functions from the binary. 239 | 240 | **Parameters:** 241 | - None required 242 | 243 | **Example:** 244 | 245 | ``` 246 | 247 | ida-pro 248 | get_functions 249 | 250 | {} 251 | 252 | 253 | ``` 254 | 255 | ### get_exports 256 | 257 | Gets the list of exports from the binary. 258 | 259 | **Parameters:** 260 | - None required 261 | 262 | **Example:** 263 | 264 | ``` 265 | 266 | ida-pro 267 | get_exports 268 | 269 | {} 270 | 271 | 272 | ``` 273 | 274 | ### get_strings 275 | 276 | Gets the list of strings from the binary. 277 | 278 | **Parameters:** 279 | - None required 280 | 281 | **Example:** 282 | 283 | ``` 284 | 285 | ida-pro 286 | get_strings 287 | 288 | {} 289 | 290 | 291 | ``` 292 | 293 | ## IDA Pro Remote Control API 294 | 295 | The IDA Pro Remote Control Plugin exposes the following HTTP endpoints: 296 | 297 | - `GET /api/info`: Get plugin information 298 | - `GET /api/strings`: Get strings from the binary 299 | - `GET /api/exports`: Get exports from the binary 300 | - `GET /api/imports`: Get imports from the binary 301 | - `GET /api/functions`: Get function list 302 | - `GET /api/search/immediate`: Search for immediate values in instructions 303 | - `GET /api/search/text`: Search for text in the binary 304 | - `GET /api/search/bytes`: Search for byte sequences in the binary 305 | - `GET /api/disassembly`: Get disassembly for an address range 306 | - `POST /api/execute`: Execute Python script (JSON/Form) 307 | - `POST /api/executebypath`: Execute Python script from file path 308 | - `POST /api/executebody`: Execute Python script from raw body 309 | 310 | ## Security Considerations 311 | 312 | By default, the IDA Pro Remote Control Plugin only listens on `127.0.0.1` (localhost) for security reasons. This prevents remote access to your IDA Pro instance. 313 | 314 | If you need to allow remote access, you can modify the `DEFAULT_HOST` variable in `ida_remote_server.py`, but be aware of the security implications. 315 | 316 | ## Development 317 | 318 | ### Building from Source 319 | 320 | ```bash 321 | npm run build 322 | ``` 323 | 324 | ### Running Tests 325 | 326 | ```bash 327 | npm test 328 | ``` 329 | 330 | ## License 331 | 332 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 333 | 334 | ## Author 335 | 336 | Florian Drechsler (@fdrechsler) fd@fdrechsler.com -------------------------------------------------------------------------------- /idaremoteclient.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * IDA Pro Remote Control SDK 3 | * 4 | * A TypeScript SDK for interacting with the IDA Pro Remote Control Server. 5 | * Provides type-safe methods for all endpoints of the IDA Pro Remote Control plugin. 6 | */ 7 | 8 | // Type definitions for responses 9 | 10 | /** 11 | * Response from /api/info endpoint 12 | */ 13 | export interface InfoResponse { 14 | plugin_name: string; 15 | plugin_version: string; 16 | ida_version: string; 17 | file_name: string; 18 | endpoints: { 19 | path: string; 20 | method: string; 21 | description: string; 22 | }[]; 23 | } 24 | 25 | /** 26 | * Response from /api/execute endpoint 27 | */ 28 | export interface ExecuteResponse { 29 | success: boolean; 30 | output: string; 31 | return_value?: any; 32 | error?: string; 33 | } 34 | 35 | /** 36 | * String information from /api/strings endpoint 37 | */ 38 | export interface StringInfo { 39 | address: string; 40 | value: string; 41 | length: number; 42 | type: 'c' | 'pascal'; 43 | } 44 | 45 | /** 46 | * Response from /api/strings endpoint 47 | */ 48 | export interface StringsResponse { 49 | count: number; 50 | strings: StringInfo[]; 51 | } 52 | 53 | /** 54 | * Immediate value search result from /api/search/immediate endpoint 55 | */ 56 | export interface ImmediateSearchResult { 57 | address: string; 58 | instruction: string; 59 | value: number; 60 | operand_index: number; 61 | } 62 | 63 | /** 64 | * Response from /api/search/immediate endpoint 65 | */ 66 | export interface ImmediateSearchResponse { 67 | count: number; 68 | results: ImmediateSearchResult[]; 69 | error?: string; 70 | } 71 | 72 | /** 73 | * Text search result from /api/search/text endpoint 74 | */ 75 | export interface TextSearchResult { 76 | address: string; 77 | value: string; 78 | length: number; 79 | type: 'c' | 'pascal'; 80 | } 81 | 82 | /** 83 | * Response from /api/search/text endpoint 84 | */ 85 | export interface TextSearchResponse { 86 | count: number; 87 | results: TextSearchResult[]; 88 | error?: string; 89 | } 90 | 91 | /** 92 | * Byte sequence search result from /api/search/bytes endpoint 93 | */ 94 | export interface ByteSequenceSearchResult { 95 | address: string; 96 | disassembly: string; 97 | bytes: string; 98 | } 99 | 100 | /** 101 | * Response from /api/search/bytes endpoint 102 | */ 103 | export interface ByteSequenceSearchResponse { 104 | count: number; 105 | results: ByteSequenceSearchResult[]; 106 | error?: string; 107 | } 108 | 109 | /** 110 | * Name search result from /api/search/names endpoint 111 | */ 112 | export interface NameSearchResult { 113 | address: string; 114 | name: string; 115 | type: string; 116 | disassembly?: string; 117 | data_type?: string; 118 | is_start?: boolean; 119 | } 120 | 121 | /** 122 | * Response from /api/search/names endpoint 123 | */ 124 | export interface NameSearchResponse { 125 | count: number; 126 | results: NameSearchResult[]; 127 | error?: string; 128 | } 129 | 130 | /** 131 | * Cross-reference information from /api/xrefs endpoints 132 | */ 133 | export interface XrefInfo { 134 | from_address: string; 135 | to_address: string; 136 | type: string; 137 | is_code: boolean; 138 | function_name?: string; 139 | function_address?: string; 140 | disassembly?: string; 141 | target_name?: string; 142 | target_is_function?: boolean; 143 | target_function_name?: string; 144 | target_disassembly?: string; 145 | } 146 | 147 | /** 148 | * Response from /api/xrefs/to and /api/xrefs/from endpoints 149 | */ 150 | export interface XrefsResponse { 151 | count: number; 152 | xrefs: XrefInfo[]; 153 | address: string; 154 | name: string; 155 | error?: string; 156 | } 157 | 158 | /** 159 | * Disassembly instruction from /api/disassembly endpoint 160 | */ 161 | export interface DisassemblyInstruction { 162 | address: string; 163 | disassembly: string; 164 | bytes: string; 165 | size: number; 166 | } 167 | 168 | /** 169 | * Response from /api/disassembly endpoint 170 | */ 171 | export interface DisassemblyResponse { 172 | count: number; 173 | disassembly: DisassemblyInstruction[]; 174 | start_address: string; 175 | end_address?: string; 176 | error?: string; 177 | } 178 | 179 | /** 180 | * Export information from /api/exports endpoint 181 | */ 182 | export interface ExportInfo { 183 | address: string; 184 | name: string; 185 | ordinal: number; 186 | } 187 | 188 | /** 189 | * Response from /api/exports endpoint 190 | */ 191 | export interface ExportsResponse { 192 | count: number; 193 | exports: ExportInfo[]; 194 | } 195 | 196 | /** 197 | * Import information from /api/imports endpoint 198 | */ 199 | export interface ImportInfo { 200 | address: string; 201 | name: string; 202 | ordinal: number; 203 | } 204 | 205 | /** 206 | * Response from /api/imports endpoint 207 | */ 208 | export interface ImportsResponse { 209 | count: number; 210 | imports: ImportInfo[]; 211 | } 212 | 213 | /** 214 | * Function information from /api/functions endpoint 215 | */ 216 | export interface FunctionInfo { 217 | address: string; 218 | name: string; 219 | size: number; 220 | start: string; 221 | end: string; 222 | flags: number; 223 | } 224 | 225 | /** 226 | * Response from /api/functions endpoint 227 | */ 228 | export interface FunctionsResponse { 229 | count: number; 230 | functions: FunctionInfo[]; 231 | } 232 | 233 | /** 234 | * Error response from any endpoint 235 | */ 236 | export interface ErrorResponse { 237 | error: string; 238 | } 239 | 240 | /** 241 | * Options for IDARemoteClient 242 | */ 243 | export interface IDARemoteClientOptions { 244 | /** Server host (default: 127.0.0.1) */ 245 | host?: string; 246 | /** Server port (default: 9045) */ 247 | port?: number; 248 | /** Request timeout in milliseconds (default: 30000) */ 249 | timeout?: number; 250 | } 251 | 252 | /** 253 | * Client for IDA Pro Remote Control Server 254 | */ 255 | export class IDARemoteClient { 256 | private baseUrl: string; 257 | private timeout: number; 258 | 259 | /** 260 | * Create a new IDA Pro Remote Control client 261 | * @param options Configuration options 262 | */ 263 | constructor(options: IDARemoteClientOptions = {}) { 264 | const host = options.host || '127.0.0.1'; 265 | const port = options.port || 9045; 266 | this.timeout = options.timeout || 30000; 267 | this.baseUrl = `http://${host}:${port}/api`; 268 | } 269 | 270 | /** 271 | * Get information about the IDA Pro Remote Control server 272 | * @returns Server information 273 | */ 274 | async getInfo(): Promise { 275 | return this.get('/info'); 276 | } 277 | 278 | /** 279 | * Execute a Python script in IDA Pro 280 | * @param script Python script to execute 281 | * @returns Script execution results 282 | */ 283 | async executeScript(script: string, logHTTP = false): Promise { 284 | 285 | return this.post('/execute', { script }); 286 | } 287 | 288 | /** 289 | * Execute a Python script in IDA Pro 290 | * @param script Python script to execute 291 | * @returns Script execution results 292 | */ 293 | async executeScriptByPath(path: string, logHTTP = false): Promise { 294 | 295 | return this.post('/executeByPath', { path }); 296 | } 297 | 298 | /** 299 | * Get strings from the binary 300 | * @returns List of strings in the binary 301 | */ 302 | async getStrings(): Promise { 303 | return this.get('/strings'); 304 | } 305 | 306 | /** 307 | * Get exports from the binary 308 | * @returns List of exports in the binary 309 | */ 310 | async getExports(): Promise { 311 | return this.get('/exports'); 312 | } 313 | 314 | /** 315 | * Get imports from the binary 316 | * @returns List of imports in the binary 317 | */ 318 | async getImports(): Promise { 319 | return this.get('/imports'); 320 | } 321 | 322 | /** 323 | * Get functions from the binary 324 | * @returns List of functions in the binary 325 | */ 326 | async getFunctions(): Promise { 327 | return this.get('/functions'); 328 | } 329 | 330 | /** 331 | * Search for immediate values in the binary 332 | * @param value The value to search for (number or string) 333 | * @param options Optional search parameters 334 | * @returns Search results 335 | */ 336 | async searchForImmediateValue( 337 | value: number | string, 338 | options: { 339 | radix?: number; 340 | startAddress?: number | string; 341 | endAddress?: number | string; 342 | } = {} 343 | ): Promise { 344 | const params = new URLSearchParams(); 345 | params.append('value', value.toString()); 346 | 347 | if (options.radix !== undefined) { 348 | params.append('radix', options.radix.toString()); 349 | } 350 | 351 | if (options.startAddress !== undefined) { 352 | const startAddr = typeof options.startAddress === 'string' 353 | ? options.startAddress 354 | : options.startAddress.toString(); 355 | params.append('start', startAddr); 356 | } 357 | 358 | if (options.endAddress !== undefined) { 359 | const endAddr = typeof options.endAddress === 'string' 360 | ? options.endAddress 361 | : options.endAddress.toString(); 362 | params.append('end', endAddr); 363 | } 364 | 365 | return this.get(`/search/immediate?${params.toString()}`); 366 | } 367 | 368 | /** 369 | * Search for text in the binary 370 | * @param text The text to search for 371 | * @param options Optional search parameters 372 | * @returns Search results 373 | */ 374 | async searchForText( 375 | text: string, 376 | options: { 377 | caseSensitive?: boolean; 378 | startAddress?: number | string; 379 | endAddress?: number | string; 380 | } = {} 381 | ): Promise { 382 | const params = new URLSearchParams(); 383 | params.append('text', text); 384 | 385 | if (options.caseSensitive !== undefined) { 386 | params.append('case_sensitive', options.caseSensitive.toString()); 387 | } 388 | 389 | if (options.startAddress !== undefined) { 390 | const startAddr = typeof options.startAddress === 'string' 391 | ? options.startAddress 392 | : options.startAddress.toString(); 393 | params.append('start', startAddr); 394 | } 395 | 396 | if (options.endAddress !== undefined) { 397 | const endAddr = typeof options.endAddress === 'string' 398 | ? options.endAddress 399 | : options.endAddress.toString(); 400 | params.append('end', endAddr); 401 | } 402 | 403 | return this.get(`/search/text?${params.toString()}`); 404 | } 405 | 406 | /** 407 | * Search for a byte sequence in the binary 408 | * @param byteSequence The byte sequence to search for (e.g., "90 90 90" for three NOPs) 409 | * @param options Optional search parameters 410 | * @returns Search results 411 | */ 412 | async searchForByteSequence( 413 | byteSequence: string, 414 | options: { 415 | startAddress?: number | string; 416 | endAddress?: number | string; 417 | } = {} 418 | ): Promise { 419 | const params = new URLSearchParams(); 420 | params.append('bytes', byteSequence); 421 | 422 | if (options.startAddress !== undefined) { 423 | const startAddr = typeof options.startAddress === 'string' 424 | ? options.startAddress 425 | : options.startAddress.toString(); 426 | params.append('start', startAddr); 427 | } 428 | 429 | if (options.endAddress !== undefined) { 430 | const endAddr = typeof options.endAddress === 'string' 431 | ? options.endAddress 432 | : options.endAddress.toString(); 433 | params.append('end', endAddr); 434 | } 435 | 436 | return this.get(`/search/bytes?${params.toString()}`); 437 | } 438 | 439 | /** 440 | * Search for names/symbols in the binary 441 | * @param pattern The pattern to search for in names 442 | * @param options Optional search parameters 443 | * @returns Search results 444 | */ 445 | async searchInNames( 446 | pattern: string, 447 | options: { 448 | caseSensitive?: boolean; 449 | type?: 'function' | 'data' | 'import' | 'export' | 'label' | 'all'; 450 | } = {} 451 | ): Promise { 452 | const params = new URLSearchParams(); 453 | params.append('pattern', pattern); 454 | 455 | if (options.caseSensitive !== undefined) { 456 | params.append('case_sensitive', options.caseSensitive.toString()); 457 | } 458 | 459 | if (options.type !== undefined) { 460 | params.append('type', options.type); 461 | } 462 | 463 | return this.get(`/search/names?${params.toString()}`); 464 | } 465 | 466 | /** 467 | * Get cross-references to an address 468 | * @param address The target address 469 | * @param options Optional parameters 470 | * @returns Cross-references information 471 | */ 472 | async getXrefsTo( 473 | address: number | string, 474 | options: { 475 | type?: 'code' | 'data' | 'all'; 476 | } = {} 477 | ): Promise { 478 | const params = new URLSearchParams(); 479 | 480 | const addr = typeof address === 'string' 481 | ? address 482 | : address.toString(); 483 | params.append('address', addr); 484 | 485 | if (options.type !== undefined) { 486 | params.append('type', options.type); 487 | } 488 | 489 | return this.get(`/xrefs/to?${params.toString()}`); 490 | } 491 | 492 | /** 493 | * Get cross-references from an address 494 | * @param address The source address 495 | * @param options Optional parameters 496 | * @returns Cross-references information 497 | */ 498 | async getXrefsFrom( 499 | address: number | string, 500 | options: { 501 | type?: 'code' | 'data' | 'all'; 502 | } = {} 503 | ): Promise { 504 | const params = new URLSearchParams(); 505 | 506 | const addr = typeof address === 'string' 507 | ? address 508 | : address.toString(); 509 | params.append('address', addr); 510 | 511 | if (options.type !== undefined) { 512 | params.append('type', options.type); 513 | } 514 | 515 | return this.get(`/xrefs/from?${params.toString()}`); 516 | } 517 | 518 | /** 519 | * Get disassembly for an address range 520 | * @param startAddress The starting address 521 | * @param options Optional parameters 522 | * @returns Disassembly instructions 523 | */ 524 | async getDisassembly( 525 | startAddress: number | string, 526 | options: { 527 | endAddress?: number | string; 528 | count?: number; 529 | } = {} 530 | ): Promise { 531 | const params = new URLSearchParams(); 532 | 533 | const startAddr = typeof startAddress === 'string' 534 | ? startAddress 535 | : startAddress.toString(); 536 | params.append('start', startAddr); 537 | 538 | if (options.endAddress !== undefined) { 539 | const endAddr = typeof options.endAddress === 'string' 540 | ? options.endAddress 541 | : options.endAddress.toString(); 542 | params.append('end', endAddr); 543 | } 544 | 545 | if (options.count !== undefined) { 546 | params.append('count', options.count.toString()); 547 | } 548 | 549 | return this.get(`/disassembly?${params.toString()}`); 550 | } 551 | 552 | /** 553 | * Make a GET request to the server 554 | * @param endpoint API endpoint 555 | * @returns Response data 556 | */ 557 | private async get(endpoint: string): Promise { 558 | const controller = new AbortController(); 559 | const timeoutId = setTimeout(() => controller.abort(), this.timeout); 560 | 561 | try { 562 | const response = await fetch(`${this.baseUrl}${endpoint}`, { 563 | method: 'GET', 564 | signal: controller.signal, 565 | }); 566 | 567 | clearTimeout(timeoutId); 568 | 569 | if (!response.ok) { 570 | const errorData = await response.json() as ErrorResponse; 571 | throw new Error(errorData.error || `HTTP Error: ${response.status}`); 572 | } 573 | 574 | return await response.json() as T; 575 | } catch (error) { 576 | if (error instanceof DOMException && error.name === 'AbortError') { 577 | throw new Error(`Request to ${endpoint} timed out after ${this.timeout}ms`); 578 | } 579 | throw error; 580 | } 581 | } 582 | 583 | /** 584 | * Make a POST request to the server 585 | * @param endpoint API endpoint 586 | * @param data Request data 587 | * @returns Response data 588 | */ 589 | private async post(endpoint: string, data: any): Promise { 590 | const controller = new AbortController(); 591 | const timeoutId = setTimeout(() => controller.abort(), this.timeout); 592 | 593 | try { 594 | const response = await fetch(`${this.baseUrl}${endpoint}`, { 595 | method: 'POST', 596 | headers: { 597 | 'Content-Type': 'application/json', 598 | }, 599 | body: JSON.stringify(data), 600 | signal: controller.signal, 601 | }); 602 | 603 | clearTimeout(timeoutId); 604 | 605 | if (!response.ok) { 606 | const errorData = await response.json() as ErrorResponse; 607 | throw new Error(errorData.error || `HTTP Error: ${response.status}`); 608 | } 609 | 610 | return await response.json() as T; 611 | } catch (error) { 612 | if (error instanceof DOMException && error.name === 'AbortError') { 613 | throw new Error(`Request to ${endpoint} timed out after ${this.timeout}ms`); 614 | } 615 | throw error; 616 | } 617 | } 618 | } 619 | 620 | // Example usage 621 | /* 622 | async function main() { 623 | const ida = new IDARemoteClient(); 624 | 625 | try { 626 | // Get server info 627 | const info = await ida.getInfo(); 628 | console.log('Connected to:', info.plugin_name, info.plugin_version); 629 | 630 | // Execute a script 631 | const scriptResult = await ida.executeScript(` 632 | import idautils 633 | 634 | # Count functions 635 | function_count = len(list(idautils.Functions())) 636 | print(f"Binary has {function_count} functions") 637 | 638 | # Return data 639 | return_value = function_count 640 | `); 641 | 642 | console.log('Script output:', scriptResult.output); 643 | console.log('Return value:', scriptResult.return_value); 644 | 645 | // Get functions 646 | const functions = await ida.getFunctions(); 647 | console.log(`Retrieved ${functions.count} functions`); 648 | 649 | // Display first 5 functions 650 | functions.functions.slice(0, 5).forEach(func => { 651 | console.log(`${func.name} at ${func.address} (size: ${func.size})`); 652 | }); 653 | } catch (error) { 654 | console.error('Error:', error.message); 655 | } 656 | } 657 | 658 | main(); 659 | */ -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | } from '@modelcontextprotocol/sdk/types.js'; 10 | import { exec } from 'child_process'; 11 | import { promisify } from 'util'; 12 | import { join, dirname } from 'path'; 13 | import { existsSync, readFileSync, writeFileSync } from 'fs'; 14 | import { IDARemoteClient } from './idaremoteclient.js'; 15 | import { exists } from 'llamaindex'; 16 | const ida = new IDARemoteClient(); 17 | const execAsync = promisify(exec); 18 | import { Collection, Document, MongoClient } from 'mongodb' 19 | const url = 'mongodb://localhost:27017'; 20 | const client = new MongoClient(url); 21 | const dbName = "strings" 22 | 23 | let db 24 | let collection: Collection 25 | 26 | 27 | interface RunIdaCommandArgs { 28 | scriptPath: string; 29 | outputPath?: string; 30 | } 31 | interface RunIdaDirectCommandArgs { 32 | script: string; 33 | } 34 | 35 | interface SearchImmediateValueArgs { 36 | value: string | number; 37 | radix?: number; 38 | startAddress?: string | number; 39 | endAddress?: string | number; 40 | } 41 | 42 | interface SearchTextArgs { 43 | text: string; 44 | caseSensitive?: boolean; 45 | startAddress?: string | number; 46 | endAddress?: string | number; 47 | } 48 | 49 | interface SearchByteSequenceArgs { 50 | bytes: string; 51 | startAddress?: string | number; 52 | endAddress?: string | number; 53 | } 54 | 55 | interface GetDisassemblyArgs { 56 | startAddress: string | number; 57 | endAddress?: string | number; 58 | count?: number; 59 | } 60 | 61 | interface SearchInNamesArgs { 62 | pattern: string; 63 | caseSensitive?: boolean; 64 | type?: 'function' | 'data' | 'import' | 'export' | 'label' | 'all'; 65 | } 66 | 67 | interface GetXrefsToArgs { 68 | address: string | number; 69 | type?: 'code' | 'data' | 'all'; 70 | } 71 | 72 | interface GetXrefsFromArgs { 73 | address: string | number; 74 | type?: 'code' | 'data' | 'all'; 75 | } 76 | 77 | interface GetFunctionsArgs { 78 | // No parameters required 79 | } 80 | 81 | interface GetExportsArgs { 82 | // No parameters required 83 | } 84 | 85 | interface GetStringsArgs { 86 | // No parameters required 87 | } 88 | 89 | const isValidRunIdaArgs = (args: any): args is RunIdaDirectCommandArgs => { 90 | return ( 91 | typeof args === 'object' && 92 | args !== null && 93 | (typeof args.script === 'string') 94 | ); 95 | }; 96 | 97 | const isValidSearchImmediateValueArgs = (args: any): args is SearchImmediateValueArgs => { 98 | return ( 99 | typeof args === 'object' && 100 | args !== null && 101 | (typeof args.value === 'string' || typeof args.value === 'number') 102 | ); 103 | }; 104 | 105 | const isValidSearchTextArgs = (args: any): args is SearchTextArgs => { 106 | return ( 107 | typeof args === 'object' && 108 | args !== null && 109 | typeof args.text === 'string' 110 | ); 111 | }; 112 | 113 | const isValidSearchByteSequenceArgs = (args: any): args is SearchByteSequenceArgs => { 114 | return ( 115 | typeof args === 'object' && 116 | args !== null && 117 | typeof args.bytes === 'string' 118 | ); 119 | }; 120 | 121 | const isValidGetDisassemblyArgs = (args: any): args is GetDisassemblyArgs => { 122 | return ( 123 | typeof args === 'object' && 124 | args !== null && 125 | (typeof args.startAddress === 'string' || typeof args.startAddress === 'number') 126 | ); 127 | }; 128 | 129 | const isValidSearchInNamesArgs = (args: any): args is SearchInNamesArgs => { 130 | return ( 131 | typeof args === 'object' && 132 | args !== null && 133 | typeof args.pattern === 'string' 134 | ); 135 | }; 136 | 137 | const isValidGetXrefsToArgs = (args: any): args is GetXrefsToArgs => { 138 | return ( 139 | typeof args === 'object' && 140 | args !== null && 141 | (typeof args.address === 'string' || typeof args.address === 'number') 142 | ); 143 | }; 144 | 145 | const isValidGetXrefsFromArgs = (args: any): args is GetXrefsFromArgs => { 146 | return ( 147 | typeof args === 'object' && 148 | args !== null && 149 | (typeof args.address === 'string' || typeof args.address === 'number') 150 | ); 151 | }; 152 | 153 | const isValidGetFunctionsArgs = (args: any): args is GetFunctionsArgs => { 154 | return ( 155 | typeof args === 'object' && 156 | args !== null 157 | ); 158 | }; 159 | 160 | const isValidGetExportsArgs = (args: any): args is GetExportsArgs => { 161 | return ( 162 | typeof args === 'object' && 163 | args !== null 164 | ); 165 | }; 166 | 167 | const isValidGetStringsArgs = (args: any): args is GetStringsArgs => { 168 | return ( 169 | typeof args === 'object' && 170 | args !== null 171 | ); 172 | }; 173 | 174 | class IdaServer { 175 | private server: Server; 176 | 177 | constructor() { 178 | this.server = new Server( 179 | { 180 | name: 'ida-pro-server', 181 | version: '1.0.0', 182 | }, 183 | { 184 | capabilities: { 185 | tools: {}, // Will be populated in setup 186 | 187 | }, 188 | } 189 | ); 190 | 191 | this.setupToolHandlers(); 192 | 193 | // Error handling 194 | this.server.onerror = (error) => console.error('[MCP Error]', error); 195 | process.on('SIGINT', async () => { 196 | await this.server.close(); 197 | process.exit(0); 198 | }); 199 | } 200 | 201 | private setupToolHandlers() { 202 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 203 | tools: [ 204 | { 205 | name: 'run_ida_command', 206 | description: 'Execute an IDA Pro Script (IdaPython, Version IDA 8.3)', 207 | inputSchema: { 208 | type: 'object', 209 | properties: { 210 | script: { 211 | type: 'string', 212 | description: 'script', 213 | } 214 | }, 215 | required: ['script'], 216 | }, 217 | }, 218 | { 219 | name: 'run_ida_command_filebased', 220 | description: '(FOR IDE USAGE) Execute an IDA Pro Script (IdaPython, Version IDA 8.3)', 221 | inputSchema: { 222 | type: 'object', 223 | properties: { 224 | scriptPath: { 225 | type: 'string', 226 | description: 'absolute Path to the script file to execute', 227 | }, 228 | outputPath: { 229 | type: 'string', 230 | description: 'absolute Path to save the scripts output to', 231 | }, 232 | }, 233 | required: ['scriptPath'], 234 | }, 235 | }, 236 | { 237 | name: 'search_immediate_value', 238 | description: 'Search for immediate values in the binary', 239 | inputSchema: { 240 | type: 'object', 241 | properties: { 242 | value: { 243 | type: 'string', 244 | description: 'Value to search for (number or string)', 245 | }, 246 | radix: { 247 | type: 'number', 248 | description: 'Radix for number conversion (default: 16)', 249 | }, 250 | startAddress: { 251 | type: 'string', 252 | description: 'Start address for search (optional)', 253 | }, 254 | endAddress: { 255 | type: 'string', 256 | description: 'End address for search (optional)', 257 | }, 258 | }, 259 | required: ['value'], 260 | }, 261 | }, 262 | { 263 | name: 'search_text', 264 | description: 'Search for text in the binary', 265 | inputSchema: { 266 | type: 'object', 267 | properties: { 268 | text: { 269 | type: 'string', 270 | description: 'Text to search for', 271 | }, 272 | caseSensitive: { 273 | type: 'boolean', 274 | description: 'Whether the search is case sensitive (default: false)', 275 | }, 276 | startAddress: { 277 | type: 'string', 278 | description: 'Start address for search (optional)', 279 | }, 280 | endAddress: { 281 | type: 'string', 282 | description: 'End address for search (optional)', 283 | }, 284 | }, 285 | required: ['text'], 286 | }, 287 | }, 288 | { 289 | name: 'search_byte_sequence', 290 | description: 'Search for a byte sequence in the binary', 291 | inputSchema: { 292 | type: 'object', 293 | properties: { 294 | bytes: { 295 | type: 'string', 296 | description: 'Byte sequence to search for (e.g., "90 90 90" for three NOPs)', 297 | }, 298 | startAddress: { 299 | type: 'string', 300 | description: 'Start address for search (optional)', 301 | }, 302 | endAddress: { 303 | type: 'string', 304 | description: 'End address for search (optional)', 305 | }, 306 | }, 307 | required: ['bytes'], 308 | }, 309 | }, 310 | { 311 | name: 'get_disassembly', 312 | description: 'Get disassembly for an address range', 313 | inputSchema: { 314 | type: 'object', 315 | properties: { 316 | startAddress: { 317 | type: 'string', 318 | description: 'Start address for disassembly', 319 | }, 320 | endAddress: { 321 | type: 'string', 322 | description: 'End address for disassembly (optional)', 323 | }, 324 | count: { 325 | type: 'number', 326 | description: 'Number of instructions to disassemble (optional)', 327 | }, 328 | }, 329 | required: ['startAddress'], 330 | }, 331 | }, 332 | { 333 | name: 'get_functions', 334 | description: 'Get list of functions from the binary', 335 | inputSchema: { 336 | type: 'object', 337 | properties: {}, 338 | required: [], 339 | }, 340 | }, 341 | { 342 | name: 'get_exports', 343 | description: 'Get list of exports from the binary', 344 | inputSchema: { 345 | type: 'object', 346 | properties: {}, 347 | required: [], 348 | }, 349 | }, 350 | { 351 | name: 'search_in_names', 352 | description: 'Search for names/symbols in the binary', 353 | inputSchema: { 354 | type: 'object', 355 | properties: { 356 | pattern: { 357 | type: 'string', 358 | description: 'Pattern to search for in names', 359 | }, 360 | caseSensitive: { 361 | type: 'boolean', 362 | description: 'Whether the search is case sensitive (default: false)', 363 | }, 364 | type: { 365 | type: 'string', 366 | description: 'Type of names to search for (function, data, import, export, label, all)', 367 | }, 368 | }, 369 | required: ['pattern'], 370 | }, 371 | }, 372 | { 373 | name: 'get_xrefs_to', 374 | description: 'Get cross-references to an address', 375 | inputSchema: { 376 | type: 'object', 377 | properties: { 378 | address: { 379 | type: 'string', 380 | description: 'Target address to find references to', 381 | }, 382 | type: { 383 | type: 'string', 384 | description: 'Type of references to find (code, data, all)', 385 | }, 386 | }, 387 | required: ['address'], 388 | }, 389 | }, 390 | { 391 | name: 'get_xrefs_from', 392 | description: 'Get cross-references from an address', 393 | inputSchema: { 394 | type: 'object', 395 | properties: { 396 | address: { 397 | type: 'string', 398 | description: 'Source address to find references from', 399 | }, 400 | type: { 401 | type: 'string', 402 | description: 'Type of references to find (code, data, all)', 403 | }, 404 | }, 405 | required: ['address'], 406 | }, 407 | }, 408 | { 409 | name: 'get_strings', 410 | description: 'Get list of strings from the binary', 411 | inputSchema: { 412 | type: 'object', 413 | properties: {}, 414 | required: [], 415 | }, 416 | }, 417 | ], 418 | })); 419 | 420 | 421 | 422 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 423 | // Handle different tool types based on the tool name 424 | switch (request.params.name) { 425 | case 'run_ida_command': 426 | if (!isValidRunIdaArgs(request.params.arguments)) { 427 | throw new McpError( 428 | ErrorCode.InvalidParams, 429 | 'Invalid run IDA command arguments' 430 | ); 431 | } 432 | 433 | try { 434 | const { script } = request.params.arguments; 435 | 436 | let result = await ida.executeScript(script); 437 | 438 | if (result.error) { 439 | return { 440 | content: [ 441 | { 442 | type: 'text', 443 | text: `Error executing IDA Pro script: ${result.error}`, 444 | }, 445 | ], 446 | isError: true, 447 | }; 448 | } 449 | 450 | return { 451 | content: [ 452 | { 453 | type: 'text', 454 | text: `IDA Pro Script Execution Results:\n\n${result.output}`, 455 | }, 456 | ], 457 | }; 458 | 459 | 460 | } catch (error: any) { 461 | return { 462 | content: [ 463 | { 464 | type: 'text', 465 | text: `Error executing IDA Pro command: ${error.message || error}`, 466 | }, 467 | ], 468 | isError: true, 469 | }; 470 | } 471 | 472 | case 'search_immediate_value': 473 | if (!isValidSearchImmediateValueArgs(request.params.arguments)) { 474 | throw new McpError( 475 | ErrorCode.InvalidParams, 476 | 'Invalid search immediate value arguments' 477 | ); 478 | } 479 | 480 | try { 481 | const { value, radix, startAddress, endAddress } = request.params.arguments; 482 | 483 | const result = await ida.searchForImmediateValue(value, { 484 | radix, 485 | startAddress, 486 | endAddress 487 | }); 488 | 489 | return { 490 | content: [ 491 | { 492 | type: 'text', 493 | text: `Found ${result.count} occurrences of immediate value ${value}:\n\n${JSON.stringify(result.results, null, 2) 494 | }`, 495 | }, 496 | ], 497 | }; 498 | } catch (error: any) { 499 | return { 500 | content: [ 501 | { 502 | type: 'text', 503 | text: `Error searching for immediate value: ${error.message || error}`, 504 | }, 505 | ], 506 | isError: true, 507 | }; 508 | } 509 | 510 | case 'search_text': 511 | if (!isValidSearchTextArgs(request.params.arguments)) { 512 | throw new McpError( 513 | ErrorCode.InvalidParams, 514 | 'Invalid search text arguments' 515 | ); 516 | } 517 | 518 | try { 519 | const { text, caseSensitive, startAddress, endAddress } = request.params.arguments; 520 | 521 | /*const result = await ida.searchForText(text, { 522 | caseSensitive, 523 | startAddress, 524 | endAddress 525 | });*/ 526 | 527 | 528 | await client.connect(); 529 | db = client.db(dbName); collection = db.collection("strings"); 530 | let searchFor = "lua"; 531 | let newRegex = new RegExp(text, "i"); 532 | collection = db.collection("strings"); 533 | let res = await collection.find({ 534 | "TEXT": newRegex 535 | }) 536 | 537 | let result = await res.toArray() 538 | 539 | let result_count = result.length; 540 | let result_str = ""; 541 | for (let i = 0; i < result.length; i++) { 542 | result_str += ` ${result[i].MEMORY_ADDR} ${result[i].TEXT} \n` 543 | } 544 | return { 545 | content: [ 546 | { 547 | type: 'text', 548 | text: `Found ${result_count} \n\n ${result_str}`, 549 | }, 550 | ], 551 | } 552 | 553 | } catch (error: any) { 554 | return { 555 | content: [ 556 | { 557 | type: 'text', 558 | text: `Error searching for text: ${error.message || error}`, 559 | }, 560 | ], 561 | isError: true, 562 | }; 563 | } 564 | break; 565 | case 'search_byte_sequence': 566 | if (!isValidSearchByteSequenceArgs(request.params.arguments)) { 567 | throw new McpError( 568 | ErrorCode.InvalidParams, 569 | 'Invalid search byte sequence arguments' 570 | ); 571 | } 572 | 573 | try { 574 | const { bytes, startAddress, endAddress } = request.params.arguments; 575 | 576 | const result = await ida.searchForByteSequence(bytes, { 577 | startAddress, 578 | endAddress 579 | }); 580 | 581 | return { 582 | content: [ 583 | { 584 | type: 'text', 585 | text: `Found ${result.count} occurrences of byte sequence "${bytes}":\n\n${JSON.stringify(result.results, null, 2) 586 | }`, 587 | }, 588 | ], 589 | }; 590 | } catch (error: any) { 591 | return { 592 | content: [ 593 | { 594 | type: 'text', 595 | text: `Error searching for byte sequence: ${error.message || error}`, 596 | }, 597 | ], 598 | isError: true, 599 | }; 600 | } 601 | 602 | case 'get_disassembly': 603 | if (!isValidGetDisassemblyArgs(request.params.arguments)) { 604 | throw new McpError( 605 | ErrorCode.InvalidParams, 606 | 'Invalid disassembly arguments' 607 | ); 608 | } 609 | 610 | try { 611 | const { startAddress, endAddress, count } = request.params.arguments; 612 | 613 | if (startAddress && typeof startAddress == 'string') { 614 | startAddress.replace("00007", "0x7") 615 | } 616 | if (endAddress && typeof endAddress == 'string') { 617 | endAddress.replace("00007", "0x7") 618 | } 619 | 620 | 621 | const result = await ida.getDisassembly(startAddress, { 622 | endAddress, 623 | count 624 | }); 625 | 626 | return { 627 | content: [ 628 | { 629 | type: 'text', 630 | text: `Disassembly from ${result.start_address}${result.end_address ? ` to ${result.end_address}` : ''}:\n\n${JSON.stringify(result.disassembly, null, 2) 631 | }`, 632 | }, 633 | ], 634 | }; 635 | } catch (error: any) { 636 | return { 637 | content: [ 638 | { 639 | type: 'text', 640 | text: `Error getting disassembly: ${error.message || error}`, 641 | }, 642 | ], 643 | isError: true, 644 | }; 645 | } 646 | 647 | case 'get_functions': 648 | if (!isValidGetFunctionsArgs(request.params.arguments)) { 649 | throw new McpError( 650 | ErrorCode.InvalidParams, 651 | 'Invalid get functions arguments' 652 | ); 653 | } 654 | 655 | try { 656 | const result = await ida.getFunctions(); 657 | 658 | return { 659 | content: [ 660 | { 661 | type: 'text', 662 | text: `Retrieved ${result.count} functions from the binary:\n\n${JSON.stringify(result.functions, null, 2) 663 | }`, 664 | }, 665 | ], 666 | }; 667 | } catch (error: any) { 668 | return { 669 | content: [ 670 | { 671 | type: 'text', 672 | text: `Error getting functions: ${error.message || error}`, 673 | }, 674 | ], 675 | isError: true, 676 | }; 677 | } 678 | 679 | case 'get_exports': 680 | if (!isValidGetExportsArgs(request.params.arguments)) { 681 | throw new McpError( 682 | ErrorCode.InvalidParams, 683 | 'Invalid get exports arguments' 684 | ); 685 | } 686 | 687 | try { 688 | const result = await ida.getExports(); 689 | 690 | return { 691 | content: [ 692 | { 693 | type: 'text', 694 | text: `Retrieved ${result.count} exports from the binary:\n\n${JSON.stringify(result.exports, null, 2) 695 | }`, 696 | }, 697 | ], 698 | }; 699 | } catch (error: any) { 700 | return { 701 | content: [ 702 | { 703 | type: 'text', 704 | text: `Error getting exports: ${error.message || error}`, 705 | }, 706 | ], 707 | isError: true, 708 | }; 709 | } 710 | 711 | case 'get_strings': 712 | if (!isValidGetStringsArgs(request.params.arguments)) { 713 | throw new McpError( 714 | ErrorCode.InvalidParams, 715 | 'Invalid get strings arguments' 716 | ); 717 | } 718 | 719 | try { 720 | const result = await ida.getStrings(); 721 | 722 | return { 723 | content: [ 724 | { 725 | type: 'text', 726 | text: `Retrieved ${result.count} strings from the binary:\n\n${JSON.stringify(result.strings, null, 2) 727 | }`, 728 | }, 729 | ], 730 | }; 731 | } catch (error: any) { 732 | return { 733 | content: [ 734 | { 735 | type: 'text', 736 | text: `Error getting strings: ${error.message || error}`, 737 | }, 738 | ], 739 | isError: true, 740 | }; 741 | } 742 | 743 | case 'search_in_names': 744 | if (!isValidSearchInNamesArgs(request.params.arguments)) { 745 | throw new McpError( 746 | ErrorCode.InvalidParams, 747 | 'Invalid search in names arguments' 748 | ); 749 | } 750 | 751 | try { 752 | const { pattern, caseSensitive, type } = request.params.arguments; 753 | 754 | const result = await ida.searchInNames(pattern, { 755 | caseSensitive, 756 | type: type as 'function' | 'data' | 'import' | 'export' | 'label' | 'all' 757 | }); 758 | 759 | return { 760 | content: [ 761 | { 762 | type: 'text', 763 | text: `Found ${result.count} names matching "${pattern}":\n\n${JSON.stringify(result.results, null, 2) 764 | }`, 765 | }, 766 | ], 767 | }; 768 | } catch (error: any) { 769 | return { 770 | content: [ 771 | { 772 | type: 'text', 773 | text: `Error searching in names: ${error.message || error}`, 774 | }, 775 | ], 776 | isError: true, 777 | }; 778 | } 779 | 780 | case 'get_xrefs_to': 781 | if (!isValidGetXrefsToArgs(request.params.arguments)) { 782 | throw new McpError( 783 | ErrorCode.InvalidParams, 784 | 'Invalid get xrefs to arguments' 785 | ); 786 | } 787 | 788 | try { 789 | const { address, type } = request.params.arguments; 790 | 791 | const result = await ida.getXrefsTo(address, { 792 | type: type as 'code' | 'data' | 'all' 793 | }); 794 | 795 | return { 796 | content: [ 797 | { 798 | type: 'text', 799 | text: `Found ${result.count} references to ${result.address} (${result.name}):\n\n${JSON.stringify(result.xrefs, null, 2) 800 | }`, 801 | }, 802 | ], 803 | }; 804 | } catch (error: any) { 805 | return { 806 | content: [ 807 | { 808 | type: 'text', 809 | text: `Error getting xrefs to address: ${error.message || error}`, 810 | }, 811 | ], 812 | isError: true, 813 | }; 814 | } 815 | 816 | case 'get_xrefs_from': 817 | if (!isValidGetXrefsFromArgs(request.params.arguments)) { 818 | throw new McpError( 819 | ErrorCode.InvalidParams, 820 | 'Invalid get xrefs from arguments' 821 | ); 822 | } 823 | 824 | try { 825 | const { address, type } = request.params.arguments; 826 | 827 | const result = await ida.getXrefsFrom(address, { 828 | type: type as 'code' | 'data' | 'all' 829 | }); 830 | 831 | return { 832 | content: [ 833 | { 834 | type: 'text', 835 | text: `Found ${result.count} references from ${result.address} (${result.name}):\n\n${JSON.stringify(result.xrefs, null, 2) 836 | }`, 837 | }, 838 | ], 839 | }; 840 | } catch (error: any) { 841 | return { 842 | content: [ 843 | { 844 | type: 'text', 845 | text: `Error getting xrefs from address: ${error.message || error}`, 846 | }, 847 | ], 848 | isError: true, 849 | }; 850 | } 851 | 852 | default: 853 | throw new McpError( 854 | ErrorCode.MethodNotFound, 855 | `Unknown tool: ${request.params.name}` 856 | ); 857 | } 858 | }); 859 | } 860 | 861 | async run() { 862 | 863 | const transport = new StdioServerTransport(); 864 | await this.server.connect(transport); 865 | console.error('IDA Pro MCP server running on stdio'); 866 | } 867 | } 868 | 869 | const server = new IdaServer(); 870 | server.run().catch(console.error); -------------------------------------------------------------------------------- /ida_remote_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | IDA Pro Remote Control Plugin 3 | 4 | This plugin creates an HTTP server to remotely control certain IDA functions. 5 | It exposes endpoints for executing scripts, getting strings, imports, exports, and functions. 6 | 7 | Author: Florian Drechsler (@fdrechsler) fd@fdrechsler.com 8 | """ 9 | 10 | import idaapi 11 | import idautils 12 | import idc 13 | import ida_funcs 14 | import ida_bytes 15 | import ida_nalt 16 | import ida_name 17 | import json 18 | from http.server import HTTPServer, BaseHTTPRequestHandler 19 | import threading 20 | import socket 21 | import ssl 22 | import base64 23 | import traceback 24 | from urllib.parse import parse_qs, urlparse 25 | import time 26 | 27 | # Default settings 28 | DEFAULT_HOST = "127.0.0.1" # Localhost only for security 29 | DEFAULT_PORT = 9045 30 | PLUGIN_NAME = "IDA Pro Remote Control" 31 | PLUGIN_VERSION = "1.0.0" 32 | AUTO_START = True # Automatically start server on plugin load 33 | 34 | # Global variables 35 | g_server = None 36 | g_server_thread = None 37 | 38 | # Synchronization flags for execute_sync 39 | MFF_FAST = 0x0 # Execute as soon as possible 40 | MFF_READ = 0x1 # Wait for the database to be read-ready 41 | MFF_WRITE = 0x2 # Wait for the database to be write-ready 42 | 43 | class RemoteControlHandler(BaseHTTPRequestHandler): 44 | """HTTP request handler for the IDA Pro remote control plugin.""" 45 | 46 | # Add timeout for HTTP requests 47 | timeout = 60 # 60-second timeout for HTTP requests 48 | 49 | def log_message(self, format, *args): 50 | """Override logging to use IDA's console.""" 51 | print(f"[RemoteControl] {format % args}") 52 | 53 | def _send_response(self, status_code, content_type, content): 54 | """Helper method to send HTTP response.""" 55 | try: 56 | self.send_response(status_code) 57 | self.send_header('Content-Type', content_type) 58 | self.send_header('Content-Length', len(content)) 59 | self.end_headers() 60 | self.wfile.write(content) 61 | except (ConnectionResetError, BrokenPipeError, socket.error) as e: 62 | print(f"[RemoteControl] Connection error when sending response: {e}") 63 | 64 | def _send_json_response(self, data, status_code=200): 65 | """Helper method to send JSON response.""" 66 | try: 67 | content = json.dumps(data).encode('utf-8') 68 | self._send_response(status_code, 'application/json', content) 69 | except Exception as e: 70 | print(f"[RemoteControl] Error preparing JSON response: {e}") 71 | # Try to send a simplified error response 72 | try: 73 | simple_error = json.dumps({'error': 'Internal server error'}).encode('utf-8') 74 | self._send_response(500, 'application/json', simple_error) 75 | except: 76 | # Silently fail if we can't even send the error 77 | pass 78 | 79 | def _send_error_response(self, message, status_code=400): 80 | """Helper method to send error response.""" 81 | self._send_json_response({'error': message}, status_code) 82 | 83 | def _parse_post_data(self): 84 | """Parse POST data from request.""" 85 | content_length = int(self.headers.get('Content-Length', 0)) 86 | post_data = self.rfile.read(content_length).decode('utf-8') 87 | 88 | # Handle different content types 89 | content_type = self.headers.get('Content-Type', '') 90 | if 'application/json' in content_type: 91 | return json.loads(post_data) 92 | elif 'application/x-www-form-urlencoded' in content_type: 93 | parsed_data = parse_qs(post_data) 94 | # Convert lists to single values where appropriate 95 | return {k: v[0] if len(v) == 1 else v for k, v in parsed_data.items()} 96 | else: 97 | return {'raw_data': post_data} 98 | 99 | def do_GET(self): 100 | """Handle GET requests.""" 101 | path = self.path.lower() 102 | 103 | try: 104 | if path == '/api/info': 105 | self._handle_info() 106 | elif path == '/api/strings': 107 | self._handle_get_strings() 108 | elif path == '/api/exports': 109 | self._handle_get_exports() 110 | elif path == '/api/imports': 111 | self._handle_get_imports() 112 | elif path == '/api/functions': 113 | self._handle_get_functions() 114 | elif path.startswith('/api/search/immediate'): 115 | self._handle_search_immediate() 116 | elif path.startswith('/api/search/text'): 117 | self._handle_search_text() 118 | elif path.startswith('/api/search/bytes'): 119 | self._handle_search_bytes() 120 | elif path.startswith('/api/search/names'): 121 | self._handle_search_in_names() 122 | elif path.startswith('/api/xrefs/to'): 123 | self._handle_get_xrefs_to() 124 | elif path.startswith('/api/xrefs/from'): 125 | self._handle_get_xrefs_from() 126 | elif path.startswith('/api/disassembly'): 127 | self._handle_get_disassembly() 128 | else: 129 | self._send_error_response('Endpoint not found', 404) 130 | except Exception as e: 131 | error_msg = f"Error processing request: {str(e)}\n{traceback.format_exc()}" 132 | print(f"[RemoteControl] {error_msg}") 133 | self._send_error_response(error_msg, 500) 134 | 135 | def do_POST(self): 136 | """Handle POST requests.""" 137 | path = self.path.lower() 138 | 139 | try: 140 | if path == '/api/execute': 141 | self._handle_execute_script() 142 | elif path == '/api/executebypath': 143 | self._handle_execute_by_path() 144 | elif path == '/api/executebody': 145 | self._handle_execute_body() 146 | else: 147 | self._send_error_response('Endpoint not found', 404) 148 | except Exception as e: 149 | error_msg = f"Error processing request: {str(e)}\n{traceback.format_exc()}" 150 | print(f"[RemoteControl] {error_msg}") 151 | self._send_error_response(error_msg, 500) 152 | 153 | def _handle_info(self): 154 | """Handle info request.""" 155 | result = self._execute_in_main_thread(self._get_info_impl) 156 | self._send_json_response(result) 157 | 158 | def _get_info_impl(self): 159 | """Implementation of getting info - runs in main thread.""" 160 | info = { 161 | 'plugin_name': PLUGIN_NAME, 162 | 'plugin_version': PLUGIN_VERSION, 163 | 'ida_version': idaapi.get_kernel_version(), 164 | 'file_name': idaapi.get_input_file_path(), 165 | 'endpoints': [ 166 | {'path': '/api/info', 'method': 'GET', 'description': 'Get plugin information'}, 167 | {'path': '/api/strings', 'method': 'GET', 'description': 'Get strings from binary'}, 168 | {'path': '/api/exports', 'method': 'GET', 'description': 'Get exports from binary'}, 169 | {'path': '/api/imports', 'method': 'GET', 'description': 'Get imports from binary'}, 170 | {'path': '/api/functions', 'method': 'GET', 'description': 'Get function list'}, 171 | {'path': '/api/search/immediate', 'method': 'GET', 'description': 'Search for immediate values'}, 172 | {'path': '/api/search/text', 'method': 'GET', 'description': 'Search for text in binary'}, 173 | {'path': '/api/search/bytes', 'method': 'GET', 'description': 'Search for byte sequence'}, 174 | {'path': '/api/search/names', 'method': 'GET', 'description': 'Search for names/symbols in binary'}, 175 | {'path': '/api/xrefs/to', 'method': 'GET', 'description': 'Get cross-references to an address'}, 176 | {'path': '/api/xrefs/from', 'method': 'GET', 'description': 'Get cross-references from an address'}, 177 | {'path': '/api/disassembly', 'method': 'GET', 'description': 'Get disassembly for an address range'}, 178 | {'path': '/api/execute', 'method': 'POST', 'description': 'Execute Python script (JSON/Form)'}, 179 | {'path': '/api/executebypath', 'method': 'POST', 'description': 'Execute Python script from file path'}, 180 | {'path': '/api/executebody', 'method': 'POST', 'description': 'Execute Python script from raw body'}, 181 | ] 182 | } 183 | return info 184 | 185 | def _handle_execute_script(self): 186 | """Handle script execution request.""" 187 | post_data = self._parse_post_data() 188 | 189 | if 'script' not in post_data: 190 | self._send_error_response('No script provided') 191 | return 192 | 193 | script = post_data['script'] 194 | 195 | # Execute script in the main thread 196 | result = self._execute_in_main_thread(self._execute_script_impl, script) 197 | 198 | if 'error' in result: 199 | self._send_error_response(result['error'], 500) 200 | else: 201 | self._send_json_response(result) 202 | 203 | def _handle_execute_by_path(self): 204 | """Handle script execution from a file path.""" 205 | post_data = self._parse_post_data() 206 | 207 | if 'path' not in post_data: 208 | self._send_error_response('No script path provided') 209 | return 210 | 211 | script_path = post_data['path'] 212 | 213 | try: 214 | # Use IDA's main thread to read the file 215 | def read_script_file(): 216 | try: 217 | with open(script_path, 'r') as f: 218 | return {'script': f.read()} 219 | except Exception as e: 220 | return {'error': f"Could not read script file: {str(e)}"} 221 | 222 | file_result = self._execute_in_main_thread(read_script_file) 223 | 224 | if 'error' in file_result: 225 | self._send_error_response(file_result['error'], 400) 226 | return 227 | 228 | script = file_result['script'] 229 | 230 | # Execute the script using our existing method 231 | result = self._execute_in_main_thread(self._execute_script_impl, script) 232 | 233 | if 'error' in result: 234 | self._send_error_response(result['error'], 500) 235 | else: 236 | self._send_json_response(result) 237 | 238 | except Exception as e: 239 | error_msg = f"Error executing script from path: {str(e)}\n{traceback.format_exc()}" 240 | print(f"[RemoteControl] {error_msg}") 241 | self._send_error_response(error_msg, 500) 242 | 243 | def _handle_execute_body(self): 244 | """Handle script execution from raw body content.""" 245 | try: 246 | # Read raw body content 247 | content_length = int(self.headers.get('Content-Length', 0)) 248 | if content_length > 1000000: # 1MB limit 249 | self._send_error_response('Script too large (>1MB)', 413) 250 | return 251 | 252 | script = self.rfile.read(content_length).decode('utf-8') 253 | 254 | # Execute the script using our existing method 255 | result = self._execute_in_main_thread(self._execute_script_impl, script) 256 | 257 | if 'error' in result: 258 | self._send_error_response(result['error'], 500) 259 | else: 260 | self._send_json_response(result) 261 | 262 | except Exception as e: 263 | error_msg = f"Error executing script from body: {str(e)}\n{traceback.format_exc()}" 264 | print(f"[RemoteControl] {error_msg}") 265 | self._send_error_response(error_msg, 500) 266 | 267 | def _execute_script_impl(self, script): 268 | """Implementation of script execution - runs in main thread with safety measures.""" 269 | # Create a safe execution environment with IDA modules 270 | exec_globals = { 271 | 'idaapi': idaapi, 272 | 'idautils': idautils, 273 | 'idc': idc, 274 | 'ida_funcs': ida_funcs, 275 | 'ida_bytes': ida_bytes, 276 | 'ida_nalt': ida_nalt, 277 | 'ida_name': ida_name, 278 | } 279 | 280 | # Redirect stdout to capture output 281 | import io 282 | import sys 283 | import signal 284 | 285 | original_stdout = sys.stdout 286 | captured_output = io.StringIO() 287 | sys.stdout = captured_output 288 | 289 | # Create hooks to automatically respond to IDA prompts 290 | original_funcs = {} 291 | 292 | # Store original functions we're going to override 293 | original_funcs['ask_yn'] = idaapi.ask_yn 294 | original_funcs['ask_buttons'] = idaapi.ask_buttons 295 | original_funcs['ask_text'] = idaapi.ask_text 296 | original_funcs['ask_str'] = idaapi.ask_str 297 | original_funcs['ask_file'] = idaapi.ask_file 298 | original_funcs['display_copyright_warning'] = idaapi.display_copyright_warning 299 | 300 | # Also handle lower-level IDA UI functions 301 | if hasattr(idaapi, "get_kernel_version") and idaapi.get_kernel_version() >= "7.0": 302 | # IDA 7+ has these functions 303 | if hasattr(idaapi, "warning"): 304 | original_funcs['warning'] = idaapi.warning 305 | idaapi.warning = lambda *args, **kwargs: print(f"[AUTO-CONFIRM] Warning suppressed: {args}") 306 | 307 | if hasattr(idaapi, "info"): 308 | original_funcs['info'] = idaapi.info 309 | idaapi.info = lambda *args, **kwargs: print(f"[AUTO-CONFIRM] Info suppressed: {args}") 310 | 311 | # For specific known dialogs like the "bad digit" dialog 312 | if hasattr(idc, "set_inf_attr"): 313 | # Suppress "bad digit" dialogs with this setting 314 | original_funcs['INFFL_ALLASM'] = idc.get_inf_attr(idc.INF_AF) 315 | idc.set_inf_attr(idc.INF_AF, idc.get_inf_attr(idc.INF_AF) | 0x2000) # Set INFFL_ALLASM flag 316 | 317 | # Create a UI hook to capture any other dialogs 318 | class DialogHook(idaapi.UI_Hooks): 319 | def populating_widget_popup(self, widget, popup): 320 | # Just suppress all popups 321 | print("[AUTO-CONFIRM] Suppressing popup") 322 | return 1 323 | 324 | def finish_populating_widget_popup(self, widget, popup): 325 | # Also suppress here 326 | print("[AUTO-CONFIRM] Suppressing popup finish") 327 | return 1 328 | 329 | def ready_to_run(self): 330 | # Always continue 331 | return 1 332 | 333 | def updating_actions(self, ctx): 334 | # Always continue 335 | return 1 336 | 337 | def updated_actions(self): 338 | # Always continue 339 | return 1 340 | 341 | def ui_refresh(self, cnd): 342 | # Suppress UI refreshes 343 | return 1 344 | 345 | # Install UI hook 346 | ui_hook = DialogHook() 347 | ui_hook.hook() 348 | 349 | # Functions to automatically respond to various prompts 350 | def auto_yes_no(*args, **kwargs): 351 | print(f"[AUTO-CONFIRM] Prompt intercepted (Yes/No): {args}") 352 | return idaapi.ASKBTN_YES # Always respond YES 353 | 354 | def auto_buttons(*args, **kwargs): 355 | print(f"[AUTO-CONFIRM] Prompt intercepted (Buttons): {args}") 356 | return 0 # Return first button (usually OK/Yes/Continue) 357 | 358 | def auto_text(*args, **kwargs): 359 | print(f"[AUTO-CONFIRM] Prompt intercepted (Text): {args}") 360 | return "" # Return empty string 361 | 362 | def auto_file(*args, **kwargs): 363 | print(f"[AUTO-CONFIRM] Prompt intercepted (File): {args}") 364 | return "" # Return empty string 365 | 366 | def auto_ignore(*args, **kwargs): 367 | print(f"[AUTO-CONFIRM] Warning intercepted: {args}") 368 | return 0 # Just return something 369 | 370 | # Override IDA's prompt functions with our auto-response versions 371 | idaapi.ask_yn = auto_yes_no 372 | idaapi.ask_buttons = auto_buttons 373 | idaapi.ask_text = auto_text 374 | idaapi.ask_str = auto_text 375 | idaapi.ask_file = auto_file 376 | idaapi.display_copyright_warning = auto_ignore 377 | 378 | # IMPORTANT: Also override searching functions with safer versions 379 | # The "Bad digit" dialog is often triggered by these 380 | if hasattr(idc, "find_binary"): 381 | original_funcs['find_binary'] = idc.find_binary 382 | def safe_find_binary(ea, flag, searchstr, radix=16): 383 | # Always treat as a string by adding quotes if not present 384 | if '"' not in searchstr and "'" not in searchstr: 385 | searchstr = f'"{searchstr}"' 386 | print(f"[AUTO-CONFIRM] Making search safe: {searchstr}") 387 | return original_funcs['find_binary'](ea, flag, searchstr, radix) 388 | idc.find_binary = safe_find_binary 389 | 390 | # Set batch mode to minimize UI interactions (stronger settings) 391 | orig_batch = idaapi.set_script_timeout(1) # Set script timeout to suppress dialogs 392 | 393 | # Additional batch mode settings 394 | orig_user_screen_ea = idaapi.get_screen_ea() 395 | 396 | # Save current IDA settings 397 | try: 398 | # Enable batch mode if available 399 | if hasattr(idaapi, "batch_mode_enabled"): 400 | original_funcs['batch_mode'] = idaapi.batch_mode_enabled() 401 | idaapi.enable_batch_mode(True) 402 | 403 | # Disable analysis wait box 404 | if hasattr(idaapi, "set_flag"): 405 | idaapi.set_flag(idaapi.SW_SHHID_ITEM, True) # Hide wait dialogs 406 | idaapi.set_flag(idaapi.SW_HIDE_UNDEF, True) # Hide undefined items 407 | idaapi.set_flag(idaapi.SW_HIDE_SEGADDRS, True) # Hide segment addressing 408 | 409 | # For newer versions of IDA 410 | if hasattr(idc, "batch"): 411 | original_funcs['batch_mode_idc'] = idc.batch(1) # Enable batch mode 412 | 413 | except Exception as e: 414 | print(f"[AUTO-CONFIRM] Error setting batch mode: {e}") 415 | 416 | # Script timeout handling 417 | class TimeoutException(Exception): 418 | pass 419 | 420 | def timeout_handler(signum, frame): 421 | raise TimeoutException("Script execution timed out") 422 | 423 | # Set timeout for script execution (10 seconds) 424 | old_handler = None 425 | try: 426 | # Only set alarm on platforms that support it (not Windows) 427 | if hasattr(signal, 'SIGALRM'): 428 | old_handler = signal.signal(signal.SIGALRM, timeout_handler) 429 | signal.alarm(10) # 10 second timeout 430 | except (AttributeError, ValueError): 431 | # Signal module might not have SIGALRM on Windows 432 | pass 433 | 434 | try: 435 | # Execute the script with size limit to prevent memory issues 436 | if len(script) > 1000000: # 1MB limit 437 | return {'error': 'Script too large (>1MB)'} 438 | 439 | # Execute the script 440 | exec(script, exec_globals) 441 | output = captured_output.getvalue() 442 | 443 | # Get return value if set 444 | return_value = exec_globals.get('return_value', None) 445 | 446 | response = { 447 | 'success': True, 448 | 'output': output[:1000000] # Limit output size to 1MB 449 | } 450 | 451 | if return_value is not None: 452 | try: 453 | # Try to serialize return_value to JSON with size limit 454 | json_str = json.dumps(return_value) 455 | if len(json_str) <= 1000000: # 1MB limit 456 | response['return_value'] = return_value 457 | else: 458 | response['return_value'] = str(return_value)[:1000000] + "... (truncated)" 459 | except (TypeError, OverflowError): 460 | # If not JSON serializable, convert to string with limit 461 | response['return_value'] = str(return_value)[:1000000] + ( 462 | "... (truncated)" if len(str(return_value)) > 1000000 else "") 463 | 464 | return response 465 | 466 | except TimeoutException: 467 | error_msg = "Script execution timed out (exceeded 10 seconds)" 468 | print(f"[RemoteControl] {error_msg}") 469 | return {'error': error_msg} 470 | except MemoryError: 471 | error_msg = "Script caused a memory error" 472 | print(f"[RemoteControl] {error_msg}") 473 | return {'error': error_msg} 474 | except Exception as e: 475 | error_msg = f"Script execution error: {str(e)}\n{traceback.format_exc()}" 476 | print(f"[RemoteControl] {error_msg}") 477 | return {'error': error_msg} 478 | finally: 479 | # Restore stdout 480 | sys.stdout = original_stdout 481 | 482 | # Restore original IDA functions 483 | for func_name, original_func in original_funcs.items(): 484 | # Special case for INFFL_ALLASM flag 485 | if func_name == 'INFFL_ALLASM': 486 | idc.set_inf_attr(idc.INF_AF, original_func) 487 | # Special case for batch mode 488 | elif func_name == 'batch_mode': 489 | if hasattr(idaapi, "enable_batch_mode"): 490 | idaapi.enable_batch_mode(original_func) 491 | elif func_name == 'batch_mode_idc': 492 | if hasattr(idc, "batch"): 493 | idc.batch(original_func) 494 | else: 495 | # For all other functions 496 | try: 497 | if hasattr(idaapi, func_name): 498 | setattr(idaapi, func_name, original_func) 499 | elif hasattr(idc, func_name): 500 | setattr(idc, func_name, original_func) 501 | except: 502 | print(f"[RemoteControl] Failed to restore {func_name}") 503 | 504 | # Restore screen position 505 | idaapi.jumpto(orig_user_screen_ea) 506 | 507 | # Unhook UI hooks 508 | ui_hook.unhook() 509 | 510 | # Restore original batch mode 511 | idaapi.set_script_timeout(orig_batch) 512 | 513 | # Cancel alarm if set (for non-Windows platforms) 514 | try: 515 | if hasattr(signal, 'SIGALRM'): 516 | signal.alarm(0) 517 | if old_handler is not None: 518 | signal.signal(signal.SIGALRM, old_handler) 519 | except (AttributeError, ValueError, UnboundLocalError): 520 | pass 521 | 522 | def _handle_get_strings(self): 523 | """Handle get strings request.""" 524 | result = self._execute_in_main_thread(self._get_strings_impl) 525 | self._send_json_response(result) 526 | 527 | def _get_strings_impl(self): 528 | """Implementation of getting strings - runs in main thread.""" 529 | min_length = 4 # Minimum string length to include 530 | strings_list = [] 531 | 532 | # Get all strings from binary 533 | for ea in idautils.Strings(): 534 | if ea.length >= min_length: 535 | string_value = str(ea) 536 | string_address = ea.ea 537 | string_info = { 538 | 'address': f"0x{string_address:X}", 539 | 'value': string_value, 540 | 'length': ea.length, 541 | 'type': 'pascal' if ea.strtype == 1 else 'c' 542 | } 543 | strings_list.append(string_info) 544 | 545 | return { 546 | 'count': len(strings_list), 547 | 'strings': strings_list 548 | } 549 | 550 | def _handle_get_exports(self): 551 | """Handle get exports request.""" 552 | result = self._execute_in_main_thread(self._get_exports_impl) 553 | self._send_json_response(result) 554 | 555 | def _get_exports_impl(self): 556 | """Implementation of getting exports - runs in main thread.""" 557 | exports_list = [] 558 | 559 | # Process exports 560 | for ordinal, ea, name in idautils.Entries(): 561 | exports_list.append({ 562 | 'address': f"0x{ea:X}", 563 | 'name': name, 564 | 'ordinal': ordinal 565 | }) 566 | 567 | return { 568 | 'count': len(exports_list), 569 | 'exports': exports_list 570 | } 571 | 572 | def _handle_get_imports(self): 573 | """Handle get imports request.""" 574 | result = self._execute_in_main_thread(self._get_imports_impl) 575 | self._send_json_response(result) 576 | 577 | def _get_imports_impl(self): 578 | """Implementation of getting imports - runs in main thread.""" 579 | imports_list = [] 580 | 581 | # Process imports 582 | nimps = ida_nalt.get_import_module_qty() 583 | for i in range(0, nimps): 584 | name = ida_nalt.get_import_module_name(i) 585 | if not name: 586 | continue 587 | 588 | def imp_cb(ea, name, ordinal): 589 | if name: 590 | imports_list.append({ 591 | 'address': f"0x{ea:X}", 592 | 'name': name, 593 | 'ordinal': ordinal 594 | }) 595 | return True 596 | 597 | ida_nalt.enum_import_names(i, imp_cb) 598 | 599 | return { 600 | 'count': len(imports_list), 601 | 'imports': imports_list 602 | } 603 | 604 | def _handle_get_functions(self): 605 | """Handle get functions request.""" 606 | result = self._execute_in_main_thread(self._get_functions_impl) 607 | self._send_json_response(result) 608 | 609 | def _get_functions_impl(self): 610 | """Implementation of getting functions - runs in main thread.""" 611 | functions_list = [] 612 | 613 | # Get all functions 614 | for ea in idautils.Functions(): 615 | func = ida_funcs.get_func(ea) 616 | if func: 617 | func_name = ida_name.get_ea_name(ea) 618 | function_info = { 619 | 'address': f"0x{ea:X}", 620 | 'name': func_name, 621 | 'size': func.size(), 622 | 'start': f"0x{func.start_ea:X}", 623 | 'end': f"0x{func.end_ea:X}", 624 | 'flags': func.flags 625 | } 626 | functions_list.append(function_info) 627 | 628 | return { 629 | 'count': len(functions_list), 630 | 'functions': functions_list 631 | } 632 | 633 | def _handle_search_immediate(self): 634 | """Handle search for immediate value request.""" 635 | # Parse query parameters 636 | parsed_url = urlparse(self.path) 637 | params = parse_qs(parsed_url.query) 638 | 639 | # Get parameters with defaults 640 | value = params.get('value', [''])[0] 641 | if not value: 642 | self._send_error_response('Missing required parameter: value') 643 | return 644 | 645 | # Optional parameters 646 | try: 647 | radix = int(params.get('radix', ['16'])[0]) 648 | except ValueError: 649 | radix = 16 650 | 651 | try: 652 | start_ea = int(params.get('start', ['0'])[0], 0) 653 | except ValueError: 654 | start_ea = 0 655 | 656 | try: 657 | end_ea = int(params.get('end', ['0'])[0], 0) 658 | except ValueError: 659 | end_ea = idc.BADADDR 660 | 661 | # Execute search in main thread 662 | result = self._execute_in_main_thread( 663 | self._search_immediate_impl, 664 | value, 665 | radix, 666 | start_ea, 667 | end_ea 668 | ) 669 | self._send_json_response(result) 670 | 671 | def _search_immediate_impl(self, value, radix, start_ea, end_ea): 672 | """Implementation of searching for immediate values - runs in main thread.""" 673 | results = [] 674 | 675 | try: 676 | # Convert value to integer if it's a number 677 | if isinstance(value, str) and value.isdigit(): 678 | value = int(value, radix) 679 | 680 | # Search for immediate values 681 | for ea in idautils.Functions(): 682 | func = ida_funcs.get_func(ea) 683 | if not func: 684 | continue 685 | 686 | # Skip if outside specified range 687 | if start_ea > 0 and func.start_ea < start_ea: 688 | continue 689 | if end_ea > 0 and func.start_ea >= end_ea: 690 | continue 691 | 692 | # Iterate through instructions in the function 693 | current_ea = func.start_ea 694 | while current_ea < func.end_ea: 695 | insn = idaapi.insn_t() 696 | insn_len = idaapi.decode_insn(insn, current_ea) 697 | if insn_len > 0: 698 | # Check operands for immediate values 699 | for i in range(len(insn.ops)): 700 | op = insn.ops[i] 701 | if op.type == idaapi.o_imm: 702 | # If searching for a specific value 703 | if isinstance(value, int) and op.value == value: 704 | disasm = idc.generate_disasm_line(current_ea, 0) 705 | results.append({ 706 | 'address': f"0x{current_ea:X}", 707 | 'instruction': disasm, 708 | 'value': op.value, 709 | 'operand_index': i 710 | }) 711 | # If searching for a string pattern in the disassembly 712 | elif isinstance(value, str) and value in idc.generate_disasm_line(current_ea, 0): 713 | disasm = idc.generate_disasm_line(current_ea, 0) 714 | results.append({ 715 | 'address': f"0x{current_ea:X}", 716 | 'instruction': disasm, 717 | 'value': op.value, 718 | 'operand_index': i 719 | }) 720 | current_ea += insn_len 721 | else: 722 | current_ea += 1 723 | except Exception as e: 724 | return {'error': f"Error searching for immediate values: {str(e)}"} 725 | 726 | return { 727 | 'count': len(results), 728 | 'results': results 729 | } 730 | 731 | def _handle_search_text(self): 732 | """Handle search for text request.""" 733 | # Parse query parameters 734 | parsed_url = urlparse(self.path) 735 | params = parse_qs(parsed_url.query) 736 | 737 | # Get parameters with defaults 738 | text = params.get('text', [''])[0] 739 | if not text: 740 | self._send_error_response('Missing required parameter: text') 741 | return 742 | 743 | # Optional parameters 744 | try: 745 | start_ea = int(params.get('start', ['0'])[0], 0) 746 | except ValueError: 747 | start_ea = 0 748 | 749 | try: 750 | end_ea = int(params.get('end', ['0'])[0], 0) 751 | except ValueError: 752 | end_ea = idc.BADADDR 753 | 754 | case_sensitive = params.get('case_sensitive', ['false'])[0].lower() == 'true' 755 | 756 | # Execute search in main thread 757 | result = self._execute_in_main_thread( 758 | self._search_text_impl, 759 | text, 760 | case_sensitive, 761 | start_ea, 762 | end_ea 763 | ) 764 | self._send_json_response(result) 765 | 766 | def _search_text_impl(self, text, case_sensitive, start_ea, end_ea): 767 | """Implementation of searching for text - runs in main thread.""" 768 | results = [] 769 | 770 | try: 771 | # Get all strings from binary 772 | for string_item in idautils.Strings(): 773 | if string_item.ea < start_ea: 774 | continue 775 | if end_ea > 0 and string_item.ea >= end_ea: 776 | continue 777 | 778 | string_value = str(string_item) 779 | 780 | # Check if text is in string 781 | if (case_sensitive and text in string_value) or \ 782 | (not case_sensitive and text.lower() in string_value.lower()): 783 | results.append({ 784 | 'address': f"0x{string_item.ea:X}", 785 | 'value': string_value, 786 | 'length': string_item.length, 787 | 'type': 'pascal' if string_item.strtype == 1 else 'c' 788 | }) 789 | except Exception as e: 790 | return {'error': f"Error searching for text: {str(e)}"} 791 | 792 | return { 793 | 'count': len(results), 794 | 'results': results 795 | } 796 | 797 | def _handle_search_bytes(self): 798 | """Handle search for byte sequence request.""" 799 | # Parse query parameters 800 | parsed_url = urlparse(self.path) 801 | params = parse_qs(parsed_url.query) 802 | 803 | # Get parameters with defaults 804 | byte_str = params.get('bytes', [''])[0] 805 | if not byte_str: 806 | self._send_error_response('Missing required parameter: bytes') 807 | return 808 | 809 | # Optional parameters 810 | try: 811 | start_ea = int(params.get('start', ['0'])[0], 0) 812 | except ValueError: 813 | start_ea = 0 814 | 815 | try: 816 | end_ea = int(params.get('end', ['0'])[0], 0) 817 | except ValueError: 818 | end_ea = idc.BADADDR 819 | 820 | # Execute search in main thread 821 | result = self._execute_in_main_thread( 822 | self._search_bytes_impl, 823 | byte_str, 824 | start_ea, 825 | end_ea 826 | ) 827 | self._send_json_response(result) 828 | 829 | def _handle_search_in_names(self): 830 | """Handle search for names/symbols in the binary.""" 831 | # Parse query parameters 832 | parsed_url = urlparse(self.path) 833 | params = parse_qs(parsed_url.query) 834 | 835 | # Get parameters with defaults 836 | pattern = params.get('pattern', [''])[0] 837 | if not pattern: 838 | self._send_error_response('Missing required parameter: pattern') 839 | return 840 | 841 | # Optional parameters 842 | case_sensitive = params.get('case_sensitive', ['false'])[0].lower() == 'true' 843 | 844 | # Get name type if specified 845 | name_type = params.get('type', ['all'])[0].lower() 846 | 847 | # Execute search in main thread 848 | result = self._execute_in_main_thread( 849 | self._search_in_names_impl, 850 | pattern, 851 | case_sensitive, 852 | name_type 853 | ) 854 | self._send_json_response(result) 855 | 856 | def _search_bytes_impl(self, byte_str, start_ea, end_ea): 857 | """Implementation of searching for byte sequence - runs in main thread.""" 858 | results = [] 859 | 860 | try: 861 | # Ensure byte_str is properly formatted for IDA's find_binary 862 | # IDA expects a string like "41 42 43" or "41 ?? 43" where ?? is a wildcard 863 | # Clean up the input to ensure it's in the right format 864 | byte_str = byte_str.strip() 865 | if not byte_str.startswith('"') and not byte_str.startswith("'"): 866 | byte_str = f'"{byte_str}"' 867 | 868 | # Start searching 869 | ea = start_ea 870 | while ea != idc.BADADDR: 871 | ea = idc.find_binary(ea, idc.SEARCH_DOWN | idc.SEARCH_NEXT, byte_str) 872 | if ea == idc.BADADDR or (end_ea > 0 and ea >= end_ea): 873 | break 874 | 875 | # Get some context around the found bytes 876 | disasm = idc.generate_disasm_line(ea, 0) 877 | 878 | # Add to results 879 | results.append({ 880 | 'address': f"0x{ea:X}", 881 | 'disassembly': disasm, 882 | 'bytes': ' '.join([f"{idc.get_wide_byte(ea + i):02X}" for i in range(8)]) # Show 8 bytes 883 | }) 884 | 885 | # Move to next byte to continue search 886 | ea += 1 887 | except Exception as e: 888 | return {'error': f"Error searching for byte sequence: {str(e)}"} 889 | 890 | return { 891 | 'count': len(results), 892 | 'results': results 893 | } 894 | 895 | def _search_in_names_impl(self, pattern, case_sensitive, name_type): 896 | """Implementation of searching in names/symbols - runs in main thread.""" 897 | results = [] 898 | 899 | try: 900 | # Prepare name type filters 901 | is_func = name_type in ['function', 'func', 'functions', 'all'] 902 | is_data = name_type in ['data', 'variable', 'variables', 'all'] 903 | is_import = name_type in ['import', 'imports', 'all'] 904 | is_export = name_type in ['export', 'exports', 'all'] 905 | is_label = name_type in ['label', 'labels', 'all'] 906 | 907 | # Get all names in the database 908 | for ea, name in idautils.Names(): 909 | # Skip null names 910 | if not name: 911 | continue 912 | 913 | # Apply pattern matching based on case sensitivity 914 | if (case_sensitive and pattern in name) or \ 915 | (not case_sensitive and pattern.lower() in name.lower()): 916 | # Determine the type of the name 917 | name_info = { 918 | 'address': f"0x{ea:X}", 919 | 'name': name, 920 | 'type': 'unknown' 921 | } 922 | 923 | # Check if it's a function 924 | if is_func and ida_funcs.get_func(ea) is not None: 925 | name_info['type'] = 'function' 926 | if ida_funcs.get_func(ea).start_ea == ea: # Function start 927 | name_info['disassembly'] = idc.generate_disasm_line(ea, 0) 928 | name_info['is_start'] = True 929 | 930 | # Check if it's part of imports (using IDA's import list) 931 | elif is_import and ida_nalt.is_imported(ea): 932 | name_info['type'] = 'import' 933 | 934 | # Check if it's an export 935 | elif is_export and ida_nalt.is_exported(ea): 936 | name_info['type'] = 'export' 937 | 938 | # Check if it's a data variable 939 | elif is_data and ida_bytes.is_data(ida_bytes.get_flags(ea)): 940 | name_info['type'] = 'data' 941 | name_info['data_type'] = idc.get_type_name(ea) 942 | 943 | # Check if it's a label (non-function named location) 944 | elif is_label and not ida_funcs.get_func(ea): 945 | name_info['type'] = 'label' 946 | name_info['disassembly'] = idc.generate_disasm_line(ea, 0) 947 | 948 | # Filter out if it doesn't match the requested type 949 | if name_type != 'all' and name_info['type'] != name_type and \ 950 | not (name_type in ['function', 'func', 'functions'] and name_info['type'] == 'function') and \ 951 | not (name_type in ['import', 'imports'] and name_info['type'] == 'import') and \ 952 | not (name_type in ['export', 'exports'] and name_info['type'] == 'export') and \ 953 | not (name_type in ['data', 'variable', 'variables'] and name_info['type'] == 'data') and \ 954 | not (name_type in ['label', 'labels'] and name_info['type'] == 'label'): 955 | continue 956 | 957 | # Add to results 958 | results.append(name_info) 959 | 960 | # Sort results by address 961 | results.sort(key=lambda x: int(x['address'], 16)) 962 | 963 | except Exception as e: 964 | return {'error': f"Error searching in names: {str(e)}\n{traceback.format_exc()}"} 965 | 966 | return { 967 | 'count': len(results), 968 | 'results': results 969 | } 970 | 971 | def _handle_get_disassembly(self): 972 | """Handle get disassembly request.""" 973 | # Parse query parameters 974 | parsed_url = urlparse(self.path) 975 | params = parse_qs(parsed_url.query) 976 | 977 | # Get parameters with defaults 978 | try: 979 | start_ea = int(params.get('start', ['0'])[0], 0) 980 | except ValueError: 981 | self._send_error_response('Invalid start address') 982 | return 983 | 984 | # Optional parameters 985 | try: 986 | end_ea = int(params.get('end', ['0'])[0], 0) 987 | except ValueError: 988 | end_ea = 0 989 | 990 | try: 991 | count = int(params.get('count', ['10'])[0]) 992 | except ValueError: 993 | count = 10 994 | 995 | # Execute in main thread 996 | result = self._execute_in_main_thread( 997 | self._get_disassembly_impl, 998 | start_ea, 999 | end_ea, 1000 | count 1001 | ) 1002 | self._send_json_response(result) 1003 | 1004 | def _get_disassembly_impl(self, start_ea, end_ea, count): 1005 | """Implementation of getting disassembly - runs in main thread.""" 1006 | disassembly = [] 1007 | 1008 | try: 1009 | # If end_ea is specified, use it, otherwise use count 1010 | if end_ea > 0: 1011 | current_ea = start_ea 1012 | while current_ea < end_ea: 1013 | disasm = idc.generate_disasm_line(current_ea, 0) 1014 | bytes_str = ' '.join([f"{idc.get_wide_byte(current_ea + i):02X}" for i in range(min(16, idc.get_item_size(current_ea)))]) 1015 | 1016 | disassembly.append({ 1017 | 'address': f"0x{current_ea:X}", 1018 | 'disassembly': disasm, 1019 | 'bytes': bytes_str, 1020 | 'size': idc.get_item_size(current_ea) 1021 | }) 1022 | 1023 | current_ea += idc.get_item_size(current_ea) 1024 | if len(disassembly) >= 1000: # Limit to 1000 instructions for safety 1025 | break 1026 | else: 1027 | # Use count to limit the number of instructions 1028 | current_ea = start_ea 1029 | for _ in range(min(count, 1000)): # Limit to 1000 instructions for safety 1030 | disasm = idc.generate_disasm_line(current_ea, 0) 1031 | bytes_str = ' '.join([f"{idc.get_wide_byte(current_ea + i):02X}" for i in range(min(16, idc.get_item_size(current_ea)))]) 1032 | 1033 | disassembly.append({ 1034 | 'address': f"0x{current_ea:X}", 1035 | 'disassembly': disasm, 1036 | 'bytes': bytes_str, 1037 | 'size': idc.get_item_size(current_ea) 1038 | }) 1039 | 1040 | current_ea += idc.get_item_size(current_ea) 1041 | if current_ea == idc.BADADDR: 1042 | break 1043 | except Exception as e: 1044 | return {'error': f"Error getting disassembly: {str(e)}"} 1045 | 1046 | return { 1047 | 'count': len(disassembly), 1048 | 'disassembly': disassembly, 1049 | 'start_address': f"0x{start_ea:X}", 1050 | 'end_address': f"0x{end_ea:X}" if end_ea > 0 else None 1051 | } 1052 | 1053 | def _handle_get_xrefs_to(self): 1054 | """Handle get xrefs to address request.""" 1055 | # Parse query parameters 1056 | parsed_url = urlparse(self.path) 1057 | params = parse_qs(parsed_url.query) 1058 | 1059 | # Get parameters with defaults 1060 | try: 1061 | address = int(params.get('address', ['0'])[0], 0) 1062 | except ValueError: 1063 | self._send_error_response('Invalid address') 1064 | return 1065 | 1066 | # Optional parameters 1067 | xref_type = params.get('type', ['all'])[0].lower() 1068 | 1069 | # Execute in main thread 1070 | result = self._execute_in_main_thread( 1071 | self._get_xrefs_to_impl, 1072 | address, 1073 | xref_type 1074 | ) 1075 | self._send_json_response(result) 1076 | 1077 | def _get_xrefs_to_impl(self, address, xref_type): 1078 | """Implementation of getting xrefs to address - runs in main thread.""" 1079 | xrefs = [] 1080 | 1081 | try: 1082 | # Get all cross-references to the specified address 1083 | for xref in idautils.XrefsTo(address, 0): 1084 | # Determine xref type 1085 | xref_info = { 1086 | 'from_address': f"0x{xref.frm:X}", 1087 | 'to_address': f"0x{xref.to:X}", 1088 | 'type': self._get_xref_type_name(xref.type), 1089 | 'is_code': xref.iscode 1090 | } 1091 | 1092 | # Filter by type if specified 1093 | if xref_type != 'all': 1094 | if xref_type == 'code' and not xref.iscode: 1095 | continue 1096 | if xref_type == 'data' and xref.iscode: 1097 | continue 1098 | 1099 | # Get function name if available 1100 | func = ida_funcs.get_func(xref.frm) 1101 | if func: 1102 | xref_info['function_name'] = ida_name.get_ea_name(func.start_ea) 1103 | xref_info['function_address'] = f"0x{func.start_ea:X}" 1104 | 1105 | # Get disassembly for context 1106 | xref_info['disassembly'] = idc.generate_disasm_line(xref.frm, 0) 1107 | 1108 | xrefs.append(xref_info) 1109 | 1110 | # Sort by address 1111 | xrefs.sort(key=lambda x: int(x['from_address'], 16)) 1112 | 1113 | except Exception as e: 1114 | return {'error': f"Error getting xrefs to address: {str(e)}\n{traceback.format_exc()}"} 1115 | 1116 | return { 1117 | 'count': len(xrefs), 1118 | 'xrefs': xrefs, 1119 | 'address': f"0x{address:X}", 1120 | 'name': ida_name.get_ea_name(address) 1121 | } 1122 | 1123 | def _handle_get_xrefs_from(self): 1124 | """Handle get xrefs from address request.""" 1125 | # Parse query parameters 1126 | parsed_url = urlparse(self.path) 1127 | params = parse_qs(parsed_url.query) 1128 | 1129 | # Get parameters with defaults 1130 | try: 1131 | address = int(params.get('address', ['0'])[0], 0) 1132 | except ValueError: 1133 | self._send_error_response('Invalid address') 1134 | return 1135 | 1136 | # Optional parameters 1137 | xref_type = params.get('type', ['all'])[0].lower() 1138 | 1139 | # Execute in main thread 1140 | result = self._execute_in_main_thread( 1141 | self._get_xrefs_from_impl, 1142 | address, 1143 | xref_type 1144 | ) 1145 | self._send_json_response(result) 1146 | 1147 | def _get_xrefs_from_impl(self, address, xref_type): 1148 | """Implementation of getting xrefs from address - runs in main thread.""" 1149 | xrefs = [] 1150 | 1151 | try: 1152 | # Get all cross-references from the specified address 1153 | for xref in idautils.XrefsFrom(address, 0): 1154 | # Determine xref type 1155 | xref_info = { 1156 | 'from_address': f"0x{xref.frm:X}", 1157 | 'to_address': f"0x{xref.to:X}", 1158 | 'type': self._get_xref_type_name(xref.type), 1159 | 'is_code': xref.iscode 1160 | } 1161 | 1162 | # Filter by type if specified 1163 | if xref_type != 'all': 1164 | if xref_type == 'code' and not xref.iscode: 1165 | continue 1166 | if xref_type == 'data' and xref.iscode: 1167 | continue 1168 | 1169 | # Get target name if available 1170 | target_name = ida_name.get_ea_name(xref.to) 1171 | if target_name: 1172 | xref_info['target_name'] = target_name 1173 | 1174 | # Check if target is a function 1175 | func = ida_funcs.get_func(xref.to) 1176 | if func and func.start_ea == xref.to: 1177 | xref_info['target_is_function'] = True 1178 | xref_info['target_function_name'] = ida_name.get_ea_name(func.start_ea) 1179 | 1180 | # Get disassembly for context 1181 | xref_info['target_disassembly'] = idc.generate_disasm_line(xref.to, 0) 1182 | 1183 | xrefs.append(xref_info) 1184 | 1185 | # Sort by address 1186 | xrefs.sort(key=lambda x: int(x['to_address'], 16)) 1187 | 1188 | except Exception as e: 1189 | return {'error': f"Error getting xrefs from address: {str(e)}\n{traceback.format_exc()}"} 1190 | 1191 | return { 1192 | 'count': len(xrefs), 1193 | 'xrefs': xrefs, 1194 | 'address': f"0x{address:X}", 1195 | 'name': ida_name.get_ea_name(address) 1196 | } 1197 | 1198 | def _get_xref_type_name(self, xref_type): 1199 | """Convert IDA xref type code to human-readable name.""" 1200 | # Code cross-reference types 1201 | if xref_type == idaapi.fl_CF: 1202 | return "call_far" 1203 | elif xref_type == idaapi.fl_CN: 1204 | return "call_near" 1205 | elif xref_type == idaapi.fl_JF: 1206 | return "jump_far" 1207 | elif xref_type == idaapi.fl_JN: 1208 | return "jump_near" 1209 | # Data cross-reference types 1210 | elif xref_type == idaapi.dr_O: 1211 | return "data_offset" 1212 | elif xref_type == idaapi.dr_W: 1213 | return "data_write" 1214 | elif xref_type == idaapi.dr_R: 1215 | return "data_read" 1216 | elif xref_type == idaapi.dr_T: 1217 | return "data_text" 1218 | elif xref_type == idaapi.dr_I: 1219 | return "data_informational" 1220 | else: 1221 | return f"unknown_{xref_type}" 1222 | 1223 | def _execute_in_main_thread(self, func, *args, **kwargs): 1224 | """Execute a function in the main thread with additional safeguards.""" 1225 | result_container = {} 1226 | execution_done = threading.Event() 1227 | 1228 | def sync_wrapper(): 1229 | """Wrapper function to capture the result safely.""" 1230 | try: 1231 | result_container['result'] = func(*args, **kwargs) 1232 | except Exception as e: 1233 | result_container['error'] = str(e) 1234 | result_container['traceback'] = traceback.format_exc() 1235 | finally: 1236 | # Signal that execution has finished 1237 | execution_done.set() 1238 | return 0 # Must return an integer 1239 | 1240 | # Schedule execution in the main thread 1241 | idaapi.execute_sync(sync_wrapper, MFF_READ) 1242 | 1243 | # Wait for the result with a timeout 1244 | max_wait = 30 # Maximum wait time in seconds 1245 | if not execution_done.wait(max_wait): 1246 | error_msg = f"Operation timed out after {max_wait} seconds" 1247 | print(f"[RemoteControl] {error_msg}") 1248 | return {'error': error_msg} 1249 | 1250 | if 'error' in result_container: 1251 | print(f"[RemoteControl] Error in main thread: {result_container['error']}") 1252 | print(result_container.get('traceback', '')) 1253 | return {'error': result_container['error']} 1254 | 1255 | return result_container.get('result', {'error': 'Unknown error occurred'}) 1256 | 1257 | 1258 | class RemoteControlServer: 1259 | """HTTP server for IDA Pro remote control.""" 1260 | 1261 | def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT): 1262 | self.host = host 1263 | self.port = port 1264 | self.server = None 1265 | self.server_thread = None 1266 | self.running = False 1267 | 1268 | def start(self): 1269 | """Start the HTTP server.""" 1270 | if self.running: 1271 | print("[RemoteControl] Server is already running") 1272 | return False 1273 | 1274 | try: 1275 | self.server = HTTPServer((self.host, self.port), RemoteControlHandler) 1276 | self.server_thread = threading.Thread(target=self.server.serve_forever) 1277 | self.server_thread.daemon = True 1278 | self.server_thread.start() 1279 | self.running = True 1280 | print(f"[RemoteControl] Server started on http://{self.host}:{self.port}") 1281 | return True 1282 | except Exception as e: 1283 | print(f"[RemoteControl] Failed to start server: {str(e)}") 1284 | return False 1285 | 1286 | def stop(self): 1287 | """Stop the HTTP server.""" 1288 | if not self.running: 1289 | print("[RemoteControl] Server is not running") 1290 | return False 1291 | 1292 | try: 1293 | self.server.shutdown() 1294 | self.server.server_close() 1295 | self.server_thread.join() 1296 | self.running = False 1297 | print("[RemoteControl] Server stopped") 1298 | return True 1299 | except Exception as e: 1300 | print(f"[RemoteControl] Failed to stop server: {str(e)}") 1301 | return False 1302 | 1303 | def is_running(self): 1304 | """Check if the server is running.""" 1305 | return self.running 1306 | 1307 | 1308 | class RemoteControlPlugin(idaapi.plugin_t): 1309 | """IDA Pro plugin for remote control.""" 1310 | 1311 | flags = idaapi.PLUGIN_KEEP 1312 | comment = "Remote control for IDA through HTTP" 1313 | help = "Provides HTTP endpoints to control IDA Pro remotely" 1314 | wanted_name = PLUGIN_NAME 1315 | wanted_hotkey = "Alt-R" 1316 | 1317 | def init(self): 1318 | """Initialize the plugin.""" 1319 | print(f"[{PLUGIN_NAME}] Initializing...") 1320 | 1321 | # Auto-start server if configured 1322 | if AUTO_START: 1323 | global g_server 1324 | g_server = RemoteControlServer(DEFAULT_HOST, DEFAULT_PORT) 1325 | success = g_server.start() 1326 | 1327 | if success: 1328 | print(f"[{PLUGIN_NAME}] Server auto-started on http://{DEFAULT_HOST}:{DEFAULT_PORT}") 1329 | print(f"[{PLUGIN_NAME}] Available endpoints:") 1330 | 1331 | else: 1332 | g_server = None 1333 | print(f"[{PLUGIN_NAME}] Failed to auto-start server") 1334 | 1335 | return idaapi.PLUGIN_KEEP 1336 | 1337 | def run(self, arg): 1338 | """Run the plugin when activated manually.""" 1339 | global g_server 1340 | 1341 | # Check if server is already running 1342 | if g_server and g_server.is_running(): 1343 | response = idaapi.ask_yn(idaapi.ASKBTN_NO, 1344 | "Remote control server is already running.\nDo you want to stop it?") 1345 | if response == idaapi.ASKBTN_YES: 1346 | g_server.stop() 1347 | g_server = None 1348 | return 1349 | 1350 | # If AUTO_START is enabled but server isn't running, start with default settings 1351 | if AUTO_START: 1352 | g_server = RemoteControlServer(DEFAULT_HOST, DEFAULT_PORT) 1353 | success = g_server.start() 1354 | 1355 | if success: 1356 | print(f"[{PLUGIN_NAME}] Server started on http://{DEFAULT_HOST}:{DEFAULT_PORT}") 1357 | 1358 | else: 1359 | g_server = None 1360 | print(f"[{PLUGIN_NAME}] Failed to start server") 1361 | return 1362 | 1363 | # Manual configuration if AUTO_START is disabled 1364 | # Get host and port from user 1365 | host = idaapi.ask_str(DEFAULT_HOST, 0, "Enter host address (e.g. 127.0.0.1):") 1366 | if not host: 1367 | host = DEFAULT_HOST 1368 | 1369 | port_str = idaapi.ask_str(str(DEFAULT_PORT), 0, "Enter port number:") 1370 | try: 1371 | port = int(port_str) 1372 | except (ValueError, TypeError): 1373 | port = DEFAULT_PORT 1374 | 1375 | # Start server 1376 | g_server = RemoteControlServer(host, port) 1377 | success = g_server.start() 1378 | 1379 | if success: 1380 | print(f"[{PLUGIN_NAME}] Server started on http://{host}:{port}") 1381 | print(f"[{PLUGIN_NAME}] Available endpoints:") 1382 | 1383 | else: 1384 | g_server = None 1385 | print(f"[{PLUGIN_NAME}] Failed to start server") 1386 | 1387 | def term(self): 1388 | """Terminate the plugin.""" 1389 | global g_server 1390 | 1391 | if g_server and g_server.is_running(): 1392 | g_server.stop() 1393 | g_server = None 1394 | 1395 | print(f"[{PLUGIN_NAME}] Plugin terminated") 1396 | 1397 | 1398 | # Register the plugin 1399 | def PLUGIN_ENTRY(): 1400 | """Return the plugin instance.""" 1401 | return RemoteControlPlugin() 1402 | 1403 | 1404 | # For testing/debugging in the script editor 1405 | if __name__ == "__main__": 1406 | # This will only run when executed in the IDA script editor 1407 | plugin = RemoteControlPlugin() 1408 | plugin.run(0) --------------------------------------------------------------------------------