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,479 @@
|
|
|
1
|
+
"""Static analysis tools for extracting strings, scanning for versions, and detecting embedded content."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
from reversecore_mcp.core.config import get_config
|
|
8
|
+
from reversecore_mcp.core.decorators import log_execution
|
|
9
|
+
from reversecore_mcp.core.error_handling import handle_tool_errors
|
|
10
|
+
from reversecore_mcp.core.execution import execute_subprocess_async
|
|
11
|
+
from reversecore_mcp.core.metrics import track_metrics
|
|
12
|
+
from reversecore_mcp.core.result import ToolResult, success
|
|
13
|
+
from reversecore_mcp.core.security import validate_file_path
|
|
14
|
+
from reversecore_mcp.core.validators import validate_tool_parameters
|
|
15
|
+
|
|
16
|
+
# Load default timeout from configuration
|
|
17
|
+
DEFAULT_TIMEOUT = get_config().default_tool_timeout
|
|
18
|
+
|
|
19
|
+
# Output size limits
|
|
20
|
+
MIN_OUTPUT_SIZE = 1024 * 1024 # 1MB - minimum output size for meaningful analysis
|
|
21
|
+
LLM_SAFE_LIMIT = 50 * 1024 # 50KB - roughly 12-15k tokens, safe for most LLMs
|
|
22
|
+
MAX_EXTRACTED_FILES = 200 # Maximum files to report in extraction results
|
|
23
|
+
MAX_SIGNATURES = 50 # Maximum signatures to report
|
|
24
|
+
|
|
25
|
+
# Pre-compile regex patterns for performance optimization
|
|
26
|
+
_VERSION_PATTERNS = {
|
|
27
|
+
"OpenSSL": re.compile(r"(OpenSSL|openssl)\s+(\d+\.\d+\.\d+[a-z]?)", re.IGNORECASE),
|
|
28
|
+
"GCC": re.compile(r"GCC:\s+\(.*\)\s+(\d+\.\d+\.\d+)"),
|
|
29
|
+
"Python": re.compile(r"(Python|python)\s+([23]\.\d+\.\d+)", re.IGNORECASE),
|
|
30
|
+
"Curl": re.compile(r"curl\s+(\d+\.\d+\.\d+)", re.IGNORECASE),
|
|
31
|
+
"BusyBox": re.compile(r"BusyBox\s+v(\d+\.\d+\.\d+)", re.IGNORECASE),
|
|
32
|
+
"Generic_Version": re.compile(r"[vV]er(?:sion)?\s?[:.]?\s?(\d+\.\d+\.\d+)"),
|
|
33
|
+
"Copyright": re.compile(r"Copyright.*(19|20)\d{2}"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Pre-compile RTTI detection patterns for performance optimization
|
|
37
|
+
# These patterns are used in extract_rtti_info to identify C++ type information
|
|
38
|
+
_RTTI_MAIN_PATTERN = re.compile(r"(_ZTS|_ZTI|_ZTV|\.?\?A[VUW]|class\s+\w+|struct\s+\w+)")
|
|
39
|
+
|
|
40
|
+
# Patterns for extracting class names from various RTTI formats
|
|
41
|
+
_RTTI_CLASS_PATTERNS = (
|
|
42
|
+
re.compile(r"(?:class|struct)\s+(\w+(?:::\w+)*)"), # class Foo, struct Bar::Baz
|
|
43
|
+
re.compile(r"\.?\?AV(\w+)@@"), # MSVC class: .?AVClassName@@
|
|
44
|
+
re.compile(r"\.?\?AU(\w+)@@"), # MSVC struct: .?AUStructName@@
|
|
45
|
+
re.compile(r"_ZTS(\d+)(\w+)"), # GCC typeinfo: _ZTS4Foo -> Foo (length prefixed)
|
|
46
|
+
re.compile(
|
|
47
|
+
r"(\w{2,}(?:Actor|Component|Manager|Controller|Handler|Service|Factory|Provider|Interface))"
|
|
48
|
+
), # Common OOP patterns
|
|
49
|
+
re.compile(r"(C[a-z][A-Z]\w{3,})"), # Hungarian notation: CzCharacter, CxMonster
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@log_execution(tool_name="run_strings")
|
|
54
|
+
@track_metrics("run_strings")
|
|
55
|
+
@handle_tool_errors
|
|
56
|
+
async def run_strings(
|
|
57
|
+
file_path: str,
|
|
58
|
+
min_length: int = 10, # Increased default from 4 to 10 to reduce noise and memory usage
|
|
59
|
+
max_output_size: int = 2_000_000, # Reduced default to 2MB for safety
|
|
60
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
61
|
+
) -> ToolResult:
|
|
62
|
+
"""Extract printable strings using the ``strings`` CLI."""
|
|
63
|
+
|
|
64
|
+
validate_tool_parameters(
|
|
65
|
+
"run_strings",
|
|
66
|
+
{"min_length": min_length, "max_output_size": max_output_size},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Enforce strict output limits
|
|
70
|
+
if max_output_size > 10_000_000:
|
|
71
|
+
max_output_size = 10_000_000 # Cap at 10MB hard limit
|
|
72
|
+
|
|
73
|
+
# Enforce a reasonable minimum output size to prevent accidental truncation
|
|
74
|
+
if max_output_size < MIN_OUTPUT_SIZE:
|
|
75
|
+
max_output_size = MIN_OUTPUT_SIZE
|
|
76
|
+
|
|
77
|
+
validated_path = validate_file_path(file_path)
|
|
78
|
+
|
|
79
|
+
# Use -n option to filter short strings at source
|
|
80
|
+
cmd = ["strings", "-n", str(min_length), str(validated_path)]
|
|
81
|
+
|
|
82
|
+
# Use execute_subprocess_async which now has robust streaming and memory limits
|
|
83
|
+
output, bytes_read = await execute_subprocess_async(
|
|
84
|
+
cmd,
|
|
85
|
+
max_output_size=max_output_size,
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Truncate output logic enhanced with file saving
|
|
90
|
+
truncated = False
|
|
91
|
+
output_files = {}
|
|
92
|
+
|
|
93
|
+
# Calculate statistics
|
|
94
|
+
text_output = output
|
|
95
|
+
lines = text_output.splitlines()
|
|
96
|
+
count = len(lines)
|
|
97
|
+
|
|
98
|
+
if len(output) > LLM_SAFE_LIMIT:
|
|
99
|
+
truncated = True
|
|
100
|
+
# Save full output to temp file (NOT source directory)
|
|
101
|
+
# This avoids: read-only mount failures, race conditions, leftover files
|
|
102
|
+
import tempfile
|
|
103
|
+
strings_filename = f"{validated_path.name}_strings.txt"
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
# Use system temp directory for output files
|
|
107
|
+
with tempfile.NamedTemporaryFile(
|
|
108
|
+
mode="w",
|
|
109
|
+
suffix="_strings.txt",
|
|
110
|
+
prefix=f"{validated_path.stem}_",
|
|
111
|
+
delete=False, # Keep file so user can access it
|
|
112
|
+
encoding="utf-8",
|
|
113
|
+
) as f:
|
|
114
|
+
f.write(text_output)
|
|
115
|
+
strings_path = f.name
|
|
116
|
+
|
|
117
|
+
output_files["full_output"] = strings_path
|
|
118
|
+
|
|
119
|
+
# Create preview
|
|
120
|
+
preview_limit = min(2000, len(text_output)) # First 2000 chars
|
|
121
|
+
preview_text = text_output[:preview_limit] + f"\n... (truncated, full content in {strings_path})"
|
|
122
|
+
|
|
123
|
+
return success(
|
|
124
|
+
preview_text,
|
|
125
|
+
bytes_read=bytes_read,
|
|
126
|
+
truncated=True,
|
|
127
|
+
string_statistics={
|
|
128
|
+
"count": count,
|
|
129
|
+
"preview": lines[:50], # First 50 lines list
|
|
130
|
+
"file_path": strings_path,
|
|
131
|
+
"full_size": len(text_output)
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
# Fallback if file write fails
|
|
136
|
+
truncated_output = output[:LLM_SAFE_LIMIT]
|
|
137
|
+
return success(
|
|
138
|
+
truncated_output + f"\n[Error saving file: {e}]",
|
|
139
|
+
bytes_read=bytes_read,
|
|
140
|
+
truncated=True
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return success(
|
|
144
|
+
output,
|
|
145
|
+
bytes_read=bytes_read,
|
|
146
|
+
string_statistics={
|
|
147
|
+
"count": count,
|
|
148
|
+
"preview": lines[:50],
|
|
149
|
+
"full_size": len(text_output)
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@log_execution(tool_name="run_binwalk")
|
|
155
|
+
@track_metrics("run_binwalk")
|
|
156
|
+
@handle_tool_errors
|
|
157
|
+
async def run_binwalk(
|
|
158
|
+
file_path: str,
|
|
159
|
+
depth: int = 8,
|
|
160
|
+
max_output_size: int = 10_000_000,
|
|
161
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
162
|
+
) -> ToolResult:
|
|
163
|
+
"""Analyze binaries for embedded content using binwalk."""
|
|
164
|
+
|
|
165
|
+
validated_path = validate_file_path(file_path)
|
|
166
|
+
cmd = ["binwalk", "-A", "-d", str(depth), str(validated_path)]
|
|
167
|
+
output, bytes_read = await execute_subprocess_async(
|
|
168
|
+
cmd,
|
|
169
|
+
max_output_size=max_output_size,
|
|
170
|
+
timeout=timeout,
|
|
171
|
+
)
|
|
172
|
+
return success(output, bytes_read=bytes_read)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@log_execution(tool_name="run_binwalk_extract")
|
|
176
|
+
@track_metrics("run_binwalk_extract")
|
|
177
|
+
@handle_tool_errors
|
|
178
|
+
async def run_binwalk_extract(
|
|
179
|
+
file_path: str,
|
|
180
|
+
output_dir: str = None,
|
|
181
|
+
matryoshka: bool = True,
|
|
182
|
+
depth: int = 8,
|
|
183
|
+
max_output_size: int = 50_000_000,
|
|
184
|
+
timeout: int = 600,
|
|
185
|
+
) -> ToolResult:
|
|
186
|
+
"""
|
|
187
|
+
Extract embedded files and file systems from a binary using binwalk.
|
|
188
|
+
|
|
189
|
+
This tool performs deep extraction of embedded content, including:
|
|
190
|
+
- Compressed archives (gzip, bzip2, lzma, xz)
|
|
191
|
+
- File systems (squashfs, cramfs, jffs2, ubifs)
|
|
192
|
+
- Firmware images and bootloaders
|
|
193
|
+
- Nested/matryoshka content (files within files)
|
|
194
|
+
|
|
195
|
+
**Use Cases:**
|
|
196
|
+
- **Firmware Analysis**: Extract file systems from router/IoT firmware
|
|
197
|
+
- **Malware Unpacking**: Extract payloads from packed/embedded malware
|
|
198
|
+
- **Forensics**: Recover embedded files from disk images
|
|
199
|
+
- **CTF Challenges**: Extract hidden data from challenge files
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
file_path: Path to the binary file to extract
|
|
203
|
+
output_dir: Directory to extract files to (default: creates temp dir)
|
|
204
|
+
matryoshka: Enable recursive extraction (files within files)
|
|
205
|
+
depth: Maximum extraction depth for nested content (default: 8)
|
|
206
|
+
max_output_size: Maximum output size in bytes
|
|
207
|
+
timeout: Extraction timeout in seconds (default: 600 for large files)
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
ToolResult with extraction summary including:
|
|
211
|
+
- extracted_files: List of extracted files with paths and types
|
|
212
|
+
- output_directory: Path to extraction output
|
|
213
|
+
- total_size: Total size of extracted content
|
|
214
|
+
- extraction_depth: Maximum depth reached during extraction
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
>>> result = await run_binwalk_extract("/path/to/firmware.bin")
|
|
218
|
+
>>> print(result.data["extracted_files"])
|
|
219
|
+
[{"path": "squashfs-root/etc/passwd", "type": "ASCII text", "size": 1234}, ...]
|
|
220
|
+
"""
|
|
221
|
+
from pathlib import Path
|
|
222
|
+
|
|
223
|
+
validated_path = validate_file_path(file_path)
|
|
224
|
+
|
|
225
|
+
# Create output directory if not specified
|
|
226
|
+
if output_dir is None:
|
|
227
|
+
# Create temp directory for extraction
|
|
228
|
+
temp_dir = tempfile.mkdtemp(prefix="binwalk_extract_")
|
|
229
|
+
extraction_dir = temp_dir
|
|
230
|
+
else:
|
|
231
|
+
# Resolve output directory path (may not exist yet)
|
|
232
|
+
from pathlib import Path
|
|
233
|
+
|
|
234
|
+
output_path = Path(output_dir).resolve()
|
|
235
|
+
extraction_dir = str(output_path)
|
|
236
|
+
os.makedirs(extraction_dir, exist_ok=True)
|
|
237
|
+
|
|
238
|
+
# Build binwalk extraction command
|
|
239
|
+
cmd = ["binwalk", "-e"] # -e for extraction
|
|
240
|
+
|
|
241
|
+
if matryoshka:
|
|
242
|
+
cmd.append("-M") # Matryoshka/recursive extraction
|
|
243
|
+
|
|
244
|
+
cmd.extend(["-d", str(depth)]) # Extraction depth
|
|
245
|
+
cmd.extend(["-C", str(extraction_dir)]) # Output directory
|
|
246
|
+
cmd.append(str(validated_path))
|
|
247
|
+
|
|
248
|
+
# Run extraction
|
|
249
|
+
output, bytes_read = await execute_subprocess_async(
|
|
250
|
+
cmd,
|
|
251
|
+
max_output_size=max_output_size,
|
|
252
|
+
timeout=timeout,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Gather extraction results
|
|
256
|
+
extracted_files = []
|
|
257
|
+
total_size = 0
|
|
258
|
+
max_depth_found = 0
|
|
259
|
+
|
|
260
|
+
# Walk the extraction directory to catalog results
|
|
261
|
+
extraction_path = Path(extraction_dir)
|
|
262
|
+
if extraction_path.exists():
|
|
263
|
+
for root, _dirs, files in os.walk(extraction_path):
|
|
264
|
+
# Calculate depth from extraction root
|
|
265
|
+
rel_path = Path(root).relative_to(extraction_path)
|
|
266
|
+
current_depth = len(rel_path.parts)
|
|
267
|
+
max_depth_found = max(max_depth_found, current_depth)
|
|
268
|
+
|
|
269
|
+
for filename in files:
|
|
270
|
+
file_full_path = Path(root) / filename
|
|
271
|
+
try:
|
|
272
|
+
file_size = file_full_path.stat().st_size
|
|
273
|
+
total_size += file_size
|
|
274
|
+
|
|
275
|
+
# Try to determine file type
|
|
276
|
+
file_type = "unknown"
|
|
277
|
+
try:
|
|
278
|
+
# Use 'file' command for type detection
|
|
279
|
+
type_cmd = ["file", "-b", str(file_full_path)]
|
|
280
|
+
type_output, _ = await execute_subprocess_async(
|
|
281
|
+
type_cmd, timeout=5, max_output_size=1024
|
|
282
|
+
)
|
|
283
|
+
file_type = type_output.strip()[:100] # Limit type string length
|
|
284
|
+
except (OSError, TimeoutError):
|
|
285
|
+
# file command failed or timed out, use default "unknown"
|
|
286
|
+
file_type = "unknown"
|
|
287
|
+
|
|
288
|
+
extracted_files.append(
|
|
289
|
+
{
|
|
290
|
+
"path": str(file_full_path.relative_to(extraction_path)),
|
|
291
|
+
"type": file_type,
|
|
292
|
+
"size": file_size,
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
except (OSError, ValueError):
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
# Sort by size (largest first) and limit entries
|
|
299
|
+
extracted_files.sort(key=lambda x: x["size"], reverse=True)
|
|
300
|
+
truncated = len(extracted_files) > MAX_EXTRACTED_FILES
|
|
301
|
+
extracted_files = extracted_files[:MAX_EXTRACTED_FILES]
|
|
302
|
+
|
|
303
|
+
# Parse binwalk output for additional info
|
|
304
|
+
signatures_found = []
|
|
305
|
+
for line in output.split("\n"):
|
|
306
|
+
line = line.strip()
|
|
307
|
+
if line and not line.startswith("DECIMAL") and not line.startswith("-"):
|
|
308
|
+
# Extract signature type from binwalk output
|
|
309
|
+
parts = line.split()
|
|
310
|
+
if len(parts) >= 3:
|
|
311
|
+
try:
|
|
312
|
+
offset = int(parts[0])
|
|
313
|
+
sig_type = " ".join(parts[2:])
|
|
314
|
+
signatures_found.append({"offset": offset, "type": sig_type[:100]})
|
|
315
|
+
except (ValueError, IndexError):
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
return success(
|
|
319
|
+
{
|
|
320
|
+
"output_directory": str(extraction_dir),
|
|
321
|
+
"extracted_files": extracted_files,
|
|
322
|
+
"total_files": len(extracted_files)
|
|
323
|
+
+ (100 if truncated else 0), # Estimate if truncated
|
|
324
|
+
"total_size": total_size,
|
|
325
|
+
"total_size_human": _format_size(total_size),
|
|
326
|
+
"extraction_depth": max_depth_found,
|
|
327
|
+
"signatures_found": signatures_found[:MAX_SIGNATURES],
|
|
328
|
+
"binwalk_output": output[:5000] if len(output) > 5000 else output,
|
|
329
|
+
"truncated": truncated,
|
|
330
|
+
},
|
|
331
|
+
bytes_read=bytes_read,
|
|
332
|
+
description=f"Extracted {len(extracted_files)} files ({_format_size(total_size)}) to {extraction_dir}",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _format_size(size_bytes: int) -> str:
|
|
337
|
+
"""Format byte size to human-readable string."""
|
|
338
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
339
|
+
if size_bytes < 1024:
|
|
340
|
+
return f"{size_bytes:.1f} {unit}"
|
|
341
|
+
size_bytes /= 1024
|
|
342
|
+
return f"{size_bytes:.1f} TB"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@log_execution(tool_name="scan_for_versions")
|
|
346
|
+
@track_metrics("scan_for_versions")
|
|
347
|
+
@handle_tool_errors
|
|
348
|
+
async def scan_for_versions(
|
|
349
|
+
file_path: str,
|
|
350
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
351
|
+
) -> ToolResult:
|
|
352
|
+
"""
|
|
353
|
+
Extract library version strings and CVE clues from a binary.
|
|
354
|
+
|
|
355
|
+
This tool acts as a "Version Detective", scanning the binary for strings that
|
|
356
|
+
look like version numbers or library identifiers (e.g., "OpenSSL 1.0.2g",
|
|
357
|
+
"GCC 5.4.0"). It helps identify outdated components and potential CVEs.
|
|
358
|
+
|
|
359
|
+
**Use Cases:**
|
|
360
|
+
- **SCA (Software Composition Analysis)**: Identify open source components
|
|
361
|
+
- **Vulnerability Scanning**: Find outdated libraries (e.g., Heartbleed-vulnerable OpenSSL)
|
|
362
|
+
- **Firmware Analysis**: Determine OS and toolchain versions
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
file_path: Path to the binary file
|
|
366
|
+
timeout: Execution timeout in seconds
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
ToolResult with detected libraries and versions.
|
|
370
|
+
"""
|
|
371
|
+
validated_path = validate_file_path(file_path)
|
|
372
|
+
|
|
373
|
+
# Run strings command
|
|
374
|
+
cmd = ["strings", str(validated_path)]
|
|
375
|
+
output, bytes_read = await execute_subprocess_async(
|
|
376
|
+
cmd,
|
|
377
|
+
max_output_size=10_000_000,
|
|
378
|
+
timeout=timeout,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
text = output
|
|
382
|
+
|
|
383
|
+
# Use pre-compiled patterns for better performance
|
|
384
|
+
detected = {}
|
|
385
|
+
|
|
386
|
+
# Process all version patterns
|
|
387
|
+
for name, pattern in _VERSION_PATTERNS.items():
|
|
388
|
+
matches = []
|
|
389
|
+
for match in pattern.finditer(text):
|
|
390
|
+
# Extract version from appropriate group (1 or 2 depending on pattern)
|
|
391
|
+
if name in ["OpenSSL", "Python"]:
|
|
392
|
+
matches.append(match.group(2))
|
|
393
|
+
else:
|
|
394
|
+
matches.append(match.group(1))
|
|
395
|
+
if matches:
|
|
396
|
+
detected[name] = list(set(matches))
|
|
397
|
+
|
|
398
|
+
return success(
|
|
399
|
+
detected,
|
|
400
|
+
bytes_read=bytes_read,
|
|
401
|
+
description=f"Detected {len(detected)} potential library versions",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@log_execution(tool_name="extract_rtti_info")
|
|
406
|
+
@track_metrics("extract_rtti_info")
|
|
407
|
+
@handle_tool_errors
|
|
408
|
+
async def extract_rtti_info(
|
|
409
|
+
file_path: str,
|
|
410
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
411
|
+
) -> ToolResult:
|
|
412
|
+
"""
|
|
413
|
+
Extract RTTI (Run-Time Type Information) from C++ binaries.
|
|
414
|
+
|
|
415
|
+
RTTI provides class names and inheritance hierarchies in C++ binaries,
|
|
416
|
+
which is invaluable for understanding object-oriented malware and game clients.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
file_path: Path to the binary file
|
|
420
|
+
timeout: Execution timeout in seconds
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
ToolResult with extracted class names and type information
|
|
424
|
+
"""
|
|
425
|
+
validated_path = validate_file_path(file_path)
|
|
426
|
+
|
|
427
|
+
# Use strings with C++ demangling to extract RTTI
|
|
428
|
+
# Look for typeinfo names which start with _ZTS (type string)
|
|
429
|
+
cmd = ["strings", str(validated_path)]
|
|
430
|
+
output, bytes_read = await execute_subprocess_async(
|
|
431
|
+
cmd,
|
|
432
|
+
max_output_size=10_000_000,
|
|
433
|
+
timeout=timeout,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Use pre-compiled module-level patterns for better performance
|
|
437
|
+
# These patterns are compiled once at module load time, avoiding the overhead
|
|
438
|
+
# of regex compilation on each function call
|
|
439
|
+
|
|
440
|
+
rtti_strings = []
|
|
441
|
+
class_names = set()
|
|
442
|
+
|
|
443
|
+
for line in output.split("\n"):
|
|
444
|
+
line_stripped = line.strip()
|
|
445
|
+
if _RTTI_MAIN_PATTERN.search(line_stripped):
|
|
446
|
+
rtti_strings.append(line_stripped)
|
|
447
|
+
|
|
448
|
+
# Try all patterns to extract class names
|
|
449
|
+
for pattern in _RTTI_CLASS_PATTERNS:
|
|
450
|
+
matches = pattern.findall(line_stripped)
|
|
451
|
+
for match in matches:
|
|
452
|
+
# Handle tuple results from patterns with groups
|
|
453
|
+
if isinstance(match, tuple):
|
|
454
|
+
class_name = match[-1] # Take the last group (usually the name)
|
|
455
|
+
else:
|
|
456
|
+
class_name = match
|
|
457
|
+
|
|
458
|
+
# Filter out noise (too short, all caps, numbers only)
|
|
459
|
+
if (
|
|
460
|
+
len(class_name) > 2
|
|
461
|
+
and not class_name.isupper()
|
|
462
|
+
and not class_name.isdigit()
|
|
463
|
+
):
|
|
464
|
+
class_names.add(class_name)
|
|
465
|
+
|
|
466
|
+
return success(
|
|
467
|
+
{
|
|
468
|
+
"rtti_strings": rtti_strings[:200], # Limit to first 200
|
|
469
|
+
"class_names": sorted(class_names), # sorted() accepts any iterable
|
|
470
|
+
"total_rtti_entries": len(rtti_strings),
|
|
471
|
+
"total_classes": len(class_names),
|
|
472
|
+
},
|
|
473
|
+
bytes_read=bytes_read,
|
|
474
|
+
description=f"Extracted {len(class_names)} C++ class names from RTTI",
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# Note: StaticAnalysisPlugin has been removed.
|
|
479
|
+
# The static analysis tools are now registered via AnalysisToolsPlugin in analysis/__init__.py.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Common utility tools package.
|
|
2
|
+
|
|
3
|
+
Provides a unified CommonToolsPlugin that registers all common utility tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from reversecore_mcp.core.logging_config import get_logger
|
|
9
|
+
from reversecore_mcp.core.plugin import Plugin
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CommonToolsPlugin(Plugin):
|
|
15
|
+
"""Unified plugin for all common utility tools."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "common_tools"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def description(self) -> str:
|
|
23
|
+
return "Unified common tools including memory management, server monitoring, file operations, and patch analysis."
|
|
24
|
+
|
|
25
|
+
def register(self, mcp_server: Any) -> None:
|
|
26
|
+
"""Register all common tools."""
|
|
27
|
+
# Import and delegate to specialized plugins
|
|
28
|
+
from reversecore_mcp.tools.common.memory_tools import MemoryToolsPlugin
|
|
29
|
+
from reversecore_mcp.tools.common.server_tools import ServerToolsPlugin
|
|
30
|
+
from reversecore_mcp.tools.common.file_operations import (
|
|
31
|
+
run_file,
|
|
32
|
+
copy_to_workspace,
|
|
33
|
+
list_workspace,
|
|
34
|
+
scan_workspace,
|
|
35
|
+
)
|
|
36
|
+
from reversecore_mcp.tools.common.patch_explainer import explain_patch
|
|
37
|
+
|
|
38
|
+
# Register memory tools (plugin handles internal registration)
|
|
39
|
+
memory_plugin = MemoryToolsPlugin()
|
|
40
|
+
memory_plugin.register(mcp_server)
|
|
41
|
+
|
|
42
|
+
# Register server tools (plugin handles internal registration)
|
|
43
|
+
server_plugin = ServerToolsPlugin()
|
|
44
|
+
server_plugin.register(mcp_server)
|
|
45
|
+
|
|
46
|
+
# File operation tools
|
|
47
|
+
mcp_server.tool(run_file)
|
|
48
|
+
mcp_server.tool(copy_to_workspace)
|
|
49
|
+
mcp_server.tool(list_workspace)
|
|
50
|
+
mcp_server.tool(scan_workspace)
|
|
51
|
+
|
|
52
|
+
# Patch explainer
|
|
53
|
+
mcp_server.tool(explain_patch)
|
|
54
|
+
|
|
55
|
+
logger.info(f"Registered {self.name} plugin with common utilities (unified)")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["CommonToolsPlugin"]
|