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
| Question | Answered from | Section |
|---|---|---|
| What format, architecture, and build is this? | the header chain; no symbols required | Identity and build |
| What can it do? | an access map of entitlements, frameworks, syscalls, and dlopen targets, plus ATT&CK-mapped rules that fired | Capabilities |
| What does it reach, open, or carry? | string classification, resolved call sites, credential scanning | Endpoints, file access, and secrets |
| Is it bad, and why? | a ledger of scored rules, each with an address and a rationale | Verdict and threat ledger |
| What is wrong with the code? | heuristic, symbolic-execution, and static-rule detectors, joined to reachability | Static-audit findings |
| What is it built from, and what is it exposed to? | linked-library records, symbol versions, banner strings → CVE matching | Components and CVE exposure |
| Can it be trusted? | signature parsing, entitlement divergence, hardening probes | Signing, entitlements, and hardening |
| Has this hash been seen elsewhere? | batch classification against the visible corpus, sightings, enrolled nodes | Batch classification, Corpus composition |
| What else in the corpus looks like this? | DNA vector shortlist, Jaccard rerank, structural hashes | Similarity |
| How is the whole corpus searched? | a compositional query language over seventy-one indexed fields | The query language |
| Does anything here appear in an intel report? | SHA-256 reconciliation against an ingested report ledger | Indicator-of-compromise reconciliation |
| What cannot be answered at all? | nothing static — these need a live process | Answers that require execution |
Evidence classes
Three kinds of thing can back an answer, and confusing the first for the second is the standard error.
| Class | What it is | Who controls it |
|---|---|---|
| Claim | read out of the file — a section name, an import, a version string, a signature | whoever built the file chose it |
| Measurement | computed over the bytes — entropy, a lifted control-flow property, a reachability walk | cannot be typed by an author, only engineered against |
| Corpus match | a 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:
| Format | Build identity |
|---|---|
| ELF | the build-id note the linker wrote |
| Mach-O | LC_UUID plus the cdhash |
| PE | the PDB GUID and age |
Language and runtime are recovered separately, each from its own surviving metadata:
| Runtime | Recovered from |
|---|---|
| Go | pclntab and buildinfo |
| .NET | CLI metadata streams |
| Objective-C, Swift | class 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.
| Count | What it counts | This curl |
|---|---|---|
| Access-map digest | rows in the access map: entitlements, linked frameworks, syscall aspects, and resolved dlopen/dlsym targets, each tagged with one of ten categories | 28 |
| ATT&CK-mapped inventory | rules that fired, with a virtual address and a technique each | 1 |
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:
| Suppression | Applies when | Effect |
|---|---|---|
| not scoring-eligible | the image is platform-signed — Apple- or Microsoft-verified | the capability stays a recorded fact, hard-excluded from the threat ledger |
| library-provided | the capability fired only inside statically linked library code, never in the program's own functions | hidden 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.
| Grade | Assigned when | Where it runs |
|---|---|---|
Confirmed | the string is referenced from a function that reaches a known network sink within two call hops | Mach-O only |
Probable | the string is referenced, but no such path exists | Mach-O only |
Unverified | no code references it at all — an orphan literal, which is exactly what a banner or a --help URL is | Mach-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:
| Rejected | Why it is not an endpoint | What 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 fetched | www.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.0 | a 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 version | a.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.Win32 | DNS 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:
- opens files
- opens network
- spawns processes
- loads code dynamically
- sends Mach messages
- has direct
svcsites - has computed
svcsites
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.
| Field | Value | Assigned when |
|---|---|---|
| role | hosted, client | the listener-flag argument const-propagated at the connection site |
| role | unknown | it did not — reported rather than guessed |
| posture | strong_validator | setCodeSigningRequirement:, auditToken, or valueForEntitlement: is present |
| posture | weak_validator | only pid-based validation is present — race-prone, and documented by Apple as insufficient |
| posture | no_validator | no validator selector is imported at all: any local peer can connect |
| posture | not_applicable | the row is a client, or applicability was not computed |
Embedded credentials — a separate scan, over twenty-three kinds:
- PEM private-key, public-key, and certificate blocks; X.509 certificates in raw DER
- PKCS#8 and PKCS#12; RSA and EC private keys in raw DER
- PKCS#7/CMS
SignedData— reported as the code-signing artifact it is, not as a leaked credential - OpenSSH and PuTTY private keys
- PGP private-key blocks, plus unarmored OpenPGP RSA session-key packets
- JWTs
- AWS, GitHub, and Slack tokens
- GCP service-account markers
- crypt(3) and Apache LDAP
{SHA}password hashes - expanded AES and SM4 key schedules, located by their key-expansion arithmetic rather than by any string
- known-leaked firmware signing keys, matched against FwHunt supply-chain fingerprints
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.
| Verdict | Binaries (of 14,570 visible) |
|---|---|
benign | 13,805 |
suspicious | 325 |
not_enough_evidence | 319 |
likely_malicious | 101 |
malicious | 20 |
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:
| Join | What it reports |
|---|---|
reachable_from_main | whether the finding sits in the resolved entry-point closure; reported as null rather than false when no closure could be established |
unauth_reachable | findings reachable before any authentication boundary — zero here |
| privilege ceiling | what 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 gap | Binaries |
|---|---|
| missing FORTIFY | 13,721 |
| missing stack canaries | 13,235 |
no GNU_STACK marking | 12,841 |
| no RELRO | 12,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 one | Binaries |
|---|---|
| critical finding | 836 |
| high finding | 1,562 |
| informational finding | 14,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:
| Condition | Covers | Blind spot |
|---|---|---|
| no call site in the walked closure without a resolved target | format-agnostic — an indirect call whatever produced it | none; this count is uncapped |
| no call to a named dispatch-boundary symbol | an Apple-shaped starter set of ten: the objc_msgSend and objc_msgSendSuper families, five dispatch_*, two xpc_connection_*, _Block_copy | a 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:
- there is no evidence address to start from — a cross-surface predicate such as Apple impersonation has none to give
- the string is never loaded, or the binary is stripped of xref data
- the finding family has no function anchor by construction — an rpath finding is a property of the image, not of a call site
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:
| Surface | Gate |
|---|---|
| call-graph tools | Pro plan floor |
| per-function decompilation | two independent gates: the operator enabled it on the deployment, and the caller is on Pro |
| static-audit findings | grant-only — no plan tier clears it, Pro included |
| whole-binary reconstruction | grant-only, issued per user; paying reaches it at no tier |
| CVE exposure | none — 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:
name_onlyfloor_onlyrange_unparseableexactrange_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 state | What the answer does |
|---|---|
| no ingest has completed | fails with a 503 naming the ingest command, rather than returning an empty list |
| an ingest under 168 hours old | ships its age in hours; freshness flag true |
| older than 168 hours, or the last ingest failed | freshness 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.
| Format | Signing answer | Entitlements |
|---|---|---|
| Mach-O | what 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 key | yes |
| PE | Authenticode 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 chain | no |
| ELF | none | no |
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:
- an entitlement declared but unused
- a capability exercised without the entitlement
- a capability reached only through a resolved dynamic-load target
- a
com.apple.private.*entitlement on a non-Apple-signed binary - an ad-hoc signature carrying a privileged entitlement
- a
com.apple.*bundle id signed by a team that is not Apple
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:
| Order | Status | Note |
|---|---|---|
| 1 | known_bad | outranks everything; set by a malicious or likely_malicious verdict, or by a recorded VirusTotal malware hit on its own |
| 2 | baseline | split into matched-build and other-build |
| 3 | suspicious | — |
| 4 | known_good | — |
| 5 | known | present but uncommitted — the verdict is not_enough_evidence, or there is no verdict yet because the binary has not been analyzed |
| 6 | unknown | also 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 |
| — | invalid | outside 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.
| Platform | Binaries |
|---|---|
| NetBSD | 11,692 |
| glibc Linux | 1,460 |
| unclassified Linux | 918 |
| FreeBSD | 333 |
| OpenBSD | 163 |
| musl Linux | 2 |
| Windows | 2 |
| Kind | Binaries |
|---|---|
| relocatable object files | 12,676 |
| executables | 997 |
| shared libraries | 897 |
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
| Metric | What it measures | On this example |
|---|---|---|
| vector | DNA cosine similarity | puts an unrelated library within 0.015 of another build of the same program — on its own it cannot separate a sibling from a coincidence |
| jaccard | overlap of feature sets | separates: 0.710 against 0.286 |
| dhash | perceptual byte-map distance | worse 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:
| Hashes | Pivot they support |
|---|---|
seven — syscalls, behavioural aspects, indicator rule sets, __text content, call-graph symbols, Objective-C classes, frameworks | a corpus-wide sweep: each is an indexed query field |
| the other fourteen, including the import hash and the entitlement hash | a 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 emit | an 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:
- visibility — restricted to what the caller can see
- format — restricted to the target's own file kind, so cosine and Jaccard stay within-format
- size — cut to the 500 nearest by cosine before the Jaccard rerank runs
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.