mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
470 lines
17 KiB
Python
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)
|