fix: relocate binary analysis skill

This commit is contained in:
username
2026-07-31 13:25:08 -04:00
parent 0d4a3652e4
commit f66ed48b8c
107 changed files with 17 additions and 2 deletions
@@ -0,0 +1,22 @@
"""Heuristic and capability rules engine.
Provides:
- TriageEngine: produces Observations, Heuristics, and Unknowns from backend data.
- SuspiciousApisEngine: evaluates priority-tagged rules against imported APIs.
- CapabilityMapEngine: produces functional area suggestions from backend data.
- Rule evaluation infrastructure (extensible for suspicious-apis, capability-map).
"""
from __future__ import annotations
from binary_analysis.rules.capabilities import CapabilityMapEngine, CapabilityResult
from binary_analysis.rules.engine import TriageEngine
from binary_analysis.rules.suspicious_apis import SuspiciousApiMatch, SuspiciousApisEngine
__all__ = [
"CapabilityMapEngine",
"CapabilityResult",
"SuspiciousApiMatch",
"SuspiciousApisEngine",
"TriageEngine",
]
@@ -0,0 +1,835 @@
"""Capability mapping rules engine.
Produces functional area suggestions from backend data: imported APIs,
strings, and section patterns. Each capability entry is labeled as a
rule-derived indicator, not verified functional proof. Confidence values
replace unconditional certainty/verified fields.
Evidence items reference concrete sources:
- import: "<api_name>" — an imported API that suggests a capability
- string: "<text>" — a string that suggests a capability
- section: "<section_name>" — a section pattern that suggests a capability
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from binary_analysis.adapters.base import BackendAdapter
from binary_analysis.domain.entities import Binary
from binary_analysis.domain.enums import Confidence
# ---------------------------------------------------------------------------
# Capability definition
# ---------------------------------------------------------------------------
@dataclass
class CapabilityRule:
"""A rule for detecting a functional capability.
Attributes:
name: Functional area name (e.g., "cryptography", "networking").
category: Broader grouping (e.g., "security", "communication").
description: Human-readable description of the capability.
import_indicators: API names that suggest this capability.
string_indicators: Substrings in strings that suggest this capability.
section_indicators: Section name patterns that suggest this capability.
"""
name: str
category: str = ""
description: str = ""
import_indicators: set[str] = field(default_factory=set)
string_indicators: list[str] = field(default_factory=list)
section_indicators: list[str] = field(default_factory=list)
def _default_capability_rules() -> list[CapabilityRule]:
"""Return the default set of capability mapping rules.
These rules are inspectable, versioned, and explainable per ADR-009.
Each rule produces rule-derived indicators, not definitive proofs.
"""
return [
CapabilityRule(
name="cryptography",
category="security",
description="Indicators of cryptographic operations (encryption, hashing, key management)",
import_indicators={
"CryptAcquireContextA",
"CryptAcquireContextW",
"CryptEncrypt",
"CryptDecrypt",
"CryptGenRandom",
"CryptHashData",
"CryptCreateHash",
"CryptDestroyHash",
"CryptExportKey",
"CryptImportKey",
"CryptDeriveKey",
"CryptStringToBinaryA",
"CryptBinaryToStringA",
"BCryptOpenAlgorithmProvider",
"BCryptGenerateSymmetricKey",
"BCryptEncrypt",
"BCryptDecrypt",
"NCryptOpenStorageProvider",
"EVP_EncryptInit",
"EVP_DecryptInit",
"EVP_CIPHER_CTX_new",
"AES_set_encrypt_key",
"AES_set_decrypt_key",
"AES_encrypt",
"AES_decrypt",
"SHA256_Init",
"SHA256_Update",
"SHA256_Final",
"MD5_Init",
"MD5_Update",
"MD5_Final",
"RSA_public_encrypt",
"RSA_private_decrypt",
"RSA_generate_key",
"BN_new",
"BN_bin2bn",
"BN_bn2bin",
"EVP_PKEY_new",
},
string_indicators=[
"AES",
"RSA",
"SHA",
"MD5",
"encrypt",
"decrypt",
"cipher",
"crypto",
"ssl",
"tls",
"certificate",
"public key",
"private key",
"BEGIN RSA",
"BEGIN CERTIFICATE",
],
section_indicators=[".crypto", ".ssl"],
),
CapabilityRule(
name="networking",
category="communication",
description="Indicators of network communication (HTTP, sockets, DNS)",
import_indicators={
"WinHttpOpen",
"WinHttpConnect",
"WinHttpOpenRequest",
"WinHttpSendRequest",
"WinHttpReceiveResponse",
"WinHttpReadData",
"WinHttpWriteData",
"WinHttpCrackUrl",
"InternetOpenA",
"InternetOpenW",
"InternetConnectA",
"InternetConnectW",
"HttpOpenRequestA",
"HttpOpenRequestW",
"HttpSendRequestA",
"HttpSendRequestW",
"URLDownloadToFileA",
"URLDownloadToFileW",
"socket",
"connect",
"send",
"recv",
"sendto",
"recvfrom",
"bind",
"listen",
"accept",
"WSAStartup",
"WSACleanup",
"WSASocketA",
"WSASocketW",
"getaddrinfo",
"freeaddrinfo",
"gethostbyname",
"inet_addr",
"inet_ntoa",
"htons",
"htonl",
"ntohs",
"ntohl",
"setsockopt",
"getsockopt",
"select",
"poll",
"epoll_create",
"epoll_ctl",
"DnsQuery_A",
"DnsQuery_W",
"getnameinfo",
"getservbyname",
},
string_indicators=[
"http://",
"https://",
"ftp://",
"ws://",
"wss://",
".com",
"www.",
"user-agent",
"content-type",
"GET ",
"POST ",
"Mozilla/",
"socket",
"port",
"proxy",
"dns",
"ip address",
],
section_indicators=[".net", ".socket"],
),
CapabilityRule(
name="file-system",
category="system",
description="Indicators of file system operations (read, write, delete, enumerate)",
import_indicators={
"CreateFileA",
"CreateFileW",
"OpenFile",
"ReadFile",
"WriteFile",
"DeleteFileA",
"DeleteFileW",
"MoveFileA",
"MoveFileW",
"CopyFileA",
"CopyFileW",
"FindFirstFileA",
"FindFirstFileW",
"FindNextFileA",
"FindNextFileW",
"FindClose",
"GetFileAttributesA",
"GetFileAttributesW",
"SetFileAttributesA",
"SetFileAttributesW",
"GetFileSize",
"GetFileSizeEx",
"SetFilePointer",
"SetEndOfFile",
"CreateDirectoryA",
"CreateDirectoryW",
"RemoveDirectoryA",
"RemoveDirectoryW",
"GetTempPathA",
"GetTempPathW",
"GetTempFileNameA",
"GetTempFileNameW",
"SHGetFolderPathA",
"SHGetFolderPathW",
"SHGetKnownFolderPath",
},
string_indicators=[
"C:\\",
"/home/",
"/etc/",
"/var/",
"/tmp/",
"/usr/",
"\\Windows\\",
"\\System32\\",
"Program Files",
"ProgramData",
"AppData",
".exe",
".dll",
".sys",
".dat",
".cfg",
".ini",
".xml",
".json",
"/etc/passwd",
"/etc/shadow",
],
section_indicators=[".fs", ".fileio"],
),
CapabilityRule(
name="process-injection",
category="security",
description="Indicators of code/process injection techniques",
import_indicators={
"VirtualAlloc",
"VirtualAllocEx",
"VirtualProtect",
"VirtualProtectEx",
"WriteProcessMemory",
"CreateRemoteThread",
"NtCreateThreadEx",
"RtlCreateUserThread",
"QueueUserAPC",
"NtQueueApcThread",
"SetThreadContext",
"MapViewOfFile",
"NtMapViewOfSection",
"UnmapViewOfFile",
"OpenProcess",
"NtOpenProcess",
"ZwOpenProcess",
"ReadProcessMemory",
"NtReadVirtualMemory",
},
string_indicators=[
"inject",
"suspend",
"resume thread",
"shellcode",
"payload",
"remote thread",
],
section_indicators=[".inject"],
),
CapabilityRule(
name="persistence",
category="security",
description="Indicators of persistence mechanisms (registry, services, startup)",
import_indicators={
"RegCreateKeyExA",
"RegCreateKeyExW",
"RegSetValueExA",
"RegSetValueExW",
"RegDeleteKeyA",
"RegDeleteKeyW",
"RegOpenKeyExA",
"RegOpenKeyExW",
"RegQueryValueExA",
"RegQueryValueExW",
"RegCloseKey",
"CreateServiceA",
"CreateServiceW",
"StartServiceA",
"StartServiceW",
"OpenSCManagerA",
"OpenSCManagerW",
"ChangeServiceConfigA",
"ChangeServiceConfigW",
"DeleteService",
"ControlService",
},
string_indicators=[
"HKEY_",
"Software\\Microsoft\\Windows\\CurrentVersion\\Run",
"Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce",
"\\Registry\\",
"HKLM\\",
"HKCU\\",
"HKCR\\",
"HKU\\",
"HKCC\\",
"HKPD\\",
"SERVICE_",
"sc start",
"sc create",
"schtasks",
"crontab",
"systemd",
"launchd",
"startup",
"autorun",
],
section_indicators=[".persist"],
),
CapabilityRule(
name="anti-analysis",
category="security",
description="Indicators of anti-debugging, anti-VM, and analysis evasion",
import_indicators={
"IsDebuggerPresent",
"CheckRemoteDebuggerPresent",
"NtQueryInformationProcess",
"NtSetInformationThread",
"DebugActiveProcess",
"DebugActiveProcessStop",
"OutputDebugStringA",
"OutputDebugStringW",
"GetTickCount",
"GetTickCount64",
"QueryPerformanceCounter",
"RDTSC",
"NtQuerySystemInformation",
"NtQueryObject",
"FindWindowA",
"FindWindowW",
"GetForegroundWindow",
"EnumWindows",
},
string_indicators=[
"debug",
"debugger",
"ollydbg",
"ida",
"x64dbg",
"x32dbg",
"immunity",
"windbg",
"vmware",
"virtualbox",
"vbox",
"qemu",
"xen",
"hyper-v",
"sandbox",
"syser",
"procmon",
"process monitor",
"wireshark",
"frida",
],
section_indicators=[".anti", ".obfuscated"],
),
CapabilityRule(
name="process-management",
category="system",
description="Indicators of process creation, termination, and management",
import_indicators={
"CreateProcessA",
"CreateProcessW",
"CreateProcessAsUserA",
"CreateProcessAsUserW",
"TerminateProcess",
"ExitProcess",
"GetExitCodeProcess",
"OpenProcess",
"CloseHandle",
"WaitForSingleObject",
"WaitForMultipleObjects",
"GetProcessId",
"GetCurrentProcessId",
"CreateToolhelp32Snapshot",
"Process32First",
"Process32Next",
"EnumProcesses",
"NtCreateProcess",
"NtTerminateProcess",
"ZwCreateProcess",
"ZwTerminateProcess",
"ShellExecuteA",
"ShellExecuteW",
"ShellExecuteExA",
"ShellExecuteExW",
"system",
"popen",
"execve",
"execvp",
"fork",
"clone",
"posix_spawn",
},
string_indicators=[
"cmd.exe",
"powershell",
"wscript",
"cscript",
"rundll32",
"regsvr32",
"mshta",
"certutil",
"bitsadmin",
"wmic",
"msiexec",
"/bin/sh",
"/bin/bash",
],
section_indicators=[".proc"],
),
CapabilityRule(
name="memory-management",
category="system",
description="Indicators of memory allocation, protection, and manipulation",
import_indicators={
"malloc",
"calloc",
"realloc",
"free",
"memset",
"memcpy",
"memmove",
"memcmp",
"VirtualAlloc",
"VirtualFree",
"VirtualProtect",
"HeapAlloc",
"HeapFree",
"HeapCreate",
"HeapDestroy",
"LocalAlloc",
"LocalFree",
"GlobalAlloc",
"GlobalFree",
"mmap",
"munmap",
"mprotect",
"brk",
"sbrk",
},
string_indicators=["heap", "stack", "memory", "alloc", "buffer"],
section_indicators=[],
),
CapabilityRule(
name="keylogging",
category="security",
description="Indicators of keyboard/mouse input monitoring",
import_indicators={
"SetWindowsHookExA",
"SetWindowsHookExW",
"UnhookWindowsHookEx",
"CallNextHookEx",
"GetAsyncKeyState",
"GetKeyState",
"GetKeyboardState",
"GetRawInputData",
"GetRawInputBuffer",
"RegisterRawInputDevices",
"SetWinEventHook",
"UnhookWinEvent",
},
string_indicators=["keylog", "keystroke", "keyboard", "hook", "input capture"],
section_indicators=[".hook"],
),
CapabilityRule(
name="privilege-escalation",
category="security",
description="Indicators of privilege escalation and token manipulation",
import_indicators={
"OpenProcessToken",
"AdjustTokenPrivileges",
"LookupPrivilegeValueA",
"LookupPrivilegeValueW",
"DuplicateToken",
"DuplicateTokenEx",
"ImpersonateLoggedOnUser",
"RevertToSelf",
"CreateProcessAsUserA",
"CreateProcessAsUserW",
"RtlAdjustPrivilege",
"SeDebugPrivilege",
"SeTakeOwnershipPrivilege",
"AllocateAndInitializeSid",
"CheckTokenMembership",
"setuid",
"setgid",
"seteuid",
"setegid",
},
string_indicators=[
"SeDebugPrivilege",
"SeTakeOwnershipPrivilege",
"SeBackupPrivilege",
"SeRestorePrivilege",
"SeTcbPrivilege",
"SeCreateTokenPrivilege",
"sudo",
"root",
"Administrator",
"SYSTEM",
"TokenElevation",
"admin",
"privilege",
],
section_indicators=[".priv"],
),
CapabilityRule(
name="data-exfiltration",
category="security",
description="Indicators of data collection and exfiltration",
import_indicators={
"WinHttpSendRequest",
"HttpSendRequestA",
"HttpSendRequestW",
"InternetWriteFile",
"send",
"sendto",
"WriteFile",
"WriteFileEx",
"FtpPutFileA",
"FtpPutFileW",
"FtpOpenFileA",
"FtpOpenFileW",
"URLDownloadToFileA",
"URLDownloadToFileW",
},
string_indicators=[
"upload",
"exfil",
"exfiltrate",
"steal",
"collect",
"archive",
"compress",
"zip",
"tar",
"gzip",
".7z",
".rar",
"base64",
"post /",
"multipart",
"content-disposition",
],
section_indicators=[".exfil"],
),
CapabilityRule(
name="service-management",
category="system",
description="Indicators of Windows service and driver management",
import_indicators={
"OpenSCManagerA",
"OpenSCManagerW",
"CreateServiceA",
"CreateServiceW",
"StartServiceA",
"StartServiceW",
"ControlService",
"DeleteService",
"CloseServiceHandle",
"ChangeServiceConfigA",
"ChangeServiceConfigW",
"QueryServiceStatus",
"QueryServiceConfigA",
"QueryServiceConfigW",
},
string_indicators=[
"sc.exe",
"net start",
"net stop",
"svchost",
"services.exe",
"\\.\\",
"\\Device\\",
"DRIVER_",
".sys",
"driver",
"kernel",
],
section_indicators=[".driver", ".service"],
),
CapabilityRule(
name="screenshot-capture",
category="surveillance",
description="Indicators of screen capture and desktop monitoring",
import_indicators={
"GetDC",
"GetWindowDC",
"CreateCompatibleDC",
"CreateCompatibleBitmap",
"BitBlt",
"StretchBlt",
"GetDIBits",
"SelectObject",
"DeleteDC",
"ReleaseDC",
"GdiplusStartup",
"GdipCreateBitmapFromHBITMAP",
"GdipSaveImageToStream",
},
string_indicators=["screenshot", "screen", "capture", "desktop", "gdi", "bitmap"],
section_indicators=[".capture"],
),
CapabilityRule(
name="audio-capture",
category="surveillance",
description="Indicators of audio/microphone capture",
import_indicators={
"waveInOpen",
"waveInPrepareHeader",
"waveInAddBuffer",
"waveInStart",
"waveInStop",
"waveInReset",
"waveInClose",
"waveInGetNumDevs",
"waveInGetDevCapsA",
"waveInGetDevCapsW",
"midiInOpen",
"midiInStart",
"DirectSoundCaptureCreate",
"DirectSoundCaptureEnumerateA",
"DirectSoundCaptureEnumerateW",
},
string_indicators=["microphone", "audio", "record", "wave", "pcm", "sound", "listen"],
section_indicators=[".audio"],
),
CapabilityRule(
name="clipboard-access",
category="surveillance",
description="Indicators of clipboard monitoring and manipulation",
import_indicators={
"OpenClipboard",
"CloseClipboard",
"GetClipboardData",
"SetClipboardData",
"EmptyClipboard",
"IsClipboardFormatAvailable",
"EnumClipboardFormats",
"RegisterClipboardFormatA",
"RegisterClipboardFormatW",
"GetClipboardSequenceNumber",
"AddClipboardFormatListener",
"RemoveClipboardFormatListener",
},
string_indicators=["clipboard", "paste", "copy", "cut"],
section_indicators=[".clipboard"],
),
]
# ---------------------------------------------------------------------------
# Capability map result
# ---------------------------------------------------------------------------
@dataclass
class CapabilityResult:
"""A single capability suggestion.
Attributes:
name: Functional area name (e.g., "cryptography", "networking").
confidence: Confidence level from the Confidence enum (never unconditional certainty).
evidence: List of concrete evidence items, each referencing a source
(e.g., import: "CreateFileW", string: "/etc/passwd", section: ".text").
"""
name: str
confidence: Confidence
evidence: list[dict[str, Any]]
# ---------------------------------------------------------------------------
# Capability map engine
# ---------------------------------------------------------------------------
class CapabilityMapEngine:
"""Evaluates capability mapping rules against backend data.
Scans the binary's imports, strings, and sections for patterns
matching known functional capability rules. Each result is a
rule-derived indicator, not verified functional proof.
Evidence items reference concrete sources (imported APIs, strings,
section names/patterns). Confidence values are used rather than
unconditional certainty/verified fields.
"""
def __init__(self, adapter: BackendAdapter, binary: Binary) -> None:
self._adapter = adapter
self._binary = binary
self._rules: list[CapabilityRule] = []
def run(self, limit: int = 100) -> tuple[list[CapabilityResult], int]:
"""Evaluate all capability rules against binary data.
Args:
limit: Maximum number of capability results to return.
Returns:
Tuple of (capabilities, total_capabilities) where capabilities is the
list of CapabilityResult entries (bounded by limit) and
total_capabilities is the original total count before slicing
(used for accurate truncation warnings).
"""
self._load_rules()
# Collect backend data
try:
imports = self._adapter.get_imports(self._binary)
except Exception:
imports = []
try:
strings = self._adapter.get_strings(self._binary)
except Exception:
strings = []
try:
sections = self._adapter.get_sections(self._binary)
except Exception:
sections = []
# Build lookup sets
imported_symbols: set[str] = {imp.symbol for imp in imports}
string_texts: list[str] = [s.text for s in strings]
section_names: set[str] = {s.name for s in sections}
results: list[CapabilityResult] = []
for rule in self._rules:
evidence: list[dict[str, Any]] = []
# Check import indicators
for api in sorted(rule.import_indicators):
if api in imported_symbols:
evidence.append({"import": api})
# Check string indicators
for pattern in rule.string_indicators:
pattern_lower = pattern.lower()
for text in string_texts:
if pattern_lower in text.lower():
evidence.append({"string": text})
break # one match per pattern is enough
# Check section indicators
for section_pattern in rule.section_indicators:
for section_name in section_names:
if section_pattern.lower() in section_name.lower():
evidence.append({"section": section_name})
break
if not evidence:
continue
# Compute confidence based on evidence diversity and count
evidence_count = len(evidence)
import_count = sum(1 for e in evidence if "import" in e)
string_count = sum(1 for e in evidence if "string" in e)
section_count = sum(1 for e in evidence if "section" in e)
# Diverse evidence across sources = higher confidence
sources_used = bool(import_count) + bool(string_count) + bool(section_count)
if evidence_count >= 10 and sources_used >= 2:
confidence = Confidence.HIGH
elif evidence_count >= 5:
confidence = Confidence.MEDIUM
elif evidence_count >= 1:
confidence = Confidence.LOW
else:
confidence = Confidence.UNKNOWN
results.append(
CapabilityResult(
name=rule.name,
confidence=confidence,
evidence=evidence[:50], # Cap evidence to keep output bounded
)
)
total_capabilities = len(results)
return results[:limit], total_capabilities
def _load_rules(self) -> None:
"""Load all capability rule definitions."""
self._rules = _default_capability_rules()
@property
def total_rules(self) -> int:
"""Total number of capability rules."""
if not self._rules:
self._load_rules()
return len(self._rules)
@@ -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
@@ -0,0 +1,469 @@
"""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)