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,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Radare2 Session Management and Security Validators.
|
|
3
|
+
|
|
4
|
+
This module provides session management and security validation utilities
|
|
5
|
+
for radare2 analysis tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from functools import lru_cache
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
# Lazy import for r2pipe to allow tests to run without it
|
|
18
|
+
try:
|
|
19
|
+
import r2pipe
|
|
20
|
+
|
|
21
|
+
R2PIPE_AVAILABLE = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
r2pipe = None # type: ignore
|
|
24
|
+
R2PIPE_AVAILABLE = False
|
|
25
|
+
|
|
26
|
+
from reversecore_mcp.core.config import get_config
|
|
27
|
+
from reversecore_mcp.core.exceptions import ValidationError
|
|
28
|
+
from reversecore_mcp.core.logging_config import get_logger
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
# Default configuration
|
|
33
|
+
DEFAULT_TIMEOUT = get_config().default_tool_timeout
|
|
34
|
+
DEFAULT_PAGE_SIZE = 1000
|
|
35
|
+
MAX_PAGE_SIZE = 10000
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# Security Validators
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
# Pattern for safe identifiers (function names, class names, etc.)
|
|
42
|
+
_SAFE_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_.]*$")
|
|
43
|
+
|
|
44
|
+
# Pattern for safe math expressions (for calculate tool)
|
|
45
|
+
# Allows: hex (0x..), decimal, operators, symbols (sym.xxx), parentheses
|
|
46
|
+
_SAFE_EXPRESSION_PATTERN = re.compile(r"^[a-zA-Z0-9_.\s+\-*/%()[\]]+$")
|
|
47
|
+
|
|
48
|
+
# Dangerous r2 commands that should be blocked
|
|
49
|
+
_BLOCKED_R2_COMMANDS = frozenset(
|
|
50
|
+
{
|
|
51
|
+
"!", # Shell escape
|
|
52
|
+
"#!", # Alternative shell
|
|
53
|
+
"=!", # Remote shell
|
|
54
|
+
"=h", # HTTP server
|
|
55
|
+
"=H", # HTTP server (alt)
|
|
56
|
+
"o+", # Open for write
|
|
57
|
+
"w", # Write
|
|
58
|
+
"wa", # Write assembly
|
|
59
|
+
"wb", # Write bytes
|
|
60
|
+
"wc", # Write comment (file modification)
|
|
61
|
+
"wf", # Write file
|
|
62
|
+
"wo", # Write operations
|
|
63
|
+
"wx", # Write hex
|
|
64
|
+
"wv", # Write value
|
|
65
|
+
"wd", # Write dword
|
|
66
|
+
"Ps", # Project save (can overwrite)
|
|
67
|
+
"rm", # Remove (radare2 built-in)
|
|
68
|
+
"r2pm", # Package manager
|
|
69
|
+
"L", # Load plugin (potential code exec)
|
|
70
|
+
".", # Interpret script
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _validate_identifier(value: str, param_name: str) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Validate that a value is a safe identifier (no injection).
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
value: The identifier to validate
|
|
81
|
+
param_name: Name of parameter for error messages
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValidationError: If identifier is invalid
|
|
85
|
+
"""
|
|
86
|
+
if not value:
|
|
87
|
+
raise ValidationError(f"{param_name} cannot be empty")
|
|
88
|
+
|
|
89
|
+
if not _SAFE_IDENTIFIER_PATTERN.match(value):
|
|
90
|
+
raise ValidationError(
|
|
91
|
+
f"{param_name} must contain only alphanumeric characters, "
|
|
92
|
+
"underscores, and dots (starting with letter or underscore)"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _validate_expression(expression: str) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Validate math expression for calculate tool.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
expression: Math expression to validate
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValidationError: If expression contains dangerous characters
|
|
105
|
+
"""
|
|
106
|
+
if not expression:
|
|
107
|
+
raise ValidationError("expression cannot be empty")
|
|
108
|
+
|
|
109
|
+
if not _SAFE_EXPRESSION_PATTERN.match(expression):
|
|
110
|
+
raise ValidationError(
|
|
111
|
+
"expression contains invalid characters. "
|
|
112
|
+
"Only alphanumeric, operators (+,-,*,/,%), parentheses, and symbols allowed."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Additional check for shell escape attempts
|
|
116
|
+
if any(c in expression for c in ["`", "$", ";", "|", "&", ">", "<", "~"]):
|
|
117
|
+
raise ValidationError("expression contains forbidden shell characters")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _validate_r2_command(command: str) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Validate radare2 command for safety.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
command: r2 command to validate
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValidationError: If command is blocked or dangerous
|
|
129
|
+
"""
|
|
130
|
+
if not command:
|
|
131
|
+
raise ValidationError("command cannot be empty")
|
|
132
|
+
|
|
133
|
+
# Check for shell escape
|
|
134
|
+
cmd_start = command.strip().split()[0] if command.strip() else ""
|
|
135
|
+
|
|
136
|
+
# Block dangerous commands
|
|
137
|
+
for blocked in _BLOCKED_R2_COMMANDS:
|
|
138
|
+
if cmd_start.startswith(blocked):
|
|
139
|
+
raise ValidationError(
|
|
140
|
+
f"Command '{blocked}' is blocked for security reasons. "
|
|
141
|
+
"Only analysis commands are allowed."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Block shell metacharacters in command
|
|
145
|
+
# SECURITY: `;` is critical - it allows command chaining in radare2
|
|
146
|
+
# Example attack: "px 10; !rm -rf /" would execute both commands
|
|
147
|
+
if any(c in command for c in ["`", "$", ";", "|", "&", ">", "<", "~", "\n", "\r"]):
|
|
148
|
+
raise ValidationError("Command contains forbidden shell metacharacters")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _sanitize_for_r2_cmd(value: str) -> str:
|
|
152
|
+
"""
|
|
153
|
+
Sanitize a value for safe use in r2 commands.
|
|
154
|
+
|
|
155
|
+
Removes/escapes dangerous characters while preserving functionality.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
value: Value to sanitize
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Sanitized value safe for r2 commands
|
|
162
|
+
"""
|
|
163
|
+
if not value:
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
# Remove shell metacharacters
|
|
167
|
+
dangerous_chars = "`$;|&><\n\r\t\\"
|
|
168
|
+
sanitized = value
|
|
169
|
+
for char in dangerous_chars:
|
|
170
|
+
sanitized = sanitized.replace(char, "")
|
|
171
|
+
|
|
172
|
+
# Remove quotes that could break command parsing
|
|
173
|
+
sanitized = sanitized.replace('"', "").replace("'", "")
|
|
174
|
+
|
|
175
|
+
return sanitized
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class R2Session:
|
|
179
|
+
"""
|
|
180
|
+
Manages a radare2 session with enhanced state tracking and diagnostics.
|
|
181
|
+
|
|
182
|
+
Includes asyncio.Lock for thread-safe command execution in async contexts.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(self, file_path: str | None = None):
|
|
186
|
+
self.session_id = str(uuid.uuid4())
|
|
187
|
+
self.file_path = file_path
|
|
188
|
+
self._r2: Any = None # r2pipe.open_sync when available
|
|
189
|
+
self._analyzed = False
|
|
190
|
+
self.created_at = datetime.now()
|
|
191
|
+
self.status = "initialized" # initialized, active, error, closed
|
|
192
|
+
self.last_error = None
|
|
193
|
+
self.retry_count = 0
|
|
194
|
+
# Async lock for safe concurrent command execution
|
|
195
|
+
import asyncio
|
|
196
|
+
|
|
197
|
+
self._command_lock = asyncio.Lock()
|
|
198
|
+
|
|
199
|
+
def open(self, file_path: str) -> bool:
|
|
200
|
+
"""Open a binary file with radare2."""
|
|
201
|
+
if not R2PIPE_AVAILABLE:
|
|
202
|
+
self.status = "error"
|
|
203
|
+
self.last_error = "r2pipe module not installed"
|
|
204
|
+
logger.error("r2pipe module not available - install with: pip install r2pipe")
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
self.close()
|
|
209
|
+
# Verify file exists strictly before passing to r2
|
|
210
|
+
if not os.path.exists(file_path):
|
|
211
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
212
|
+
|
|
213
|
+
self._r2 = r2pipe.open(file_path)
|
|
214
|
+
if not self._r2:
|
|
215
|
+
raise RuntimeError("r2pipe.open returned None")
|
|
216
|
+
|
|
217
|
+
self.file_path = file_path
|
|
218
|
+
self.status = "active"
|
|
219
|
+
return True
|
|
220
|
+
except Exception as e:
|
|
221
|
+
self.status = "error"
|
|
222
|
+
self.last_error = str(e)
|
|
223
|
+
logger.error(f"Failed to open file {file_path}: {e}")
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
def close(self) -> None:
|
|
227
|
+
"""Close the current radare2 session."""
|
|
228
|
+
if self._r2:
|
|
229
|
+
try:
|
|
230
|
+
self._r2.quit()
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
self._r2 = None
|
|
234
|
+
self.status = "closed"
|
|
235
|
+
self._analyzed = False
|
|
236
|
+
|
|
237
|
+
def cmd(self, command: str) -> str:
|
|
238
|
+
"""Execute a radare2 command and return the output."""
|
|
239
|
+
if not self._r2:
|
|
240
|
+
return ""
|
|
241
|
+
try:
|
|
242
|
+
result = self._r2.cmd(command)
|
|
243
|
+
return result if result else ""
|
|
244
|
+
except Exception as e:
|
|
245
|
+
self.last_error = str(e)
|
|
246
|
+
logger.error(f"R2 command failed: {e}")
|
|
247
|
+
return f"Error: {e}"
|
|
248
|
+
|
|
249
|
+
def cmdj(self, command: str) -> Any:
|
|
250
|
+
"""Execute a radare2 command and return JSON output."""
|
|
251
|
+
if not self._r2:
|
|
252
|
+
return None
|
|
253
|
+
try:
|
|
254
|
+
return self._r2.cmdj(command)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
self.last_error = str(e)
|
|
257
|
+
logger.error(f"R2 JSON command failed: {e}")
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
async def safe_cmd(self, command: str) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Execute a radare2 command with session-level locking.
|
|
263
|
+
|
|
264
|
+
This method is safe for concurrent async access and prevents
|
|
265
|
+
command interleaving when multiple coroutines use the same session.
|
|
266
|
+
"""
|
|
267
|
+
import asyncio
|
|
268
|
+
|
|
269
|
+
async with self._command_lock:
|
|
270
|
+
return await asyncio.to_thread(self.cmd, command)
|
|
271
|
+
|
|
272
|
+
async def safe_cmdj(self, command: str) -> Any:
|
|
273
|
+
"""
|
|
274
|
+
Execute a radare2 JSON command with session-level locking.
|
|
275
|
+
|
|
276
|
+
This method is safe for concurrent async access.
|
|
277
|
+
"""
|
|
278
|
+
import asyncio
|
|
279
|
+
|
|
280
|
+
async with self._command_lock:
|
|
281
|
+
return await asyncio.to_thread(self.cmdj, command)
|
|
282
|
+
|
|
283
|
+
def analyze(self, level: int = 2) -> str:
|
|
284
|
+
"""Run analysis with specified depth level."""
|
|
285
|
+
if self._analyzed and level <= 2:
|
|
286
|
+
return "Already analyzed"
|
|
287
|
+
|
|
288
|
+
analysis_cmds = {
|
|
289
|
+
0: "aa", # Basic analysis
|
|
290
|
+
1: "aaa", # Auto-analysis
|
|
291
|
+
2: "aaaa", # Experimental analysis
|
|
292
|
+
3: "aaaaa", # Deep analysis
|
|
293
|
+
4: "aaaaaa", # Very deep analysis
|
|
294
|
+
}
|
|
295
|
+
cmd = analysis_cmds.get(level, "aaa")
|
|
296
|
+
result = self.cmd(cmd)
|
|
297
|
+
self._analyzed = True
|
|
298
|
+
return result
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def is_open(self) -> bool:
|
|
302
|
+
return self._r2 is not None and self.status == "active"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# =============================================================================
|
|
306
|
+
# Utility Functions
|
|
307
|
+
# =============================================================================
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@lru_cache(maxsize=64)
|
|
311
|
+
def _compile_regex_cached(pattern: str) -> re.Pattern | None:
|
|
312
|
+
"""Compile and cache regex pattern."""
|
|
313
|
+
try:
|
|
314
|
+
return re.compile(pattern)
|
|
315
|
+
except re.error:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _filter_lines_by_regex(text: str, pattern: str) -> str:
|
|
320
|
+
"""Filter lines matching a regex pattern."""
|
|
321
|
+
if not pattern or not text:
|
|
322
|
+
return text
|
|
323
|
+
|
|
324
|
+
# Limit pattern length to prevent ReDoS
|
|
325
|
+
if len(pattern) > 500:
|
|
326
|
+
return "Error: Regex pattern too long (max 500 chars)"
|
|
327
|
+
|
|
328
|
+
regex = _compile_regex_cached(pattern)
|
|
329
|
+
if regex is None:
|
|
330
|
+
return f"Invalid regex pattern: {pattern}"
|
|
331
|
+
|
|
332
|
+
lines = text.split("\n")
|
|
333
|
+
filtered = [line for line in lines if regex.search(line)]
|
|
334
|
+
return "\n".join(filtered)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _filter_named_functions(text: str) -> str:
|
|
338
|
+
"""Filter out functions with numeric suffixes (e.g., sym.func.1000016c8)."""
|
|
339
|
+
if not text:
|
|
340
|
+
return text
|
|
341
|
+
lines = text.split("\n")
|
|
342
|
+
filtered = []
|
|
343
|
+
for line in lines:
|
|
344
|
+
# Check if last part after dot is a number (hex)
|
|
345
|
+
parts = line.split(".")
|
|
346
|
+
if parts:
|
|
347
|
+
last_part = parts[-1].split()[0] if parts[-1] else ""
|
|
348
|
+
# Skip if last part looks like a hex address
|
|
349
|
+
if last_part and last_part[0].isdigit():
|
|
350
|
+
continue
|
|
351
|
+
filtered.append(line)
|
|
352
|
+
return "\n".join(filtered)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _paginate_text(text: str, cursor: str | None, page_size: int) -> tuple[str, bool, str | None]:
|
|
356
|
+
"""
|
|
357
|
+
Paginate text by lines.
|
|
358
|
+
|
|
359
|
+
Returns: (paginated_text, has_more, next_cursor)
|
|
360
|
+
"""
|
|
361
|
+
if not text:
|
|
362
|
+
return "", False, None
|
|
363
|
+
|
|
364
|
+
lines = text.split("\n")
|
|
365
|
+
start_index = int(cursor) if cursor and cursor.isdigit() else 0
|
|
366
|
+
|
|
367
|
+
if start_index < 0:
|
|
368
|
+
start_index = 0
|
|
369
|
+
|
|
370
|
+
end_index = start_index + page_size
|
|
371
|
+
paginated_lines = lines[start_index:end_index]
|
|
372
|
+
|
|
373
|
+
has_more = end_index < len(lines)
|
|
374
|
+
next_cursor = str(end_index) if has_more else None
|
|
375
|
+
|
|
376
|
+
return "\n".join(paginated_lines), has_more, next_cursor
|