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,61 @@
|
|
|
1
|
+
"""Malware analysis tools package.
|
|
2
|
+
|
|
3
|
+
Provides a unified MalwareToolsPlugin that registers all malware-related 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 MalwareToolsPlugin(Plugin):
|
|
15
|
+
"""Unified plugin for all malware analysis tools."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def name(self) -> str:
|
|
19
|
+
return "malware_tools"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def description(self) -> str:
|
|
23
|
+
return "Unified malware analysis tools including detection, vaccine generation, IOC extraction, and YARA scanning."
|
|
24
|
+
|
|
25
|
+
def register(self, mcp_server: Any) -> None:
|
|
26
|
+
"""Register all malware tools."""
|
|
27
|
+
# Import tool functions from submodules
|
|
28
|
+
from reversecore_mcp.tools.malware.dormant_detector import dormant_detector
|
|
29
|
+
from reversecore_mcp.tools.malware.adaptive_vaccine import adaptive_vaccine
|
|
30
|
+
from reversecore_mcp.tools.malware.vulnerability_hunter import vulnerability_hunter
|
|
31
|
+
from reversecore_mcp.tools.malware.ioc_tools import extract_iocs
|
|
32
|
+
from reversecore_mcp.tools.malware.yara_tools import run_yara
|
|
33
|
+
|
|
34
|
+
# Register all tools
|
|
35
|
+
mcp_server.tool(dormant_detector)
|
|
36
|
+
mcp_server.tool(adaptive_vaccine)
|
|
37
|
+
mcp_server.tool(vulnerability_hunter)
|
|
38
|
+
mcp_server.tool(extract_iocs)
|
|
39
|
+
mcp_server.tool(run_yara)
|
|
40
|
+
|
|
41
|
+
logger.info(f"Registered {self.name} plugin with 5 malware tools")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Re-export submodules for convenience
|
|
45
|
+
from reversecore_mcp.tools.malware import dormant_detector as dormant_detector_module
|
|
46
|
+
from reversecore_mcp.tools.malware import adaptive_vaccine as adaptive_vaccine_module
|
|
47
|
+
from reversecore_mcp.tools.malware import vulnerability_hunter as vulnerability_hunter_module
|
|
48
|
+
from reversecore_mcp.tools.malware import ioc_tools
|
|
49
|
+
from reversecore_mcp.tools.malware import yara_tools
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"MalwareToolsPlugin",
|
|
53
|
+
"dormant_detector_module",
|
|
54
|
+
"adaptive_vaccine_module",
|
|
55
|
+
"vulnerability_hunter_module",
|
|
56
|
+
"ioc_tools",
|
|
57
|
+
"yara_tools",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
# NOTE: Deprecated plugin classes (DormantDetectorPlugin, AdaptiveVaccinePlugin,
|
|
61
|
+
# VulnerabilityHunterPlugin) were removed in v1.0.0. Use MalwareToolsPlugin instead.
|
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adaptive Vaccine: Automated Defense Generation Tool.
|
|
3
|
+
|
|
4
|
+
This tool generates defensive measures against detected threats:
|
|
5
|
+
1. YARA rule generation from threat patterns
|
|
6
|
+
2. Binary patching (NOP injection, JMP override)
|
|
7
|
+
3. Safety checks and backups
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import lief
|
|
17
|
+
try:
|
|
18
|
+
from capstone import Cs, CS_ARCH_X86, CS_ARCH_ARM, CS_MODE_32, CS_MODE_64, CS_MODE_ARM, CS_MODE_THUMB
|
|
19
|
+
except ImportError:
|
|
20
|
+
Cs = None
|
|
21
|
+
|
|
22
|
+
from fastmcp import Context, FastMCP
|
|
23
|
+
|
|
24
|
+
from reversecore_mcp.core.decorators import log_execution
|
|
25
|
+
from reversecore_mcp.core.error_handling import handle_tool_errors
|
|
26
|
+
from reversecore_mcp.core.logging_config import get_logger
|
|
27
|
+
from reversecore_mcp.core.metrics import track_metrics
|
|
28
|
+
from reversecore_mcp.core.result import ToolResult, failure, success
|
|
29
|
+
from reversecore_mcp.core.security import validate_file_path
|
|
30
|
+
from reversecore_mcp.core.audit import audit_logger, AuditAction
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
# Validation constants for YARA rule generation
|
|
35
|
+
YARA_RULE_NAME_MAX_LENGTH = 64
|
|
36
|
+
YARA_META_VALUE_MAX_LENGTH = 256
|
|
37
|
+
YARA_MAX_PATTERNS = 10
|
|
38
|
+
YARA_MAX_STRING_LITERAL_LENGTH = 200
|
|
39
|
+
|
|
40
|
+
# OPTIMIZATION: Pre-compile regex patterns used in hot paths
|
|
41
|
+
_YARA_FUNCTION_NAME_CLEAN = re.compile(r"[^\w\-.]")
|
|
42
|
+
_YARA_ADDRESS_PATTERN = re.compile(r"^(0x[0-9a-fA-F]+|\d+)$")
|
|
43
|
+
_YARA_CONTROL_CHARS = re.compile(r"[\x00-\x1f\x7f-\x9f]")
|
|
44
|
+
_YARA_RULE_NAME_CLEAN = re.compile(r"[^a-zA-Z0-9_]")
|
|
45
|
+
_YARA_HEX_PATTERN = re.compile(r"0x([0-9a-fA-F]+)")
|
|
46
|
+
_YARA_STRING_LITERAL = re.compile(r'"([^"]{1,200})"')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _escape_yara_meta(s: str) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Escape function for YARA meta strings.
|
|
52
|
+
|
|
53
|
+
Escapes backslashes and quotes. Order matters: escape backslashes first.
|
|
54
|
+
Note: str.translate() cannot handle multi-character escape sequences,
|
|
55
|
+
so we use chained replace() which is appropriate for this use case.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
s: String to escape
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Escaped string
|
|
62
|
+
"""
|
|
63
|
+
# Order matters: escape backslashes first, then quotes
|
|
64
|
+
return s.replace("\\", "\\\\").replace('"', '\\"')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _validate_threat_report(threat_report: Any) -> dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Validate and sanitize threat_report input for YARA rule generation.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
threat_report: Input to validate
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Validated and sanitized threat report dictionary
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
ValueError: If validation fails
|
|
79
|
+
"""
|
|
80
|
+
# Type check
|
|
81
|
+
if not isinstance(threat_report, dict):
|
|
82
|
+
raise ValueError(f"threat_report must be a dictionary, got {type(threat_report).__name__}")
|
|
83
|
+
|
|
84
|
+
# Extract and validate fields with defaults
|
|
85
|
+
validated = {}
|
|
86
|
+
|
|
87
|
+
# Function name validation
|
|
88
|
+
function_name = threat_report.get("function", "unknown")
|
|
89
|
+
if not isinstance(function_name, str):
|
|
90
|
+
function_name = str(function_name)
|
|
91
|
+
# OPTIMIZATION: Use pre-compiled regex pattern (faster)
|
|
92
|
+
function_name = _YARA_FUNCTION_NAME_CLEAN.sub("_", function_name)[:YARA_RULE_NAME_MAX_LENGTH]
|
|
93
|
+
validated["function"] = function_name
|
|
94
|
+
|
|
95
|
+
# Address validation
|
|
96
|
+
address = threat_report.get("address", "0x0")
|
|
97
|
+
if not isinstance(address, str):
|
|
98
|
+
address = str(address)
|
|
99
|
+
# OPTIMIZATION: Use pre-compiled regex pattern (faster)
|
|
100
|
+
if not _YARA_ADDRESS_PATTERN.match(address):
|
|
101
|
+
address = "0x0"
|
|
102
|
+
validated["address"] = address
|
|
103
|
+
|
|
104
|
+
# Instruction validation - sanitize for YARA meta
|
|
105
|
+
instruction = threat_report.get("instruction", "")
|
|
106
|
+
if not isinstance(instruction, str):
|
|
107
|
+
instruction = str(instruction)
|
|
108
|
+
# Escape quotes and backslashes for YARA meta
|
|
109
|
+
instruction = _escape_yara_meta(instruction)
|
|
110
|
+
instruction = instruction[:YARA_META_VALUE_MAX_LENGTH]
|
|
111
|
+
validated["instruction"] = instruction
|
|
112
|
+
|
|
113
|
+
# Reason validation - sanitize for YARA meta
|
|
114
|
+
reason = threat_report.get("reason", "Suspicious behavior detected")
|
|
115
|
+
if not isinstance(reason, str):
|
|
116
|
+
reason = str(reason)
|
|
117
|
+
# Escape quotes and backslashes for YARA meta
|
|
118
|
+
reason = _escape_yara_meta(reason)
|
|
119
|
+
reason = reason[:YARA_META_VALUE_MAX_LENGTH]
|
|
120
|
+
validated["reason"] = reason
|
|
121
|
+
|
|
122
|
+
# Refined code (optional) - just sanitize
|
|
123
|
+
refined_code = threat_report.get("refined_code", "")
|
|
124
|
+
if not isinstance(refined_code, str):
|
|
125
|
+
refined_code = str(refined_code) if refined_code else ""
|
|
126
|
+
validated["refined_code"] = refined_code
|
|
127
|
+
|
|
128
|
+
return validated
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _sanitize_yara_string(s: str, max_length: int = YARA_MAX_STRING_LITERAL_LENGTH) -> str:
|
|
132
|
+
"""
|
|
133
|
+
Sanitize a string for safe use in YARA rules.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
s: String to sanitize
|
|
137
|
+
max_length: Maximum allowed length
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Sanitized string safe for YARA
|
|
141
|
+
"""
|
|
142
|
+
# OPTIMIZATION: Use pre-compiled regex pattern (faster)
|
|
143
|
+
s = _YARA_CONTROL_CHARS.sub("", s)
|
|
144
|
+
# Escape quotes and backslashes for YARA
|
|
145
|
+
s = _escape_yara_meta(s)
|
|
146
|
+
# Limit length
|
|
147
|
+
return s[:max_length]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def register_adaptive_vaccine(mcp: FastMCP) -> None:
|
|
151
|
+
"""Register the Adaptive Vaccine tool with the FastMCP server."""
|
|
152
|
+
mcp.tool(adaptive_vaccine)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@log_execution(tool_name="adaptive_vaccine")
|
|
156
|
+
@track_metrics("adaptive_vaccine")
|
|
157
|
+
@handle_tool_errors
|
|
158
|
+
async def adaptive_vaccine(
|
|
159
|
+
threat_report: dict[str, Any],
|
|
160
|
+
action: str = "yara",
|
|
161
|
+
file_path: str | None = None,
|
|
162
|
+
dry_run: bool = True,
|
|
163
|
+
ctx: Context = None,
|
|
164
|
+
) -> ToolResult:
|
|
165
|
+
"""
|
|
166
|
+
Generate automated defenses against detected threats.
|
|
167
|
+
|
|
168
|
+
Actions:
|
|
169
|
+
- "yara": Generate YARA detection rule
|
|
170
|
+
- "patch": Generate binary patch (requires file_path)
|
|
171
|
+
- "both": Generate both YARA rule and patch
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
threat_report: Threat information from Ghost Trace or Trinity Defense
|
|
175
|
+
Format: {
|
|
176
|
+
"function": "func_name",
|
|
177
|
+
"address": "0x401000",
|
|
178
|
+
"instruction": "cmp eax, 0xDEADBEEF",
|
|
179
|
+
"reason": "Magic value detected",
|
|
180
|
+
"refined_code": "if (magic_val == 0xDEADBEEF) ..." (optional)
|
|
181
|
+
}
|
|
182
|
+
action: Type of defense to generate
|
|
183
|
+
file_path: Path to binary (required for "patch" action)
|
|
184
|
+
dry_run: If True, only preview patch without applying
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
ToolResult containing generated defenses
|
|
188
|
+
"""
|
|
189
|
+
# Validate action parameter
|
|
190
|
+
valid_actions = {"yara", "patch", "both"}
|
|
191
|
+
if action not in valid_actions:
|
|
192
|
+
return failure(
|
|
193
|
+
"INVALID_ACTION",
|
|
194
|
+
f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Pre-validate threat_report
|
|
198
|
+
try:
|
|
199
|
+
validated_report = _validate_threat_report(threat_report)
|
|
200
|
+
except ValueError as e:
|
|
201
|
+
return failure(
|
|
202
|
+
"INVALID_THREAT_REPORT",
|
|
203
|
+
f"Invalid threat_report: {str(e)}",
|
|
204
|
+
hint="threat_report must be a dictionary with 'function', 'address', "
|
|
205
|
+
"'instruction', and 'reason' fields",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if ctx:
|
|
209
|
+
await ctx.info(f"🛡️ Adaptive Vaccine: Generating {action} defense...")
|
|
210
|
+
|
|
211
|
+
result = {"action": action, "dry_run": dry_run}
|
|
212
|
+
|
|
213
|
+
# Detect architecture if file_path provided
|
|
214
|
+
arch = "x86" # default
|
|
215
|
+
if file_path:
|
|
216
|
+
try:
|
|
217
|
+
validated_path = validate_file_path(file_path)
|
|
218
|
+
arch = _detect_architecture(validated_path)
|
|
219
|
+
except Exception: # Use default if detection fails
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
# Generate YARA rule
|
|
223
|
+
if action in ["yara", "both"]:
|
|
224
|
+
yara_rule = _generate_yara_rule(validated_report, arch)
|
|
225
|
+
result["yara_rule"] = yara_rule
|
|
226
|
+
result["architecture"] = arch
|
|
227
|
+
if ctx:
|
|
228
|
+
await ctx.info(f"✅ YARA rule generated (arch: {arch})")
|
|
229
|
+
|
|
230
|
+
# Generate binary patch
|
|
231
|
+
if action in ["patch", "both"]:
|
|
232
|
+
if not file_path:
|
|
233
|
+
return failure("MISSING_FILE_PATH", "file_path is required for patch action")
|
|
234
|
+
|
|
235
|
+
validated_path = validate_file_path(file_path)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
patch_info = _create_binary_patch(validated_path, threat_report, dry_run=dry_run)
|
|
239
|
+
result["patch"] = patch_info
|
|
240
|
+
|
|
241
|
+
if ctx:
|
|
242
|
+
if dry_run:
|
|
243
|
+
await ctx.info("✅ Patch preview generated (dry-run, not applied)")
|
|
244
|
+
else:
|
|
245
|
+
await ctx.info("✅ Patch applied successfully")
|
|
246
|
+
except Exception as e:
|
|
247
|
+
return failure("PATCH_FAILED", f"Patch generation failed: {e}")
|
|
248
|
+
|
|
249
|
+
return success(result)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _detect_architecture(file_path: Path) -> str:
|
|
253
|
+
"""Detect binary architecture using LIEF."""
|
|
254
|
+
try:
|
|
255
|
+
binary = lief.parse(str(file_path))
|
|
256
|
+
if binary is None:
|
|
257
|
+
return "unknown"
|
|
258
|
+
|
|
259
|
+
# Detect architecture from binary type
|
|
260
|
+
if isinstance(binary, lief.PE.Binary):
|
|
261
|
+
machine = binary.header.machine
|
|
262
|
+
if machine == lief.PE.MACHINE_TYPES.I386:
|
|
263
|
+
return "x86"
|
|
264
|
+
elif machine == lief.PE.MACHINE_TYPES.AMD64:
|
|
265
|
+
return "x86_64"
|
|
266
|
+
elif machine == lief.PE.MACHINE_TYPES.ARM:
|
|
267
|
+
return "arm"
|
|
268
|
+
elif isinstance(binary, lief.ELF.Binary):
|
|
269
|
+
arch = binary.header.machine_type
|
|
270
|
+
if arch == lief.ELF.ARCH.i386:
|
|
271
|
+
return "x86"
|
|
272
|
+
elif arch == lief.ELF.ARCH.x86_64:
|
|
273
|
+
return "x86_64"
|
|
274
|
+
elif arch == lief.ELF.ARCH.ARM:
|
|
275
|
+
return "arm"
|
|
276
|
+
|
|
277
|
+
return "unknown"
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"Failed to detect architecture: {e}")
|
|
280
|
+
return "unknown"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _hex_to_yara_bytes(hex_val: str, arch: str = "x86") -> str:
|
|
284
|
+
"""Convert hex value to YARA byte pattern with proper endianness."""
|
|
285
|
+
# Pad to even length
|
|
286
|
+
if len(hex_val) % 2 != 0:
|
|
287
|
+
hex_val = "0" + hex_val
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
# Convert to bytes
|
|
291
|
+
byte_array = bytes.fromhex(hex_val)
|
|
292
|
+
|
|
293
|
+
# Reverse if little-endian architecture
|
|
294
|
+
# Note: ARM is typically little-endian on Android/iOS (armv7, aarch64)
|
|
295
|
+
little_endian_arches = {"x86", "x86_64", "arm", "arm64", "aarch64", "armle"}
|
|
296
|
+
if arch.lower() in little_endian_arches:
|
|
297
|
+
byte_array = byte_array[::-1]
|
|
298
|
+
|
|
299
|
+
return " ".join(f"{b:02x}" for b in byte_array)
|
|
300
|
+
except ValueError:
|
|
301
|
+
# If conversion fails, return as-is
|
|
302
|
+
return hex_val
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _generate_yara_rule(threat_report: dict[str, Any], arch: str = "x86") -> str:
|
|
306
|
+
"""
|
|
307
|
+
Generate YARA rule from threat information.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
threat_report: Validated threat information including instruction, reason, etc.
|
|
311
|
+
arch: Target architecture for endianness handling
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
YARA rule as string
|
|
315
|
+
"""
|
|
316
|
+
function_name = threat_report.get("function", "unknown")
|
|
317
|
+
address = threat_report.get("address", "0x0")
|
|
318
|
+
instruction = threat_report.get("instruction", "")
|
|
319
|
+
reason = threat_report.get("reason", "Suspicious behavior detected")
|
|
320
|
+
refined_code = threat_report.get("refined_code", "")
|
|
321
|
+
|
|
322
|
+
# Sanitize rule name (alphanumeric and underscore only)
|
|
323
|
+
# OPTIMIZATION: Use pre-compiled regex pattern (faster)
|
|
324
|
+
rule_name = _YARA_RULE_NAME_CLEAN.sub("_", function_name)
|
|
325
|
+
# Ensure rule name starts with letter or underscore
|
|
326
|
+
if not rule_name or rule_name[0].isdigit():
|
|
327
|
+
rule_name = f"Threat_{address.replace('0x', '').replace('-', '_')}"
|
|
328
|
+
# Limit rule name length
|
|
329
|
+
rule_name = rule_name[:YARA_RULE_NAME_MAX_LENGTH]
|
|
330
|
+
|
|
331
|
+
# Extract hex patterns from instruction with validation
|
|
332
|
+
# OPTIMIZATION: Use pre-compiled regex pattern (faster)
|
|
333
|
+
hex_patterns = _YARA_HEX_PATTERN.findall(instruction)
|
|
334
|
+
|
|
335
|
+
# Build strings section with proper endianness
|
|
336
|
+
strings_section = []
|
|
337
|
+
for i, hex_val in enumerate(hex_patterns[:YARA_MAX_PATTERNS]):
|
|
338
|
+
# Validate hex pattern length (prevent extremely long patterns)
|
|
339
|
+
if len(hex_val) > 16: # Max 8 bytes
|
|
340
|
+
hex_val = hex_val[:16]
|
|
341
|
+
byte_str = _hex_to_yara_bytes(hex_val, arch)
|
|
342
|
+
strings_section.append(f" $hex_{i} = {{ {byte_str} }}")
|
|
343
|
+
|
|
344
|
+
# Extract string literals if present in refined code (with sanitization)
|
|
345
|
+
# OPTIMIZATION: Use pre-compiled regex pattern (faster)
|
|
346
|
+
string_literals = _YARA_STRING_LITERAL.findall(refined_code)
|
|
347
|
+
for i, literal in enumerate(string_literals[:3]): # Limit to 3 strings
|
|
348
|
+
sanitized = _sanitize_yara_string(literal)
|
|
349
|
+
if sanitized: # Only add non-empty strings
|
|
350
|
+
strings_section.append(f' $str_{i} = "{sanitized}" ascii')
|
|
351
|
+
|
|
352
|
+
# Build condition
|
|
353
|
+
if strings_section:
|
|
354
|
+
condition = " or ".join([s.split(" = ")[0].strip() for s in strings_section])
|
|
355
|
+
else:
|
|
356
|
+
condition = "true // Manual review required - no patterns extracted"
|
|
357
|
+
|
|
358
|
+
# Generate timestamp
|
|
359
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
360
|
+
|
|
361
|
+
# Build YARA rule with escaped metadata
|
|
362
|
+
yara_rule = f"""rule {rule_name} {{
|
|
363
|
+
meta:
|
|
364
|
+
description = "{reason}"
|
|
365
|
+
address = "{address}"
|
|
366
|
+
architecture = "{arch}"
|
|
367
|
+
generated = "{timestamp}"
|
|
368
|
+
source = "Reversecore TDS - Adaptive Vaccine"
|
|
369
|
+
|
|
370
|
+
strings:
|
|
371
|
+
{chr(10).join(strings_section) if strings_section else " // No patterns extracted - manual analysis required"}
|
|
372
|
+
|
|
373
|
+
condition:
|
|
374
|
+
{condition}
|
|
375
|
+
}}"""
|
|
376
|
+
|
|
377
|
+
return yara_rule
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _va_to_file_offset(file_path: Path, va: int) -> tuple[int, str]:
|
|
381
|
+
"""Convert virtual address to file offset using LIEF.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
file_path: Path to binary
|
|
385
|
+
va: Virtual address
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Tuple of (file_offset, section_name)
|
|
389
|
+
"""
|
|
390
|
+
try:
|
|
391
|
+
binary = lief.parse(str(file_path))
|
|
392
|
+
if binary is None:
|
|
393
|
+
raise ValueError("Failed to parse binary with LIEF")
|
|
394
|
+
|
|
395
|
+
# Handle PE format
|
|
396
|
+
if isinstance(binary, lief.PE.Binary):
|
|
397
|
+
for section in binary.sections:
|
|
398
|
+
va_start = section.virtual_address + binary.optional_header.imagebase
|
|
399
|
+
va_end = va_start + section.virtual_size
|
|
400
|
+
if va_start <= va < va_end:
|
|
401
|
+
offset = va - va_start + section.offset
|
|
402
|
+
return offset, section.name
|
|
403
|
+
|
|
404
|
+
# Handle ELF format
|
|
405
|
+
elif isinstance(binary, lief.ELF.Binary):
|
|
406
|
+
for segment in binary.segments:
|
|
407
|
+
va_start = segment.virtual_address
|
|
408
|
+
va_end = va_start + segment.virtual_size
|
|
409
|
+
if va_start <= va < va_end:
|
|
410
|
+
offset = va - va_start + segment.file_offset
|
|
411
|
+
# Find section name
|
|
412
|
+
section_name = "unknown"
|
|
413
|
+
for section in binary.sections:
|
|
414
|
+
if section.virtual_address <= va < section.virtual_address + section.size:
|
|
415
|
+
section_name = section.name
|
|
416
|
+
break
|
|
417
|
+
return offset, section_name
|
|
418
|
+
|
|
419
|
+
raise ValueError(f"VA {hex(va)} not found in any section")
|
|
420
|
+
|
|
421
|
+
except Exception as e:
|
|
422
|
+
logger.error(f"VA to offset conversion failed: {e}")
|
|
423
|
+
raise
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _create_binary_patch(
|
|
427
|
+
file_path: Path, threat_report: dict[str, Any], dry_run: bool = True
|
|
428
|
+
) -> dict[str, Any]:
|
|
429
|
+
"""
|
|
430
|
+
Create binary patch to neutralize threat.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
file_path: Path to binary file
|
|
434
|
+
threat_report: Threat information
|
|
435
|
+
dry_run: If True, only preview without applying
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
Dictionary with patch information
|
|
439
|
+
"""
|
|
440
|
+
address_str = threat_report.get("address", "0x0")
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
# Parse virtual address
|
|
444
|
+
if address_str.startswith("0x"):
|
|
445
|
+
va = int(address_str, 16)
|
|
446
|
+
else:
|
|
447
|
+
va = int(address_str)
|
|
448
|
+
except ValueError:
|
|
449
|
+
raise ValueError(f"Invalid address format: {address_str}")
|
|
450
|
+
|
|
451
|
+
# Convert VA to file offset
|
|
452
|
+
try:
|
|
453
|
+
file_offset, section_name = _va_to_file_offset(file_path, va)
|
|
454
|
+
logger.info(
|
|
455
|
+
f"Converted VA {address_str} to file offset {hex(file_offset)} (section: {section_name})"
|
|
456
|
+
)
|
|
457
|
+
except Exception as e:
|
|
458
|
+
raise ValueError(f"Failed to convert VA to file offset: {e}")
|
|
459
|
+
|
|
460
|
+
# Determine patch type based on instruction
|
|
461
|
+
instruction = threat_report.get("instruction", "").lower()
|
|
462
|
+
|
|
463
|
+
# FIX: Use Capstone to get exact instruction length
|
|
464
|
+
if Cs is None:
|
|
465
|
+
raise RuntimeError("Capstone engine not available. Cannot perform safe patching.")
|
|
466
|
+
|
|
467
|
+
# Determine Capstone mode
|
|
468
|
+
# We need to re-detect architecture slightly differently for Capstone constants
|
|
469
|
+
# _detect_architecture returns string, we need to map to Capstone constants
|
|
470
|
+
arch_str = _detect_architecture(file_path)
|
|
471
|
+
if arch_str == "x86":
|
|
472
|
+
cs_arch, cs_mode = CS_ARCH_X86, CS_MODE_32
|
|
473
|
+
elif arch_str == "x86_64":
|
|
474
|
+
cs_arch, cs_mode = CS_ARCH_X86, CS_MODE_64
|
|
475
|
+
elif arch_str == "arm":
|
|
476
|
+
cs_arch, cs_mode = CS_ARCH_ARM, CS_MODE_ARM # Simplification; real world is complex
|
|
477
|
+
else:
|
|
478
|
+
# Fallback to x86_64 if unknown
|
|
479
|
+
cs_arch, cs_mode = CS_ARCH_X86, CS_MODE_64
|
|
480
|
+
|
|
481
|
+
# Read bytes to disassemble
|
|
482
|
+
try:
|
|
483
|
+
with open(file_path, "rb") as f:
|
|
484
|
+
f.seek(file_offset)
|
|
485
|
+
# Max instruction length for x86 is 15 bytes
|
|
486
|
+
code_bytes = f.read(16)
|
|
487
|
+
except Exception as e:
|
|
488
|
+
raise ValueError(f"Failed to read file for disassembly: {e}")
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
md = Cs(cs_arch, cs_mode)
|
|
492
|
+
# Disassemble one instruction at the VA
|
|
493
|
+
instrs = list(md.disasm(code_bytes, va))
|
|
494
|
+
if not instrs:
|
|
495
|
+
raise ValueError("Capstone failed to disassemble any instruction at target.")
|
|
496
|
+
|
|
497
|
+
target_instr = instrs[0]
|
|
498
|
+
actual_length = target_instr.size
|
|
499
|
+
disasm_str = f"{target_instr.mnemonic} {target_instr.op_str}"
|
|
500
|
+
|
|
501
|
+
logger.info(f"Disassembled at {hex(va)}: '{disasm_str}' (Size: {actual_length} bytes)")
|
|
502
|
+
|
|
503
|
+
# Verify if the disassembled instruction matches the threat report (optional safety check)
|
|
504
|
+
# implementation removed for brevity, trust the address for now.
|
|
505
|
+
|
|
506
|
+
except Exception as e:
|
|
507
|
+
raise RuntimeError(f"Disassembly error at {hex(va)}: {e}")
|
|
508
|
+
|
|
509
|
+
# Generate Patch
|
|
510
|
+
patch_type = "NOP"
|
|
511
|
+
description = f"Safe NOP patch ({actual_length} bytes)"
|
|
512
|
+
|
|
513
|
+
if "cmp" in instruction or "test" in instruction:
|
|
514
|
+
patch_bytes = b"\x90" * actual_length
|
|
515
|
+
elif "jne" in instruction or "je" in instruction or "jz" in instruction:
|
|
516
|
+
patch_type = "NOP_JUMP"
|
|
517
|
+
patch_bytes = b"\x90" * actual_length
|
|
518
|
+
description = f"Neutralize conditional jump ({actual_length} bytes)"
|
|
519
|
+
else:
|
|
520
|
+
# Generic NOP for whatever instruction is there
|
|
521
|
+
patch_bytes = b"\x90" * actual_length
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
patch_info = {
|
|
525
|
+
"type": patch_type,
|
|
526
|
+
"virtual_address": address_str,
|
|
527
|
+
"file_offset": hex(file_offset),
|
|
528
|
+
"section": section_name,
|
|
529
|
+
"bytes": patch_bytes.hex(),
|
|
530
|
+
"length": len(patch_bytes),
|
|
531
|
+
"description": description,
|
|
532
|
+
"applied": False,
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if not dry_run:
|
|
536
|
+
# Create backup
|
|
537
|
+
backup_path = file_path.with_suffix(file_path.suffix + ".backup")
|
|
538
|
+
shutil.copy2(file_path, backup_path)
|
|
539
|
+
logger.info(f"Created backup: {backup_path}")
|
|
540
|
+
|
|
541
|
+
# Apply patch using file offset (not VA!)
|
|
542
|
+
try:
|
|
543
|
+
with open(file_path, "r+b") as f:
|
|
544
|
+
f.seek(file_offset) # Use file offset, not VA
|
|
545
|
+
f.write(patch_bytes)
|
|
546
|
+
|
|
547
|
+
patch_info["applied"] = True
|
|
548
|
+
patch_info["backup"] = str(backup_path)
|
|
549
|
+
|
|
550
|
+
audit_logger.log_event(
|
|
551
|
+
AuditAction.BINARY_PATCH,
|
|
552
|
+
str(file_path),
|
|
553
|
+
"SUCCESS",
|
|
554
|
+
details={
|
|
555
|
+
"type": patch_type,
|
|
556
|
+
"address": address_str,
|
|
557
|
+
"file_offset": hex(file_offset),
|
|
558
|
+
"bytes": patch_bytes.hex()
|
|
559
|
+
}
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
logger.info(f"Applied patch at file offset {hex(file_offset)} (VA: {address_str})")
|
|
563
|
+
except Exception as e:
|
|
564
|
+
audit_logger.log_event(
|
|
565
|
+
AuditAction.BINARY_PATCH,
|
|
566
|
+
str(file_path),
|
|
567
|
+
"FAILURE",
|
|
568
|
+
details={"error": str(e), "address": address_str}
|
|
569
|
+
)
|
|
570
|
+
# Restore from backup
|
|
571
|
+
shutil.copy2(backup_path, file_path)
|
|
572
|
+
raise RuntimeError(f"Patch failed, restored from backup: {e}")
|
|
573
|
+
|
|
574
|
+
return patch_info
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# Note: AdaptiveVaccinePlugin has been removed.
|
|
578
|
+
# The adaptive_vaccine tool is now registered via MalwareToolsPlugin in malware/__init__.py.
|
|
579
|
+
|