Files
magnus919_agent-skills/binary-analysis/scripts/binary_analysis/rules/suspicious_apis.py
T

470 lines
17 KiB
Python

"""Suspicious API detection rules engine.
Evaluates only priority-tagged rules against imported APIs to detect
potentially suspicious or dangerous API usage. Returns structured matches
with risk scores, confidence levels, and rule identifiers.
Each rule has a risk_score (0.0-10.0), a category, and a priority flag.
Only priority-tagged rules are evaluated. The rules_applied list
identifies which rules were evaluated.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from binary_analysis.adapters.base import BackendAdapter
from binary_analysis.domain.entities import Binary, Import
from binary_analysis.domain.enums import Confidence
# ---------------------------------------------------------------------------
# Priority rule definitions
# ---------------------------------------------------------------------------
@dataclass
class SuspiciousApiRule:
"""A single suspicious API detection rule.
Attributes:
rule_id: Unique rule identifier (e.g., "suspicious-process-injection").
name: Human-readable rule name.
category: Functional category (e.g., "process-injection", "anti-analysis").
priority: Whether this rule is a priority rule (only priority rules are evaluated).
risk_score_base: Base risk score (0.0-10.0) for matches from this rule.
apis: Set of API names that trigger this rule (matched case-sensitively).
module_hints: Optional set of module name prefixes/hints for narrowing.
description: Human-readable description of what this rule detects.
"""
rule_id: str = ""
name: str = ""
category: str = ""
priority: bool = False
risk_score_base: float = 5.0
apis: set[str] = field(default_factory=set)
module_hints: set[str] = field(default_factory=set)
description: str = ""
def _default_priority_rules() -> list[SuspiciousApiRule]:
"""Return the default set of priority-tagged suspicious API rules.
These rules are inspectable, versioned, and explainable per ADR-009.
Only priority=True rules are evaluated during suspicious-apis analysis.
"""
return [
SuspiciousApiRule(
rule_id="suspicious-process-injection",
name="Process Injection APIs",
category="process-injection",
priority=True,
risk_score_base=7.5,
apis={
"VirtualAlloc",
"VirtualAllocEx",
"VirtualProtect",
"VirtualProtectEx",
"WriteProcessMemory",
"CreateRemoteThread",
"NtCreateThreadEx",
"QueueUserAPC",
"SetThreadContext",
"RtlCreateUserThread",
"NtQueueApcThread",
"NtMapViewOfSection",
"MapViewOfFile",
"UnmapViewOfFile",
},
module_hints={"kernel32", "ntdll", "kernelbase"},
description="APIs commonly used for code injection into remote processes",
),
SuspiciousApiRule(
rule_id="suspicious-dynamic-loading",
name="Dynamic Library Loading APIs",
category="dynamic-loading",
priority=True,
risk_score_base=6.0,
apis={
"GetProcAddress",
"LoadLibraryA",
"LoadLibraryW",
"LoadLibraryExA",
"LoadLibraryExW",
"LdrLoadDll",
"LdrGetProcedureAddress",
"LdrGetDllHandle",
"GetModuleHandleA",
"GetModuleHandleW",
},
module_hints={"kernel32", "ntdll", "kernelbase"},
description="APIs for resolving symbols at runtime, used in reflective loading and API obfuscation",
),
SuspiciousApiRule(
rule_id="suspicious-anti-analysis",
name="Anti-Analysis / Anti-Debug APIs",
category="anti-analysis",
priority=True,
risk_score_base=6.5,
apis={
"IsDebuggerPresent",
"CheckRemoteDebuggerPresent",
"NtQueryInformationProcess",
"NtSetInformationThread",
"OutputDebugStringA",
"OutputDebugStringW",
"GetTickCount",
"GetTickCount64",
"QueryPerformanceCounter",
"NtClose",
"CloseHandle",
"DebugActiveProcess",
"DebugActiveProcessStop",
},
module_hints={"kernel32", "ntdll", "kernelbase"},
description="APIs used to detect or evade debugging and analysis environments",
),
SuspiciousApiRule(
rule_id="suspicious-network-activity",
name="Network / C2 Communication APIs",
category="network-activity",
priority=True,
risk_score_base=7.0,
apis={
"WinHttpOpen",
"WinHttpConnect",
"WinHttpOpenRequest",
"WinHttpSendRequest",
"WinHttpReceiveResponse",
"InternetOpenA",
"InternetOpenW",
"InternetConnectA",
"InternetConnectW",
"HttpOpenRequestA",
"HttpOpenRequestW",
"HttpSendRequestA",
"HttpSendRequestW",
"URLDownloadToFileA",
"URLDownloadToFileW",
"WinHttpCrackUrl",
"WinHttpReadData",
"WinHttpWriteData",
},
module_hints={"winhttp", "wininet", "urlmon"},
description="Windows HTTP/WinINet APIs commonly used for command-and-control communication",
),
SuspiciousApiRule(
rule_id="suspicious-crypto",
name="Cryptography APIs",
category="cryptography",
priority=True,
risk_score_base=5.5,
apis={
"CryptAcquireContextA",
"CryptAcquireContextW",
"CryptEncrypt",
"CryptDecrypt",
"CryptGenRandom",
"CryptHashData",
"CryptCreateHash",
"CryptDestroyHash",
"CryptExportKey",
"CryptImportKey",
"CryptDeriveKey",
"CryptStringToBinaryA",
"CryptStringToBinaryW",
"CryptBinaryToStringA",
"CryptBinaryToStringW",
"NCryptOpenStorageProvider",
"BCryptOpenAlgorithmProvider",
},
module_hints={"advapi32", "crypt32", "ncrypt", "bcrypt"},
description="Cryptographic APIs that may indicate data encryption (ransomware) or decryption of embedded payloads",
),
SuspiciousApiRule(
rule_id="suspicious-persistence",
name="Persistence Mechanism APIs",
category="persistence",
priority=True,
risk_score_base=7.0,
apis={
"RegCreateKeyExA",
"RegCreateKeyExW",
"RegSetValueExA",
"RegSetValueExW",
"RegDeleteKeyA",
"RegDeleteKeyW",
"RegOpenKeyExA",
"RegOpenKeyExW",
"RegQueryValueExA",
"RegQueryValueExW",
"CreateServiceA",
"CreateServiceW",
"StartServiceA",
"StartServiceW",
"OpenSCManagerA",
"OpenSCManagerW",
"ChangeServiceConfigA",
"ChangeServiceConfigW",
"CopyFileA",
"CopyFileW",
"MoveFileA",
"MoveFileW",
},
module_hints={"advapi32", "kernel32"},
description="Registry and service APIs used to establish persistence on a system",
),
SuspiciousApiRule(
rule_id="suspicious-privilege-escalation",
name="Privilege Escalation APIs",
category="privilege-escalation",
priority=True,
risk_score_base=8.0,
apis={
"OpenProcessToken",
"AdjustTokenPrivileges",
"LookupPrivilegeValueA",
"LookupPrivilegeValueW",
"DuplicateToken",
"DuplicateTokenEx",
"ImpersonateLoggedOnUser",
"RevertToSelf",
"CreateProcessAsUserA",
"CreateProcessAsUserW",
"RtlAdjustPrivilege",
},
module_hints={"advapi32", "ntdll", "kernel32"},
description="APIs for token manipulation and privilege adjustment, often used for privilege escalation",
),
SuspiciousApiRule(
rule_id="suspicious-process-enumeration",
name="Process Enumeration APIs",
category="process-enumeration",
priority=True,
risk_score_base=4.5,
apis={
"CreateToolhelp32Snapshot",
"Process32First",
"Process32Next",
"Module32First",
"Module32Next",
"EnumProcesses",
"EnumProcessModules",
"NtQuerySystemInformation",
"ZwQuerySystemInformation",
},
module_hints={"kernel32", "psapi", "ntdll"},
description="APIs for enumerating processes and modules, used for process injection target discovery",
),
SuspiciousApiRule(
rule_id="suspicious-hooking",
name="Hooking / Keylogging APIs",
category="hooking",
priority=True,
risk_score_base=6.0,
apis={
"SetWindowsHookExA",
"SetWindowsHookExW",
"UnhookWindowsHookEx",
"CallNextHookEx",
"GetAsyncKeyState",
"GetKeyState",
"GetKeyboardState",
"SetWinEventHook",
"UnhookWinEvent",
},
module_hints={"user32", "kernel32"},
description="APIs for installing hooks and monitoring input, indicators of keylogging or UI manipulation",
),
SuspiciousApiRule(
rule_id="suspicious-timing-evasion",
name="Timing Evasion APIs",
category="timing-evasion",
priority=True,
risk_score_base=4.0,
apis={
"Sleep",
"SleepEx",
"NtDelayExecution",
"ZwDelayExecution",
"WaitForSingleObject",
"WaitForMultipleObjects",
"WaitForSingleObjectEx",
"WaitForMultipleObjectsEx",
},
module_hints={"kernel32", "ntdll"},
description="APIs used for timing-based sandbox evasion and delayed execution",
),
# Non-priority rules (excluded from evaluation)
SuspiciousApiRule(
rule_id="info-file-operations",
name="File Operation APIs",
category="file-system",
priority=False,
risk_score_base=3.0,
apis={
"CreateFileA",
"CreateFileW",
"WriteFile",
"ReadFile",
"DeleteFileA",
"DeleteFileW",
"FindFirstFileA",
"FindFirstFileW",
},
module_hints={"kernel32"},
description="Common file operations (informational only, not priority)",
),
]
# ---------------------------------------------------------------------------
# Suspicious API match result
# ---------------------------------------------------------------------------
@dataclass
class SuspiciousApiMatch:
"""A single suspicious API match.
Attributes:
api_name: The matched import/export API name.
risk_score: Numeric risk score (float, 0.0-10.0).
confidence: Confidence level from the Confidence enum.
rule_id: The identifier of the priority rule that produced this match.
"""
api_name: str
risk_score: float
confidence: Confidence
rule_id: str
# ---------------------------------------------------------------------------
# Suspicious APIs engine
# ---------------------------------------------------------------------------
class SuspiciousApisEngine:
"""Evaluates priority-tagged rules against imported APIs.
Scans the binary's import table for API names matching known
suspicious patterns. Only rules tagged as priority=True are
evaluated. Non-priority rules are skipped silently.
Each match includes the API name that triggered the rule, a numeric
risk score, a confidence level derived from the number of matches
per rule, and the rule_id of the matching priority rule.
"""
def __init__(self, adapter: BackendAdapter, binary: Binary) -> None:
self._adapter = adapter
self._binary = binary
self._rules: list[SuspiciousApiRule] = []
self._active_rules: list[SuspiciousApiRule] = []
def run(self, limit: int = 100) -> tuple[list[SuspiciousApiMatch], list[str], int]:
"""Evaluate all priority rules against the binary's imports.
Args:
limit: Maximum number of matches to return.
Returns:
Tuple of (matches, rules_applied, total_matches) where matches is the
list of SuspiciousApiMatch results (bounded by limit), rules_applied
is the list of rule_id strings that were evaluated, and total_matches
is the original total count of matches before slicing (used for
accurate truncation warnings).
"""
# Load and filter to priority rules only
self._load_rules()
priority_rules = [r for r in self._rules if r.priority]
self._active_rules = priority_rules
rules_applied: list[str] = []
# Collect imports from the adapter
try:
imports: list[Import] = self._adapter.get_imports(self._binary)
except Exception:
imports = []
matches: list[SuspiciousApiMatch] = []
total_matches: int = 0
# Build a set of imported symbols for fast lookup
imported_symbols: dict[str, Import] = {}
for imp in imports:
imported_symbols[imp.symbol] = imp
# Evaluate each priority rule
for rule in priority_rules:
rules_applied.append(rule.rule_id)
# Find matching APIs
matching_symbols: list[str] = []
for api_name in rule.apis:
if api_name in imported_symbols:
matching_symbols.append(api_name)
if not matching_symbols:
continue
# Count total matches across all matching symbols (before slicing)
total_matches += len(matching_symbols)
# Compute confidence based on match density
match_count = len(matching_symbols)
total_in_rule = len(rule.apis)
density = match_count / max(total_in_rule, 1)
if match_count >= 5 and density >= 0.3:
confidence = Confidence.HIGH
elif match_count >= 2:
confidence = Confidence.MEDIUM
elif match_count == 1:
confidence = Confidence.LOW
else:
confidence = Confidence.UNKNOWN
# Adjust risk score based on match count
adjusted_risk = min(10.0, rule.risk_score_base * (1.0 + 0.1 * (match_count - 1)))
for api_name in matching_symbols:
if len(matches) >= limit:
break
matches.append(
SuspiciousApiMatch(
api_name=api_name,
risk_score=round(adjusted_risk, 1),
confidence=confidence,
rule_id=rule.rule_id,
)
)
# Stop adding matches if we've hit the limit, but continue counting
# for accurate total_matches
return matches[:limit], rules_applied, total_matches
def _load_rules(self) -> None:
"""Load all rule definitions (including non-priority ones)."""
self._rules = _default_priority_rules()
@property
def total_rules(self) -> int:
"""Total number of rules (including non-priority)."""
if not self._rules:
self._load_rules()
return len(self._rules)
@property
def priority_rule_count(self) -> int:
"""Number of priority-tagged rules."""
if not self._rules:
self._load_rules()
return sum(1 for r in self._rules if r.priority)