mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 12:06:29 +03:00
fix: relocate binary analysis skill
This commit is contained in:
@@ -0,0 +1,949 @@
|
||||
"""Rule evaluation engine for triage analysis.
|
||||
|
||||
Generates Observations, Heuristics, and Unknowns from backend adapter data.
|
||||
All output is structured, deterministic, machine-generated evidence — no
|
||||
free-form narrative prose, no agent-generated conclusions.
|
||||
|
||||
The engine is designed to be backend-neutral: it works with any
|
||||
BackendAdapter and produces canonical domain entities.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from binary_analysis.adapters.base import BackendAdapter
|
||||
from binary_analysis.domain.entities import (
|
||||
Binary,
|
||||
Heuristic,
|
||||
Observation,
|
||||
Unknown,
|
||||
)
|
||||
from binary_analysis.domain.enums import Confidence
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pre-defined heuristic rule sets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _has_suspicious_import(imp_symbol: str, imp_module: str) -> tuple[bool, str | None]:
|
||||
"""Check if an import matches known suspicious API patterns.
|
||||
|
||||
Returns (is_suspicious, category).
|
||||
"""
|
||||
suspicious_apis: dict[str, str] = {
|
||||
# Process injection / code execution
|
||||
"VirtualAlloc": "process-injection",
|
||||
"VirtualAllocEx": "process-injection",
|
||||
"VirtualProtect": "process-injection",
|
||||
"VirtualProtectEx": "process-injection",
|
||||
"WriteProcessMemory": "process-injection",
|
||||
"CreateRemoteThread": "process-injection",
|
||||
"NtCreateThreadEx": "process-injection",
|
||||
"QueueUserAPC": "process-injection",
|
||||
"SetThreadContext": "process-injection",
|
||||
"MapViewOfFile": "process-injection",
|
||||
# Dynamic loading / reflective loading
|
||||
"GetProcAddress": "dynamic-loading",
|
||||
"LoadLibraryA": "dynamic-loading",
|
||||
"LoadLibraryW": "dynamic-loading",
|
||||
"LoadLibraryExA": "dynamic-loading",
|
||||
"LoadLibraryExW": "dynamic-loading",
|
||||
"LdrLoadDll": "dynamic-loading",
|
||||
"LdrGetProcedureAddress": "dynamic-loading",
|
||||
# Anti-analysis / anti-debug
|
||||
"IsDebuggerPresent": "anti-analysis",
|
||||
"CheckRemoteDebuggerPresent": "anti-analysis",
|
||||
"NtQueryInformationProcess": "anti-analysis",
|
||||
"OutputDebugStringA": "anti-analysis",
|
||||
"OutputDebugStringW": "anti-analysis",
|
||||
"NtSetInformationThread": "anti-analysis",
|
||||
"GetTickCount": "anti-analysis",
|
||||
"QueryPerformanceCounter": "anti-analysis",
|
||||
"Rdtsc": "anti-analysis",
|
||||
# Network / C2 indicators
|
||||
"WinHttpOpen": "network-activity",
|
||||
"WinHttpConnect": "network-activity",
|
||||
"WinHttpOpenRequest": "network-activity",
|
||||
"WinHttpSendRequest": "network-activity",
|
||||
"InternetOpenA": "network-activity",
|
||||
"InternetOpenW": "network-activity",
|
||||
"InternetConnectA": "network-activity",
|
||||
"InternetConnectW": "network-activity",
|
||||
"URLDownloadToFileA": "network-activity",
|
||||
"URLDownloadToFileW": "network-activity",
|
||||
"socket": "network-activity",
|
||||
"connect": "network-activity",
|
||||
"send": "network-activity",
|
||||
"recv": "network-activity",
|
||||
"WSAStartup": "network-activity",
|
||||
"WSASocketA": "network-activity",
|
||||
"WSASocketW": "network-activity",
|
||||
# Crypto
|
||||
"CryptAcquireContextA": "cryptography",
|
||||
"CryptAcquireContextW": "cryptography",
|
||||
"CryptEncrypt": "cryptography",
|
||||
"CryptDecrypt": "cryptography",
|
||||
"CryptGenRandom": "cryptography",
|
||||
"CryptHashData": "cryptography",
|
||||
"EVP_EncryptInit": "cryptography",
|
||||
"EVP_DecryptInit": "cryptography",
|
||||
"AES_encrypt": "cryptography",
|
||||
"AES_decrypt": "cryptography",
|
||||
"SHA256_Init": "cryptography",
|
||||
# File system / persistence
|
||||
"CreateFileA": "file-system",
|
||||
"CreateFileW": "file-system",
|
||||
"WriteFile": "file-system",
|
||||
"ReadFile": "file-system",
|
||||
"DeleteFileA": "file-system",
|
||||
"DeleteFileW": "file-system",
|
||||
"MoveFileA": "file-system",
|
||||
"MoveFileW": "file-system",
|
||||
"RegCreateKeyExA": "registry",
|
||||
"RegCreateKeyExW": "registry",
|
||||
"RegSetValueExA": "registry",
|
||||
"RegSetValueExW": "registry",
|
||||
"RegDeleteKeyA": "registry",
|
||||
"RegDeleteKeyW": "registry",
|
||||
# Privilege escalation
|
||||
"OpenProcessToken": "privilege-escalation",
|
||||
"AdjustTokenPrivileges": "privilege-escalation",
|
||||
"LookupPrivilegeValueA": "privilege-escalation",
|
||||
"LookupPrivilegeValueW": "privilege-escalation",
|
||||
"RtlAdjustPrivilege": "privilege-escalation",
|
||||
"SeDebugPrivilege": "privilege-escalation",
|
||||
# Service / driver
|
||||
"OpenSCManagerA": "service-management",
|
||||
"OpenSCManagerW": "service-management",
|
||||
"CreateServiceA": "service-management",
|
||||
"CreateServiceW": "service-management",
|
||||
"StartServiceA": "service-management",
|
||||
"StartServiceW": "service-management",
|
||||
"ControlService": "service-management",
|
||||
"DeleteService": "service-management",
|
||||
# Process enumeration
|
||||
"CreateToolhelp32Snapshot": "process-enumeration",
|
||||
"Process32First": "process-enumeration",
|
||||
"Process32Next": "process-enumeration",
|
||||
"EnumProcesses": "process-enumeration",
|
||||
"NtQuerySystemInformation": "process-enumeration",
|
||||
# Keylogging / hooking
|
||||
"SetWindowsHookExA": "hooking",
|
||||
"SetWindowsHookExW": "hooking",
|
||||
"GetAsyncKeyState": "keylogging",
|
||||
"GetKeyState": "keylogging",
|
||||
"GetKeyboardState": "keylogging",
|
||||
# Mutex / synchronization (anti-sandbox)
|
||||
"CreateMutexA": "anti-sandbox",
|
||||
"CreateMutexW": "anti-sandbox",
|
||||
"OpenMutexA": "anti-sandbox",
|
||||
"OpenMutexW": "anti-sandbox",
|
||||
# Sleep / timing evasion
|
||||
"Sleep": "timing-evasion",
|
||||
"SleepEx": "timing-evasion",
|
||||
"NtDelayExecution": "timing-evasion",
|
||||
}
|
||||
if imp_symbol in suspicious_apis:
|
||||
return True, suspicious_apis[imp_symbol]
|
||||
# Check for crypto-related module patterns
|
||||
crypto_modules = {"libcrypto", "libssl", "crypt32.dll", "advapi32.dll", "ncrypt.dll"}
|
||||
if imp_module.lower() in crypto_modules:
|
||||
return True, "cryptography"
|
||||
return False, None
|
||||
|
||||
|
||||
def _classify_entrypoint_kind(kind: str) -> Confidence:
|
||||
"""Assign confidence to entrypoint classification."""
|
||||
return Confidence.HIGH if kind != "unknown" else Confidence.LOW
|
||||
|
||||
|
||||
def _compute_entropy_confidence(entropy: float | None) -> Confidence:
|
||||
"""Compute confidence of entropy measurement."""
|
||||
if entropy is None:
|
||||
return Confidence.LOW
|
||||
if entropy < 1.0 or entropy > 7.0:
|
||||
return Confidence.MEDIUM # Very low or high entropy is suspicious
|
||||
return Confidence.HIGH
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Triage engine
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TriageEngine:
|
||||
"""Evaluates backend data to produce Observations, Heuristics, and Unknowns.
|
||||
|
||||
The engine takes a BackendAdapter and a Binary and produces structured
|
||||
triage results. All output is deterministic and machine-generated.
|
||||
"""
|
||||
|
||||
def __init__(self, adapter: BackendAdapter, binary: Binary) -> None:
|
||||
self._adapter = adapter
|
||||
self._binary = binary
|
||||
self._binary_id: UUID | None = binary.id
|
||||
|
||||
def run(self) -> tuple[list[Observation], list[Heuristic], list[Unknown], list[dict[str, Any]]]:
|
||||
"""Run the full triage pipeline.
|
||||
|
||||
Returns:
|
||||
Tuple of (observations, heuristics, unknowns, diagnostics).
|
||||
Diagnostics contain any issues encountered during rule evaluation
|
||||
(e.g., backend timeouts for specific analyzers).
|
||||
"""
|
||||
diagnostics: list[dict[str, Any]] = []
|
||||
observations: list[Observation] = []
|
||||
heuristics: list[Heuristic] = []
|
||||
unknowns: list[Unknown] = []
|
||||
|
||||
# Collect observations from backend data
|
||||
try:
|
||||
observations.extend(self._collect_binary_observations())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "binary-observations",
|
||||
"message": f"Failed to collect binary observations: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
observations.extend(self._collect_section_observations())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "section-observations",
|
||||
"message": f"Failed to collect section observations: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
observations.extend(self._collect_function_observations())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "function-observations",
|
||||
"message": f"Failed to collect function observations: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
observations.extend(self._collect_string_observations())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "string-observations",
|
||||
"message": f"Failed to collect string observations: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
observations.extend(self._collect_import_observations())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "import-observations",
|
||||
"message": f"Failed to collect import observations: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
# Evaluate heuristics
|
||||
try:
|
||||
heuristics.extend(self._evaluate_suspicious_imports())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "suspicious-imports-heuristic",
|
||||
"message": f"Failed to evaluate suspicious imports: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
heuristics.extend(self._evaluate_packing_indicators())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "packing-heuristic",
|
||||
"message": f"Failed to evaluate packing indicators: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
heuristics.extend(self._evaluate_debug_presence())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "debug-heuristic",
|
||||
"message": f"Failed to evaluate debug presence: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
heuristics.extend(self._evaluate_string_indicators())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "string-heuristic",
|
||||
"message": f"Failed to evaluate string indicators: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
# Collect unknowns
|
||||
try:
|
||||
unknowns.extend(self._collect_unknowns())
|
||||
except Exception as e:
|
||||
diagnostics.append(
|
||||
{
|
||||
"severity": "ERROR",
|
||||
"category": "unknowns",
|
||||
"message": f"Failed to collect unknowns: {e}",
|
||||
"recoverable": False,
|
||||
}
|
||||
)
|
||||
|
||||
return observations, heuristics, unknowns, diagnostics
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Observations — direct deterministic facts
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _collect_binary_observations(self) -> list[Observation]:
|
||||
"""Collect observations about the binary's basic properties."""
|
||||
obs: list[Observation] = []
|
||||
b = self._binary
|
||||
bid = self._binary_id
|
||||
|
||||
obs.append(
|
||||
Observation(
|
||||
category="binary",
|
||||
description=f"Binary format: {b.format}",
|
||||
source="import",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
obs.append(
|
||||
Observation(
|
||||
category="binary",
|
||||
description=f"Architecture: {b.architecture or 'unknown'}",
|
||||
source="import",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
if b.endianness:
|
||||
obs.append(
|
||||
Observation(
|
||||
category="binary",
|
||||
description=f"Endianness: {b.endianness.value}",
|
||||
source="import",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
obs.append(
|
||||
Observation(
|
||||
category="binary",
|
||||
description=f"File size: {b.size_bytes} bytes",
|
||||
source="import",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
obs.append(
|
||||
Observation(
|
||||
category="binary",
|
||||
description=f"SHA-256: {b.sha256}",
|
||||
source="import",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
if b.entry_point:
|
||||
obs.append(
|
||||
Observation(
|
||||
category="binary",
|
||||
description=f"Entry point at {b.entry_point.display}",
|
||||
source="import",
|
||||
address=b.entry_point,
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
if b.analysis_profile:
|
||||
obs.append(
|
||||
Observation(
|
||||
category="binary",
|
||||
description=f"Analysis profile: {b.analysis_profile}",
|
||||
source="analysis",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
return obs
|
||||
|
||||
def _collect_section_observations(self) -> list[Observation]:
|
||||
"""Collect observations about sections."""
|
||||
obs: list[Observation] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
sections = self._adapter.get_sections(self._binary)
|
||||
except Exception:
|
||||
return obs
|
||||
|
||||
obs.append(
|
||||
Observation(
|
||||
category="sections",
|
||||
description=f"Total sections: {len(sections)}",
|
||||
source="backend",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
for s in sections:
|
||||
flags_str = ",".join(s.flags) if s.flags else "none"
|
||||
entropy_str = f"{s.entropy:.2f}" if s.entropy is not None else "N/A"
|
||||
addr_display = s.address.display if s.address else "unknown"
|
||||
|
||||
obs.append(
|
||||
Observation(
|
||||
category="sections",
|
||||
description=(
|
||||
f"Section '{s.name}' at {addr_display}: "
|
||||
f"vsize={s.virtual_size}, rsize={s.raw_size}, "
|
||||
f"flags=[{flags_str}], entropy={entropy_str}"
|
||||
),
|
||||
source="backend",
|
||||
address=s.address,
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return obs
|
||||
|
||||
def _collect_function_observations(self) -> list[Observation]:
|
||||
"""Collect observations about functions."""
|
||||
obs: list[Observation] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
functions = self._adapter.get_functions(
|
||||
self._binary, exclude_external=False, exclude_thunks=False
|
||||
)
|
||||
except Exception:
|
||||
return obs
|
||||
|
||||
internal = [f for f in functions if not f.is_external and not f.is_thunk]
|
||||
external = [f for f in functions if f.is_external]
|
||||
thunks = [f for f in functions if f.is_thunk]
|
||||
|
||||
obs.append(
|
||||
Observation(
|
||||
category="functions",
|
||||
description=f"Total functions: {len(functions)} "
|
||||
f"(internal: {len(internal)}, external: {len(external)}, "
|
||||
f"thunks: {len(thunks)})",
|
||||
source="backend",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
largest_fn = None
|
||||
for fn in internal:
|
||||
if largest_fn is None or fn.size_bytes > largest_fn.size_bytes:
|
||||
largest_fn = fn
|
||||
|
||||
if largest_fn and largest_fn.address:
|
||||
obs.append(
|
||||
Observation(
|
||||
category="functions",
|
||||
description=f"Largest function: '{largest_fn.name}' "
|
||||
f"({largest_fn.size_bytes} bytes)",
|
||||
source="backend",
|
||||
address=largest_fn.address,
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return obs
|
||||
|
||||
def _collect_string_observations(self) -> list[Observation]:
|
||||
"""Collect observations about strings."""
|
||||
obs: list[Observation] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
strings = self._adapter.get_strings(self._binary)
|
||||
except Exception:
|
||||
return obs
|
||||
|
||||
ascii_count = sum(1 for s in strings if s.encoding == "ASCII")
|
||||
utf16_count = sum(1 for s in strings if s.encoding == "UTF-16")
|
||||
|
||||
obs.append(
|
||||
Observation(
|
||||
category="strings",
|
||||
description=f"Total strings: {len(strings)} "
|
||||
f"(ASCII: {ascii_count}, UTF-16: {utf16_count})",
|
||||
source="backend",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return obs
|
||||
|
||||
def _collect_import_observations(self) -> list[Observation]:
|
||||
"""Collect observations about imports."""
|
||||
obs: list[Observation] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
imports = self._adapter.get_imports(self._binary)
|
||||
except Exception:
|
||||
return obs
|
||||
|
||||
modules: dict[str, int] = {}
|
||||
for imp in imports:
|
||||
modules[imp.module] = modules.get(imp.module, 0) + 1
|
||||
|
||||
obs.append(
|
||||
Observation(
|
||||
category="imports",
|
||||
description=f"Total imports: {len(imports)} across {len(modules)} modules",
|
||||
source="backend",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
for module, count in sorted(modules.items(), key=lambda x: -x[1]):
|
||||
obs.append(
|
||||
Observation(
|
||||
category="imports",
|
||||
description=f"Imports from {module}: {count} symbols",
|
||||
source="backend",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return obs
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Heuristics — rule-derived interpretations with confidence
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _evaluate_suspicious_imports(self) -> list[Heuristic]:
|
||||
"""Evaluate suspicious API import patterns."""
|
||||
heuristics: list[Heuristic] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
imports = self._adapter.get_imports(self._binary)
|
||||
except Exception:
|
||||
return heuristics
|
||||
|
||||
suspicious: dict[str, list[str]] = {}
|
||||
total_suspicious = 0
|
||||
|
||||
for imp in imports:
|
||||
is_susp, category = _has_suspicious_import(imp.symbol, imp.module)
|
||||
if is_susp and category:
|
||||
if category not in suspicious:
|
||||
suspicious[category] = []
|
||||
suspicious[category].append(imp.symbol)
|
||||
total_suspicious += 1
|
||||
|
||||
if total_suspicious == 0:
|
||||
# No suspicious imports found
|
||||
heuristics.append(
|
||||
Heuristic(
|
||||
name="no-suspicious-imports",
|
||||
description="No known suspicious API imports detected",
|
||||
confidence=Confidence.LOW,
|
||||
rule_id="suspicious-imports",
|
||||
evidence=[
|
||||
{
|
||||
"observation": "No import symbols matched the suspicious API list",
|
||||
"total_imports": len(imports),
|
||||
}
|
||||
],
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
return heuristics
|
||||
|
||||
# Report each suspicious category
|
||||
for category, symbols in sorted(suspicious.items()):
|
||||
count = len(symbols)
|
||||
# Higher counts = higher confidence
|
||||
if count >= 10:
|
||||
conf = Confidence.HIGH
|
||||
elif count >= 4:
|
||||
conf = Confidence.MEDIUM
|
||||
else:
|
||||
conf = Confidence.LOW
|
||||
|
||||
heuristics.append(
|
||||
Heuristic(
|
||||
name=f"suspicious-{category}",
|
||||
description=f"Binary imports {count} APIs associated with {category} "
|
||||
f"({', '.join(symbols[:5])}{'...' if count > 5 else ''})",
|
||||
confidence=conf,
|
||||
rule_id="suspicious-imports",
|
||||
evidence=[
|
||||
{
|
||||
"category": category,
|
||||
"match_count": count,
|
||||
"matched_symbols": symbols,
|
||||
}
|
||||
],
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return heuristics
|
||||
|
||||
def _evaluate_packing_indicators(self) -> list[Heuristic]:
|
||||
"""Evaluate potential packing/obfuscation indicators."""
|
||||
heuristics: list[Heuristic] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
sections = self._adapter.get_sections(self._binary)
|
||||
imports = self._adapter.get_imports(self._binary)
|
||||
except Exception:
|
||||
return heuristics
|
||||
|
||||
evidence: list[dict[str, Any]] = []
|
||||
packing_score = 0
|
||||
|
||||
# Check for high-entropy sections (> 7.0)
|
||||
high_entropy_sections = []
|
||||
for s in sections:
|
||||
if s.entropy is not None and s.entropy > 7.0:
|
||||
high_entropy_sections.append(s.name)
|
||||
packing_score += 2
|
||||
|
||||
if high_entropy_sections:
|
||||
evidence.append(
|
||||
{
|
||||
"indicator": "high-entropy-sections",
|
||||
"details": f"Sections with entropy > 7.0: {', '.join(high_entropy_sections)}",
|
||||
"score_contribution": len(high_entropy_sections) * 2,
|
||||
}
|
||||
)
|
||||
|
||||
# Check for writable + executable sections
|
||||
wx_sections = []
|
||||
for s in sections:
|
||||
if "w" in s.flags and "x" in s.flags:
|
||||
wx_sections.append(s.name)
|
||||
packing_score += 3
|
||||
|
||||
if wx_sections:
|
||||
evidence.append(
|
||||
{
|
||||
"indicator": "writable-executable-sections",
|
||||
"details": f"W+X sections: {', '.join(wx_sections)}",
|
||||
"score_contribution": len(wx_sections) * 3,
|
||||
}
|
||||
)
|
||||
|
||||
# Check for low import count (small IAT)
|
||||
if len(imports) < 2:
|
||||
packing_score += 3
|
||||
evidence.append(
|
||||
{
|
||||
"indicator": "small-import-table",
|
||||
"details": f"Only {len(imports)} imports detected",
|
||||
"score_contribution": 3,
|
||||
}
|
||||
)
|
||||
elif len(imports) < 5:
|
||||
packing_score += 1
|
||||
evidence.append(
|
||||
{
|
||||
"indicator": "small-import-table",
|
||||
"details": f"Only {len(imports)} imports detected",
|
||||
"score_contribution": 1,
|
||||
}
|
||||
)
|
||||
|
||||
# Check for section size mismatch (raw vs virtual)
|
||||
size_mismatches = []
|
||||
for s in sections:
|
||||
if s.virtual_size > 0 and s.raw_size > 0:
|
||||
ratio = s.virtual_size / max(s.raw_size, 1)
|
||||
if ratio > 2.0:
|
||||
size_mismatches.append(s.name)
|
||||
packing_score += 1
|
||||
|
||||
if size_mismatches:
|
||||
evidence.append(
|
||||
{
|
||||
"indicator": "section-size-mismatch",
|
||||
"details": f"Sections with virtual/raw size ratio > 2: "
|
||||
f"{', '.join(size_mismatches)}",
|
||||
"score_contribution": len(size_mismatches),
|
||||
}
|
||||
)
|
||||
|
||||
if packing_score >= 8:
|
||||
confidence = Confidence.HIGH
|
||||
desc = "Strong indicators of packing or obfuscation detected"
|
||||
elif packing_score >= 4:
|
||||
confidence = Confidence.MEDIUM
|
||||
desc = "Moderate indicators of packing or obfuscation detected"
|
||||
elif packing_score >= 1:
|
||||
confidence = Confidence.LOW
|
||||
desc = "Weak indicators of packing or obfuscation detected"
|
||||
else:
|
||||
confidence = Confidence.LOW
|
||||
desc = "No significant packing or obfuscation indicators detected"
|
||||
|
||||
heuristics.append(
|
||||
Heuristic(
|
||||
name="packing-indicators",
|
||||
description=f"{desc} (score: {packing_score})",
|
||||
confidence=confidence,
|
||||
rule_id="packing-detection",
|
||||
evidence=evidence,
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return heuristics
|
||||
|
||||
def _evaluate_debug_presence(self) -> list[Heuristic]:
|
||||
"""Evaluate debug symbol and PDB presence."""
|
||||
heuristics: list[Heuristic] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
symbols = self._adapter.get_symbols(self._binary)
|
||||
strings = self._adapter.get_strings(self._binary)
|
||||
except Exception:
|
||||
return heuristics
|
||||
|
||||
evidence: list[dict[str, Any]] = []
|
||||
|
||||
# Check for debug symbols
|
||||
debug_symbols = [s for s in symbols if s.source.value == "DEBUG"]
|
||||
if debug_symbols:
|
||||
evidence.append(
|
||||
{
|
||||
"indicator": "debug-symbols",
|
||||
"details": f"Found {len(debug_symbols)} debug symbols",
|
||||
}
|
||||
)
|
||||
|
||||
# Check for PDB references in strings
|
||||
pdb_strings = [s for s in strings if (s.text.endswith(".pdb") or ".pdb" in s.text.lower())]
|
||||
if pdb_strings:
|
||||
for ps in pdb_strings:
|
||||
evidence.append(
|
||||
{
|
||||
"indicator": "pdb-reference",
|
||||
"details": f"PDB path: {ps.text}",
|
||||
"address": ps.address.to_dict() if ps.address else None,
|
||||
}
|
||||
)
|
||||
|
||||
if evidence:
|
||||
heuristics.append(
|
||||
Heuristic(
|
||||
name="debug-information-present",
|
||||
description=f"Debug information detected: {len(evidence)} indicator(s)",
|
||||
confidence=Confidence.HIGH,
|
||||
rule_id="debug-presence",
|
||||
evidence=evidence,
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
else:
|
||||
heuristics.append(
|
||||
Heuristic(
|
||||
name="debug-information-present",
|
||||
description="No debug symbols or PDB references found",
|
||||
confidence=Confidence.LOW,
|
||||
rule_id="debug-presence",
|
||||
evidence=[],
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return heuristics
|
||||
|
||||
def _evaluate_string_indicators(self) -> list[Heuristic]:
|
||||
"""Evaluate strings for interesting indicators (URLs, IPs, paths)."""
|
||||
heuristics: list[Heuristic] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
strings = self._adapter.get_strings(self._binary)
|
||||
except Exception:
|
||||
return heuristics
|
||||
|
||||
# Check for network indicators in strings
|
||||
ip_pattern_strings = []
|
||||
url_pattern_strings = []
|
||||
path_pattern_strings = []
|
||||
registry_pattern_strings = []
|
||||
mutex_pattern_strings = []
|
||||
|
||||
for s in strings:
|
||||
txt = s.text
|
||||
# Simple heuristics for IP-like strings
|
||||
if "." in txt and any(c.isdigit() for c in txt):
|
||||
parts = txt.split(".")
|
||||
if len(parts) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in parts):
|
||||
ip_pattern_strings.append(txt)
|
||||
# URL-like patterns
|
||||
if txt.startswith(("http://", "https://", "ftp://")) or ".com" in txt or ".org" in txt:
|
||||
url_pattern_strings.append(txt)
|
||||
# Path-like patterns
|
||||
if (
|
||||
("/" in txt or "\\" in txt)
|
||||
and len(txt) > 5
|
||||
and (
|
||||
any(
|
||||
ext in txt.lower()
|
||||
for ext in (".exe", ".dll", ".sys", ".dat", ".ini", ".cfg", ".xml", ".json")
|
||||
)
|
||||
or txt.startswith(("C:\\", "/home/", "/etc/", "/var/", "/usr/", "/tmp/"))
|
||||
)
|
||||
):
|
||||
path_pattern_strings.append(txt)
|
||||
# Registry-like
|
||||
if "HKEY_" in txt or "Software\\" in txt:
|
||||
registry_pattern_strings.append(txt)
|
||||
# Mutex-like
|
||||
if "Mutex" in txt or "mutex" in txt:
|
||||
mutex_pattern_strings.append(txt)
|
||||
|
||||
# Build heuristic evidence
|
||||
all_evidence: list[dict[str, Any]] = []
|
||||
|
||||
if ip_pattern_strings:
|
||||
all_evidence.append(
|
||||
{
|
||||
"indicator": "ip-addresses",
|
||||
"details": f"Found {len(ip_pattern_strings)} IP-like strings",
|
||||
"examples": ip_pattern_strings[:5],
|
||||
}
|
||||
)
|
||||
|
||||
if url_pattern_strings:
|
||||
all_evidence.append(
|
||||
{
|
||||
"indicator": "urls",
|
||||
"details": f"Found {len(url_pattern_strings)} URL-like strings",
|
||||
"examples": url_pattern_strings[:5],
|
||||
}
|
||||
)
|
||||
|
||||
if path_pattern_strings:
|
||||
all_evidence.append(
|
||||
{
|
||||
"indicator": "file-paths",
|
||||
"details": f"Found {len(path_pattern_strings)} file path references",
|
||||
"examples": path_pattern_strings[:5],
|
||||
}
|
||||
)
|
||||
|
||||
if registry_pattern_strings:
|
||||
all_evidence.append(
|
||||
{
|
||||
"indicator": "registry-keys",
|
||||
"details": f"Found {len(registry_pattern_strings)} registry key references",
|
||||
"examples": registry_pattern_strings[:5],
|
||||
}
|
||||
)
|
||||
|
||||
if mutex_pattern_strings:
|
||||
all_evidence.append(
|
||||
{
|
||||
"indicator": "mutex-references",
|
||||
"details": f"Found {len(mutex_pattern_strings)} mutex references",
|
||||
"examples": mutex_pattern_strings[:5],
|
||||
}
|
||||
)
|
||||
|
||||
confidence = Confidence.LOW
|
||||
if len(all_evidence) >= 3:
|
||||
confidence = Confidence.HIGH
|
||||
elif len(all_evidence) >= 1:
|
||||
confidence = Confidence.MEDIUM
|
||||
|
||||
heuristics.append(
|
||||
Heuristic(
|
||||
name="string-indicators",
|
||||
description=f"String analysis found {len(all_evidence)} indicator categories",
|
||||
confidence=confidence,
|
||||
rule_id="string-indicators",
|
||||
evidence=all_evidence,
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return heuristics
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Unknowns — unresolved questions with address + question
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _collect_unknowns(self) -> list[Unknown]:
|
||||
"""Collect unresolved questions."""
|
||||
unknowns: list[Unknown] = []
|
||||
bid = self._binary_id
|
||||
|
||||
try:
|
||||
imports = self._adapter.get_imports(self._binary)
|
||||
functions = self._adapter.get_functions(
|
||||
self._binary, exclude_external=False, exclude_thunks=False
|
||||
)
|
||||
except Exception:
|
||||
return unknowns
|
||||
|
||||
# Unresolved imports
|
||||
for imp in imports:
|
||||
if imp.resolution.value in ("UNRESOLVED", "PARTIAL"):
|
||||
unknowns.append(
|
||||
Unknown(
|
||||
address=imp.address,
|
||||
question=f"Import '{imp.symbol}' from '{imp.module}' "
|
||||
f"is {imp.resolution.value.lower()}. "
|
||||
f"Where is this symbol resolved at runtime?",
|
||||
category="unresolved-import",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
# Functions with no meaningful name (backend-generated)
|
||||
for fn in functions:
|
||||
if fn.name_source.value == "BACKEND_GENERATED" and not fn.is_external:
|
||||
unknowns.append(
|
||||
Unknown(
|
||||
address=fn.address,
|
||||
question=f"Function at {fn.address.display if fn.address else 'unknown'} "
|
||||
f"has a backend-generated name '{fn.name}'. "
|
||||
f"What is the purpose of this function?",
|
||||
category="unnamed-function",
|
||||
binary_id=bid,
|
||||
)
|
||||
)
|
||||
|
||||
return unknowns
|
||||
Reference in New Issue
Block a user