Sign in

Malware

A 638,976-byte PE32 from MalwareBazaar, sha256 0065b818…a08d64, labelled Medusa. Scored, it produces six rows and one number:

$ openbinary malware-score training/malware/0065b818…/sample

  tier=Malicious score=142
  ----------------------------------------------------------------------
    +8  malware.lolbin_subprocess       binary embeds LOLBin literal "powershell
                                        -executionpolicy bypass -File %s" AND imports
                                        "CreateProcessA" — subprocess-spawn corroboration
   +18  malware.stealth_child_process   site 0x41443e: CreateProcess(NULL, …,
                                        bInheritHandles=TRUE, dwCreationFlags=0x08000000
                                        — CREATE_NO_WINDOW set)
    +8  malware.discovery_local_recon   local-recon command "systeminfo" + spawn
                                        capability "CreateProcessA"
   +45  impact-inhibit-recovery.config  promoted capability impact-inhibit-recovery.config:
                                        Ransomware operator-config key for Volume Shadow
                                        Copy deletion, corroborated by a spawn primitive
                                        or further operator-config keys — inhibits system
                                        recovery (T1490)
   +45  ransom-note.strong              promoted capability ransom-note.strong: A
                                        ransomware announcement phrase plus a payment
                                        channel or a drop-note filename — the full
                                        extortion note (T1486)
   +18  malware.ransom_crypto_loop      bulk file-encryption crypto loop: symmetric
                                        cipher "BCryptEncrypt" + all-drive enumeration
                                        "GetLogicalDriveStringsW" + directory walk
                                        "FindFirstFileExW"/"FindNextFileW"

8 + 18 + 8 + 45 + 45 + 18 = 142. That is the whole computation. The score is the sum of a flat ledger, the tier is a lookup on the sum, and the ledger is the only stored artifact — there is no second, hidden set of weights that the displayed rows summarize. Every row names a rule, a point contribution, and the evidence that earned it: a literal, a call site at 0x41443e, an argument value of 0x08000000.

Not one row on its own says "ransomware." CreateProcessA with CREATE_NO_WINDOW is what every silent updater does. systeminfo is what every support tool runs. The claim lives in the composition, which is why the output is a ledger rather than a label.

The scoring model

The score is the sum of a flat ledger and the tier is a lookup on that sum; there is no second computation.

The tiers

Four tiers partition the score, and the boundaries sit where they do so that no single row of the weakest band can cross one.

TierScoreWhat it means
Benignunder 15No suspicious composition. The dominant case for legitimate software.
Suspicious15 – 34Composition warrants a look. Pen-testing utilities, dual-use tooling, and aggressive ad-tech live here legitimately.
LikelyMalicious35 – 69Composition matches malware shape. Worth analyst review.
Malicious70 and aboveMultiple independent malware patterns at once.

One dual-use string match (8 points) stays Benign, two reach Suspicious, one pattern-level row (18) reaches Suspicious, one proven structural shape (45) lands LikelyMalicious.

A fifth label sits outside the score. NotEnoughEvidence — rendered "Undetermined" — replaces the computed tier when the engine analyzed too little of the binary to stand behind it, so that a scan which saw nothing never reads as a confident clean result. Any one condition trips it:

ConditionMeasured as
Unsupported architecturethe analysis resolved no architecture at all
Likely packedthe packing flag, or a tiny import table with high-entropy code
Nothing decodedzero functions discovered
Almost nothing nameableunder 8 functions discovered and under 5% of them named
The malware pass never completedno assessment block, or an assessment that marked itself unassessed

A real detection survives thin coverage — only the thin-evidence middle is eligible:

VerdictDowngradeable
Maliciousnever
LikelyMalicious carrying a Proven or Definitive rownever
Anomaly-flagged, any tiernever — the independent ML pass is itself evidence
LikelyMalicious reached purely by Presence and Suggestive rowsyes
Suspicious, Benignyes

A missing coverage block reads as sufficient, never as thin, so an older stored analysis is not retroactively downgraded by a field it predates.

Evidence bands

Every ledger row carries one of four bands, and the band fixes both the points and whether the row can set a floor independent of the sum.

BandPointsFloors the tierWhat earns it
Presence8neverPresence of a string, import, or token. Dual-use.
Suggestive18neverA specific pattern or template — a /proc/%d/ format template, a C2 URI shape.
Proven45at least LikelyMaliciousDataflow, an indirect-call site, a proven structural shape (a constant-propagated API sequence, RWX allocation followed by memcpy and jump), or a successfully decrypted family configuration.
Definitive80MaliciousAn unpacked child payload whose own verdict is Malicious — no ledger source emits this band.

Presence and Suggestive rows contribute points and nothing else, so no single dual-use string match can lift a legitimate system binary to LikelyMalicious on its own. The nearest-neighbour family row is banded Suggestive whatever its similarity, so an attribution can never floor a tier.

Five triggers set a floor that the arithmetic cannot undercut — one from the band of a code-shape row, one from an unpacked child, three from outside the ledger's sum:

TriggerFloor
Proven-band code-shape rowat least LikelyMalicious
Definitive-band row — payload promotion onlyMalicious
Malware-family indicator hit — a catalog category hit, not the family-neighbour rowMalicious
Targeting indicator — an Apple impersonation, a team-ID lieat least LikelyMalicious
Anomaly flagat least Suspicious

process_hollowing is the case for the flooring direction. It requires one function to call at least three of five primitives — CreateProcess, NtUnmapViewOfSection, WriteProcessMemory, SetThreadContext, ResumeThread — with CreateProcess's dwCreationFlags constant-propagated to a value carrying CREATE_SUSPENDED (0x4), read out of the mov dword ptr [rsp+0x28], imm32 in the argument-setup window. When that constant cannot be resolved the rule abstains. One such row is 45 points, 25 short of Malicious, but the shape is unambiguous, so the floor carries the verdict where the sum would not.

The corpus-evidence cap

A verdict that crosses 70 on corpus evidence with no intrinsic corroborator is held at LikelyMalicious, because a nearest-neighbour family attribution is a claim about a binary's neighbours in a corpus, not about its bytes. An AmosStealer sample shows it:

$ openbinary malware-score training/malware/0062ec5d…/sample

  tier=LikelyMalicious score=74
  ----------------------------------------------------------------------
   +15  malware.consistency_used_undeclared  capability exhibited without matching
                                             entitlement
   +41  malware.family_match_known           resembles AmosStealer (similarity 0.75)
   +18  malware.opaque_exec_stub             opaque execution stub: imports system but
                                             carries a vestigial __TEXT,__cstring of 26
                                             bytes — commands are reconstructed at
                                             runtime rather than stored as literals
                                             (31 imports, 7496 bytes of __TEXT,__text)

74 is over the Malicious threshold. Remove the 41 corpus points and 33 remain, which is Suspicious; no remaining row carries a flooring band. So the verdict is withheld at LikelyMalicious. The family row stays fully visible at its 41 points — it is suppressed as a conclusion, never deleted as evidence.

The family row's points come from a ramp over the elected family's own best-neighbour similarity multiplied by the share of the top-k vote that family won — a strong-but-split match is discounted so one neighbour cannot ride to the ceiling. The ramp starts at 30 at the promotion threshold of 0.55 and stops at a hard ceiling of 55, which sits deliberately below 70 so an attribution can never solo-cross the top tier at any similarity. Similarity 0.75 with a unanimous vote gives 30 + round((0.75 − 0.55) / 0.45 × 25) = 41.

Evidence derived from emulation is excluded from releasing the cap for the same reason, and the exclusion covers its points as well as its flooring: a 45-point emulation-derived row plus two unrelated dual-use 18-point rows sums to 81, over the threshold, on evidence that is not deductive.

Suppression

Trust is expressed as abstention rather than as a discount, because no ledger row carries negative points — a Hardened Runtime and a Developer ID signature buy nothing against the sum. Platform-signed means one thing: a Mach-O whose parsed PKCS#7 chain anchors to an Apple root CA. A bare AppleSigned status does not qualify — absent a CMS blob it derives from the code directory's platform byte and the reserved team ID 0000000000, two single attacker-controlled values a repackager can forge, where an anchored chain cannot be. Developer ID third parties anchor to an Apple root too, and are excluded. On a binary that does qualify:

A Microsoft-signed PE gets none of this. The Authenticode signer CN is an unvalidated string and no certificate-chain validation exists on the PE path — a self-signed leaf named "Microsoft Windows" buys exactly what a real one buys. A separate check confirms the signature is mathematically valid under the certificate it names, which says nothing about whether that certificate chains to a trusted root. The PE path suppresses nothing, which is why the two Windows false positives below are not spared.

An abstention expressed as a weight can be outvoted by enough other weights. An abstention expressed as absence cannot.

Ledger sources

Twenty-three registered sources each read the shared analysis context and emit rows.

Semantic code-shape scanners — the largest source: 46 scanners, one per malicious primitive. Persistence run keys and launch agents, LOLBin subprocess spawning, reflective loading, process hollowing, API-hash resolution, keylogging, credential and keychain dumping, ELF and Linux implant shapes. Ransomware crypto loops are not among them — that is its own registered source, and it produced the last row of the Medusa ledger above.

Most of the 46 match at a specific call site and report its address; several are file-scope rules over strings and the import table with no call site to anchor, and their detail says so — discovery_local_recon matches a recon command literal plus any spawn capability, and reports the same "systeminfo"-plus-CreateProcess pairing, addressless, on both the Medusa sample and ripgrep/windows-x86_64.exe. Each scanner runs behind its own panic boundary, so a crafted input that crashes one costs that scanner's findings rather than the whole verdict — which would otherwise fall back to an empty assessment, indistinguishable from Benign. When a scanner does crash, the assessment is marked incomplete and reported as NotEnoughEvidence, and a zero-point marker row stays in the ledger recording the gap.

macOS behaviour families — eight further sources: evasion, implant shapes, credential theft, host recon, session detach, process primitives, Objective-C behaviours, and foreign-toolchain provenance. Their number reflects where the corpus is: 1,336 of 1,729 samples are Mach-O.

Whole-binary signals — fixed weights over facts about the image as a whole:

SignalPointsWhy it scores
Dynamic-code finding density20 at 0.01 findings per KB of code, 10 at 0.005; +10 more when the 0.01 band fires and code is under 500 KBReal packers and reflective loaders trip the upper bucket; legitimate plugin hosts trip the lower. The +10 shape row never pairs with the 10-point band — small code alone is not a signal. Abstains entirely when no code-section denominator resolves: flooring that denominator to 1 KB manufactured a loader signal on every stripped, section-merged ELF
Likely-packed shape18 (Suggestive)Either arm fires it: a tiny import table with high-entropy code — the UPX/Themida silhouette — or a maximum section entropy above 7.7 bits/byte carried by a .text-family section. The second arm is the custom-packer catch, so a binary with a full import table and a high-entropy .text fires this row too
Max section entropy at or above 7.95 bits/byte15Computed over payload-eligible sections only. Compressed DWARF (.zdebug_*) and zero-fill .bss are excluded: all four committed go build hello-world fixtures report a maximum above 7.996 attributable solely to those two classes
Embedded carrier in the overlay15 per carrier, capped at 45Any structurally validated carrier past the last section — the dropper signal. Validation is header-valid rather than magic-only, but the format is unconstrained: a gzip or zip blob scores the same 15 as an embedded PE. The validation requirement and the cap are what stopped a FreeBSD ps from scoring 705
Indicator-category hits10 Communication, 10 Collection, 15 AntiAnalysis, 25 MalwareFamily, 25 Targeting, each multiplied by unique rules and capped at 455 of the taxonomy's 11 categories score. Persistence, Data-manipulation, Host-interaction and Load-code score zero individually — they fire equally on benign and malicious code — and re-enter only through the co-occurrence rules below. Impact and Exploitation — wiping, shadow-copy deletion, local privilege-escalation primitives — score zero with no co-occurrence path either, and reach the verdict only where a promoted capability or a code-shape scanner covers the same fact

Co-occurrence rules — the only route back for the four dropped indicator categories, and the source emits at most one row per binary. Five rules are tried in a fixed order and the first match wins:

RuleShapeBand
Autonomous botingress transfer + persistence + C2 networkProven
Staged implantdiscovery + ingress transfer + persistenceProven
Persistent evasive network processpersistence + defence evasion + a raw socket or an outbound IP literalProven
IoT/DDoS botscanning + ingress transfer + raw socket (T1046 + T1105 + T1095)Suggestive
Discovery with persistencediscovery + persistence, no network or ingressSuggestive

Persistence is the load-bearing predicate: every rule but the IoT/DDoS one requires it, and a benign CLI tool installs none. That rule is banded Suggestive for exactly that reason — a persistence-less scanner is offence/defence-ambiguous, so it must never solo-floor a weak-credential auditor to LikelyMalicious. Its three-family triad is the replacement backstop: nmap scans and raw-sockets but never fetches, and a legitimate busybox fetches and may raw-socket but embeds no credential-gated scan. The whole source goes inert on any binary whose library matching resolves a dominant, high-confidence identity to a known benign tool, so one recognized library function is not enough to switch it off.

Decrypted configuration — one source scores what a decryptor recovered rather than what the file stores in the clear. AmosStealer encrypts its strings with a per-sample custom hex-plus-Base64 cipher; recovering the alphabet and decrypting is the discriminator, not the wallet and browser paths it yields, which a legitimate wallet app also carries in plaintext. Where it succeeds:

PopulationDecrypts
AmosStealer, ciphertext directly in __TEXT,__cstring13
AmosStealer, ciphertext reassembled from __TEXT,__const tables by an obfuscated literal builder504
AmosStealer, all 684 labelled corpus samples517
previously unlabelled corpus samples, newly attributed31
benign third-party and system Mach-O0 of 6,730

It emits a Proven, tier-flooring row for the family-and-cipher proof and a typed Communication row for each recovered C2 endpoint. The recovered plaintext is routed onto a decrypted-only string surface that the capability catalog matches separately, so a benign browser's cleartext copy of the same paths can never trip those rules. The AmosStealer sample in the corpus-cap example above is one where this source does not fire.

Entitlement and signing divergences — ordered by how decisive the inconsistency is:

DivergencePointsSuppressed on platform-signed binaries
Ad-hoc-signed with privileged entitlements30no
Bundle-ID impersonation30no
com.apple.private.* on a non-Apple binary20no
Capability exercised without a matching entitlement15yes
Capability resolved via dlopen10no
Entitlement declared but unused3yes

Patched Go function-name magic — one source scores a four-byte edit. Go's function-name table opens with a magic word that only selects a version layout — the runtime finds its own table through a separate structure and never reads it — so overwriting those four bytes leaves a working program while every magic-keyed tool reports the binary as stripped and loses all Go symbols. Structural recovery validates the header's padding, quantum and pointer size instead of trusting the magic, and records the mismatch.

The measurement runs only over corpus samples that parse as a real executable — a byte-substring test for Go markers alone also matches JavaScript, TypeScript declarations and Electron archives, and must not be used. Of the 1,407 that qualified, 115 carry Go runtime markers and 33 of those have a patched magic. Across 2,832 real executables installed on a developer machine — the whole Go 1.25.3 toolchain plus everything under /opt/homebrew, /usr/local, /usr/bin, /usr/libexec and /Applications — 23 are Go and every one has an intact magic. No toolchain, linker or packer produces this shape.

Capability promotion

Capability facts reach the verdict through exactly one door: a curated table of 35 capability ids, 34 of them the replacements for 17 retired hand-written Rust detectors, each promoted id carrying the exact band its retired detector carried. Every id not in that table stays enrichment-only. The two 45-point rows in the Medusa ledger above are promoted capabilities.

The door is narrow because the Capabilities catalog fires on benign software by design — it is an enrichment surface, and scoring it uniformly would reopen every false-positive path the band system closes.

The table is frozen to new promotions. A promotion is a Rust-to-TOML refactor that changes no verdict by construction — that equivalence is the retirement gate — while a looser rule can false-floor a benign binary the tighter detector would have spared, and there is no standing corpus gate in CI to catch it, so the remaining detectors stay in Rust. The 35th id is the exception, and it is the impact-inhibit-recovery.config row at the top of this page: the family's two existing command-literal shapes scored 0 of the corpus's 21 Medusa samples, which drive shadow-copy deletion from an operator-config key rather than an embedded command line. It closes a measured recall hole instead of re-expressing an existing detector's facts, and it is fenced by a test pinning two firing shapes — the config key with a spawn import, and two or more corroborating config keys with no spawn import at all (the sample that resolves CreateProcess dynamically) — against three silent ones: a bare token, exactly one corroborating key, and a clean baseline carrying the spawn import and nothing else.

A second config-token rule was authored and left unscored. impact-service-kill.config is the T1489 sibling — the operator-config vocabulary for "kill these services, kill these processes" — and covers the same blind spot from the other side: the command-literal forms are near-absent from this corpus (taskkill / in 1 sample of 1,416, net stop in 1, sc stop in 0), while two or more distinct config keys are carried by 22 samples (21 Medusa PEs and one LockBit macOS locker) and 0 of 113 benign fixtures. It is measured and fenced by its own gating test, and its id is not in the promotion table — so it fires as enrichment and contributes 0 points.

The static-library guardrail

Capabilities never fire inside code that FLIRT or signature-database matching identified as a statically linked library, and promoted rows inherit that guardrail. It has a measured blind spot on Go.

The Go toolchain compiles its runtime and standard library into the same image as main, so no signature matches: library matching names 0 of 1,552 functions in hello-go1.18-linux-amd64 and 0 of 2,337 in hello-go1.25-darwin-arm64. Every capability the Go runtime trips is therefore attributed to the program's author — data-manipulation.decrypt-data-using-aes-via-x86-extensions landing on runtime.memhash32, anti-analysis.check-for-software-breakpoints on crypto/sha1.blockAVX2.abi0. On hello-go1.16-linux-386, 2,089 of 2,107 capability rows are pure toolchain code.

A separate toolchain-origin pass relabels them as library-provided: across the whole benign fixture tree it moves 2,102 rows, every one on the three symbol-bearing Go fixtures, adding and removing none. On the malware side it was replayed against every Go sample in the corpus, spanning 19 distinct families, and flips zero rows in the promoted-capability table — the only path from a capability to the verdict — so no scoring evidence is lost. What it does relabel there is attribution noise: on Mach-O, Go routes syscalls through runtime.*_trampoline stubs, so communication.connect-socket had been landing on runtime.pthread_attr_getstacksize_trampoline.

Packing

Where the pipeline can natively unpack — UPX today — the payload is extracted, analyzed as its own binary, and its evidence promoted into the parent's verdict, so the sample is judged by what it drops rather than by the stub's opacity. That promotion is the one place a Definitive row is produced: a child whose own verdict is Malicious floors the parent to Malicious, and floor-bearing child rows pass through preserving their evidence class, so an emulation-derived child signal stays emulation-derived on the parent instead of being laundered into a deductive proof.

Where the pipeline cannot unpack, the verdict abstains rather than concluding. The packed flag alone trips the thin-coverage gate, so the label becomes NotEnoughEvidence — the 18-point entropy row stays in the ledger as the reason, but it is not the answer. A packed sample carries a label again only when payload promotion supplies a real floor: a Malicious child, a pass-through Proven or Definitive child row, a classified C2 beacon, or the mangled-UPX corroboration of a specific child signal. A points-only accumulation, even one reaching LikelyMalicious by sum, never defeats the abstention.

A second, generic unpacker also runs: an emulated entry-point driver executes the stub and dumps what it writes, on all three formats. Nothing reads the result — no malware source, no detector and no interface consumes those dumps — so they are extracted and discarded, and the abstention above stands regardless of what the emulator recovered. See Unpacking.

Corpus and benign fixtures

The malware corpus is 1,729 samples, gitignored — real samples are never committed. By file magic:

FormatSamplesNote
Mach-O1,336881 of them universal binaries
PE376
ELF1
Scripts and text droppers16

By label — the largest groups, with smaller labelled families not enumerated:

LabelSamples
AmosStealer684
AgentTesla27
Medusa21
Heodo20
Formbook20
Unlabelled328

The skew matters when reading any aggregate number off this corpus: it is a macOS-stealer corpus with a Windows tail, not a balanced sample of malware.

The benign precision set is six committed fixtures: curl, jq, and ripgrep, each as linux-x86_64 and windows-x86_64.exe. The Linux three are statically linked and stripped — no import table at all, which is exactly the shape that defeats import-based heuristics. aarch64 fixtures are deliberately out of scope: ripgrep/linux-aarch64 trips the loader-density rule, and cross-architecture recalibration has not been done.

Test gates

Four gates guard the scoring, and the tier-asserting one covers two Linux binaries.

GateWhat it assertsWhat it covers
Corpus recallevery parseable corpus sample reaches at least Suspiciousskipped by default — the corpus is absent from CI, so this is a real gate only on a provisioned box
Per-invocation benignthe 46 code-shape scanners emit zero rows, the tier stays Benign, the scan completed rather than aborting a scanner, and no enrichment-only capability id leaked into the ledgertwo binaries: curl and jq at linux-x86_64, plus ripgrep/linux-x86_64 for two rule ids only
Windows-fixture rule silencethe PE timestamp-anomaly rule and the five lateral-movement techniques emit nothingcurl, jq and ripgrep at windows-x86_64.exe — those rules only, never the tier
Capability coverage floora capability that used to fire on a benign fixture and no longer does fails; new capabilities firing never dothe capability layer over benign fixtures

The per-invocation gate is narrower than the six-fixture precision set. Its fixture list is curl, wget, and jq at linux-x86_64, and wget has no fixture checked in, so it skips rather than faking a pass — leaving two binaries. ripgrep/linux-x86_64 is exercised for two rule ids and not for its tier, because it is an anomalous static build linking libcurl, libgnutls, libsqlite3 and libsodium and importing reverse-shell-shaped socket, dup2 and exec primitives, which trips two communication rules outside this crate. Two narrow gates reach the Windows fixtures and neither checks a tier, which is how the two Windows false positives below survive a green suite. Every one of these assertions is that detectors stay silent, so a detector that goes silent everywhere passes them all. The coverage floor is the separate check that catches loss rather than noise.

Known false positives and misses

Two of the six benign fixtures do not classify Benign. Measured through the same code path the eval gate uses:

tests/binaries/curl/linux-x86_64            Benign            0
tests/binaries/jq/linux-x86_64              Benign            0
tests/binaries/ripgrep/linux-x86_64         Benign            0
tests/binaries/curl/windows-x86_64.exe      LikelyMalicious  36
tests/binaries/jq/windows-x86_64.exe        Benign            8
tests/binaries/ripgrep/windows-x86_64.exe   Malicious        70

ripgrep for Windows lands exactly on the threshold: seven functions match the decryptor-loop shape, the three densest (66%, 48%, 48% bitwise operations) keep 18 points each while the other four are kept at zero, systeminfo plus CreateProcessW adds 8, one TLS callback adds 8. 54 + 8 + 8 = 70. Every contributing row is Suggestive or Presence — no floor fires — so this is a pure sum crossing a boundary, and it is Rust codegen, not a decryptor. curl for Windows reaches 36 the same way: 8 for carrying two TLS callbacks, 8 for importing SetFileTime, and 20 for two unique Communication indicator hits, one of them a protocol decoder flagged for an unbounded memcpy. No gate that runs in CI asserts a tier on either binary.

The decryptor-loop shape is the loudest false-positive source. A corpus sweep of 1,206 samples saw benign Rust codegen in ripgrep for macOS emit 33 matching loops, a raw contribution of 627 points. Two bounds contain it: a per-binary cap of three point-bearing hits (54 points maximum, with the excess kept in the ledger at zero points rather than deleted), and a 16-basic-block ceiling that excludes branchy parsers. The ceiling was set by a residual: jq/linux-x86_64 matched at function 0x413970 with 23 basic blocks, 80 IL operations and 26% bitwise density — a UTF-8 and number decoder sitting a hair over the density floor. Neither bound removes the Windows case above.

Nearest-neighbour attribution confuses toolchain with family. A leave-one-out replay over all 1,130 entries of the bundled DNA index emitted 811 family rows: 786 named the sample's true family, 25 named a wrong one. In 28 of the 811, the globally best neighbour belonged to a different family than the one elected, so the row quoted a similarity figure that was not the named family's. Three gates now guard the row:

GateRequiresWhat it caught
Votea strict majority of the returned neighbours name the elected family
Self-evidencethe elected family's own best neighbour clears the 0.55 promotion bar by itselfthe classifier checks that bar against the globally best neighbour, and published a RedLineStealer sample as Medusa on a global top of 1.00 while Medusa's own best neighbour sat at 0.335
Coherencethe global top sits within 0.15 of the elected family's own top0.15 sits inside a measured empty band — none of the 811 rows had a margin between 0.15 and 0.20, and 6 of the 7 above 0.20 were wrong

The vote and coherence gates cost 1 correct attribution out of 786 and removed 8 of the 25 errors. The case that exposed them was a go build hello-world scoring LikelyMalicious/49 on a 34-point row reading "resembles PEASS-NG (similarity 0.82)": its actual top neighbour was a Turtle sample at 0.8229 while PEASS-NG's own best neighbour sat at 0.6036. All 19 contributing DNA axes were Go toolchain provenance — reflect.* and fmt symbols, the runtime's 28-strong BSD-wrapper set, crypto.aes_rcon from Go's crypto standard library. The similarity measured "both were built by the Go toolchain." The same fixture now scores Suspicious/23 and emits no family row, but the residual is not fixable at this layer: a Go binary whose top neighbour happens to be the elected Go family still passes all three gates.

The absence-of-strings shape has an unmeasured benign population. A Mach-O that imports an execution primitive, carries a __cstring section under 256 bytes, has 64 or fewer imports, and still holds real code is the string-obfuscated macOS dropper shape. Measured on this corpus it fires on 106 of 119 AmosStealer samples and 79 of 653 other macOS malware, against 0 of 1,424 Apple-shipped binaries and 0 of 481 third-party binaries from Homebrew and /Applications. Go and Rust executables are the natural false-positive shape here — the Go linker never emits __cstring at all, putting string data in __rodata, and the Go runtime imports posix_spawn whether or not the program ever execs. The ceiling of 64 was pinned against measured Go tools at 67 to 228 imports, and that proxy failed downward: the committed hello-go1.25-darwin-arm64 fixture, a hello-world that prints a UUID, has 58 imports and fired this row for 18 points. The fix was to detect the toolchain structurally — __gopclntab or __go_buildinfo, both present even under -ldflags="-s -w" — and abstain on any Go image, rather than loosen a threshold for everyone. Because the wider third-party benign population is thin, the row is banded Suggestive: it corroborates, it never floors.

Process hollowing is blind to delegate-dispatched .NET injection. The detector needs three of its five APIs called from one function, and on c829be0e… — PureLogsStealer, a 32-bit .NET PE — it emits nothing. The abstention is correct rather than a naming gap. The sample declares ten injection imports on a single type (CreateProcess, GetThreadContext, SetThreadContext, ReadProcessMemory, WriteProcessMemory, NtUnmapViewOfSection, VirtualAllocEx, ResumeThread and two Wow64 variants), but a clean walk of all 1,887 method bodies finds zero call instructions naming any of those tokens — the declaring type is never referenced at all. The only imports with real call sites are LoadLibrary and GetProcAddress; the injection APIs are reached through delegates built at runtime by Marshal::GetDelegateForFunctionPointer. There is no static edge to find, and fabricating one from the declaration would fire on any assembly that merely declares the imports.

The recall gate's directory walk does not match the corpus layout. The corpus stores each sample one directory deep, as training/malware/SHA256/sample; the skipped-by-default recall test iterates the top level and ignores anything that is not a regular file. Run on a provisioned box it finds nothing and fails on its own emptiness assertion rather than measuring recall — loudly, which is the only reason it is not a silent pass. The command-line eval path walks training/malware/*/sample correctly and is the one that produced every number above.

Catalogued technique coverage is an inventory, not a detection rate. Harvesting the semantic scanners against a trigger fixture yields 36 behaviour indicators tagging 40 of 712 native-binary-relevant ATT&CK techniques across 8 of 14 tactics. Exfiltration, impact, initial access and lateral movement are at zero — the impact-tagged rows in the Medusa ledger come from promoted capabilities, which this harvest does not cover. Three structural detectors — reflective loading, process hollowing, and PE TLS callbacks — are hand-declared rather than harvested, because their inputs (call-graph shape, PE header fields, raw bytes) are too heavy to synthesize into the trigger fixture. Regeneration is on demand with no CI assertion, so the file can lag the detectors it claims to reflect.

Upstream dependencies

The verdict reads no facts of its own. Packing, entropy, embedded carriers, indicator hits, capabilities and consistency divergences all come from earlier analysis; the code-shape scanners walk the same SSA-form IL, resolved call graph, and constant-propagation results everything else uses. The security surfaces have a strict dependency order:

analyze → indicators / capabilities → findings (CWE) → malware (verdict)

A binary that fires CWE-94 — dynamic code through dlopen with a tainted argument — emits a finding to the Findings stream. The same call-site shape feeds the verdict composer, where it gets a band and contributes to a tier. These are not duplicates but different read-outs of one fact: an auditor wants the per-CWE detail, a triage pipeline wants the tier.

Cross-surface disagreement

The surfaces disagree usefully — a malware tier and a finding count move independently:

BinaryMalware tierFindings and CVEs
A well-written implantMaliciouszero CWE findings
Unaudited legacy codeBenigndozens of CWE findings
A Linux image with old packagesBenignthirty SBOM CVEs

Agreement is informative; so is disagreement.

A tier transition across versions of the same package is the signal worth alerting on — a binary that was Benign in v1.0 and LikelyMalicious in v1.1 is a release blocker even when no individual finding or indicator changed.