├── .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 |
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 | 
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)
--------------------------------------------------------------------------------