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,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core utilities for Reversecore_MCP.
|
|
3
|
+
|
|
4
|
+
This package contains security, execution, and exception handling utilities
|
|
5
|
+
used across all tool modules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Import decorators and helpers for public API
|
|
9
|
+
# Import dependency injection container
|
|
10
|
+
from reversecore_mcp.core.container import (
|
|
11
|
+
ServiceContainer,
|
|
12
|
+
container,
|
|
13
|
+
get_ghidra_service,
|
|
14
|
+
get_r2_pool,
|
|
15
|
+
get_resource_manager,
|
|
16
|
+
)
|
|
17
|
+
from reversecore_mcp.core.decorators import log_execution
|
|
18
|
+
from reversecore_mcp.core.error_formatting import format_error, get_validation_hint
|
|
19
|
+
from reversecore_mcp.core.exceptions import (
|
|
20
|
+
ExecutionTimeoutError,
|
|
21
|
+
OutputLimitExceededError,
|
|
22
|
+
ReversecoreError,
|
|
23
|
+
ToolNotFoundError,
|
|
24
|
+
ValidationError,
|
|
25
|
+
)
|
|
26
|
+
from reversecore_mcp.core.execution import (
|
|
27
|
+
execute_subprocess_async,
|
|
28
|
+
execute_subprocess_streaming,
|
|
29
|
+
)
|
|
30
|
+
from reversecore_mcp.core.logging_config import get_logger, setup_logging
|
|
31
|
+
|
|
32
|
+
# Import shared R2 helper functions
|
|
33
|
+
from reversecore_mcp.core.r2_helpers import (
|
|
34
|
+
build_r2_cmd,
|
|
35
|
+
calculate_dynamic_timeout,
|
|
36
|
+
escape_mermaid_chars,
|
|
37
|
+
execute_r2_command,
|
|
38
|
+
parse_json_output,
|
|
39
|
+
strip_address_prefixes,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Import performance optimization modules
|
|
43
|
+
from reversecore_mcp.core.r2_pool import R2ConnectionPool, r2_pool
|
|
44
|
+
from reversecore_mcp.core.resource_manager import ResourceManager, resource_manager
|
|
45
|
+
from reversecore_mcp.core.security import validate_file_path
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"ReversecoreError",
|
|
49
|
+
"ToolNotFoundError",
|
|
50
|
+
"ExecutionTimeoutError",
|
|
51
|
+
"OutputLimitExceededError",
|
|
52
|
+
"ValidationError",
|
|
53
|
+
"execute_subprocess_streaming",
|
|
54
|
+
"execute_subprocess_async",
|
|
55
|
+
"validate_file_path",
|
|
56
|
+
"format_error",
|
|
57
|
+
"get_validation_hint",
|
|
58
|
+
"get_logger",
|
|
59
|
+
"setup_logging",
|
|
60
|
+
"log_execution",
|
|
61
|
+
"R2ConnectionPool",
|
|
62
|
+
"r2_pool",
|
|
63
|
+
"ResourceManager",
|
|
64
|
+
"resource_manager",
|
|
65
|
+
# R2 helper functions
|
|
66
|
+
"strip_address_prefixes",
|
|
67
|
+
"escape_mermaid_chars",
|
|
68
|
+
"build_r2_cmd",
|
|
69
|
+
"execute_r2_command",
|
|
70
|
+
"parse_json_output",
|
|
71
|
+
"calculate_dynamic_timeout",
|
|
72
|
+
# Dependency injection
|
|
73
|
+
"ServiceContainer",
|
|
74
|
+
"container",
|
|
75
|
+
"get_r2_pool",
|
|
76
|
+
"get_resource_manager",
|
|
77
|
+
"get_ghidra_service",
|
|
78
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit Logging Module
|
|
3
|
+
|
|
4
|
+
Provides a tamper-evident record of security-critical actions (uploads, patches, etc.).
|
|
5
|
+
Logs are written to a separate file (audit.json) to distinguish from operational logs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from reversecore_mcp.core.config import get_config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuditAction(str, Enum):
|
|
20
|
+
"""Types of actions that require auditing."""
|
|
21
|
+
|
|
22
|
+
FILE_UPLOAD = "FILE_UPLOAD"
|
|
23
|
+
BINARY_PATCH = "BINARY_PATCH"
|
|
24
|
+
FILE_DELETE = "FILE_DELETE"
|
|
25
|
+
CONFIG_CHANGE = "CONFIG_CHANGE"
|
|
26
|
+
AUTH_FAILURE = "AUTH_FAILURE"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuditLogger:
|
|
30
|
+
"""Specialized logger for security audit trails."""
|
|
31
|
+
|
|
32
|
+
_instance = None
|
|
33
|
+
|
|
34
|
+
def __new__(cls):
|
|
35
|
+
if cls._instance is None:
|
|
36
|
+
cls._instance = super(AuditLogger, cls).__new__(cls)
|
|
37
|
+
cls._instance._initialized = False
|
|
38
|
+
return cls._instance
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
if self._initialized:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
settings = get_config()
|
|
45
|
+
self.log_file = settings.workspace.parent / "audit.json" # Store outside workspace if possible, or root
|
|
46
|
+
# If workspace is root, maybe put in /var/log/reversecore if possible?
|
|
47
|
+
# For Docker, settings.workspace is /app/workspace. Parent is /app.
|
|
48
|
+
# Let's verify permission. If not, fallback to workspace.
|
|
49
|
+
|
|
50
|
+
# Prepare file handler
|
|
51
|
+
self._logger = logging.getLogger("reversecore_audit")
|
|
52
|
+
self._logger.setLevel(logging.INFO)
|
|
53
|
+
self._logger.propagate = False
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
handler = logging.FileHandler(str(self.log_file), encoding="utf-8")
|
|
57
|
+
formatter = logging.Formatter("%(message)s")
|
|
58
|
+
handler.setFormatter(formatter)
|
|
59
|
+
self._logger.addHandler(handler)
|
|
60
|
+
except Exception:
|
|
61
|
+
# Fallback to standard log if separate file fails
|
|
62
|
+
self._logger = logging.getLogger("reversecore_mcp")
|
|
63
|
+
self._logger.warning("Failed to initialize separate audit log. Merging with app log.")
|
|
64
|
+
|
|
65
|
+
self._initialized = True
|
|
66
|
+
|
|
67
|
+
def log_event(
|
|
68
|
+
self,
|
|
69
|
+
action: AuditAction | str,
|
|
70
|
+
resource: str,
|
|
71
|
+
status: str,
|
|
72
|
+
user: str = "system",
|
|
73
|
+
ip: str = "local",
|
|
74
|
+
details: Optional[dict[str, Any]] = None
|
|
75
|
+
) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Record a security event.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
action: Type of action (e.g., FILE_UPLOAD)
|
|
81
|
+
resource: Target resource (filename, path)
|
|
82
|
+
status: SUCCESS or FAILURE
|
|
83
|
+
user: Username or ID
|
|
84
|
+
ip: Source IP
|
|
85
|
+
details: Additional context
|
|
86
|
+
"""
|
|
87
|
+
event = {
|
|
88
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
89
|
+
"action": str(action),
|
|
90
|
+
"resource": resource,
|
|
91
|
+
"status": status,
|
|
92
|
+
"user": user,
|
|
93
|
+
"ip": ip,
|
|
94
|
+
"details": details or {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
self._logger.info(json.dumps(event))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Global instance
|
|
101
|
+
audit_logger = AuditLogger()
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Binary Metadata Cache
|
|
3
|
+
|
|
4
|
+
This module provides caching for binary analysis results.
|
|
5
|
+
It prevents redundant analysis of the same files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from reversecore_mcp.core.logging_config import get_logger
|
|
13
|
+
from reversecore_mcp.core.metrics import metrics_collector
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
# Configuration constants
|
|
18
|
+
DEFAULT_CACHE_TTL_SECONDS = 60 # Default time-to-live for cache validation
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BinaryMetadataCache:
|
|
22
|
+
"""
|
|
23
|
+
Caches metadata for analyzed binaries.
|
|
24
|
+
|
|
25
|
+
Keyed by file path and modification time (or hash).
|
|
26
|
+
|
|
27
|
+
Performance optimizations:
|
|
28
|
+
- TTL-based validation to reduce stat() calls
|
|
29
|
+
- Cached mtime stored with last check time
|
|
30
|
+
- Configurable cache lifetime (default: 60 seconds)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, ttl_seconds: int = DEFAULT_CACHE_TTL_SECONDS):
|
|
34
|
+
self._cache: dict[str, Any] = {}
|
|
35
|
+
# Store (mtime, last_check_time) tuple to reduce stat() calls
|
|
36
|
+
self._file_timestamps: dict[str, tuple[float, float]] = {}
|
|
37
|
+
self._ttl_seconds = ttl_seconds
|
|
38
|
+
|
|
39
|
+
def _get_cache_key(self, file_path: str) -> str:
|
|
40
|
+
"""Generate a cache key based on file path."""
|
|
41
|
+
return str(Path(file_path).absolute())
|
|
42
|
+
|
|
43
|
+
def _is_valid(self, file_path: str) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Check if cache entry is valid (file hasn't changed).
|
|
46
|
+
|
|
47
|
+
Uses TTL-based checking to avoid excessive stat() calls:
|
|
48
|
+
- If checked within TTL window, assume valid (fast path)
|
|
49
|
+
- Otherwise, verify mtime hasn't changed (slow path)
|
|
50
|
+
"""
|
|
51
|
+
key = self._get_cache_key(file_path)
|
|
52
|
+
if key not in self._cache:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
if key not in self._file_timestamps:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
cached_mtime, last_check_time = self._file_timestamps[key]
|
|
59
|
+
current_time = time.time()
|
|
60
|
+
|
|
61
|
+
# Fast path: If within TTL window, trust the cache without stat()
|
|
62
|
+
# Using < instead of <= for strict TTL boundary to avoid edge case stat() calls
|
|
63
|
+
if current_time - last_check_time < self._ttl_seconds:
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
# Slow path: TTL expired, need to check file modification time
|
|
67
|
+
try:
|
|
68
|
+
actual_mtime = Path(file_path).stat().st_mtime
|
|
69
|
+
is_valid = cached_mtime == actual_mtime
|
|
70
|
+
|
|
71
|
+
if is_valid:
|
|
72
|
+
# Update last check time to reset TTL window
|
|
73
|
+
self._file_timestamps[key] = (cached_mtime, current_time)
|
|
74
|
+
else:
|
|
75
|
+
# File changed, invalidate cache
|
|
76
|
+
if key in self._cache:
|
|
77
|
+
del self._cache[key]
|
|
78
|
+
if key in self._file_timestamps:
|
|
79
|
+
del self._file_timestamps[key]
|
|
80
|
+
|
|
81
|
+
return is_valid
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
# File deleted, invalidate cache
|
|
84
|
+
if key in self._cache:
|
|
85
|
+
del self._cache[key]
|
|
86
|
+
if key in self._file_timestamps:
|
|
87
|
+
del self._file_timestamps[key]
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
def get(self, file_path: str, key: str) -> Any | None:
|
|
91
|
+
"""Get a specific metadata item for a file."""
|
|
92
|
+
cache_key = self._get_cache_key(file_path)
|
|
93
|
+
if self._is_valid(file_path):
|
|
94
|
+
val = self._cache[cache_key].get(key)
|
|
95
|
+
if val is not None:
|
|
96
|
+
metrics_collector.record_cache_hit("binary_cache")
|
|
97
|
+
return val
|
|
98
|
+
|
|
99
|
+
metrics_collector.record_cache_miss("binary_cache")
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def set(self, file_path: str, key: str, value: Any):
|
|
103
|
+
"""Set a specific metadata item for a file."""
|
|
104
|
+
cache_key = self._get_cache_key(file_path)
|
|
105
|
+
|
|
106
|
+
# Initialize if needed
|
|
107
|
+
if cache_key not in self._cache:
|
|
108
|
+
self._cache[cache_key] = {}
|
|
109
|
+
|
|
110
|
+
# Update timestamp with current time as last check
|
|
111
|
+
try:
|
|
112
|
+
mtime = Path(file_path).stat().st_mtime
|
|
113
|
+
self._file_timestamps[cache_key] = (mtime, time.time())
|
|
114
|
+
except FileNotFoundError:
|
|
115
|
+
# For memory/stream analysis or temp files that no longer exist,
|
|
116
|
+
# use current time as mtime to enable caching
|
|
117
|
+
# Without this, cache always misses because _is_valid returns False
|
|
118
|
+
current_time = time.time()
|
|
119
|
+
self._file_timestamps[cache_key] = (current_time, current_time)
|
|
120
|
+
|
|
121
|
+
self._cache[cache_key][key] = value
|
|
122
|
+
logger.debug(f"Cached {key} for {file_path}")
|
|
123
|
+
|
|
124
|
+
def clear(self, file_path: str = None):
|
|
125
|
+
"""Clear cache for a specific file or all files."""
|
|
126
|
+
if file_path:
|
|
127
|
+
key = self._get_cache_key(file_path)
|
|
128
|
+
if key in self._cache:
|
|
129
|
+
del self._cache[key]
|
|
130
|
+
if key in self._file_timestamps:
|
|
131
|
+
del self._file_timestamps[key]
|
|
132
|
+
else:
|
|
133
|
+
self._cache.clear()
|
|
134
|
+
self._file_timestamps.clear()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# Global instance with default TTL
|
|
138
|
+
binary_cache = BinaryMetadataCache(ttl_seconds=DEFAULT_CACHE_TTL_SECONDS)
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command specification and validation with regex patterns.
|
|
3
|
+
|
|
4
|
+
This module provides strict command validation using regular expressions
|
|
5
|
+
to prevent command injection attacks. It addresses the vulnerability where
|
|
6
|
+
commands like "pdf @ main; w hello" could bypass simple prefix matching.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Literal, NewType
|
|
12
|
+
|
|
13
|
+
from reversecore_mcp.core.exceptions import ValidationError
|
|
14
|
+
|
|
15
|
+
CommandType = Literal["read", "write", "analyze", "system"]
|
|
16
|
+
ValidatedR2Command = NewType("ValidatedR2Command", str)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CommandSpec:
|
|
21
|
+
"""
|
|
22
|
+
Specification for a command with strict regex validation.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
name: Human-readable command name
|
|
26
|
+
type: Command type (read, write, analyze, system)
|
|
27
|
+
regex: Compiled regex pattern for strict validation
|
|
28
|
+
description: Optional description of what the command does
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
type: CommandType
|
|
33
|
+
regex: re.Pattern
|
|
34
|
+
description: str = ""
|
|
35
|
+
|
|
36
|
+
def validate(self, cmd: str) -> bool:
|
|
37
|
+
"""
|
|
38
|
+
Validate a command string against this spec's regex.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
cmd: Command string to validate
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if command matches, False otherwise
|
|
45
|
+
"""
|
|
46
|
+
return self.regex.match(cmd.strip()) is not None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Radare2 command specifications with strict regex patterns
|
|
50
|
+
R2_COMMAND_SPECS: list[CommandSpec] = [
|
|
51
|
+
# Disassembly commands
|
|
52
|
+
CommandSpec(
|
|
53
|
+
name="pdf",
|
|
54
|
+
type="read",
|
|
55
|
+
regex=re.compile(r"^pdf(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
56
|
+
description="Print disassembly function",
|
|
57
|
+
),
|
|
58
|
+
CommandSpec(
|
|
59
|
+
name="pd",
|
|
60
|
+
type="read",
|
|
61
|
+
regex=re.compile(r"^pd(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
62
|
+
description="Print disassembly",
|
|
63
|
+
),
|
|
64
|
+
CommandSpec(
|
|
65
|
+
name="pdfj",
|
|
66
|
+
type="read",
|
|
67
|
+
regex=re.compile(r"^pdfj(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
68
|
+
description="Print disassembly function (JSON)",
|
|
69
|
+
),
|
|
70
|
+
CommandSpec(
|
|
71
|
+
name="pdj",
|
|
72
|
+
type="read",
|
|
73
|
+
regex=re.compile(r"^pdj(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
74
|
+
description="Print disassembly (JSON)",
|
|
75
|
+
),
|
|
76
|
+
CommandSpec(
|
|
77
|
+
name="pdc",
|
|
78
|
+
type="read",
|
|
79
|
+
regex=re.compile(r"^pdc(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
80
|
+
description="Print C-like pseudo code",
|
|
81
|
+
),
|
|
82
|
+
# Analysis commands
|
|
83
|
+
CommandSpec(
|
|
84
|
+
name="aaa",
|
|
85
|
+
type="analyze",
|
|
86
|
+
regex=re.compile(r"^aaa$"),
|
|
87
|
+
description="Analyze all referenced code",
|
|
88
|
+
),
|
|
89
|
+
CommandSpec(
|
|
90
|
+
name="aa",
|
|
91
|
+
type="analyze",
|
|
92
|
+
regex=re.compile(r"^aa[a]?$"),
|
|
93
|
+
description="Analyze all",
|
|
94
|
+
),
|
|
95
|
+
CommandSpec(
|
|
96
|
+
name="afl",
|
|
97
|
+
type="read",
|
|
98
|
+
regex=re.compile(r"^afl[j]?(\s*~.+)?$"),
|
|
99
|
+
description="Analyze functions list",
|
|
100
|
+
),
|
|
101
|
+
CommandSpec(
|
|
102
|
+
name="aflj",
|
|
103
|
+
type="read",
|
|
104
|
+
regex=re.compile(r"^aflj(\s*~.+)?$"),
|
|
105
|
+
description="Analyze functions list (JSON)",
|
|
106
|
+
),
|
|
107
|
+
CommandSpec(
|
|
108
|
+
name="af",
|
|
109
|
+
type="analyze",
|
|
110
|
+
regex=re.compile(r"^af(\s+@\s+[a-zA-Z0-9_.]+)?$"),
|
|
111
|
+
description="Analyze function",
|
|
112
|
+
),
|
|
113
|
+
CommandSpec(
|
|
114
|
+
name="afi",
|
|
115
|
+
type="read",
|
|
116
|
+
regex=re.compile(r"^afi(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
117
|
+
description="Analyze function info",
|
|
118
|
+
),
|
|
119
|
+
CommandSpec(
|
|
120
|
+
name="afv",
|
|
121
|
+
type="read",
|
|
122
|
+
regex=re.compile(r"^afv[j]?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
123
|
+
description="Analyze function variables",
|
|
124
|
+
),
|
|
125
|
+
# Graph commands
|
|
126
|
+
CommandSpec(
|
|
127
|
+
name="agfj",
|
|
128
|
+
type="read",
|
|
129
|
+
regex=re.compile(r"^agfj(\s+@\s+[a-zA-Z0-9_.]+)?$"),
|
|
130
|
+
description="Print function graph in JSON format",
|
|
131
|
+
),
|
|
132
|
+
# ESIL emulation commands
|
|
133
|
+
CommandSpec(
|
|
134
|
+
name="aei",
|
|
135
|
+
type="analyze",
|
|
136
|
+
regex=re.compile(r"^aei$"),
|
|
137
|
+
description="Initialize ESIL VM",
|
|
138
|
+
),
|
|
139
|
+
CommandSpec(
|
|
140
|
+
name="aeim",
|
|
141
|
+
type="analyze",
|
|
142
|
+
regex=re.compile(r"^aeim$"),
|
|
143
|
+
description="Initialize ESIL VM memory (stack)",
|
|
144
|
+
),
|
|
145
|
+
CommandSpec(
|
|
146
|
+
name="aeip",
|
|
147
|
+
type="analyze",
|
|
148
|
+
regex=re.compile(r"^aeip$"),
|
|
149
|
+
description="Initialize ESIL VM program counter",
|
|
150
|
+
),
|
|
151
|
+
CommandSpec(
|
|
152
|
+
name="aes",
|
|
153
|
+
type="analyze",
|
|
154
|
+
regex=re.compile(r"^aes(\s+\d+)?$"),
|
|
155
|
+
description="ESIL step execution",
|
|
156
|
+
),
|
|
157
|
+
CommandSpec(
|
|
158
|
+
name="ar",
|
|
159
|
+
type="read",
|
|
160
|
+
regex=re.compile(r"^ar[j]?$"),
|
|
161
|
+
description="Show all register values",
|
|
162
|
+
),
|
|
163
|
+
CommandSpec(
|
|
164
|
+
name="s",
|
|
165
|
+
type="analyze",
|
|
166
|
+
regex=re.compile(r"^s(\s+[a-zA-Z0-9_.]+)?$"),
|
|
167
|
+
description="Seek to address",
|
|
168
|
+
),
|
|
169
|
+
# Information commands
|
|
170
|
+
CommandSpec(
|
|
171
|
+
name="i",
|
|
172
|
+
type="read",
|
|
173
|
+
regex=re.compile(r"^i[IiSszeEhj]?(\s*~.+)?$"),
|
|
174
|
+
description="File information",
|
|
175
|
+
),
|
|
176
|
+
CommandSpec(
|
|
177
|
+
name="iI",
|
|
178
|
+
type="read",
|
|
179
|
+
regex=re.compile(r"^iI(\s*~.+)?$"),
|
|
180
|
+
description="Binary info",
|
|
181
|
+
),
|
|
182
|
+
CommandSpec(
|
|
183
|
+
name="ii",
|
|
184
|
+
type="read",
|
|
185
|
+
regex=re.compile(r"^ii[j]?(\s*~.+)?$"),
|
|
186
|
+
description="Imports",
|
|
187
|
+
),
|
|
188
|
+
CommandSpec(
|
|
189
|
+
name="iS",
|
|
190
|
+
type="read",
|
|
191
|
+
regex=re.compile(r"^iS[j]?(\s*~.+)?$"),
|
|
192
|
+
description="Sections",
|
|
193
|
+
),
|
|
194
|
+
CommandSpec(
|
|
195
|
+
name="iz",
|
|
196
|
+
type="read",
|
|
197
|
+
regex=re.compile(r"^iz[j]?(\s*~.+)?$"),
|
|
198
|
+
description="Strings in data sections",
|
|
199
|
+
),
|
|
200
|
+
CommandSpec(
|
|
201
|
+
name="izz",
|
|
202
|
+
type="read",
|
|
203
|
+
regex=re.compile(r"^izz[j]?(\s*~.+)?$"),
|
|
204
|
+
description="All strings",
|
|
205
|
+
),
|
|
206
|
+
CommandSpec(
|
|
207
|
+
name="ie",
|
|
208
|
+
type="read",
|
|
209
|
+
regex=re.compile(r"^ie[j]?(\s*~.+)?$"),
|
|
210
|
+
description="Entry points",
|
|
211
|
+
),
|
|
212
|
+
CommandSpec(
|
|
213
|
+
name="iE",
|
|
214
|
+
type="read",
|
|
215
|
+
regex=re.compile(r"^iE[j]?(\s*~.+)?$"),
|
|
216
|
+
description="Exports",
|
|
217
|
+
),
|
|
218
|
+
# Hexdump commands
|
|
219
|
+
CommandSpec(
|
|
220
|
+
name="px",
|
|
221
|
+
type="read",
|
|
222
|
+
regex=re.compile(r"^px[wqd]?(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
223
|
+
description="Print hexdump",
|
|
224
|
+
),
|
|
225
|
+
CommandSpec(
|
|
226
|
+
name="pxw",
|
|
227
|
+
type="read",
|
|
228
|
+
regex=re.compile(r"^pxw(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
229
|
+
description="Print hexdump (words)",
|
|
230
|
+
),
|
|
231
|
+
CommandSpec(
|
|
232
|
+
name="pxq",
|
|
233
|
+
type="read",
|
|
234
|
+
regex=re.compile(r"^pxq(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
235
|
+
description="Print hexdump (qwords)",
|
|
236
|
+
),
|
|
237
|
+
CommandSpec(
|
|
238
|
+
name="p8",
|
|
239
|
+
type="read",
|
|
240
|
+
regex=re.compile(r"^p8(\s+\d+)?(\s+@\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
241
|
+
description="Print raw bytes in hexadecimal",
|
|
242
|
+
),
|
|
243
|
+
# Seek commands (read-only navigation)
|
|
244
|
+
CommandSpec(
|
|
245
|
+
name="s",
|
|
246
|
+
type="read",
|
|
247
|
+
regex=re.compile(r"^s(\s+[a-zA-Z0-9_.+\-]+)?(\s*~.+)?$"),
|
|
248
|
+
description="Seek to address",
|
|
249
|
+
),
|
|
250
|
+
# Flag commands (read-only)
|
|
251
|
+
CommandSpec(
|
|
252
|
+
name="f",
|
|
253
|
+
type="read",
|
|
254
|
+
regex=re.compile(r"^f[sj]?(\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
255
|
+
description="Flags",
|
|
256
|
+
),
|
|
257
|
+
CommandSpec(
|
|
258
|
+
name="fs",
|
|
259
|
+
type="read",
|
|
260
|
+
regex=re.compile(r"^fs(\s+[a-zA-Z0-9_.]+)?(\s*~.+)?$"),
|
|
261
|
+
description="Flag spaces",
|
|
262
|
+
),
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# Dangerous patterns that should always be blocked
|
|
267
|
+
DANGEROUS_PATTERNS = [
|
|
268
|
+
re.compile(r";"), # SECURITY: Block ALL semicolons (prevents `pdf ~;!id` bypass)
|
|
269
|
+
re.compile(r"\|(?!})"), # Pipe to another command (but allow JSON `|` in output)
|
|
270
|
+
re.compile(r"`.*`"), # Backticks (command substitution)
|
|
271
|
+
re.compile(r"\$\(.*\)"), # Command substitution
|
|
272
|
+
re.compile(r"&&"), # Logical AND (command chaining)
|
|
273
|
+
re.compile(r"\|\|"), # Logical OR (command chaining)
|
|
274
|
+
re.compile(r"^w[oxa]?\s"), # Write commands
|
|
275
|
+
re.compile(r"^!"), # System commands
|
|
276
|
+
re.compile(r"^#!"), # Scripts
|
|
277
|
+
re.compile(r"~.*;"), # Tilde filter followed by semicolon (extra protection)
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def validate_r2_command(cmd: str, allow_write: bool = False) -> ValidatedR2Command:
|
|
282
|
+
"""
|
|
283
|
+
Validate a radare2 command using strict regex patterns.
|
|
284
|
+
|
|
285
|
+
This function provides comprehensive validation to prevent command injection:
|
|
286
|
+
1. Checks for dangerous patterns (semicolons, pipes, command substitution)
|
|
287
|
+
2. Validates against known safe command patterns
|
|
288
|
+
3. Blocks write commands unless explicitly allowed
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
cmd: Radare2 command string to validate
|
|
292
|
+
allow_write: If True, allow write commands (default: False)
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
ValidatedR2Command: Command string marked as validated
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
ValidationError: If command is invalid, dangerous, or not in allowlist
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
>>> validate_r2_command("pdf @ main")
|
|
302
|
+
ValidatedR2Command('pdf @ main')
|
|
303
|
+
|
|
304
|
+
>>> validate_r2_command("pdf @ main; w hello")
|
|
305
|
+
ValidationError: Dangerous command pattern detected
|
|
306
|
+
"""
|
|
307
|
+
if not cmd or not cmd.strip():
|
|
308
|
+
raise ValidationError("Command string cannot be empty", details={"command": cmd})
|
|
309
|
+
|
|
310
|
+
cmd_stripped = cmd.strip()
|
|
311
|
+
|
|
312
|
+
# Check for dangerous patterns first
|
|
313
|
+
for pattern in DANGEROUS_PATTERNS:
|
|
314
|
+
if pattern.search(cmd_stripped):
|
|
315
|
+
raise ValidationError(
|
|
316
|
+
f"Dangerous command pattern detected: {pattern.pattern}. "
|
|
317
|
+
"Command chaining, pipes, and command substitution are not allowed.",
|
|
318
|
+
details={"command": cmd_stripped, "pattern": pattern.pattern},
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Try to match against command specifications
|
|
322
|
+
for spec in R2_COMMAND_SPECS:
|
|
323
|
+
if spec.validate(cmd_stripped):
|
|
324
|
+
# Check if write command when not allowed
|
|
325
|
+
if spec.type == "write" and not allow_write:
|
|
326
|
+
raise ValidationError(
|
|
327
|
+
f"Write commands are not allowed: {spec.name}",
|
|
328
|
+
details={"command": cmd_stripped, "command_type": spec.type},
|
|
329
|
+
)
|
|
330
|
+
return ValidatedR2Command(cmd_stripped)
|
|
331
|
+
|
|
332
|
+
# No match found
|
|
333
|
+
raise ValidationError(
|
|
334
|
+
f"Command not in allowlist: {cmd_stripped}. "
|
|
335
|
+
"Only read-only and analysis commands are allowed.",
|
|
336
|
+
details={
|
|
337
|
+
"command": cmd_stripped,
|
|
338
|
+
"allowed_commands": [spec.name for spec in R2_COMMAND_SPECS],
|
|
339
|
+
},
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def is_safe_r2_command(cmd: str) -> bool:
|
|
344
|
+
"""
|
|
345
|
+
Check if a radare2 command is safe (non-blocking validation).
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
cmd: Command string to check
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
True if command is safe, False otherwise
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
validate_r2_command(cmd, allow_write=False)
|
|
355
|
+
return True
|
|
356
|
+
except ValidationError:
|
|
357
|
+
return False
|