Sign in

Answers

Asked "is this malicious, and why?", a 334,312-byte Linux curl answers benign — and two indicator rules fired on it anyway:

indicators
  cgi.input_with_unsafe_strings      Communication    T1190, T1059.004
    matched "CONTENT_LENGTH + getenv" at 0x33376, in sub_25350 (0x25350)
  generic.curl_retry                 HostInteraction  T1105
    matched "curl + --retry + --retry-delay" at 0x1718
    "the retry pattern indicates the binary expects the network call to
     fail intermittently and is willing to wait — typical of resilient
     C2 callbacks rather than a one-shot download"

threat ledger
  malware.indicators_communication   Suggestive   10 points   Deductive
score 10 → tier Benign

The second rule described curl's own help text as a C2 callback pattern. It cost nothing, because it never reached the ledger, and the one rule that did was worth 10 points against a threshold it does not clear. An answer decomposes into named rules carrying an address, a strength, and a point value, which is what makes one specific enough to reject.

Question index

QuestionAnswered fromSection
What format, architecture, and build is this?the header chain; no symbols requiredIdentity and build
What can it do?an access map of entitlements, frameworks, syscalls, and dlopen targets, plus ATT&CK-mapped rules that firedCapabilities
What does it reach, open, or carry?string classification, resolved call sites, credential scanningEndpoints, file access, and secrets
Is it bad, and why?a ledger of scored rules, each with an address and a rationaleVerdict and threat ledger
What is wrong with the code?heuristic, symbolic-execution, and static-rule detectors, joined to reachabilityStatic-audit findings
What is it built from, and what is it exposed to?linked-library records, symbol versions, banner strings → CVE matchingComponents and CVE exposure
Can it be trusted?signature parsing, entitlement divergence, hardening probesSigning, entitlements, and hardening
Has this hash been seen elsewhere?batch classification against the visible corpus, sightings, enrolled nodesBatch classification, Corpus composition
What else in the corpus looks like this?DNA vector shortlist, Jaccard rerank, structural hashesSimilarity
How is the whole corpus searched?a compositional query language over seventy-one indexed fieldsThe query language
Does anything here appear in an intel report?SHA-256 reconciliation against an ingested report ledgerIndicator-of-compromise reconciliation
What cannot be answered at all?nothing static — these need a live processAnswers that require execution

Evidence classes

Three kinds of thing can back an answer, and confusing the first for the second is the standard error.

ClassWhat it isWho controls it
Claimread out of the file — a section name, an import, a version string, a signaturewhoever built the file chose it
Measurementcomputed over the bytes — entropy, a lifted control-flow property, a reachability walkcannot be typed by an author, only engineered against
Corpus matcha comparison against everything else already analyzed

See Binary anatomy for where each field falls.

Identity and build

Format, architecture, and build identity come out of the header chain and need no symbols. The same curl:

image_format  ELF        arches  [x86_64]      binary_type  executable
file_size     334,312    image_size  334,280   header_size  848
build-id      0ece1ef25bd636ef1895c3165bd02066b4d1df41
signing_status  unknown  functions  167        network_endpoints  5

signing_status: unknown is not a failure to check — ELF has no code-signature model to check. Build identity is format-bound:

FormatBuild identity
ELFthe build-id note the linker wrote
Mach-OLC_UUID plus the cdhash
PEthe PDB GUID and age

Language and runtime are recovered separately, each from its own surviving metadata:

RuntimeRecovered from
Gopclntab and buildinfo
.NETCLI metadata streams
Objective-C, Swiftclass metadata
Rust, C++demangled symbols

A statically linked Go binary has no import table to read, and its runtime metadata becomes the naming layer instead.

For a container — a firmware image, an OCI layer, an installer — the same questions resolve per member, plus the format chain each child was reached through.

Capabilities

Two counts of "capability" exist and they measure different things.

CountWhat it countsThis curl
Access-map digestrows in the access map: entitlements, linked frameworks, syscall aspects, and resolved dlopen/dlsym targets, each tagged with one of ten categories28
ATT&CK-mapped inventoryrules that fired, with a virtual address and a technique each1

The ten categories are location, keychain, network, storage, hardware, IPC, process, analytics, security, and system. The single ATT&CK-mapped rule:

data-manipulation.reference-base64-string   heuristic   0x49940
  T1027 Obfuscated Files or Information      MBC C0026.001, C0019

One rule, for a network transfer tool. That is not a gap in the rule set so much as a fact about the binary: curl is dynamically linked against libcurl.so.4, and every socket, TLS, and protocol decision lives over there. Asking a dynamically linked executable what it does returns what its own 167 functions do. The capability inventory answers for the code that is actually present.

A capability that fired is not automatically a capability that counts. Two suppressions apply, and both keep the fact while removing its weight:

SuppressionApplies whenEffect
not scoring-eligiblethe image is platform-signed — Apple- or Microsoft-verifiedthe capability stays a recorded fact, hard-excluded from the threat ledger
library-providedthe capability fired only inside statically linked library code, never in the program's own functionshidden from the default threat surface, kept for the supply-chain view, and never scoring-eligible

A row attributed to toolchain or standard-library code also names the package implementing it — crypto/aes for a hit landing on a Go image's key expansion. Only toolchain-mandatory packages (runtime, reflect, sync) count as library-provided; an author-elected import (crypto/chacha20, os/exec) stays visible and scoring-eligible.

The category map is coarse where it matters most for privacy questions: camera, microphone, Bluetooth, USB, and IOKit all resolve to hardware. "Does it use the camera" is answerable only down to "it touches hardware", with the specific framework or entitlement name as the sole discriminator.

Endpoints, file access, and secrets

Network endpoints are strings that survived a classifier, and the list makes that obvious:

endpoints (5)
  Url       https://curl.se/libcurl/c/curl_easy_setopt.html
  Url       https://curl.se/docs/ssl-sessions.html
  Url       https://curl.se/docs/sslcerts.html
  Hostname  curl.se
  Ipv6      ::b
network apis   socket, setsockopt
call sites     dns: []   outbound: []   inbound: []   sockets_created: 1

Three documentation URLs, the hostname parsed out of them, and ::b — a byte sequence that satisfied an IPv6 pattern and is not an address anyone dials. Nothing here is a destination. The one socket site resolves to neither a connect nor a bind, which is what sockets_created: 1 counts: a socket() call whose follow-on is not discoverable in the same function. Every real transfer is inside libcurl.so.4, so no outbound call site exists in this file to find.

Endpoint grading

The separator between a doc URL and a C2 host is the xref, and it only works on one format.

GradeAssigned whenWhere it runs
Confirmedthe string is referenced from a function that reaches a known network sink within two call hopsMach-O only
Probablethe string is referenced, but no such path existsMach-O only
Unverifiedno code references it at all — an orphan literal, which is exactly what a banner or a --help URL isMach-O; also the only outcome on ELF and PE

The ELF and PE analyzers call the same classifier with an empty string-address map and no data-xref index, and the classifier returns Unverified on the first missing lookup before it consults the call graph at all — so all five endpoints above are Unverified whether or not code touches them, and on those two formats the grading separates nothing.

Ahead of grading, a format-agnostic rejection filter drops four classes of string that are never dialled:

RejectedWhy it is not an endpointWhat still gets through
namespace URIs under fifteen roots — www.w3.org/2001/, schemas.xmlsoap.org/, schemas.openxmlformats.org/, docs.oasis-open.org/, java.sun.com/xml/, …an xmlns value is compared as a string, never fetchedwww.w3.org alone is not a root, so a sample fetching www.w3.org/index.html reports
version tuples in VS_VERSION_INFO, <assemblyIdentity version=…>, mscorlib, Version=4.0.0.0a dotted quad in version context is a build number, not an IPv4 address
dotted quads whose first octet is 0, or whose last two are (4.0.0.0, 3.5.0.0)0.0.0.0/8 is not a reachable peer, and a.b.0.0 is a network address — the exact shape of a four-part product versiona.b.c.0, RFC1918 and loopback, which are real endpoints in lateral-movement samples
PascalCase dotted identifiers whose trailing label collides with a TLD — System.IO, Microsoft.Win32DNS names in binaries are lowercase essentially without exception; an uppercase label marks a code namespace

What survives is documentation URLs and library homepages — the three curl.se rows above. Raw URL presence still is not evidence of a destination.

File and syscall access

File and syscall access is answered from resolved call sites rather than import names: this binary's one recorded file-IO site is open("/dev/tty") at offset 265 inside sub_210e0, argument fully resolved. Seven booleans summarize the same surface at a glance:

The packing bit is separate and comes from the entropy heuristic, not from the syscall surface.

IPC and embedded credentials

XPC services — on macOS, IPC gets its own answer: each service with its Mach name, a role, and an authentication posture. Posture is not per-service — it is derived from which validator selectors appear anywhere in the image, so every hosted row on one binary carries the same grade.

FieldValueAssigned when
rolehosted, clientthe listener-flag argument const-propagated at the connection site
roleunknownit did not — reported rather than guessed
posturestrong_validatorsetCodeSigningRequirement:, auditToken, or valueForEntitlement: is present
postureweak_validatoronly pid-based validation is present — race-prone, and documented by Apple as insufficient
postureno_validatorno validator selector is imported at all: any local peer can connect
posturenot_applicablethe row is a client, or applicability was not computed

Embedded credentials — a separate scan, over twenty-three kinds:

Verdict and threat ledger

The verdict is a total over ledger rows, each naming a rule, a strength, a point value, an evidence class, and — where a reverse xref reaches one — the functions the evidence sits in. That structure is what lets an answer be overturned.

Five verdict values exist, and not_enough_evidence is emitted when the malware pass did not complete, so that a crashed analysis never reads as a confident clean result.

VerdictBinaries (of 14,570 visible)
benign13,805
suspicious325
not_enough_evidence319
likely_malicious101
malicious20

Worked example: a wrong verdict

glibc's thread-debugging library scores malicious at 72, and the ledger prints exactly why it is wrong:

libthread_db-1.0.so   ARM ELF dylib   22,280 bytes   41 functions
verdict malicious     score 72

  malware.decryptor_loop   Suggestive  18   T1140   ×3
  malware.decryptor_loop   Suggestive   0   T1140   4th hit, capped
  malware.likely_packed    Suggestive  18   T1027.002

  function 0x1ca5: loop_depth=1, op_bitwise=76 (64% of 119 ops)
                   > op_arith=15, op_load=3, op_store=6,
                   crypto_calls=0, framework_calls=0, fan_out=0

Three of its 41 functions are bitwise-dense loops with no calls out — the same shape a hand-rolled string decryptor has, and the shape any bit-manipulation routine has. A fourth loop matched and is carried as evidence at zero points: the rule saturates after three scoring hits, capping its own contribution at 54 points, so a file full of bit-twiddling cannot run the score up on this rule alone. The rationale prints the operand histogram it decided on, so the error is legible in the answer itself rather than hidden behind a score — with one exception. crypto_calls=0 is a literal baked into the format string, not a count: it reads zero for every function of every binary. The nine other numbers on that line are measured. Family attribution, when it fires, is a DNA nearest-neighbour result carried with the confidence of its top neighbour, not a signature name asserted flat.

Static-audit findings

The finding count is not the answer; the joins over it are. On that same benign curl:

findings 121     critical 14   high 60   medium 46   info 1
by detector      heuristic 57  symbolic-exec 49  static-rule 15
by CWE           125:22  476:21  22:19  415:14  369:10  787:10  789:7  416:4
reachability     reachable 111   unreachable 9   unknown 1
entry root       entry @ 0xe510
unauth_reachable 0

A count of 121 across 167 functions of stock system C is what per-call-site heuristic and symbolic detectors produce. The by CWE line does not reconcile to it: it is clipped to the eight most-frequent CWEs, count-descending, 107 of the 121 here — the tail is dropped, not zero. The severity and reachability breakdowns are complete, and both total 121. Three joins rank the findings:

JoinWhat it reports
reachable_from_mainwhether the finding sits in the resolved entry-point closure; reported as null rather than false when no closure could be established
unauth_reachablefindings reachable before any authentication boundary — zero here
privilege ceilingwhat a successful exploit would actually get: sandboxed, user, elevated (hardened-runtime-bypass entitlements present), or system

Corpus-wide finding distribution

The dominant finding class corpus-wide is almost entirely build posture rather than bugs: 14,092 of the 14,570 visible binaries carry at least one CWE-693 finding.

Build-posture gapBinaries
missing FORTIFY13,721
missing stack canaries13,235
no GNU_STACK marking12,841
no RELRO12,820

Those are facts about how an image was compiled, not bugs inside it, and no reachability join applies to them. The severity tail is much thinner:

Carries at least oneBinaries
critical finding836
high finding1,562
informational finding14,289

The export path for all of it is covered in Outputs.

Detector collisions

Individual findings carry their own reasoning, including when two detectors collide:

compound.2-detector-stack   critical   confidence medium
  in sub_11230 (0x11230), 32 forward calls
  [double-free.same-function + taint.flow.deref.store] spans CWE-415, CWE-476
  "the cwe_id reflects the most-severe constituent only … the bugs are
   likely independent; investigate each separately"

Reachability coverage

Reachability answers have a hard boundary. The static call graph cannot follow indirect dispatch — objc_msgSend, dispatch_*, xpc_*, block invocations — so each of the three reachability answers, what is reachable and whether a path exists and what the attack surface is, carries a coverage object. When it reports complete, a negative means nothing reaches the target. When it does not, the negative means the walk hit edges it could not follow, and on an Objective-C or XPC daemon that is the normal case.

Complete requires two conditions at once, and they cover different ground:

ConditionCoversBlind spot
no call site in the walked closure without a resolved targetformat-agnostic — an indirect call whatever produced itnone; this count is uncapped
no call to a named dispatch-boundary symbolan Apple-shaped starter set of ten: the objc_msgSend and objc_msgSendSuper families, five dispatch_*, two xpc_connection_*, _Block_copya C++ vtable dispatch on ELF or PE registers only as an unresolved site with no name, so the returned blind-spot list names Apple primitives even on binaries that have none, and is capped at ten entries

The orientation digest is the one answer with no coverage object. It reports a binary-wide dispatch-site count only when the caller already holds the call-graph adjacency and omits the field otherwise, because the digest is walk-free and never forces the up-to-410 MB call-graph parse — it declines to report rather than reporting zero.

Addresses and access gates

An indicator or capability carries the address of its evidence — the string, the section offset, the import entry — not of the function it sits in. Function attribution is a second, best-effort field, filled by walking a reverse data-xref index back from the evidence address. It is empty when:

Where attribution does land, "where does this happen" is a lookup rather than a search.

Surfaces are gated differently, and no single plan tier clears them all:

SurfaceGate
call-graph toolsPro plan floor
per-function decompilationtwo independent gates: the operator enabled it on the deployment, and the caller is on Pro
static-audit findingsgrant-only — no plan tier clears it, Pro included
whole-binary reconstructiongrant-only, issued per user; paying reaches it at no tier
CVE exposurenone — open to every caller including anonymous. The tier changes the body, not the access: Pro gets per-CVE detail, everyone else counts per severity

Where a gate does apply, a caller without it gets the same not-found as for a binary they cannot see, so the gate never doubles as an existence oracle.

Components and CVE exposure

Components are identified from linked-library records, symbol-version requirements, and banner strings, and each carries the evidence that produced it:

glibc              2.34    abi_major   dt_needed libc.so.6 + verneed GLIBC_2.34
libcurl            —       abi_major   dt_needed libcurl.so.4
zlib               —       abi_major   dt_needed libz.so.1
linux-kernel-min   4.3.0   floor       .note.ABI-tag "Linux 4.3.0"
linkage            dynamic-glibc   exact

Two of the three real libraries have no version at all, because libcurl.so.4 and libz.so.1 are ABI sonames — they name a library and its ABI major and nothing else. The glibc row is internally inconsistent — 2.34 beside abi_major — because that component is built twice: from dt_needed libc.so.6, carrying no version and an ABI-major precision, then from the GLIBC_2.34 symbol-version requirement, carrying a version and a floor precision. The merge copies the incoming version but never the incoming precision, so the printed precision belongs to the value it replaced. Nothing downstream changes, since floor and ABI major demote CVE confidence identically. Versionless and floor-only components propagate straight into the CVE answer:

384 findings    libcurl 325 · zlib 36 · glibc 21 · linux-kernel-min 2
match_confidence   name_only 361 · floor_only 23 · range_matched 0
severity           unknown 359 · medium 18 · low 5 · high 2

Not one of the 384 was confirmed against a version. The matcher deliberately over-reports — a component with no version still emits findings, ranked at the lowest of five confidence tiers, so the filter is the operator's to apply. The tiers, lowest to highest:

  1. name_only
  2. floor_only
  3. range_unparseable
  4. exact
  5. range_matched

Read honestly, that answer says "every CVE ever filed against curl, zlib, and glibc", not "your exposure". A version floor such as glibc's 2.34 is no better: the real release could be any later one, so the range check is declined rather than faked.

None of those 384 findings is stored at analysis time. Components come out of the stored record, but the match runs against the in-memory vulnerability database on every request, so a newly published CVE appears without reanalysis — and the answer depends on database state the binary knows nothing about:

Database stateWhat the answer does
no ingest has completedfails with a 503 naming the ingest command, rather than returning an empty list
an ingest under 168 hours oldships its age in hours; freshness flag true
older than 168 hours, or the last ingest failedfreshness flag false

The full CVE mechanics are in Known vulnerabilities.

Static linkage and component currency

Statically linked libraries are found by function-level identification against library signatures, not from linkage records. On this dynamically linked ELF that identifies zero libraries, which is correct and useless — there is nothing statically linked in it to find.

"Which of these components is out of date" is not answerable at all. Nothing here tracks upstream release feeds, so the only currency signal is a CVE naming a component, which answers a different question. Component identity, versions, and the evidence rows behind them export as CycloneDX 1.6 or SPDX 3.0.

Signing, entitlements, and hardening

Signing answers are format-bound, and one format has none.

FormatSigning answerEntitlements
Mach-Owhat the signature claims — authority, Team ID, cdhash, per-slice status — and separately whether it holds: every code page rehashed against its CodeDirectory slot, in-file special slots rehashed against theirs, and the CMS messageDigest bound to that CodeDirectory with its RSA or ECDSA signature verified under the embedded leaf keyyes
PEAuthenticode digest state — absent, matching, mismatching, or unverifiable, where unverifiable is never a tamper claim — plus a verified flag true only on all four of: content binding, valid signature math, a matching recomputed image digest, a structurally consistent embedded certificate chainno
ELFnoneno

Each Mach-O special slot reports verified, mismatch, absent, or unchecked, and unchecked is load-bearing: the entitlements XML, the entitlements DER, and the requirements blob live inside the binary and are checked; the Info.plist and CodeResources slots point at files outside it and cannot be. An uncheckable slot is never reported as a mismatch, so a parse gap cannot masquerade as tampering — or as an intact signature.

Entitlement divergence

The interesting question is not what a binary claims but whether the claim matches the code, graded in six divergence kinds:

Hardening and tamper detection

Build hardening reports each mitigation as on, off, or not-applicable — never off for a check that could not run.

What none of it does is contact an authority: certificate chains are parsed and reported, never checked against a revocation list or a notarization service, so "validly signed" here means the digest and the chain are internally consistent, not that the certificate is still trusted today.

"Has it been tampered with" is answered by comparing hashes — a host's inventory against the corpus, or one build against a reference, down to which sections, symbols, strings, and endpoints differ.

Batch classification

Batch classification takes up to 1,000 hashes in one call and answers each against the visible corpus:

0513ba35…  apple_baseline  benign      sources [upload]  builds [latest]
00e8c696…  known_bad       malicious
00000000…  unknown         —

The ladder is first-match-wins:

OrderStatusNote
1known_badoutranks everything; set by a malicious or likely_malicious verdict, or by a recorded VirusTotal malware hit on its own
2baselinesplit into matched-build and other-build
3suspicious
4known_good
5knownpresent but uncommitted — the verdict is not_enough_evidence, or there is no verdict yet because the binary has not been analyzed
6unknownalso what a hash the caller cannot see reads as — never a permission error, so the batch answer is never an existence oracle for someone else's private corpus
invalidoutside the ladder: the string is not a well-formed SHA-256, echoed back verbatim and counted separately from the six

Because baseline membership outranks suspicious, a suspicious binary that ships in an OS package classifies as baseline, not as suspicious. The status vocabulary is Apple-derived and outgrew its origin: the first row is a Linux ELF matched against a Linux baseline package and still labelled apple_baseline. The second is the libthread_db above — a batch answer inherits the verdict's error at fleet scale, stripped of the operand histogram that made the error legible one binary at a time.

Corpus composition and prevalence

The corpus of analyzed binaries is the fleet, and what a fleet-wide answer covers is exactly what is in it.

PlatformBinaries
NetBSD11,692
glibc Linux1,460
unclassified Linux918
FreeBSD333
OpenBSD163
musl Linux2
Windows2
KindBinaries
relocatable object files12,676
executables997
shared libraries897

A corpus-wide statistic drawn from it is therefore mostly a statistic about compiled fragments, not shipped programs. Prevalence and timeline come from sightings (first seen, last seen, and every filename a hash travelled under); the host dimension comes from enrolled nodes reporting which paths carry a hash now.

The query language

Cross-corpus questions use a compositional query language over seventy-one indexed fields — entitlements, frameworks, imported symbols, BSD syscalls, Mach traps, dlsym targets, XPC service names, called APIs, ATT&CK technique ids, section entropy, function and endpoint counts, structural hashes. Verdict is not among them: it is a search facet, and verdict:malicious is a syntax error, because the colon form exists nowhere in the grammar. That grammar is an identifier, one of == != > >= < <= CONTAINS ANY ALL, and a value, composed with AND / OR / NOT and parentheses.

The endpoint reverse lookup — "does anything reach this host" — does not currently work: endpoints CONTAINS "curl" returns zero hits across the whole corpus, including the binary shown above, because the indexer reads a key named url off each endpoint record while the extractor writes value. network_endpoint_count >= 3 matches 228 binaries; no query on endpoint text matches anything. A measured zero and a broken measurement look identical, which is the entire reason the two queries have to be run against each other.

Indicator-of-compromise reconciliation

Indicator-of-compromise reconciliation against an operator-ingested report ledger matches on SHA-256 only. A report's domain, IP, mutex, and filename indicators are not matched against the corpus, and nothing fires when a listed hash next appears — the ledger is queried, it does not notify. Neither is there a YARA engine: a rule cannot be run across the corpus, and no rule is emitted for a sample. The substitute pivot is the structural hash set plus similarity, which answers "what else looks like this" but not "what else matches this pattern".

Similarity

Similarity is answered in two stages — a DNA vector shortlist, then a Jaccard rerank that does the actual separating:

target 0513ba35… (curl) — 3,899 binaries in the vector plane
  02c1abdd…  curl                  vector 0.987  jaccard 0.710  dhash 16
  03d34911…  libdpv.so.3           vector 0.972  jaccard 0.286  dhash 11
  02ffe04e…  libattr.so.1.1.2501   vector 0.971  jaccard 0.239  dhash 16
  025adc9a…  pam_filter.so         vector 0.970  jaccard 0.104  dhash 20
  041fa89c…  ssh-agent             vector 0.959  jaccard 0.090  dhash  9
MetricWhat it measuresOn this example
vectorDNA cosine similarityputs an unrelated library within 0.015 of another build of the same program — on its own it cannot separate a sibling from a coincidence
jaccardoverlap of feature setsseparates: 0.710 against 0.286
dhashperceptual byte-map distanceworse than useless — ranks ssh-agent (9) and libdpv.so.3 (11) as closer than the correct answer (16)

The byte-map distance is a repackaging and look-alike signal, not a code-similarity one, and reading it as the latter inverts the result. Alongside these sit structural hashes for exact pivots — twenty-one computed per binary, and not equally usable:

HashesPivot they support
seven — syscalls, behavioural aspects, indicator rule sets, __text content, call-graph symbols, Objective-C classes, frameworksa corpus-wide sweep: each is an indexed query field
the other fourteen, including the import hash and the entitlement hasha two-binary comparison, surfaced as an identical-hash callout
within that fourteen, the industry standards — imphash, telfhash, TLSH, the Rich-header hash, symhash, dylib_hash — computed to match what other tools emitan external lookup: paste the digest into VirusTotal or a public report

A rarity report naming functions unusual against the reference corpus sits beside them.

The comparison set is its own limit, and the number printed with the answer is not it. 3,899 is every row in the vector plane minus the target — binaries carrying a DNA vector at all, out of 14,570 visible — scoped by neither visibility nor format. The set that produced these five is narrower on three axes:

A thin neighbour list can still mean a thin comparison set rather than an unusual binary, but the printed denominator is an upper bound on that set, not its size.

Answers that require execution

Two answers are outside static analysis entirely: the exfiltration payload and its live destination, and live C2 beaconing. Static analysis surfaces the hosts a binary embeds, the collection primitives it links, and the intent connecting them — never the session. A packed sample gives up less again until its payload is recovered; see Unpacking for what survives that boundary.

An encrypted C2 address baked into the file is not in that set: what a live session picks is out of reach, what the sample was configured with is not. A bounded IL emulator runs decryptor-shaped functions offline and returns the bytes they produce with provenance — which function, which site, how many operations were emulated, which gate the result passed, at what confidence. Config extractors then read those bytes as malware configuration: family plugins first, a generic one last, first structured match winning, and no match returning an abstain rather than a guess.

Emulation-derived evidence is labelled as such and weighted below a static proof. A proven emulated row floors the tier to likely_malicious and contributes its points, but is excluded from both paths that release the corpus-evidence cap — the single-row check and the summed-points check — so it cannot reach malicious alone, nor by compounding with a lone weak family-neighbour hit. Only deductive evidence — a taint witness, a structural proof, a dataflow-proven API sequence, reproducible without running anything — releases that cap.