Malware
A four-tier classifier that answers a single question about a compiled binary: is this malicious?
4 tiers · 19 semantic detectors across 7 threat categories · 0 false positives on the benign baseline · score-and-promote composition over the engine's other security surfaces.
The classifier is deterministic and reads only static facts the engine already produces. No execution, no sandbox, no hash lookup, no ML. Weights and thresholds are hand-tuned against a measured corpus and pinned in tests so a regression that loosens separation reds CI.
The four tiers
| Tier | Score | What it means |
|---|---|---|
| Benign | < 15 | No suspicious composition. The dominant case for legitimate software. |
| Suspicious | 15 – 34 | Composition suggests caution. Pen-testing utilities, dual-use tooling, and aggressive ad-tech live here legitimately. |
| LikelyMalicious | 35 – 69 | Composition matches malware shape. Worth analyst review. |
| Malicious | ≥ 70 | Composition is decisive. Multiple independent malware patterns at once. |
The boundaries (15, 35, 70) are the lowest values that hold zero false positives on the benign baseline while keeping every malware sample at Suspicious or above.
Promotion rule. Any single semantic finding fires the binary into at least LikelyMalicious, even if the numeric score is below 35. The semantic detectors are precision-floor by design — each fires only when a multi-primitive shape resolves at a specific call site. A process_hollowing hit on a binary that otherwise looks clean is the canonical case: aggregate signals are quiet because the binary does one thing, but the one thing is unambiguous.
Negative clamp. Some signals subtract points (a binary with Hardened Runtime and a valid Developer ID signature loses points). The clamp floors scores at zero before the tier lookup so a heavily-scrubbed binary can't underflow the bucket.
The seven threat categories
| Category | Detectors | Weight range | What it catches |
|---|---|---|---|
| Persistence | 3 | 40–50 | Auto-start mechanisms across macOS, Linux, Windows |
| Code injection and execution | 5 | 50–75 | Process hollowing, reflective load, API-hash resolvers, ObjC swizzling, dyld injection |
| Surveillance and TCC bypass | 4 | 40–65 | Keystroke capture, screen capture, clipboard, TCC subversion |
| Credential theft | 1 (4 shapes) | 45–55 | Four independent keychain-dump routes |
| C2 and network evasion | 3 | 40–45 | Hardcoded callback URIs, SSL-pinning bypass, bundle-ID masquerade |
| Process reconnaissance | 1 | 35 | Linux /proc walks and macOS process-enumeration imports |
| Low-level execution | 2 | 40 | LOLBin command literals and resolved subprocess arguments |
Each detector walks the SSA-form IL, the resolved call graph, and constant-propagation results, and fires only when its multi-primitive pattern resolves at a specific call site. Weights are per-hit.
Persistence
- persistence_run_key (50) — Windows registry-write API + literal
\Run\or\RunOnce\key path - persistence_launch_agent (40) — macOS LaunchAgent or LaunchDaemon path string + a write or spawn primitive that can drop the plist
- scheduled_task (45 / 45) —
cronpath +crontabcommand + spawn/write; orlaunchctl bootstrapliteral + spawn
Code injection and execution
- dyld_inject (50 / 60) —
DYLD_INSERT_LIBRARIESliteral + an environment-mutation primitive; or a Mach VM injection chain (task_for_pid+mach_vm_write/mach_vm_allocate+ thread-state setter) - process_hollowing (50 / 65 / 75 by API hit count) — three of five Windows hollowing primitives in the same function:
CreateProcesswithCREATE_SUSPENDED,NtUnmapViewOfSection,WriteProcessMemory,SetThreadContext,ResumeThread; 3 APIs → 50, 4 APIs → 65, 5 APIs → 75 - reflective_loader (50 / 70) — an RWX allocation + a
memcpyinto that allocation in one function; 50 base, 70 when an indirect call lands inside the allocation - api_hash_resolver (60) — a function with a loop, a hash-accumulator rotation or CRC, and an indirect call to a hash-resolved address — the shape implants use to avoid leaving imported symbols visible
- objc_swizzling (50) — Objective-C method-table manipulation (swizzle or replace) +
NSClassFromStringorNSSelectorFromString
Surveillance and TCC bypass
- keylogger_cgevent (60 / 65) —
CGEventTapCreate+ callback + loop-source; orIOHIDManager+ input-value callback - screen_capture (55 / 60) —
CGWindowListCreateImageorCGDisplayCreateImage+ a write; orCGDisplayStreamorAVCaptureScreenInput+ a write - clipboard_steal (40) —
NSPasteboardaccessor + a data-read selector + a network or write primitive in the same function - tcc_bypass (60 / 45) — direct
TCC.dbpath + a file-write primitive; or AppleScriptSystem Eventskeystroke + a spawn primitive
Credential theft
- keychain_dump (55 / 45 / 50 / 50) — four independent routes: direct keychain database file access; broad
Security.frameworkenumeration viakSecMatchLimitAll; thesecurityCLI dump; thesecurity find-internet-passwordshape
C2 and network evasion
- c2_endpoint_uri (40) — hardcoded HTTP, HTTPS, or WebSocket URI template with format-string placeholders (
http://%s/%s) - ssl_pinning_manipulation (40) —
SecTrustSetAnchorCertificatesor a chain-validation disable + an HTTP-client capability - bundle_id_masquerade (45) — ad-hoc-signed Mach-O with a
com.apple.*bundle ID or/System/Library/install path
Process reconnaissance
- proc_discovery (35 / 35) —
/proc/*path templates + a read primitive on Linux; orproc_listpidsandproc_pidinfoimports on macOS
Low-level execution
- lolbin_osascript (40) — LOLBin command-line literal (
osascript,bash -c,defaults write,curl …,chmod +x) + a subprocess-spawn primitive - lolbin_subprocess (40) — a subprocess-invocation call where the argument resolves via RIP-relative constant propagation to a LOLBin token
Score composition (aggregation rules)
In addition to the semantic detectors above, five families of shallow signals accumulate evidence. None alone fires the tier, but they add up — and the indicator-category and consistency hits in particular are where most benign binaries earn their few points.
| Signal | Weight | Why it scores |
|---|---|---|
| Loader density | 20 if ≥ 0.01 calls/KB, 10 if ≥ 0.005 | Real packers and reflective loaders trip the upper bucket; legitimate plugin hosts trip the lower |
| Max section entropy | 15 if any section ≥ 7.95 bits/byte | The packed-payload signal |
| Embedded carrier in overlay | 15 | Embedded Mach-O, ELF, or PE in the overlay region — the dropper signal |
| Indicator-category hits | 10/Communication, 10/Collection, 15/AntiAnalysis, 25/MalwareFamily | Persistence, Crypto, DataManipulation score zero — they fire equally on benign and malicious code |
| Consistency divergences | 30 ad-hoc-privileged or bundle-ID impersonation; 20 private entitlement on non-Apple; 15 capability used without entitlement; 10 capability via dlopen; 5 declared-but-unused | Ordered by how decisive the inconsistency is |
Corpus and evaluation
Thresholds are pinned from measurement on:
- Malware: 1,417 samples (PE32+ and ELF64 focus; macOS-only sample set seeded v1)
- Benign:
curl,jq,ripgrepon macOS arm64 and Linux x86_64; Apple daemons (airportd,configd); Apple utilities. The benign set includes binaries that exercise individual suspicious primitives in legitimate context (curl invokessystem, ripgrepmmaps with execute permissions for its JIT), so the precision floor is not an artifact of an undemanding baseline.
Three hard gates run on every release:
- Zero false positives on benign. Every benign fixture must classify Benign. A single Suspicious-or-above on the benign set reds the gate.
- Floor on malware. Every malware fixture must classify at least Suspicious.
- Median separation. The median malware score minus the median benign score must be ≥ 15 points. Catches drift that doesn't trip the per-fixture gates but erodes the headroom between them.
What it composes over
Malware classification is pure composition over the engine's earlier surfaces. Nothing here reads bytes or walks the IL directly — every signal it sums is already in the analysis output (packing, entropy, embedded carriers, indicator hits, consistency divergences) or in the findings stream (the semantic shape detectors, which are themselves CWE-tagged).
The four security surfaces have a strict dependency order:
analyze → indicators → findings (CWE) → malware (verdict)
A binary that fires CWE-94 (dynamic code via dlopen with a tainted argument) emits a finding to the Findings stream. The same call-site shape that produced the finding also feeds the malware composer, where it gets a weight and contributes to the tier verdict. The finding and the verdict are not duplicates — they are different read-outs of the same fact for different consumers. An auditor reading findings wants the per-CWE detail; a triage pipeline reading malware wants the verdict.
The four surfaces are independent at the consumer level. A binary that scores Malicious can have zero CWE findings (a well-written implant). A binary that scores Benign can have dozens (legacy code that hasn't been audited). A binary with thirty SBOM CVEs can score Benign (a Linux distribution image with old packages). Agreement is informative; disagreement is also informative.
Where this fits
The verdict is one decision per binary. openbinary surfaces it on every binary page; CI pipelines key alerts on tier transitions (a binary that was Benign in v1.0 and is LikelyMalicious in v1.1 is a release-blocker even if no individual finding or indicator changed); the Compare view diffs verdicts across versions of the same package, since a tier promotion across a patch release is the canonical supply-chain compromise signal.
See Findings for the CWE catalog the verdict composes over, Indicators for the capability signals it weighs, and CVEs & SBOM for the parallel version-based surface that runs on the same input.