mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-13 04:26:28 +03:00
fix: relocate binary analysis skill
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user