iflow-mcp_developermode-korea_reversecore-mcp 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/METADATA +543 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/RECORD +79 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_developermode_korea_reversecore_mcp-1.0.0.dist-info/top_level.txt +1 -0
- reversecore_mcp/__init__.py +9 -0
- reversecore_mcp/core/__init__.py +78 -0
- reversecore_mcp/core/audit.py +101 -0
- reversecore_mcp/core/binary_cache.py +138 -0
- reversecore_mcp/core/command_spec.py +357 -0
- reversecore_mcp/core/config.py +432 -0
- reversecore_mcp/core/container.py +288 -0
- reversecore_mcp/core/decorators.py +152 -0
- reversecore_mcp/core/error_formatting.py +93 -0
- reversecore_mcp/core/error_handling.py +142 -0
- reversecore_mcp/core/evidence.py +229 -0
- reversecore_mcp/core/exceptions.py +296 -0
- reversecore_mcp/core/execution.py +240 -0
- reversecore_mcp/core/ghidra.py +642 -0
- reversecore_mcp/core/ghidra_helper.py +481 -0
- reversecore_mcp/core/ghidra_manager.py +234 -0
- reversecore_mcp/core/json_utils.py +131 -0
- reversecore_mcp/core/loader.py +73 -0
- reversecore_mcp/core/logging_config.py +206 -0
- reversecore_mcp/core/memory.py +721 -0
- reversecore_mcp/core/metrics.py +198 -0
- reversecore_mcp/core/mitre_mapper.py +365 -0
- reversecore_mcp/core/plugin.py +45 -0
- reversecore_mcp/core/r2_helpers.py +404 -0
- reversecore_mcp/core/r2_pool.py +403 -0
- reversecore_mcp/core/report_generator.py +268 -0
- reversecore_mcp/core/resilience.py +252 -0
- reversecore_mcp/core/resource_manager.py +169 -0
- reversecore_mcp/core/result.py +132 -0
- reversecore_mcp/core/security.py +213 -0
- reversecore_mcp/core/validators.py +238 -0
- reversecore_mcp/dashboard/__init__.py +221 -0
- reversecore_mcp/prompts/__init__.py +56 -0
- reversecore_mcp/prompts/common.py +24 -0
- reversecore_mcp/prompts/game.py +280 -0
- reversecore_mcp/prompts/malware.py +1219 -0
- reversecore_mcp/prompts/report.py +150 -0
- reversecore_mcp/prompts/security.py +136 -0
- reversecore_mcp/resources.py +329 -0
- reversecore_mcp/server.py +727 -0
- reversecore_mcp/tools/__init__.py +49 -0
- reversecore_mcp/tools/analysis/__init__.py +74 -0
- reversecore_mcp/tools/analysis/capa_tools.py +215 -0
- reversecore_mcp/tools/analysis/die_tools.py +180 -0
- reversecore_mcp/tools/analysis/diff_tools.py +643 -0
- reversecore_mcp/tools/analysis/lief_tools.py +272 -0
- reversecore_mcp/tools/analysis/signature_tools.py +591 -0
- reversecore_mcp/tools/analysis/static_analysis.py +479 -0
- reversecore_mcp/tools/common/__init__.py +58 -0
- reversecore_mcp/tools/common/file_operations.py +352 -0
- reversecore_mcp/tools/common/memory_tools.py +516 -0
- reversecore_mcp/tools/common/patch_explainer.py +230 -0
- reversecore_mcp/tools/common/server_tools.py +115 -0
- reversecore_mcp/tools/ghidra/__init__.py +19 -0
- reversecore_mcp/tools/ghidra/decompilation.py +975 -0
- reversecore_mcp/tools/ghidra/ghidra_tools.py +1052 -0
- reversecore_mcp/tools/malware/__init__.py +61 -0
- reversecore_mcp/tools/malware/adaptive_vaccine.py +579 -0
- reversecore_mcp/tools/malware/dormant_detector.py +756 -0
- reversecore_mcp/tools/malware/ioc_tools.py +228 -0
- reversecore_mcp/tools/malware/vulnerability_hunter.py +519 -0
- reversecore_mcp/tools/malware/yara_tools.py +214 -0
- reversecore_mcp/tools/patch_explainer.py +19 -0
- reversecore_mcp/tools/radare2/__init__.py +13 -0
- reversecore_mcp/tools/radare2/r2_analysis.py +972 -0
- reversecore_mcp/tools/radare2/r2_session.py +376 -0
- reversecore_mcp/tools/radare2/radare2_mcp_tools.py +1183 -0
- reversecore_mcp/tools/report/__init__.py +4 -0
- reversecore_mcp/tools/report/email.py +82 -0
- reversecore_mcp/tools/report/report_mcp_tools.py +344 -0
- reversecore_mcp/tools/report/report_tools.py +1076 -0
- reversecore_mcp/tools/report/session.py +194 -0
- reversecore_mcp/tools/report_tools.py +11 -0
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Radare2 MCP Tools - Direct port from r2mcp C implementation.
|
|
3
|
+
|
|
4
|
+
This module provides MCP-compatible tools that mirror the official r2mcp server,
|
|
5
|
+
enabling full radare2 functionality through the MCP protocol.
|
|
6
|
+
|
|
7
|
+
All tools are prefixed with 'Radare2_' for namespace clarity.
|
|
8
|
+
|
|
9
|
+
SECURITY PHILOSOPHY:
|
|
10
|
+
- All user inputs are strictly validated before passing to r2pipe
|
|
11
|
+
- Address/expression parameters are sanitized to prevent command injection
|
|
12
|
+
- Path validation uses the project's security module
|
|
13
|
+
- No shell=True, no f-strings with unsanitized input in commands
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import os
|
|
20
|
+
import shutil
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from fastmcp import FastMCP
|
|
24
|
+
|
|
25
|
+
from reversecore_mcp.core.config import get_config
|
|
26
|
+
from reversecore_mcp.core.exceptions import ValidationError
|
|
27
|
+
from reversecore_mcp.core.logging_config import get_logger
|
|
28
|
+
from reversecore_mcp.core.plugin import Plugin
|
|
29
|
+
from reversecore_mcp.core.security import validate_file_path
|
|
30
|
+
from reversecore_mcp.core.validators import validate_address_format
|
|
31
|
+
|
|
32
|
+
# Import session management and utilities from r2_session module
|
|
33
|
+
from reversecore_mcp.tools.radare2.r2_session import (
|
|
34
|
+
DEFAULT_PAGE_SIZE,
|
|
35
|
+
MAX_PAGE_SIZE,
|
|
36
|
+
R2Session,
|
|
37
|
+
_filter_lines_by_regex,
|
|
38
|
+
_filter_named_functions,
|
|
39
|
+
_paginate_text,
|
|
40
|
+
_sanitize_for_r2_cmd,
|
|
41
|
+
_validate_expression,
|
|
42
|
+
_validate_identifier,
|
|
43
|
+
_validate_r2_command,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
logger = get_logger(__name__)
|
|
47
|
+
|
|
48
|
+
# Default configuration
|
|
49
|
+
DEFAULT_TIMEOUT = get_config().default_tool_timeout
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Radare2ToolsPlugin(Plugin):
|
|
53
|
+
"""Plugin for Radare2 MCP tools - port from r2mcp."""
|
|
54
|
+
|
|
55
|
+
name = "radare2_mcp_tools"
|
|
56
|
+
description = "Radare2 binary analysis tools (r2mcp compatible)"
|
|
57
|
+
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self._sessions: dict[str, R2Session] = {} # session_id -> Session
|
|
60
|
+
self._file_to_session: dict[str, str] = {} # file_path -> session_id
|
|
61
|
+
self._lock = asyncio.Lock() # Protects session creation race conditions
|
|
62
|
+
|
|
63
|
+
def _diagnose_error(self, file_path: str, error: Exception) -> dict[str, Any]:
|
|
64
|
+
"""Diagnose why r2 failed to open a file."""
|
|
65
|
+
diagnosis = {
|
|
66
|
+
"error": str(error),
|
|
67
|
+
"file_exists": os.path.exists(file_path),
|
|
68
|
+
"is_file": os.path.isfile(file_path) if os.path.exists(file_path) else False,
|
|
69
|
+
"permissions": oct(os.stat(file_path).st_mode)[-3:]
|
|
70
|
+
if os.path.exists(file_path)
|
|
71
|
+
else "N/A",
|
|
72
|
+
"file_size": os.path.getsize(file_path) if os.path.exists(file_path) else 0,
|
|
73
|
+
"r2_available": shutil.which("radare2") is not None,
|
|
74
|
+
"hints": [],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if not diagnosis["file_exists"]:
|
|
78
|
+
diagnosis["hints"].append(
|
|
79
|
+
"Check if the file path is correct (relative to /app/workspace?)"
|
|
80
|
+
)
|
|
81
|
+
elif not diagnosis["is_file"]:
|
|
82
|
+
diagnosis["hints"].append("Path exists but is not a file (directory?)")
|
|
83
|
+
elif diagnosis["file_size"] == 0:
|
|
84
|
+
diagnosis["hints"].append("File is empty (0 bytes)")
|
|
85
|
+
|
|
86
|
+
return diagnosis
|
|
87
|
+
|
|
88
|
+
async def _get_or_create_session(self, file_path: str, auto_analyze: bool = False) -> R2Session:
|
|
89
|
+
"""
|
|
90
|
+
Get existing session or create new one with strict validation.
|
|
91
|
+
Protected by lock to prevent race conditions.
|
|
92
|
+
"""
|
|
93
|
+
# 1. Normalize Path
|
|
94
|
+
try:
|
|
95
|
+
validated_path = validate_file_path(file_path)
|
|
96
|
+
file_path = str(validated_path)
|
|
97
|
+
except ValidationError:
|
|
98
|
+
return R2Session(file_path)
|
|
99
|
+
|
|
100
|
+
async with self._lock:
|
|
101
|
+
# 2. Check existing session (double-checked locking pattern)
|
|
102
|
+
if file_path in self._file_to_session:
|
|
103
|
+
sid = self._file_to_session[file_path]
|
|
104
|
+
if sid in self._sessions:
|
|
105
|
+
session = self._sessions[sid]
|
|
106
|
+
if session.is_open:
|
|
107
|
+
return session
|
|
108
|
+
else:
|
|
109
|
+
# Stale session, remove it
|
|
110
|
+
del self._sessions[sid]
|
|
111
|
+
del self._file_to_session[file_path]
|
|
112
|
+
|
|
113
|
+
# 3. Create new session (blocking I/O wrapped in thread)
|
|
114
|
+
try:
|
|
115
|
+
# Validate file availability again inside lock
|
|
116
|
+
if not os.path.exists(file_path):
|
|
117
|
+
raise ValueError(f"File not found: {file_path}")
|
|
118
|
+
|
|
119
|
+
# Use to_thread for blocking R2 spawning
|
|
120
|
+
session = await asyncio.to_thread(R2Session, file_path)
|
|
121
|
+
# IMPORTANT: R2Session(file_path) does NOT open the file automatically.
|
|
122
|
+
# We must explicitly open it, otherwise all Radare2_* tools will fail
|
|
123
|
+
# with is_open == False (and Radare2_open_file always returns R2_OPEN_FAILED).
|
|
124
|
+
opened = await asyncio.to_thread(session.open, file_path)
|
|
125
|
+
if not opened:
|
|
126
|
+
raise ValueError(session.last_error or "Failed to open file with r2pipe")
|
|
127
|
+
|
|
128
|
+
# 4. Store session
|
|
129
|
+
self._sessions[session.session_id] = session
|
|
130
|
+
self._file_to_session[file_path] = session.session_id
|
|
131
|
+
|
|
132
|
+
# 5. Auto analyze if requested
|
|
133
|
+
if auto_analyze:
|
|
134
|
+
# Async analysis call (assuming session.analyze is async or needs wrapping)
|
|
135
|
+
# For now, R2Session methods are sync, so we wrap them
|
|
136
|
+
await asyncio.to_thread(session.cmd, "aaa")
|
|
137
|
+
|
|
138
|
+
return session
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Failed to create R2 session for {file_path}: {e}")
|
|
142
|
+
# Raise exception instead of returning dummy session that may also fail
|
|
143
|
+
from reversecore_mcp.core.exceptions import ToolExecutionError
|
|
144
|
+
|
|
145
|
+
raise ToolExecutionError(f"Cannot open file with radare2: {file_path}") from e
|
|
146
|
+
|
|
147
|
+
def _ensure_analyzed(self, session: R2Session, level: int = 1) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Ensure session has been analyzed at least once.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
session: R2Session to check
|
|
153
|
+
level: Minimum analysis level required
|
|
154
|
+
"""
|
|
155
|
+
if not session._analyzed:
|
|
156
|
+
session.analyze(level)
|
|
157
|
+
|
|
158
|
+
def register(self, mcp: FastMCP) -> None:
|
|
159
|
+
"""Register all Radare2 tools with the MCP server."""
|
|
160
|
+
|
|
161
|
+
# =====================================================================
|
|
162
|
+
# File Management Tools
|
|
163
|
+
# =====================================================================
|
|
164
|
+
|
|
165
|
+
@mcp.tool()
|
|
166
|
+
async def Radare2_open_file(file_path: str) -> dict[str, Any]:
|
|
167
|
+
"""
|
|
168
|
+
Opens a binary file with radare2 for analysis.
|
|
169
|
+
|
|
170
|
+
Call this tool before any other r2mcp tool. Use an absolute file_path.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
file_path: Absolute path to the binary file to analyze
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Status of the file opening operation, including session_id
|
|
177
|
+
"""
|
|
178
|
+
# Validate path using project security module
|
|
179
|
+
try:
|
|
180
|
+
validated_path = validate_file_path(file_path)
|
|
181
|
+
abs_path = str(validated_path)
|
|
182
|
+
except ValidationError as e:
|
|
183
|
+
return {"status": "error", "message": str(e), "error_code": "INVALID_PATH"}
|
|
184
|
+
|
|
185
|
+
session = await self._get_or_create_session(abs_path)
|
|
186
|
+
|
|
187
|
+
if session.is_open:
|
|
188
|
+
return {
|
|
189
|
+
"status": "success",
|
|
190
|
+
"message": "File opened successfully",
|
|
191
|
+
"file_path": abs_path,
|
|
192
|
+
"session_id": session.session_id,
|
|
193
|
+
"file_size": os.path.getsize(abs_path) if os.path.exists(abs_path) else 0,
|
|
194
|
+
"status_code": "OPENED",
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# Diagnose failure
|
|
198
|
+
diagnosis = self._diagnose_error(
|
|
199
|
+
abs_path, Exception(session.last_error or "Unknown error")
|
|
200
|
+
)
|
|
201
|
+
return {
|
|
202
|
+
"status": "error",
|
|
203
|
+
"message": f"Failed to open file: {session.last_error}",
|
|
204
|
+
"error_code": "R2_OPEN_FAILED",
|
|
205
|
+
"diagnosis": diagnosis,
|
|
206
|
+
"hints": diagnosis["hints"],
|
|
207
|
+
"attempts": 1,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@mcp.tool()
|
|
211
|
+
async def Radare2_close_file(file_path: str) -> dict[str, Any]:
|
|
212
|
+
"""
|
|
213
|
+
Close the currently open radare2 session for a file.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
file_path: Path to the file to close
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Status of the close operation
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
validated_path = validate_file_path(file_path)
|
|
223
|
+
abs_path = str(validated_path)
|
|
224
|
+
|
|
225
|
+
# Check mapping
|
|
226
|
+
if abs_path in self._file_to_session:
|
|
227
|
+
sid = self._file_to_session[abs_path]
|
|
228
|
+
if sid in self._sessions:
|
|
229
|
+
self._sessions[sid].close()
|
|
230
|
+
del self._sessions[sid]
|
|
231
|
+
del self._file_to_session[abs_path]
|
|
232
|
+
return {
|
|
233
|
+
"status": "success",
|
|
234
|
+
"message": "File closed successfully",
|
|
235
|
+
"session_id": sid,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {"status": "success", "message": "File was not open (no active session)"}
|
|
239
|
+
except ValidationError as e:
|
|
240
|
+
return {"status": "error", "message": str(e)}
|
|
241
|
+
|
|
242
|
+
# =====================================================================
|
|
243
|
+
# Analysis Tools
|
|
244
|
+
# =====================================================================
|
|
245
|
+
|
|
246
|
+
@mcp.tool()
|
|
247
|
+
async def Radare2_analyze(
|
|
248
|
+
file_path: str,
|
|
249
|
+
level: int = 2,
|
|
250
|
+
) -> dict[str, Any]:
|
|
251
|
+
"""
|
|
252
|
+
Run binary analysis with optional depth level.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
file_path: Path to the binary file
|
|
256
|
+
level: Analysis level 0-4 (higher = more thorough, slower)
|
|
257
|
+
0: Basic (aa)
|
|
258
|
+
1: Auto (aaa)
|
|
259
|
+
2: Experimental (aaaa) - default
|
|
260
|
+
3: Deep (aaaaa)
|
|
261
|
+
4: Very deep (aaaaaa)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Analysis result with function count
|
|
265
|
+
"""
|
|
266
|
+
# Validate level is in range
|
|
267
|
+
if not isinstance(level, int) or level < 0 or level > 4:
|
|
268
|
+
return {"status": "error", "message": "level must be 0-4"}
|
|
269
|
+
|
|
270
|
+
session = await self._get_or_create_session(file_path)
|
|
271
|
+
if not session.is_open:
|
|
272
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
273
|
+
|
|
274
|
+
session.analyze(level)
|
|
275
|
+
func_count = session.cmd("aflc").strip()
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"status": "success",
|
|
279
|
+
"message": f"Analysis completed with level {level}",
|
|
280
|
+
"function_count": int(func_count) if func_count.isdigit() else 0,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
@mcp.tool()
|
|
284
|
+
async def Radare2_run_command(
|
|
285
|
+
file_path: str,
|
|
286
|
+
command: str,
|
|
287
|
+
) -> dict[str, Any]:
|
|
288
|
+
"""
|
|
289
|
+
Execute a raw radare2 command directly.
|
|
290
|
+
|
|
291
|
+
NOTE: Only analysis commands are allowed. Write and shell commands are blocked.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
file_path: Path to the binary file
|
|
295
|
+
command: The radare2 command to execute
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Command output
|
|
299
|
+
"""
|
|
300
|
+
# Validate command for security
|
|
301
|
+
try:
|
|
302
|
+
_validate_r2_command(command)
|
|
303
|
+
except ValidationError as e:
|
|
304
|
+
return {"status": "error", "message": str(e)}
|
|
305
|
+
|
|
306
|
+
session = await self._get_or_create_session(file_path)
|
|
307
|
+
if not session.is_open:
|
|
308
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
309
|
+
|
|
310
|
+
result = session.cmd(command)
|
|
311
|
+
return {"status": "success", "output": result}
|
|
312
|
+
|
|
313
|
+
@mcp.tool()
|
|
314
|
+
async def Radare2_calculate(
|
|
315
|
+
file_path: str,
|
|
316
|
+
expression: str,
|
|
317
|
+
) -> dict[str, Any]:
|
|
318
|
+
"""
|
|
319
|
+
Evaluate a math expression using radare2's number parser.
|
|
320
|
+
|
|
321
|
+
Useful for: 64-bit math, resolving addresses for symbols,
|
|
322
|
+
avoiding hallucinated results.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
file_path: Path to the binary file
|
|
326
|
+
expression: Math expression to evaluate (e.g., "0x100 + sym.flag - 4")
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Calculated result in hex and decimal
|
|
330
|
+
"""
|
|
331
|
+
# Validate expression for security
|
|
332
|
+
try:
|
|
333
|
+
_validate_expression(expression)
|
|
334
|
+
except ValidationError as e:
|
|
335
|
+
return {"status": "error", "message": str(e)}
|
|
336
|
+
|
|
337
|
+
session = await self._get_or_create_session(file_path)
|
|
338
|
+
if not session.is_open:
|
|
339
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
340
|
+
|
|
341
|
+
# Use validated expression
|
|
342
|
+
result = session.cmd(f"?v {expression}").strip()
|
|
343
|
+
return {
|
|
344
|
+
"status": "success",
|
|
345
|
+
"result": result,
|
|
346
|
+
"expression": expression,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# =====================================================================
|
|
350
|
+
# Function Listing Tools
|
|
351
|
+
# =====================================================================
|
|
352
|
+
|
|
353
|
+
@mcp.tool()
|
|
354
|
+
async def Radare2_list_functions(
|
|
355
|
+
file_path: str,
|
|
356
|
+
only_named: bool = False,
|
|
357
|
+
filter: str | None = None,
|
|
358
|
+
) -> dict[str, Any]:
|
|
359
|
+
"""
|
|
360
|
+
List all functions discovered during analysis.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
file_path: Path to the binary file
|
|
364
|
+
only_named: If true, exclude functions with numeric suffixes
|
|
365
|
+
filter: Regular expression to filter results
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
List of functions with addresses and names
|
|
369
|
+
"""
|
|
370
|
+
session = await self._get_or_create_session(file_path)
|
|
371
|
+
if not session.is_open:
|
|
372
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
373
|
+
|
|
374
|
+
# Ensure analysis is done (lazy - only if not already analyzed)
|
|
375
|
+
self._ensure_analyzed(session)
|
|
376
|
+
result = session.cmd("afl")
|
|
377
|
+
|
|
378
|
+
if only_named:
|
|
379
|
+
result = _filter_named_functions(result)
|
|
380
|
+
|
|
381
|
+
if filter:
|
|
382
|
+
result = _filter_lines_by_regex(result, filter)
|
|
383
|
+
|
|
384
|
+
lines = [line for line in result.strip().split("\n") if line]
|
|
385
|
+
return {
|
|
386
|
+
"status": "success",
|
|
387
|
+
"count": len(lines),
|
|
388
|
+
"functions": result,
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@mcp.tool()
|
|
392
|
+
async def Radare2_list_functions_tree(
|
|
393
|
+
file_path: str,
|
|
394
|
+
) -> dict[str, Any]:
|
|
395
|
+
"""
|
|
396
|
+
List functions and their successors (call tree).
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
file_path: Path to the binary file
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
Function call tree
|
|
403
|
+
"""
|
|
404
|
+
session = await self._get_or_create_session(file_path)
|
|
405
|
+
if not session.is_open:
|
|
406
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
407
|
+
|
|
408
|
+
result = session.cmd("aflm")
|
|
409
|
+
return {"status": "success", "output": result.strip()}
|
|
410
|
+
|
|
411
|
+
@mcp.tool()
|
|
412
|
+
async def Radare2_show_function_details(
|
|
413
|
+
file_path: str,
|
|
414
|
+
address: str | None = None,
|
|
415
|
+
) -> dict[str, Any]:
|
|
416
|
+
"""
|
|
417
|
+
Display detailed information about a function.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
file_path: Path to the binary file
|
|
421
|
+
address: Function address (uses current if not specified)
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Detailed function information
|
|
425
|
+
"""
|
|
426
|
+
session = await self._get_or_create_session(file_path)
|
|
427
|
+
if not session.is_open:
|
|
428
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
429
|
+
|
|
430
|
+
if address:
|
|
431
|
+
# Validate address format
|
|
432
|
+
try:
|
|
433
|
+
validate_address_format(address)
|
|
434
|
+
except ValidationError as e:
|
|
435
|
+
return {"status": "error", "message": str(e)}
|
|
436
|
+
result = session.cmd(f"afi @ {address}")
|
|
437
|
+
else:
|
|
438
|
+
result = session.cmd("afi")
|
|
439
|
+
|
|
440
|
+
return {"status": "success", "output": result}
|
|
441
|
+
|
|
442
|
+
@mcp.tool()
|
|
443
|
+
async def Radare2_get_current_address(
|
|
444
|
+
file_path: str,
|
|
445
|
+
) -> dict[str, Any]:
|
|
446
|
+
"""
|
|
447
|
+
Show the current seek position and function name.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
file_path: Path to the binary file
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Current address and function name
|
|
454
|
+
"""
|
|
455
|
+
session = await self._get_or_create_session(file_path)
|
|
456
|
+
if not session.is_open:
|
|
457
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
458
|
+
|
|
459
|
+
address = session.cmd("s").strip()
|
|
460
|
+
func_name = session.cmd("fd").strip()
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
"status": "success",
|
|
464
|
+
"address": address,
|
|
465
|
+
"function": func_name,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
@mcp.tool()
|
|
469
|
+
async def Radare2_get_function_prototype(
|
|
470
|
+
file_path: str,
|
|
471
|
+
address: str,
|
|
472
|
+
) -> dict[str, Any]:
|
|
473
|
+
"""
|
|
474
|
+
Retrieve the function signature at the specified address.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
file_path: Path to the binary file
|
|
478
|
+
address: Address of the function
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Function prototype/signature
|
|
482
|
+
"""
|
|
483
|
+
# Validate address
|
|
484
|
+
try:
|
|
485
|
+
validate_address_format(address)
|
|
486
|
+
except ValidationError as e:
|
|
487
|
+
return {"status": "error", "message": str(e)}
|
|
488
|
+
|
|
489
|
+
session = await self._get_or_create_session(file_path)
|
|
490
|
+
if not session.is_open:
|
|
491
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
492
|
+
|
|
493
|
+
result = session.cmd(f"afs @ {address}").strip()
|
|
494
|
+
return {"status": "success", "prototype": result}
|
|
495
|
+
|
|
496
|
+
@mcp.tool()
|
|
497
|
+
async def Radare2_set_function_prototype(
|
|
498
|
+
file_path: str,
|
|
499
|
+
address: str,
|
|
500
|
+
prototype: str,
|
|
501
|
+
) -> dict[str, Any]:
|
|
502
|
+
"""
|
|
503
|
+
Set the function signature (return type, name, arguments).
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
file_path: Path to the binary file
|
|
507
|
+
address: Address of the function
|
|
508
|
+
prototype: Function signature in C-like syntax
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Confirmation
|
|
512
|
+
"""
|
|
513
|
+
# Validate address
|
|
514
|
+
try:
|
|
515
|
+
validate_address_format(address)
|
|
516
|
+
except ValidationError as e:
|
|
517
|
+
return {"status": "error", "message": str(e)}
|
|
518
|
+
|
|
519
|
+
# Sanitize prototype (remove dangerous chars)
|
|
520
|
+
safe_prototype = _sanitize_for_r2_cmd(prototype)
|
|
521
|
+
if not safe_prototype:
|
|
522
|
+
return {"status": "error", "message": "Invalid prototype"}
|
|
523
|
+
|
|
524
|
+
session = await self._get_or_create_session(file_path)
|
|
525
|
+
if not session.is_open:
|
|
526
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
527
|
+
|
|
528
|
+
session.cmd(f"afs {safe_prototype} @ {address}")
|
|
529
|
+
return {"status": "success", "message": "Function prototype set"}
|
|
530
|
+
|
|
531
|
+
# =====================================================================
|
|
532
|
+
# Binary Information Tools
|
|
533
|
+
# =====================================================================
|
|
534
|
+
|
|
535
|
+
@mcp.tool()
|
|
536
|
+
async def Radare2_show_headers(
|
|
537
|
+
file_path: str,
|
|
538
|
+
) -> dict[str, Any]:
|
|
539
|
+
"""
|
|
540
|
+
Display binary headers and file information.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
file_path: Path to the binary file
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
Binary header information
|
|
547
|
+
"""
|
|
548
|
+
session = await self._get_or_create_session(file_path)
|
|
549
|
+
if not session.is_open:
|
|
550
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
551
|
+
|
|
552
|
+
info = session.cmd("i")
|
|
553
|
+
headers = session.cmd("iH")
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
"status": "success",
|
|
557
|
+
"info": info,
|
|
558
|
+
"headers": headers,
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
@mcp.tool()
|
|
562
|
+
async def Radare2_list_sections(
|
|
563
|
+
file_path: str,
|
|
564
|
+
) -> dict[str, Any]:
|
|
565
|
+
"""
|
|
566
|
+
Display memory sections and segments from the binary.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
file_path: Path to the binary file
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Sections and segments information
|
|
573
|
+
"""
|
|
574
|
+
session = await self._get_or_create_session(file_path)
|
|
575
|
+
if not session.is_open:
|
|
576
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
577
|
+
|
|
578
|
+
sections = session.cmd("iS")
|
|
579
|
+
segments = session.cmd("iSS")
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
"status": "success",
|
|
583
|
+
"sections": sections,
|
|
584
|
+
"segments": segments,
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
@mcp.tool()
|
|
588
|
+
async def Radare2_list_imports(
|
|
589
|
+
file_path: str,
|
|
590
|
+
filter: str | None = None,
|
|
591
|
+
) -> dict[str, Any]:
|
|
592
|
+
"""
|
|
593
|
+
List imported symbols.
|
|
594
|
+
|
|
595
|
+
Note: Use list_symbols for addresses with sym.imp. prefix.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
file_path: Path to the binary file
|
|
599
|
+
filter: Regular expression to filter results
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
List of imports
|
|
603
|
+
"""
|
|
604
|
+
session = await self._get_or_create_session(file_path)
|
|
605
|
+
if not session.is_open:
|
|
606
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
607
|
+
|
|
608
|
+
result = session.cmd("ii")
|
|
609
|
+
|
|
610
|
+
if filter:
|
|
611
|
+
result = _filter_lines_by_regex(result, filter)
|
|
612
|
+
|
|
613
|
+
return {"status": "success", "imports": result}
|
|
614
|
+
|
|
615
|
+
@mcp.tool()
|
|
616
|
+
async def Radare2_list_symbols(
|
|
617
|
+
file_path: str,
|
|
618
|
+
filter: str | None = None,
|
|
619
|
+
) -> dict[str, Any]:
|
|
620
|
+
"""
|
|
621
|
+
Show all symbols (functions, variables, imports) with addresses.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
file_path: Path to the binary file
|
|
625
|
+
filter: Regular expression to filter results
|
|
626
|
+
|
|
627
|
+
Returns:
|
|
628
|
+
List of symbols
|
|
629
|
+
"""
|
|
630
|
+
session = await self._get_or_create_session(file_path)
|
|
631
|
+
if not session.is_open:
|
|
632
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
633
|
+
|
|
634
|
+
result = session.cmd("is")
|
|
635
|
+
|
|
636
|
+
if filter:
|
|
637
|
+
result = _filter_lines_by_regex(result, filter)
|
|
638
|
+
|
|
639
|
+
return {"status": "success", "symbols": result}
|
|
640
|
+
|
|
641
|
+
@mcp.tool()
|
|
642
|
+
async def Radare2_list_entrypoints(
|
|
643
|
+
file_path: str,
|
|
644
|
+
) -> dict[str, Any]:
|
|
645
|
+
"""
|
|
646
|
+
Display program entrypoints, constructors and main function.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
file_path: Path to the binary file
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
Entrypoint information
|
|
653
|
+
"""
|
|
654
|
+
session = await self._get_or_create_session(file_path)
|
|
655
|
+
if not session.is_open:
|
|
656
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
657
|
+
|
|
658
|
+
result = session.cmd("ie")
|
|
659
|
+
return {"status": "success", "entrypoints": result}
|
|
660
|
+
|
|
661
|
+
@mcp.tool()
|
|
662
|
+
async def Radare2_list_libraries(
|
|
663
|
+
file_path: str,
|
|
664
|
+
) -> dict[str, Any]:
|
|
665
|
+
"""
|
|
666
|
+
List all shared libraries linked to the binary.
|
|
667
|
+
|
|
668
|
+
Args:
|
|
669
|
+
file_path: Path to the binary file
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
List of linked libraries
|
|
673
|
+
"""
|
|
674
|
+
session = await self._get_or_create_session(file_path)
|
|
675
|
+
if not session.is_open:
|
|
676
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
677
|
+
|
|
678
|
+
result = session.cmd("il")
|
|
679
|
+
return {"status": "success", "libraries": result}
|
|
680
|
+
|
|
681
|
+
@mcp.tool()
|
|
682
|
+
async def Radare2_list_strings(
|
|
683
|
+
file_path: str,
|
|
684
|
+
filter: str | None = None,
|
|
685
|
+
cursor: str | None = None,
|
|
686
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
687
|
+
) -> dict[str, Any]:
|
|
688
|
+
"""
|
|
689
|
+
List strings from data sections with optional regex filter.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
file_path: Path to the binary file
|
|
693
|
+
filter: Regular expression to filter results
|
|
694
|
+
cursor: Pagination cursor (line number to start from)
|
|
695
|
+
page_size: Number of lines per page (default: 1000, max: 10000)
|
|
696
|
+
|
|
697
|
+
Returns:
|
|
698
|
+
List of strings with pagination
|
|
699
|
+
"""
|
|
700
|
+
session = await self._get_or_create_session(file_path)
|
|
701
|
+
if not session.is_open:
|
|
702
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
703
|
+
|
|
704
|
+
if page_size > MAX_PAGE_SIZE:
|
|
705
|
+
page_size = MAX_PAGE_SIZE
|
|
706
|
+
|
|
707
|
+
result = session.cmd("iz")
|
|
708
|
+
|
|
709
|
+
if filter:
|
|
710
|
+
result = _filter_lines_by_regex(result, filter)
|
|
711
|
+
|
|
712
|
+
paginated, has_more, next_cursor = _paginate_text(result, cursor, page_size)
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
"status": "success",
|
|
716
|
+
"strings": paginated,
|
|
717
|
+
"has_more": has_more,
|
|
718
|
+
"next_cursor": next_cursor,
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
@mcp.tool()
|
|
722
|
+
async def Radare2_list_all_strings(
|
|
723
|
+
file_path: str,
|
|
724
|
+
filter: str | None = None,
|
|
725
|
+
cursor: str | None = None,
|
|
726
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
727
|
+
) -> dict[str, Any]:
|
|
728
|
+
"""
|
|
729
|
+
Scan the entire binary for strings with optional regex filter.
|
|
730
|
+
|
|
731
|
+
More thorough than list_strings, but slower.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
file_path: Path to the binary file
|
|
735
|
+
filter: Regular expression to filter results
|
|
736
|
+
cursor: Pagination cursor
|
|
737
|
+
page_size: Number of lines per page
|
|
738
|
+
|
|
739
|
+
Returns:
|
|
740
|
+
List of all strings with pagination
|
|
741
|
+
"""
|
|
742
|
+
session = await self._get_or_create_session(file_path)
|
|
743
|
+
if not session.is_open:
|
|
744
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
745
|
+
|
|
746
|
+
if page_size > MAX_PAGE_SIZE:
|
|
747
|
+
page_size = MAX_PAGE_SIZE
|
|
748
|
+
|
|
749
|
+
result = session.cmd("izz")
|
|
750
|
+
|
|
751
|
+
if filter:
|
|
752
|
+
result = _filter_lines_by_regex(result, filter)
|
|
753
|
+
|
|
754
|
+
paginated, has_more, next_cursor = _paginate_text(result, cursor, page_size)
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
"status": "success",
|
|
758
|
+
"strings": paginated,
|
|
759
|
+
"has_more": has_more,
|
|
760
|
+
"next_cursor": next_cursor,
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
# =====================================================================
|
|
764
|
+
# Class/OOP Tools
|
|
765
|
+
# =====================================================================
|
|
766
|
+
|
|
767
|
+
@mcp.tool()
|
|
768
|
+
async def Radare2_list_classes(
|
|
769
|
+
file_path: str,
|
|
770
|
+
filter: str | None = None,
|
|
771
|
+
) -> dict[str, Any]:
|
|
772
|
+
"""
|
|
773
|
+
List class names from various languages (C++, ObjC, Swift, Java, Dalvik).
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
file_path: Path to the binary file
|
|
777
|
+
filter: Regular expression to filter results
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
List of classes
|
|
781
|
+
"""
|
|
782
|
+
session = await self._get_or_create_session(file_path)
|
|
783
|
+
if not session.is_open:
|
|
784
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
785
|
+
|
|
786
|
+
result = session.cmd("ic")
|
|
787
|
+
|
|
788
|
+
if filter:
|
|
789
|
+
result = _filter_lines_by_regex(result, filter)
|
|
790
|
+
|
|
791
|
+
return {"status": "success", "classes": result}
|
|
792
|
+
|
|
793
|
+
@mcp.tool()
|
|
794
|
+
async def Radare2_list_methods(
|
|
795
|
+
file_path: str,
|
|
796
|
+
classname: str,
|
|
797
|
+
) -> dict[str, Any]:
|
|
798
|
+
"""
|
|
799
|
+
List all methods belonging to the specified class.
|
|
800
|
+
|
|
801
|
+
Args:
|
|
802
|
+
file_path: Path to the binary file
|
|
803
|
+
classname: Name of the class to list methods for
|
|
804
|
+
|
|
805
|
+
Returns:
|
|
806
|
+
List of methods in the class
|
|
807
|
+
"""
|
|
808
|
+
# Validate classname to prevent injection
|
|
809
|
+
try:
|
|
810
|
+
_validate_identifier(classname, "classname")
|
|
811
|
+
except ValidationError as e:
|
|
812
|
+
return {"status": "error", "message": str(e)}
|
|
813
|
+
|
|
814
|
+
session = await self._get_or_create_session(file_path)
|
|
815
|
+
if not session.is_open:
|
|
816
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
817
|
+
|
|
818
|
+
result = session.cmd(f"ic {classname}")
|
|
819
|
+
return {"status": "success", "methods": result}
|
|
820
|
+
|
|
821
|
+
# =====================================================================
|
|
822
|
+
# Disassembly & Decompilation Tools
|
|
823
|
+
# =====================================================================
|
|
824
|
+
|
|
825
|
+
@mcp.tool()
|
|
826
|
+
async def Radare2_disassemble(
|
|
827
|
+
file_path: str,
|
|
828
|
+
address: str,
|
|
829
|
+
num_instructions: int = 10,
|
|
830
|
+
) -> dict[str, Any]:
|
|
831
|
+
"""
|
|
832
|
+
Disassemble a specific number of instructions from an address.
|
|
833
|
+
|
|
834
|
+
Use this to inspect a portion of memory as code without depending
|
|
835
|
+
on function analysis boundaries.
|
|
836
|
+
|
|
837
|
+
Args:
|
|
838
|
+
file_path: Path to the binary file
|
|
839
|
+
address: Address to start disassembly
|
|
840
|
+
num_instructions: Number of instructions to disassemble (default: 10, max: 1000)
|
|
841
|
+
|
|
842
|
+
Returns:
|
|
843
|
+
Disassembled instructions
|
|
844
|
+
"""
|
|
845
|
+
# Validate address
|
|
846
|
+
try:
|
|
847
|
+
validate_address_format(address)
|
|
848
|
+
except ValidationError as e:
|
|
849
|
+
return {"status": "error", "message": str(e)}
|
|
850
|
+
|
|
851
|
+
# Limit instructions to prevent abuse
|
|
852
|
+
if not isinstance(num_instructions, int) or num_instructions < 1:
|
|
853
|
+
num_instructions = 10
|
|
854
|
+
if num_instructions > 1000:
|
|
855
|
+
num_instructions = 1000
|
|
856
|
+
|
|
857
|
+
session = await self._get_or_create_session(file_path)
|
|
858
|
+
if not session.is_open:
|
|
859
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
860
|
+
|
|
861
|
+
result = session.cmd(f"pd {num_instructions} @ {address}")
|
|
862
|
+
return {"status": "success", "disassembly": result}
|
|
863
|
+
|
|
864
|
+
@mcp.tool()
|
|
865
|
+
async def Radare2_disassemble_function(
|
|
866
|
+
file_path: str,
|
|
867
|
+
address: str,
|
|
868
|
+
cursor: str | None = None,
|
|
869
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
870
|
+
) -> dict[str, Any]:
|
|
871
|
+
"""
|
|
872
|
+
Show assembly listing of the function at the specified address.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
file_path: Path to the binary file
|
|
876
|
+
address: Address of the function to disassemble
|
|
877
|
+
cursor: Pagination cursor
|
|
878
|
+
page_size: Number of lines per page
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
Function disassembly with pagination
|
|
882
|
+
"""
|
|
883
|
+
# Validate address
|
|
884
|
+
try:
|
|
885
|
+
validate_address_format(address)
|
|
886
|
+
except ValidationError as e:
|
|
887
|
+
return {"status": "error", "message": str(e)}
|
|
888
|
+
|
|
889
|
+
session = await self._get_or_create_session(file_path)
|
|
890
|
+
if not session.is_open:
|
|
891
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
892
|
+
|
|
893
|
+
if page_size > MAX_PAGE_SIZE:
|
|
894
|
+
page_size = MAX_PAGE_SIZE
|
|
895
|
+
|
|
896
|
+
result = session.cmd(f"pdf @ {address}")
|
|
897
|
+
paginated, has_more, next_cursor = _paginate_text(result, cursor, page_size)
|
|
898
|
+
|
|
899
|
+
return {
|
|
900
|
+
"status": "success",
|
|
901
|
+
"disassembly": paginated,
|
|
902
|
+
"has_more": has_more,
|
|
903
|
+
"next_cursor": next_cursor,
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
@mcp.tool()
|
|
907
|
+
async def Radare2_decompile_function(
|
|
908
|
+
file_path: str,
|
|
909
|
+
address: str,
|
|
910
|
+
cursor: str | None = None,
|
|
911
|
+
page_size: int = DEFAULT_PAGE_SIZE,
|
|
912
|
+
) -> dict[str, Any]:
|
|
913
|
+
"""
|
|
914
|
+
Show C-like pseudocode of the function at the given address.
|
|
915
|
+
|
|
916
|
+
Use this to inspect code in a function. Do not run multiple times
|
|
917
|
+
on the same offset.
|
|
918
|
+
|
|
919
|
+
Args:
|
|
920
|
+
file_path: Path to the binary file
|
|
921
|
+
address: Address of the function to decompile
|
|
922
|
+
cursor: Pagination cursor
|
|
923
|
+
page_size: Number of lines per page
|
|
924
|
+
|
|
925
|
+
Returns:
|
|
926
|
+
Decompiled pseudocode with pagination
|
|
927
|
+
"""
|
|
928
|
+
# Validate address
|
|
929
|
+
try:
|
|
930
|
+
validate_address_format(address)
|
|
931
|
+
except ValidationError as e:
|
|
932
|
+
return {"status": "error", "message": str(e)}
|
|
933
|
+
|
|
934
|
+
session = await self._get_or_create_session(file_path)
|
|
935
|
+
if not session.is_open:
|
|
936
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
937
|
+
|
|
938
|
+
if page_size > MAX_PAGE_SIZE:
|
|
939
|
+
page_size = MAX_PAGE_SIZE
|
|
940
|
+
|
|
941
|
+
result = session.cmd(f"pdc @ {address}")
|
|
942
|
+
paginated, has_more, next_cursor = _paginate_text(result, cursor, page_size)
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
"status": "success",
|
|
946
|
+
"decompiled": paginated,
|
|
947
|
+
"has_more": has_more,
|
|
948
|
+
"next_cursor": next_cursor,
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
@mcp.tool()
|
|
952
|
+
async def Radare2_list_decompilers(
|
|
953
|
+
file_path: str,
|
|
954
|
+
) -> dict[str, Any]:
|
|
955
|
+
"""
|
|
956
|
+
Show all available decompiler backends.
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
file_path: Path to the binary file
|
|
960
|
+
|
|
961
|
+
Returns:
|
|
962
|
+
List of available decompilers
|
|
963
|
+
"""
|
|
964
|
+
session = await self._get_or_create_session(file_path)
|
|
965
|
+
if not session.is_open:
|
|
966
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
967
|
+
|
|
968
|
+
result = session.cmd("e cmd.pdc=?")
|
|
969
|
+
return {"status": "success", "decompilers": result}
|
|
970
|
+
|
|
971
|
+
@mcp.tool()
|
|
972
|
+
async def Radare2_use_decompiler(
|
|
973
|
+
file_path: str,
|
|
974
|
+
name: str,
|
|
975
|
+
) -> dict[str, Any]:
|
|
976
|
+
"""
|
|
977
|
+
Select which decompiler backend to use.
|
|
978
|
+
|
|
979
|
+
Args:
|
|
980
|
+
file_path: Path to the binary file
|
|
981
|
+
name: Decompiler name (ghidra, r2dec, pdc)
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
Confirmation or error
|
|
985
|
+
"""
|
|
986
|
+
session = await self._get_or_create_session(file_path)
|
|
987
|
+
if not session.is_open:
|
|
988
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
989
|
+
|
|
990
|
+
available = session.cmd("e cmd.pdc=?")
|
|
991
|
+
|
|
992
|
+
# Whitelist of allowed decompilers
|
|
993
|
+
decompiler_map = {
|
|
994
|
+
"ghidra": "pdg",
|
|
995
|
+
"r2dec": "pdd",
|
|
996
|
+
"pdc": "pdc",
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
name_lower = name.lower()
|
|
1000
|
+
if name_lower not in decompiler_map:
|
|
1001
|
+
return {
|
|
1002
|
+
"status": "error",
|
|
1003
|
+
"message": f"Unknown decompiler: {name}. Allowed: ghidra, r2dec, pdc",
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
cmd_name = decompiler_map[name_lower]
|
|
1007
|
+
if cmd_name not in available:
|
|
1008
|
+
return {"status": "error", "message": f"Decompiler {name} is not available"}
|
|
1009
|
+
|
|
1010
|
+
session.cmd(f"e cmd.pdc={cmd_name}")
|
|
1011
|
+
return {"status": "success", "message": f"Decompiler set to {name}"}
|
|
1012
|
+
|
|
1013
|
+
# =====================================================================
|
|
1014
|
+
# Cross-Reference Tools
|
|
1015
|
+
# =====================================================================
|
|
1016
|
+
|
|
1017
|
+
@mcp.tool()
|
|
1018
|
+
async def Radare2_xrefs_to(
|
|
1019
|
+
file_path: str,
|
|
1020
|
+
address: str,
|
|
1021
|
+
) -> dict[str, Any]:
|
|
1022
|
+
"""
|
|
1023
|
+
Find all code references TO the specified address.
|
|
1024
|
+
|
|
1025
|
+
Args:
|
|
1026
|
+
file_path: Path to the binary file
|
|
1027
|
+
address: Address to check for cross-references
|
|
1028
|
+
|
|
1029
|
+
Returns:
|
|
1030
|
+
List of xrefs to the address
|
|
1031
|
+
"""
|
|
1032
|
+
# Validate address
|
|
1033
|
+
try:
|
|
1034
|
+
validate_address_format(address)
|
|
1035
|
+
except ValidationError as e:
|
|
1036
|
+
return {"status": "error", "message": str(e)}
|
|
1037
|
+
|
|
1038
|
+
session = await self._get_or_create_session(file_path)
|
|
1039
|
+
if not session.is_open:
|
|
1040
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
1041
|
+
|
|
1042
|
+
result = session.cmd(f"axt @ {address}")
|
|
1043
|
+
return {"status": "success", "xrefs": result}
|
|
1044
|
+
|
|
1045
|
+
# =====================================================================
|
|
1046
|
+
# Modification Tools
|
|
1047
|
+
# =====================================================================
|
|
1048
|
+
|
|
1049
|
+
@mcp.tool()
|
|
1050
|
+
async def Radare2_rename_function(
|
|
1051
|
+
file_path: str,
|
|
1052
|
+
address: str,
|
|
1053
|
+
name: str,
|
|
1054
|
+
) -> dict[str, Any]:
|
|
1055
|
+
"""
|
|
1056
|
+
Rename the function at the specified address.
|
|
1057
|
+
|
|
1058
|
+
Args:
|
|
1059
|
+
file_path: Path to the binary file
|
|
1060
|
+
address: Address of the function to rename
|
|
1061
|
+
name: New function name
|
|
1062
|
+
|
|
1063
|
+
Returns:
|
|
1064
|
+
Confirmation
|
|
1065
|
+
"""
|
|
1066
|
+
# Validate inputs
|
|
1067
|
+
try:
|
|
1068
|
+
validate_address_format(address)
|
|
1069
|
+
_validate_identifier(name, "name")
|
|
1070
|
+
except ValidationError as e:
|
|
1071
|
+
return {"status": "error", "message": str(e)}
|
|
1072
|
+
|
|
1073
|
+
session = await self._get_or_create_session(file_path)
|
|
1074
|
+
if not session.is_open:
|
|
1075
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
1076
|
+
|
|
1077
|
+
session.cmd(f"afn {name} @ {address}")
|
|
1078
|
+
return {"status": "success", "message": f"Function renamed to {name}"}
|
|
1079
|
+
|
|
1080
|
+
@mcp.tool()
|
|
1081
|
+
async def Radare2_rename_flag(
|
|
1082
|
+
file_path: str,
|
|
1083
|
+
address: str,
|
|
1084
|
+
name: str,
|
|
1085
|
+
new_name: str,
|
|
1086
|
+
) -> dict[str, Any]:
|
|
1087
|
+
"""
|
|
1088
|
+
Rename a flag (variable or data reference) at the specified address.
|
|
1089
|
+
|
|
1090
|
+
Args:
|
|
1091
|
+
file_path: Path to the binary file
|
|
1092
|
+
address: Address of the flag
|
|
1093
|
+
name: Current flag name
|
|
1094
|
+
new_name: New flag name
|
|
1095
|
+
|
|
1096
|
+
Returns:
|
|
1097
|
+
Confirmation
|
|
1098
|
+
"""
|
|
1099
|
+
# Validate all inputs
|
|
1100
|
+
try:
|
|
1101
|
+
validate_address_format(address)
|
|
1102
|
+
_validate_identifier(name, "name")
|
|
1103
|
+
_validate_identifier(new_name, "new_name")
|
|
1104
|
+
except ValidationError as e:
|
|
1105
|
+
return {"status": "error", "message": str(e)}
|
|
1106
|
+
|
|
1107
|
+
session = await self._get_or_create_session(file_path)
|
|
1108
|
+
if not session.is_open:
|
|
1109
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
1110
|
+
|
|
1111
|
+
result = session.cmd(f"fr {name} {new_name} @ {address}")
|
|
1112
|
+
if result.strip():
|
|
1113
|
+
return {"status": "error", "message": result}
|
|
1114
|
+
return {"status": "success", "message": f"Flag renamed to {new_name}"}
|
|
1115
|
+
|
|
1116
|
+
@mcp.tool()
|
|
1117
|
+
async def Radare2_set_comment(
|
|
1118
|
+
file_path: str,
|
|
1119
|
+
address: str,
|
|
1120
|
+
message: str,
|
|
1121
|
+
) -> dict[str, Any]:
|
|
1122
|
+
"""
|
|
1123
|
+
Add a comment at the specified address.
|
|
1124
|
+
|
|
1125
|
+
Args:
|
|
1126
|
+
file_path: Path to the binary file
|
|
1127
|
+
address: Address to add comment
|
|
1128
|
+
message: Comment text
|
|
1129
|
+
|
|
1130
|
+
Returns:
|
|
1131
|
+
Confirmation
|
|
1132
|
+
"""
|
|
1133
|
+
# Validate address
|
|
1134
|
+
try:
|
|
1135
|
+
validate_address_format(address)
|
|
1136
|
+
except ValidationError as e:
|
|
1137
|
+
return {"status": "error", "message": str(e)}
|
|
1138
|
+
|
|
1139
|
+
# Sanitize message (remove dangerous chars but allow more characters for comments)
|
|
1140
|
+
safe_message = _sanitize_for_r2_cmd(message)
|
|
1141
|
+
if not safe_message:
|
|
1142
|
+
return {"status": "error", "message": "Comment message is empty or invalid"}
|
|
1143
|
+
|
|
1144
|
+
session = await self._get_or_create_session(file_path)
|
|
1145
|
+
if not session.is_open:
|
|
1146
|
+
return {"status": "error", "message": "Failed to open file"}
|
|
1147
|
+
|
|
1148
|
+
session.cmd(f"CC {safe_message} @ {address}")
|
|
1149
|
+
return {"status": "success", "message": "Comment added"}
|
|
1150
|
+
|
|
1151
|
+
# NOTE: Radare2_list_files and Radare2_run_javascript are REMOVED
|
|
1152
|
+
# for security reasons:
|
|
1153
|
+
# - list_files: potential path traversal attack vector
|
|
1154
|
+
# - run_javascript: arbitrary code execution risk
|
|
1155
|
+
|
|
1156
|
+
# =====================================================================
|
|
1157
|
+
# Advanced Analysis Tools (from r2_analysis module)
|
|
1158
|
+
# =====================================================================
|
|
1159
|
+
# Import and register advanced analysis tools for unified plugin management
|
|
1160
|
+
from reversecore_mcp.tools.radare2.r2_analysis import (
|
|
1161
|
+
analyze_xrefs,
|
|
1162
|
+
generate_function_graph,
|
|
1163
|
+
run_radare2,
|
|
1164
|
+
trace_execution_path,
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
mcp.tool(run_radare2)
|
|
1168
|
+
mcp.tool(trace_execution_path)
|
|
1169
|
+
mcp.tool(generate_function_graph)
|
|
1170
|
+
mcp.tool(analyze_xrefs)
|
|
1171
|
+
|
|
1172
|
+
logger.info(f"Registered {self.name} plugin with 34 Radare2 tools (security hardened)")
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
def register_radare2_tools(mcp: FastMCP) -> None:
|
|
1176
|
+
"""
|
|
1177
|
+
Register Radare2 tools with an MCP server instance.
|
|
1178
|
+
|
|
1179
|
+
Args:
|
|
1180
|
+
mcp: FastMCP server instance
|
|
1181
|
+
"""
|
|
1182
|
+
plugin = Radare2ToolsPlugin()
|
|
1183
|
+
plugin.register(mcp)
|