blint is a binary analysis tool that examines executable files to extract a wide range of metadata. This document serves as a technical guide for security analysts and reverse engineers who want to understand the JSON output produced by blint.
The primary goal of blint's metadata generation is to act as a "Rosetta Stone" for binary formats. It parses different and often complex structures from ELF, PE, and Mach-O files and presents them in a single, standardized JSON format. This allows for consistent analysis, scripting, and threat hunting across different operating systems and architectures.
This guide details the attributes found in the metadata, their purpose, and the methods blint uses to obtain them, including notable strengths and limitations.
blint sbom emits a CycloneDX document generated from the official 1.7 schema (specification) (blint/cyclonedx/spec.py, regenerated with datamodel-codegen: the exact command and version are recorded in that file's header). specVersion is 1.7 by default and 1.6 remains selectable with --sbom-spec-version 1.6 for consumers that have not caught up: blint populates no CycloneDX 1.7-only field, so a document declared 1.6 is exactly the 1.6 shape; the two emissions differ only in specVersion (and the per-run serialNumber/timestamp), and both validate against their respective official schemas. Emitted documents were validated against the official bom-1.7.schema.json and bom-1.6.schema.json (jsonschema) and ingested by Dependency-Track 4.14.4 (both declarations parsed, identical component counts); cdxgen 12.8.4 emits 1.7 itself and its output validates against the same schema files.
The 1.7-only fields are deliberately not populated yet: a considered "not yet" per field:
citations(root): bibliographic attribution for the BOM itself; static binary analysis produces no citation data.component.isExternal: whether a component is not built from the project's own source. blint cannot determine build provenance from binaries; every component would carry the same value, and a constant field is noise a reader cannot act on.component.versionRange: blint identifies exact versions (withinternal:version_sourcenaming which kind) or nothing; a range would be fabricated.metadata.distributionConstraints/tlpClassification: marking and export-control constraints require a policy input blint does not have; hardcoding a default would state a classification blint did not determine. Both wait for an explicit CLI input.
At the highest level, the JSON output contains attributes that identify the binary and provide universally applicable information.
| Attribute | Description | Use Case |
|---|---|---|
file_path |
The absolute path to the analyzed binary file on the filesystem. | Basic file identification and tracking. |
binary_type |
The format of the binary, such as ELF, PE, MachO, or WASM. This is the primary key for interpreting format-specific sections. |
Directing further analysis; knowing which format-specific tools to use next. |
elf_type |
ELF only: the ELF header type as a plain name (EXEC, DYN, REL). A REL object has no load address and a DYN image without an interpreter is a shared library, so file-class questions (PIE, NX applicability) read this first. |
Deciding whether a hardening property applies to the file class at all (F1a). |
macho_filetype |
Mach-O only: the Mach-O filetype as a plain name (EXECUTE, DYLIB, BUNDLE, ...). PIE is a property of main executables; a dylib is position-independent by construction. |
Deciding whether a hardening property applies to the file class at all (F1a). |
hashes |
A collection of cryptographic hashes for the file, including MD5, SHA1, SHA256, and SHA512. | File identification, malware signature matching, and searching in threat intelligence platforms like VirusTotal. |
llvm_target_tuple |
A string constructed to represent the binary's target environment in a format recognized by LLVM. The format is arch-vendor-os-environment. For example: x86_64-pc-win32-msvc or mipsel-unknown-linux-muslsf. This is crucial for accurate disassembly. |
Configuring disassemblers and decompilers; understanding the intended operating system and ABI. |
callgraph |
Optional compact function-call graph derived from disassembled_functions when --disassemble is enabled, or converted from the wasm_tools call graph for WASM inputs. Includes nodes, internal edges (with call counts), and unresolved/ambiguous external targets. |
Control-flow triage, function reachability analysis, and quick hotspot detection without parsing full assembly text. |
strings |
A list of strings extracted from the binary that exhibit high entropy or match patterns for secrets (API keys, private keys, etc.). Non-secret strings are filtered out to reduce noise. Base64-encoded strings are automatically decoded. On a managed (W3.2) assembly the list is the #US heap's actual string literals (the compiler's own store, not a byte scan) led by the heap values with the byte scan unioned in behind them (strings_source: user_strings_heap / user_strings_heap+binary_scan names the provenance; absent means the byte scan, as before). |
Triage for hardcoded credentials, sensitive URLs, or cryptographic material. A primary step in vulnerability and malware analysis. |
informative_strings |
Optional list of selected non-secret strings that match stable operational or exploit-triage indicators (for example network stack hooks, raw socket constants, DNS redirection hints, or Windows local-elevation technique markers). Each item includes value and category. |
Capability clustering for behaviors that may be visible in constants or embedded paths rather than symbol tables alone. |
- Some parser fields can include raw bytes that are not valid UTF-8.
- blint serializes undecodable bytes as hex strings for compatibility with JSON reports.
- To avoid oversized report fields, hex output is capped by
BLINT_MAX_HEX_BYTES(default:4096). - If capped, the value is emitted as
<hex>...<truncated:N_bytes>whereNis the original byte length. - Set
BLINT_MAX_HEX_BYTES=0to disable truncation.
blint provides detailed information specific to each binary format, normalized where possible.
ELF (Executable and Linkable Format) files are the standard for Linux, BSD, and many embedded systems.
-
Header Information (
header): Contains fundamental properties of the ELF file.class:ELF32orELF64, indicating a 32-bit or 64-bit binary.endianness:LSB(Little-Endian) orMSB(Big-Endian). Crucial for MIPS and ARM analysis.identity_os_abi: The target OS Application Binary Interface (e.g.,LINUX,FREEBSD).machine_type: The target CPU architecture (e.g.,AARCH64,MIPS,X86_64).
-
Dynamic Entries (
dynamic_entries): Lists entries from the.dynamicsection, which are essential for the dynamic linker.NEEDED: Specifies a required shared library (e.g.,libc.so.6). This is the basis for dependency analysis.SONAME: The "shared object name" this binary provides if it's a library.RPATH/RUNPATH: Library search paths hardcoded into the binary. A common focus for security review, as they can be used for library hijacking.
-
Notes (
notes): Contains metadata from.notesections.GNU_BUILD_ID: A unique hash identifying the specific build, useful for matching the binary with its corresponding debug symbols.ANDROID_IDENT: If present, provides Android-specific information likesdk_versionandndk_version.dlopen_dependencies: Metadata extracted from the FDO ELF Note designed to declare dependencies loaded dynamically at runtime viadlopen().- Context: Standard binary analysis usually only detects libraries linked at build time (found in
NEEDEDentries). However, many modern applications load plugins, codecs, or optional modules programmatically during execution. - Content: This attribute parses the embedded JSON note to list these "hidden" dependencies, including the library name (
soname), its necessity (required,recommended, orsuggested), and the specific application feature it enables. - Use Case: Critical for discovering the full dependency tree of modular applications (like media players or system daemons) that would otherwise appear to have very few dependencies during static analysis.
- Context: Standard binary analysis usually only detects libraries linked at build time (found in
-
Android bionic facts (
android, optional): One nested block, emitted only for ELF files that target Android (.note.android.identpresent); every key inside is additive and mirrors whatllvm-readelf -a --notesreports for the same file (thetests/scripts/android/elf_facts_probe.pyoracle compares them on every fixture). Arm64-only facts are emitted only for AArch64 binaries; absent means "not applicable", never "not checked".android_ident:{min_api, ndk_version, ndk_build_number}from the Android ident note;min_apiis an int (the minimum API level the build targets).packed_relocations: list of packed-relocation tables, each{kind, size_bytes}withentry_countadded for RELR (whose entry size the file declares).kindisaps2(DT_ANDROID_REL[A], bionic reads from API 23),relr(standardDT_RELR, API 30+), orandroid_relr(Android's obsolete pre-API-30 RELR spelling). Presence is recorded even when the table itself cannot be decoded.memtag(arm64):{level, heap, stack}decoded from.note.android.memtag;levelisnone/async/sync, and the booleans say which memory the loader must prepare for MTE.aarch64_features(arm64): named AArch64 GNU-property bits found in.note.gnu.property(BTI,PAC,GCS); absent when the file carries no such property.text_relocations: true whenDT_TEXTRELis present orDF_TEXTRELis set; text segments need write access during relocation.soname: theDT_SONAMEstring, or null when the file has none (the loader derives the name from the path in that case).needed_absolute: true when anyDT_NEEDEDentry contains a/(a path was recorded instead of a SONAME).tls_segment(optional):{align, vaddr}of thePT_TLSsegment when one exists.page_alignment:{min_load_align, mod_16384_incongruent}; the minimumPT_LOADp_alignand the list of LOAD segments whosep_offsetandp_vaddrare not congruent modulo 16384 (the two facts the 16 KB page-size verdict is built from). Emitted for every Android ELF; the app-level verdict below is 64-bit-only.
-
App 16 KB page-size verdict (
android_native.page_size_16k, apk-so-member apps): the per-ABI aggregation of the library facts above plus each stored zip location'soffset % 16384(01/B). 32-bit ABIs are exempt and never flagged (rule 35); assets never feed the verdict.compatible: true/false over the 64-bit ABIs present; null when no 64-bit ABI ships (nothing was judged).compatible_abis/incompatible_abis/exempt_abis: every ABI is named; never first-or-best (rule 36).summary: one string, e.g. "16 KB page-size compatible on 2 of 3 64-bit ABIs".per_abi.<abi>.libraries.<name>:{elf_16k, locations, reasons};elf_16kis the library's ELF-layout verdict,locationscarry{entry, split, compression, offset_mod_16384, zip_16k}(zip_16kis null for deflated entries, whichzipalign -c -P 16does not judge), andreasonsnames the failure in words. Verified per app againstzipalign -v -c -P 16 4and Google'scheck_elf_alignment.sh(seetests/scripts/android/verify_16k_verdict.py).sanitizers(optional): sanitizer runtime markers in the dynamic symbol table;{sanitizers: ["hwasan"|"asan"|"ubsan"|"tsan"|"msan", ...], cfi}wherecfireports the__cfi_checkentry point. The NDK links the runtime statically, so markers appear as imports or exports.fortify(optional):{symbols: [...]}; the bionic__*_chkimports (__memcpy_chk,__read_chk, ...; the NDK r28 sysroot headers declare 37).__stack_chk_*is the canary's signal and is deliberately not here.unwind(optional):{eh_frame, arm_exidx, gnu_debugdata}; unwind-table presence (.eh_frameeverywhere,.ARM.exidxon arm32) and the stripped mini-debuginfo.shadow_call_stack(optional,--disassembleonly):{functions, function_count}; functions whose disassembly text shows the arm64 shadow-call-stackx18push/pop pair (str x30, [x18], #8/ldr x30, [x18, #-8]!).jni(optional, A5.1): the statically registered JNI surface, decoded from the defined dynamicFUNCsymbols (so an unstripped and a stripped twin of the same build carry an identical block). Absent when the library has noJava_*export and neither lifecycle hook; which for this fact is the whole answer.static_methods: one entry perJava_*export,{symbol, class, method, address}withsignaturepresent only when the name carries the spec's overloaded__<parameters>form (the decoded parameter descriptors, e.g.I,Ljava/lang/String;,[I); a name that does not decode keeps{symbol, address, decode_error}; never dropped silently.on_load/on_unload:{symbol, address}forJNI_OnLoad/JNI_OnUnload, or null.counts:{java_exports, decoded, decode_errors}.register_natives(optional, A5.2 F1):JNINativeMethodtables recovered without disassembly, from the relocations of.data.rel.ro/.data: relative (R_*_RELATIVE, RELR and Android's packed tables as LIEF decodes them) or, for fbjni's merged registration shape (A8 N2), absolute relocations against preemptible weak dynsym symbols defined in this object (jmethod_traits<F>::kDescriptorfor the signature,MethodWrapper<...>::callforfnPtr), which the linker cannot fold toRELATIVE. A triple is accepted only whennameis a Java identifier,signaturematches the JNI method-signature grammar, andfnPtrlands on a function start in an executable section (the Thumb bit is allowed on arm32).{tables: [{address, count, entries: [{name, signature, fn_addr, thumb?, fn_name?, slot}]}], counts: {tables, entries}};slotis the triple's own address (A8 N3: the FindClass confirmer maps registrations to address ranges). The class stays unset here (it lives in theFindClasscall besideRegisterNatives, which needs the call-site layer);scan_incompletenames a packed-relocation table that could not be decoded instead of guessing around it. The decoding follows the JNI specification's "Resolving Native Method Names" (Java SE 24);tests/scripts/android/jni_probe.pycompares the block againstllvm-nm -Dplus the spec decode on every fixture.
-
Android native guide:
docs/ANDROID.mdis the user-facing guide to what blint does with Android native code: inputs, per-ABI facts and rules, framework identification, the SBOM shape, the JNI join and its known limits. The field reference below stays authoritative for every emitted key. -
App dex↔native JNI join (
android_jni, app-level, A5.2 E2): the static half of the join between the app's dexnativedeclarations and its libraries'android.jnisurfaces, one result per ABI (rule 36); never a silent first-or-best. Each ABI's rows come from that ABI's own copy of every library (tables, surfaces and FindClass confirmations are parsed per(abi, library)), so anfn_addralways names an address in that ABI's bytes; a library that does not ship in an ABI answers nothing there; never another ABI's tables in its place. A library whose first parsed copy owns neither a static surface nor a recovered table is not parsed again for its other ABIs (the same sources build every ABI's copy). Absent when the app declares no natives and has no loadLibrary site. Matching is by class and method name, and by parameter descriptors whenever the dex class overloads the name or the export carries the__<sig>form.counts:{dex_natives, load_library_sites, abis}.load_library: everySystem.loadLibrary(<literal>)call site as{class, library, member, abis};memberislib<name>.soandabisthe ABIs where that member actually ships (empty when the app does not carry it).per_abi.<abi>:{counts: {libraries, bound, bound_dynamic, jna_direct, ambiguous_dynamic, unbound_dex_natives, undeclared_exports}}plus the five lists, each capped at 256 entries with a<list>_truncatedflag beside (counts always reflect the full sets, and the callgraph's JNI edges and the reviews read the full sets, not the capped listing;jna_directcounts thebound_dynamicrows bound through JNA direct mapping).bound:{class, method, descriptor, abi, library, symbol}; each dex declaration with the export that implements it.bound_dynamic: declarations no static export answers but a recoveredJNINativeMethodtable entry does; matched on name and signature (the table carries no class, so signature equality is required);{class, method, descriptor, abi, library, fn_addr, fn_name?}. With--disassemble, an entry bound through the FindClass confirmer carriesconfirmed_by: "findclass"(orconfirmed_by: "runtime_table"when the entry itself came from the registrar walk's stores; aJNINativeMethodtable the registrar built at run time, so no static triple exists; it binds only a declaration on the class that registration'sFindClassnamed (or, for a registrar that names a class only below itsRegisterNativescall, the one class it names there), only the i386 and arm32 word-store shapes recover, every word is a store the walk saw, and every fnPtr is a function start): the registering function chain's constant class name named exactly one declaring class and exactly one candidate entry sat in that registration's address range. The confirmer runs on the arm64, x86_64, x86 (i386) and armeabi-v7a (arm32, in both its Thumb and ARM instruction set states) call-site layers only (the absint models over nyxstone text; on i386 the arguments are read from the cdecl stack slots, GOT slot loads resolve through the relocation maps, and the pc idioms (the inlinecall .+0; popand the named__x86.get_pc_thunk.*) fold the GOT base; on arm32 the arguments come from r2/r3, the pc-relative literal-pool loads and GOT slot reads resolve against the section bytes and the relocation maps beside the model, and the ARM-mode PLT stubs (add ip, pc, #A; add ip, ip, #B; ldr pc, [ip, #C]!) decode into names so a staging call through the preemptible registerHybrid symbols reaches its callee); other ABIs never bind through it. Also with--disassemble, a declaration no table answers can bind through JNA direct mapping withconfirmed_by: "jna_direct". JNA binds the native methods of the class aNative.registercall registers, at run time, to the exported symbols of the same name in the registered library, so all of these must hold: acom.sun.jna.Native.registercall in the dex registers the declaring class (the call'sClassargument is that class's literal on every path to the call, or, for the overloads without one, the calling class or the nearest enclosing class that declares a native method; a class that only calls another class's registrar is not registered);libjnidispatch.soships in that ABI (JNA's dispatch library; without it no binding can load); and exactly one same-ABI library exports the name as a defined dynamicFUNC. The register call's library name decides the candidate library when the dex yields one (a string constant held on every path to the call, or the one constant the invoked helper returns: uniffi'sfindLibraryNamefallback, followed through itsaccess$accessor; calls that register the same class with different names yield none), with JNA's own mappingfoo→libfoo.so; a registered name the named library does not export stays unbound.fn_addris the symbol's address in that ABI's own copy. The evidence is a dex bytecode walk the default join does not run, so no row ever binds this way without--disassemble.ambiguous_dynamic: declarations whose name and signature match a recovered table entry, but that pair is declared by more than one dex class or matches more than one entry in the ABI, so the class-less table cannot say which declaration it implements; each carriestable_candidates. Bound only throughconfirmed_byabove; a class the confirmer cannot name (a runtime-composed class name, a chain that names two declaring classes, two clean entries) stays here. With--disassemblesuch a row may carrycandidates_registered_elsewhere: true: every candidate entry sits inside a registration range the confirmer resolved for another class, and no resolved registration in that ABI names this declaration's class. None of the recovered entries implements it; its own registration, if any, is in a table the join did not recover (for example one the registrar builds at run time). A mark, not a binding. It cannot appear without the confirmer, and a row with any candidate outside every resolved range stays plain ambiguous. A JNA-registered declaration whose register call yields no library name, where more than one same-ABI library exports the method name, stays here too, carryingjna_exporters: every exporting library listed, no arbitrary pick.unbound_dex_natives: declarations no library in the ABI answers statically or through a recovered table; loaded from another library, obfuscated, or missing.undeclared_exports: the ABI'sJava_*exports no dex declaration claims (plus any that do not decode).
- With
--disassemble, the app callgraph (metadata["callgraph"], normally the merged dex graph) also carries the native side and the JNI edges (A5.2 F2): each bound declaration's dex node gains an edge of kindjni_static(to the decoded export's node) orjni_dynamic(to the recovered table entry's node). The native nodes merge in under a<library>@<abi>:id namespace withlibrary/abifields, the member graphs' internal edges ride along namespaced, and their unresolved external edges (PLT thunks, indirect hints) ride along under the same namespace so a path can leave a JNI function the way it leaves a standalone native callgraph. Edges are drawn only where both nodes exist; without--disassemblethere is no native side, the join stays the fact above, and no edge is drawn.jni_edge_countstates how many JNI edges the graph carries.
-
ABI Requirements (
abi_analysis): The runtime the binary requires and the ABI features that constrain where it can run. Seeabi_analysisbelow. -
Runtime Loading (
runtime_loading,recovered_dependencies): Libraries the binary opens at runtime rather than linking against, recovered from the image itself rather than from a declarative note. Seeruntime_loadingandrecovered_dependenciesbelow. -
Link Closure (
link_closure, optional): The result of resolving the dependency graph the way the dynamic loader would. Seelink_closurebelow. -
Layout Coherence (
entry_point_section,segments_summary,layout_anomalies): Where execution starts, the full program-header table, and the contradictions between them. Seelayout_anomaliesbelow.
PE (Portable Executable) files are the standard for Windows.
-
Headers (
dos_header,header,optional_header):machine_type: The target architecture (e.g.,AMD64,I386), rendered from the COFFMachinefield.machine_type_value: The raw numericIMAGE_FILE_MACHINE_*value (e.g.,0x8664). Rulemachine_types:gates and the arch mapping resolve names through blint's own PE-spec table (blint/lib/pe_constants.py), never through a dependency's enum rendering.subsystem: The subsystem (e.g.,WINDOWS_GUI,WINDOWS_CUI).subsystem_value: The raw numericIMAGE_SUBSYSTEM_*value (e.g.,2).dll_characteristics_structured: The DLL characteristics bitfield decoded by blint, not by the parsing library:{"value": 352, "flags": ["HIGH_ENTROPY_VA", "DYNAMIC_BASE", "NX_COMPAT"], "source": "optional_header"}. Unknown bits surface asUNKNOWN(<bit>); names follow the PE specification.dll_characteristics: Compat alias for one release; the joined form ofdll_characteristics_structured.flags("HIGH_ENTROPY_VA, DYNAMIC_BASE, NX_COMPAT"). Consumers should move to the structured block; the string is scheduled for removal.
-
Load Configuration (
load_configuration): This structure is the bridge between the static binary and the OS Loader/Hypervisor security features.guard_flags: The raw integer flags indicating various security settings processed by the OS loader.guard_cf_flags: List of active Guard features, such asCF_INSTRUMENTED(Control Flow Guard) andRF_INSTRUMENTED(Return Flow Guard/PAC).code_integrity: Configuration for Hypervisor-Protected Code Integrity (HVCI).flags: Settings determining how the kernel verifies the digital signature of this binary at runtime.catalog: Indicates if the signature is stored in an external catalog file rather than embedded in the binary.
enclave_config: Metadata for running inside a Trusted Execution Environment (TEE), such as Intel SGX or Windows VBS (Virtualization-based Security) Enclaves.policy_flags: Security policies enforced by the enclave (e.g., debugging allowed).imports: Specific functions imported by the enclave code from the host process.
volatile_metadata: Information used by Virtual Secure Mode (VSM).- Defines memory ranges that are mutable vs. executable, allowing the Hypervisor to enforce W^X (Write XOR Execute) policies more granularly than standard page tables.
runtime_checks: A dictionary of specific function pointers present in the binary that correspond to hardware-backed security checks.guard_rf_verify_stackpointer: Indicates the binary expects the OS to verify the Stack Pointer using ARM64 PAC keys (Key B).guard_xfg_check: Indicates support for Extended Flow Guard (Type-based CFI).guard_eh_continuation: Indicates support for Intel CET (Shadow Stack) during exception handling.
-
Authenticode (
authenticode,signatures): Detailed information about the binary's digital signature.- Provides hashes (
authentihash_*) of the signed content. - Extracts information about the signer, including the issuer (
cert_signer) and serial number. This is vital for trust verification and threat intelligence. - Kept for one release after the structured
code_signatureblock below landed (additive rule);verification_flagsis LIEF's structural verdict, not trust.
- Provides hashes (
-
Code signature (
code_signature): the structured Authenticode block, parsed byblint/lib/pe_signature.pyfrom the certificate table's DER (WIN_CERTIFICATEentries of typePKCS_SIGNED_DATA), mirroring the Mach-Ocode_signatureblock so both formats answer the same questions (W2.1/W2.2).trust_validationisnot_performedin band; blint names chains, it never validates trust (no root-store anchoring, no revocation), so "signed" is never readable as "trusted".parse_status:parsed,malformed, orabsent. A present-but-unwalkable table or signature ismalformedwith aparse_errorreason and per-signaturesignature_errors; never folded into a confident "unsigned".scope:embeddedwhen a certificate table exists,catalogwhen a--catalog-dirlookup matched the file's authentihash against a catalog member (W2.3;blint/lib/pe_catalog.py),noneotherwise. Embedded wins: a table present isembeddedeven when a catalog would also match, because Windows uses the embedded blob first.catalog_lookup: the three states, and the honesty each requires:not_performed(no--catalog-dirsupplied; a catalog-signed file cannot be distinguished from an unsigned one, so nothing may readis_signed: false),positive(the authentihash matched a catalog member),negative(the lookup was performed against a complete index and the file is in none of its catalogs; the only state from which "genuinely unsigned" follows;CHECK_AUTHENTICODEfires only here), andindex_incomplete(a directory was supplied, the hash was not found, and the index refused or truncated catalogs, or found none; "not found in the part of the index we built" is not "not signed"). Only the negative needs a complete index: a hash the index did store is proof regardless, so a positive match still resolves against an incomplete index and carriescatalog_index_incomplete: truebeside it.catalog_lookup_error: "authentihash_unavailable"names the rare case where the file's reference hashes could not be computed at all.catalog(on apositivematch):path(the matching.catfile),member_hash(the hex that matched) andmember_hash_algorithm(SHA256preferred,SHA1fallback; catalogs store one entry per width and do not name the algorithm in band; it is implied by the digest width).catalog_directorystates the--catalog-dirthe index was built from whenever one was supplied. On a positive matchsignatures[]is repopulated from the catalog's own signer through the same walk, so a consumer reads one block for both scopes (rule 21), andstructural_integritystates the byte equality between blint's computed authentihash and the catalog's stored member hash.catalog_signature_errorsmarks a member match whose catalog signer could not be read; a structural match, not a readable one.- Catalog files as input (W2.3): a
.catis PKCS#7SignedDatawhose encapsulated content is a CTL (1.3.6.1.4.1.311.10.1). Both layouts parse by member-entry shape; simplified package catalogs (entries in a plain SEQUENCE, every member as a SHA-1 + SHA-256 entry pair) and the classic RFC 5283-style CTL (version INTEGER first, entries under[1] IMPLICIT,CatalogNameValue/CatalogMemberInfoattributes naming member and indirect-catalog files). Index limits (ground rule 30): 32 MiB per catalog file, 65,536 members per catalog, 8,192 catalogs and 1,000,000 entries per index, 65,536 files walked; tripping any of them is a recorded degradation that marks the index incomplete, and counts beside capped stores stay exact. certificate_entries(exact),certificate_revision, andunparsed_certificate_entriesfor non-PKCS#7 table entries.signatures: a list; dual signing is the normal case, not an exception.signature_countis exact unlesssignature_walk_truncatedis present, which says the hostile-input walk window (64 signatures, nested included) stopped short; the count is then a floor, and so is the listing beside it. Each entry:digest_algorithm: the SignerInfo digest (SHA256,SHA1, dotted OID when unmapped).signer:cn,o,serial(hex),not_before/not_after(ISO 8601 Z),issuer_cn,eku(named:codeSigning,whql,kernelModeCodeSigning, …; dotted OIDs when unmapped).chain: the certificates the blob ships above the signer (leaf's issuer upward);cn,o,serial,is_ca,issuer_cn.chain_lengthis exact andchain_truncatedmarks a listing past the 16-entry window; unlesschain_length_exact: falsesays the chain ran past the certificate parse window (32), in which case the length is a floor andchain_terminates_at/chain_completearenull: a walk that ran out of parsed certificates found the end of the window, not the end of the chain. Otherwisechain_terminates_atnames the last link's CN andchain_completesays whether that link is self-signed (a root); a chain the blob leaves incomplete says so rather than implying a root it never carried.signer_certificate_not_parsedmarks the case where the signer's own certificate was past that window, so anullsigner is not read as one the blob never carried.timestamp:{"present": true, "kind": "rfc3161"|"pkcs9", "time": "<ISO>", "tsa_cn": "<CN>", "signature_valid_at_timestamp": true|false}; both countersignature forms are extracted (countersignatureslists every one) and validity is stated relative to the timestamp, because short-lived certificates make "is the cert expired?" the wrong question. Nested signatures stateinherited: truewhen they reuse the outer signature's timestamp. With no timestamp at all:{"present": false}andexpires_hard: true; such a signature really does stop being verifiable at certificate expiry (CHECK_SIGNATURE_NOT_TIMESTAMPED).page_hashes:{"present": true, "count": <exact>, "algorithm": "SHA256"}from theSpcPeImageDatamoniker. Presence is a hardening property; blint does not recompute them and claims no verification.opus_info(program_name,urlwhen present),statements(individual/commercial code signing), anddigest: the per-signaturestructural_integrity:algorithm,embedded,computed(the authentihash blint recomputed) anddigest_match. When blint does not recompute (unsupported algorithm), the block saysrecompute: "not_performed"instead of echoing the embedded value.
structural_integrity: the same facts for the first signature whose digest blint recomputed.weak_digest_only: true only when no signature, nested ones included, uses a modern digest; the outer SHA-1 of a dual-signed binary is not a SHA-1-signed binary (CHECK_WEAK_SIGNATURE_DIGEST).nullwhensignature_walk_truncatedcut the walk short before a modern digest was seen: the verdict needs every signature, so a walk that saw only some of them declines it rather than guessing true.signing_class(W2.4, plan 02/C): what kind of signed this file is, derived from facts the walk already carries;self_signed(the signer certificate is its own root),kernel_mode(EKU1.3.6.1.4.1.311.61.1.1),attestation_signed(EKU1.3.6.1.4.1.311.10.3.5.1,szOID_ATTEST_WHQL_CRYPTO: attested rather than HLK-tested; a signer certificate carrying both this and the WHQL EKU (the Hardware Compatibility Publisher cert does) decides asattestation_signed, the stricter child OID),whql(EKU1.3.6.1.4.1.311.10.3.5),microsoft_1st_party(a Microsoft leaf (the exact subject organization fromblint/data/pe_publisher_identities.yml) whose chain anchors at a Microsoft root, either by fingerprint when the blob ships the root or by the top shipped link's issuer naming a Microsoft root in the anchor snapshot;signing_class_anchorsays which of the two, and it is worth reading: 223 of the 224 first-party classes measured across tiers 0/1/5 areissuer_name, an issuer CN the signer wrote rather than a root blint hashed),commercial_ev/commercial_ov(code-signing EKU plus, respectively, the CA/Browser Forum EV policy OID2.23.140.1.3, or an OV policy / an organization on the signer),unknown_root(a complete chain whose self-signed root's SHA-256 fingerprint is outside the shipped snapshot), andunsigned, which follows only fromcatalog_lookup: "negative", a performed lookup against a complete index. When the inputs do not determine a class the key is absent, and its absence means undetermined; neverunsigned: no catalog directory was supplied, the index was incomplete, the signature walk was truncated, or the signer's facts (no EKU, no policy, no organization, no anchor) name nothing in the class table. A truncated walk never yields a class; a verdict is not decided from a sample, the same disciplineweak_digest_onlyfollows. Within one signature the chain decides before the leaf:self_signedandunknown_rootoutrank the leaf-stated classes, because a self-issued chain shipped whole still carries a code-signing EKU and an organization name and would otherwise read ascommercial_ov. The class is decided by the first signature in walk order whose facts determine one (the primary signature first, the order Windows evaluates them);signing_class_signaturenames the deciding index when it is not the first. Catalog-scope blocks derive the class from the catalog's own signer, the same way.- Root-anchor facts, per signature:
root_fingerprint(SHA-256 of the self-signed certificate the chain terminates at),root_known(whether that fingerprint is in the shipped snapshot) androot_microsoft(whether the snapshot flags it as a Microsoft-operated root); stated only when the walk actually reached a self-signed certificate. Where the blob ships leaf and intermediates but no root (the normal Authenticode shape) nothing is claimed about the root; the top shipped link'sissuer_cnis still the anchor name a consumer can read. - The shipped root snapshot (
blint/data/pe_roots.yml) is data with provenance: what was hashed (the DER of every trusted root certificate), from which stores (LocalMachine\Root,LocalMachine\AuthRoot), on which date, at which Windows build; regenerate withtests/scripts/generate_pe_roots.pyagainst a newer store export. Matching a chain's terminating root against it is a statement about the file, not about the world: blint performs no trust validation, consults no live store, fetches no CRL/OCSP data and builds no chain beyond what the signature blob itself ships (trust_validationstaysnot_performed), so a root outside the snapshot reads as "outside the shipped snapshot", never as untrusted in the CryptoAPI sense; blint does not claim parity withsigntool verify, which resolves live trust; blint states structure. signer.policies(when the signer carries certificatePolicies): the CA/Browser Forum code-signing policy OIDs, named (evCodeSigning2.23.140.1.3,ovCodeSigning2.23.140.1.2.1,individualCodeSigning2.23.140.1.2.2; dotted OIDs when unmapped).- Rules that consume the block:
CHECK_SIGNATURE_NOT_TIMESTAMPEDandCHECK_WEAK_SIGNATURE_DIGEST(W2.2), and the W2.4 additions;CHECK_SELF_SIGNED(the signer vouches only for itself),CHECK_SIGNATURE_UNKNOWN_ROOT(complete chain to a root outside the shipped snapshot; a chain that stops below the root is never reported, because the blob not carrying a root says nothing about the root),CHECK_KERNEL_SIGNING_CLASS(informational; the kernel-signing class, consumed by the driver lane), andCHECK_SIGNER_MISMATCH(the VERSIONINFOCompanyNameclaims a publisher fromblint/data/pe_publisher_identities.ymlwhose identity tokens the signature's signer does not carry; the one-directional impersonation check; the reverse, a publisher signing software whose CompanyName names an acquired brand or an upstream project, is normal and never a finding). All four follow the class verdict and stay silent when it is withheld.
-
Managed metadata (
dotnet): the ECMA-335 CLI block, parsed byblint/lib/pe_dotnet.pyfrom the#~/#-table stream and the#Strings,#Blob,#GUIDand#USheaps (W3.1, plan 03/A.1). Present only for binaries carrying a CLI header (data directory 14); those binaries are alsoexe_type: "dotnetbinary", decoupled from bitness (the other half of #114), withis_dotnetkeeping its old meaning for existing consumers.parse_status:parsed(clean),partial(read with named degradations),malformed(structure unusable). Partial and malformed both land inanalysis_coverage.degradations(dotnet_metadata_partial,dotnet_metadata_malformed) so a thin result never reads as clean. Absence of the whole block means "no CLI header", never "assembly without dependencies".runtime_version: the metadata root's version string (v4.0.30319,v2.0.50727).cli_flags/cli_flags_value: theCOMIMAGE_FLAGSdecoded through blint's own name table (ILONLY,32BITREQUIRED,IL_LIBRARY,STRONGNAMESIGNED,NATIVE_ENTRYPOINT,TRACKDEBUGDATA,32BITPREFERRED) plus the raw bitfield.assembly: identity from the Assembly table;name,version(four-part),culture(neutralwhen the string index is 0),public_key_token(the eight-byte token; for full-key blobs the low eight bytes of SHA-1 reversed, for eight-byte blobs the blob itself, absent when there is no key),hash_algorithm(+hash_algorithm_id),mvid(the module GUID, dumpbin canonical form).target_framework: theSystem.Runtime.Versioning.TargetFrameworkAttributevalue (.NETFramework,Version=v4.5,.NETCoreApp,Version=v8.0), decoded from the custom-attribute blob (prolog + SerString). Absent when the attribute is absent, .NET Framework 2.0-4.0 assemblies legitimately carry none.assembly_refs: the AssemblyRef table as{name, version, culture, public_key_token}rows, in table order. These becomepkg:nuget/<name>@<version>SBOM components whose purl carries the public key token as atokenqualifier (pkg:nuget/Newtonsoft.Json@13.0.0.0?token=30ad4fe6b2a6aeed, W3.5; a non-neutral culture rides as aninternal:cultureproperty). The version is the four-part assembly version, which is not the NuGet package version, Newtonsoft.Json 13.0.3 ships assembly version 13.0.0.0, so every such component carriesinternal:version_source: assembly_version, and where a.deps.jsonoverlay already named the package the overlay's component wins: one package, one component, never the same library twice at two kinds of version. Listing capped at 1,024 withassembly_refs_listed_capped;counts.assembly_refstays exact. The SBOM parent of a managed file mirrors how complete this table's evidence is asinternal:dotnet_assemblyref_state(seedocs/CUSTOM_PROPERTIES.md).module_refs: the ModuleRef table; the native DLLs named as P/Invoke scopes. Capped at 256 withmodule_refs_listed_capped.pinvoke: the ImplMap surface;{module, entry_point, method}rows, the managed analogue of the import table. Capped at 512 withpinvoke_listed_capped. Mixed-mode C++/CLI images ship ImplMap rows for native IJW methods with an emptyImportName; the Windows oracle treats an empty import name as no P/Invoke entry (SRM skipsName.IsNilrows), blint matches, and the rows still count incounts.implmap. Each distinct module also joinsdynamic_entriesunder thePINVOKEtag: a DllImport maps at first call, not at image load, so it is a declared dependency the import table never carries; the declaration set, the dependency graph andCHECK_UNDECLARED_DEPENDENCIES(whose scope includesdotnetbinarysince W3.2) read it.typerefs(W3.2): the TypeRef table as{name, scope}rows in table order; the referenced types with the assembly (or module of this assembly) each comes from. An AssemblyRef scope renders as the assembly's bare name (mscorlib); a ModuleRef or the Module row renders asmodule:<name>; a type provided by a module of this assembly is a different fact from one provided by another assembly, and the two never collapse. A nested TypeRef (scope naming an enclosing type) follows its enclosing row to the scope that names a provider. Rows whose scope cannot be resolved are omitted and named (typeref_scope_unresolved) rather than read as module-provided; listing capped at 1,024 withtyperefs_listed_capped.memberrefs(W3.2): the MemberRef table as{name, parent}rows in table order; the referenced members with the type, module or method that defines them (Type::member,module:<name>,method:<name>). A TypeSpec parent is a generic instantiation (a signature blob, not a name) so those rows are not rendered;memberrefs_typespec_parentscarries their count as a fact (it is not a degradation and does not flipparse_status). Unresolvable parents are omitted and named (memberref_parent_unresolved); listing capped at 16,384 withmemberrefs_listed_capped. The cap is deliberately far above the largest table measured (8,126 rows across 790 assemblies) because it is a detection boundary and not only a listing one;review_managed_dotnetmatches on this list, so a row past the cap is a capability blint never looks for.user_strings_count/user_strings_sha256(W3.2): facts about the#USheap walk; the exact number of entries decoded, and a SHA-256 over each value's UTF-8 bytes plus a 0x00 separator in heap order (the digest the ground-truth oracle computes, so the two walks compare byte for byte). Absent entirely when the assembly ships no#USheap (us_heap_missing; legitimate for facade assemblies, and never read as "zero strings"); a clean empty heap reports a zero count and the empty-input digest.strings(block-level, W3.2): the walked #US literals promoted to the top-levelstringskey bybinary.parse(which also setsstrings_sourcetouser_strings_heap, oruser_strings_heap+binary_scanwhere the native byte scan contributed values the heap cannot know; mixed-mode native strings). Admission is by length only (4–4,096 characters; the native scan needs an entropy gate because a byte scan cannot tell a literal from noise, but a #US entry is the literal; the dex path ships raw strings the same way). Capped at 4,096 entries withuser_strings_listed_capped; walk-level bounds: 262,144 entries, 64 MiB decoded, 1 MiB per entry (a longer entry is skipped and named, not fatal); a walk that stops early reports the prefix's count and digest and namesuser_strings_heap_truncated.counts: declared row counts for the tables the block consumes (typedef,methoddef,field,typeref,memberref,assembly_ref,module_ref,implmap,assembly). Real streams leave zero-row tables out of the Valid mask, so an unset bit means zero rows (the reader looked); only a table the overrun logic dropped stays absent.entry_point: from the CLI header token;{token, method, type}when the token resolves through MethodDef (or through MethodSpec to one), withtypethe declaring TypeDef's namespace-qualified name;{token, kind: "native"}whenNATIVE_ENTRYPOINTis set (the token is an RVA, not a metadata token; mixed-mode images); token-only plusentry_point_unresolvedwhen the token names no readable row. Absent for DLLs without an entry point.shape(W3.3, plan 03/A.3): how the application was published, as{kind, evidence}and (for a single-file publish) the decodedbundlemanifest.kindis one ofil_only,ready_to_run,mixed_mode,native_image_unknown,single_file_bundle,native_aotorapphost, andevidencenames the facts that decided it. Unlike the rest of this block,shapeis also emitted for PEs with no CLI header: a single-file bundle and a NativeAOT image are .NET applications whose managed origin is invisible to a metadata reader, and reporting them as ordinary native binaries would be exactly the "absence reads as clean" that ground rule 32 forbids. Those two carryparse_status: "no_cli_metadata";is_dotnetstays false andexe_typeis unchanged, because there is no CLI metadata for the managed rules to read.ready_to_runis decided from the CLI header's ManagedNativeHeader pointing at anRTR\0signature, and is decided before theILONLYflag is consulted: the ReadyToRun assemblies the .NET 11 SDK produces havecli_flags_value0x4, so ILONLY is clear on an ordinary R2R build and a mixed-mode test that ran first would call every one of them C++/CLI. A ManagedNativeHeader that is present but is not an R2R one isnative_image_unknown, never folded intoil_only.single_file_bundlevsapphostturns on theint64immediately before the 32-byte bundle signature, not on the signature itself: every .NET apphost embeds the signature as a placeholder whether it was bundled or not (measured at offset 71,856 in the framework-dependent, self-contained and trimmed apphosts), and what the bundler writes is the manifest's file offset. Zero means unbundled. A written offset whose manifest cannot be decoded still reportssingle_file_bundlewithbundle_header_unreadable; a bundle blint could not read, not a host with no payload.bundlecarriesversion,bundle_id,member_count, thedeps.json/runtimeconfig.jsonsizes andmembers: one{path, offset, size, type}entry per embedded file (typeisassembly,native_binary,deps_json,runtime_config_json,symbolsorunknown), listed up to 4,096 withmembers_listing_capped. No rule reads that list, so the cap bounds metadata size and not detection. A manifest that stops mid-entry ismembers_truncatedinstead; blint's own bound and a damaged file are different facts (ground rule 14).native_aotrequires two conditions, because neither alone is specific: aDotNetRuntimeContractDescriptor/DotNetRuntimeDebugHeaderexport and anRTR\0header in an initialized, non-executable section.coreclr.dllexports the same descriptor and is an ordinary native runtime DLL; the measured NativeAOT image also contains the four signature bytes inside.textas instruction encoding, which is why the section restriction is part of the test.- Measured false positives before it shipped: over a full
C:\Windows\System32(3,984 PEs, 0 parse errors) the classifier emits a shape for 15 files and stays silent on 3,969; 7il_onlyand 8mixed_mode, the latter all real C++/CLI images (the MFC managed-interop DLLs,dnscmmc,NAPCRYPT), and not onenative_aot,single_file_bundleorapphostamong the native PEs. - A self-contained single-file publish embeds the managed assemblies but not the native runtime: the measured bundle lists 182 assemblies plus
deps.jsonandruntimeconfig.json, and the 15 native runtime files beside the equivalent self-contained publish (coreclr.dll,clrjit.dll,hostfxr.dlland the rest) are not members; the runtime is linked into the host image itself, whose sections total 11.7 MB against the plain apphost's 76 KB. So the member list is the shipped managed set, which is what it says and all it says. - What
shapedeliberately does not claim: whether a publish was framework-dependent, self-contained or trimmed. Those differ only in the contents of the output directory (5, 200 and 27 files in the measurement), not in any byte of the binary blint is handed.
strong_name(W3.4, plan 03/A.4): the strong-name facts as separate presence facts, never one verdict. Strong names are distinct from Authenticode and are never merged with thecode_signatureblock: a strong name says the assembly's identity is bound to a key, Authenticode says a publisher vouched for the file, and an assembly can have either, both or neither. The block carries no verification result anywhere; blint does not hash the assembly with the signature region excluded, so no field here may be read as "the signature verifies".declares_public_key/public_key_size/public_key_token: whether the Assembly row'sPublicKeyblob is present, its size in bytes (160 for the 1024-bit RSA keys every assembly measured ships; 8 when the column holds a bare token), and the token computed from the key (the samepublic_key_token()the identity block flows from; the two tokens can never disagree). Absent rather than false when the Assembly table itself could not be read.strongnamesigned_flag: the CLI header'sCOMIMAGE_FLAGS_STRONGNAMESIGNEDbit, restated beside the facts it belongs with. It is a header bit, not a verdict: a/publicsignbuild sets it over an all-zero signature region.signature_present/signature_size(andsignature_all_zero): the CLI header'sStrongNameSignaturedirectory (RVA/size at offset 32; the field W3.3 did not read, two fields before theManagedNativeHeaderit did).signature_all_zerostates whether the region's bytes are all zero, the fact a delay-signed assembly ships, and a public-signed one too (measured: 215 of 1,186 .NET 10 shared-framework assemblies carry a null signature with the flag set; a delay-signed build carries one with the flag clear). It is emitted only over bytes blint read in full: an unmapped RVA (strong_name_signature_unmapped), a declared size past the 4 KiB read bound (strong_name_signature_exceeds_cap) a region running past end of file (strong_name_signature_short_read) or a directory whose two halves contradict each other (a size with no RVA, or an RVA with no size, reported assignature_present: falsebecause no region exists, with the contradiction in the degradation rather than in a number;strong_name_signature_directory_half_declared) withholds the field and names the reason; an absent region reportssignature_present: falseand omitssignature_all_zerorather than vacuously calling it true.delay_sign: anAssemblyDelaySignAttribute(true)row exists. This is the one metadata location that names DelaySign, and it is rarely written: Roslyn consumes the pseudo-attribute and emits no row (measured with and without/delaysign, attribute in source or not), so a Roslyn delay-signed assembly reportsdelay_sign: falseand shows its shape insignature_all_zeroinstead. Rows survive from toolchains that write them; the MSVC-managed MFC pair (mfcm140.dll) carries one over a fully-written signature, which is exactly why this is a fact and not a verdict.internals_visible_to/internals_visible_to_count: the friend assembly names theInternalsVisibleToAttributerows name; an attack-surface fact, because a friend name is a name anyone can build an assembly under when the key does not constrain it. Each entry is{name, public_key?, public_key_token?}: the name byte-exactly as stored, anyPublicKey=the string names, byte-exactly as the string writes it (a value that is not parseable hex is still reported, withinternals_visible_to_public_key_invalidnamed and no token emitted), and the token computed from that key. Only rows parented on the Assembly row are listed; the scope the runtime honours and the ground-truth oracle walks. A strong-named parent must key its friends (CS1726); a keyless friend under an unsigned parent is legal and reportsnamealone. Listing capped at 128 withinternals_visible_to_listed_cappedand the exact count ininternals_visible_to_count; no rule reads this list, so the cap bounds metadata size and not detection. The largest count measured across 1,248 assemblies is 55 (System.Private.Windows.Core.dll).
- Bounds (ground rule 30): stream count ≤ 16, version area ≤ 1 KiB, string reads ≤ 4 KiB (a longer entry is
strings_entry_truncated_by_capwhen a NUL exists beyond the window,strings_entry_unterminatedwhen one does not), blob reads ≤ 64 KiB, declared rows must fit the stream (tables_exceed_streamdrops the tables from the first overrun on, and their counts stay absent rather than being reported as facts), unknown tables (0x30+, portable-PDB space) stop the layout instead of guessing row widths. Every refusal is a named degradation in the block; a table blint could not read never reads as "this assembly has no X". table_stream_heapsizes_unhandled:<byte>: the table stream's HeapSizes byte carries a bit beyond the three heap-index widths. Those bits mark delta-only and extra-data metadata and move where the rows begin; no assembly measured sets one, so blint names the shape rather than laying rows out on an assumption.- Layout cross-check: a correct row layout accounts for the whole table stream bar an alignment tail (measured at 0, 2 or 4 bytes over 375 real assemblies). A larger shortfall means the computed column widths are not the writer's, so every row read under them is some other table's bytes;
tables_layout_short:<bytes>is named and every row-derived fact (assembly,assembly_refs,module_refs,pinvoke,entry_point,target_framework, the strong-name block's key/attribute facts) is withheld rather than reported.countssurvives, because the row-count array is header data independent of the widths. The CLI-header half ofstrong_name(signature_present,signature_size,signature_all_zero,strongnamesigned_flag) survives too; those are header facts, not row-derived.
-
Resources (
resources): Metadata extracted from the.rsrcsection.version_metadata: Contains key-value pairs likeProductName,CompanyName, andFileVersion. Useful for identifying the software and its origin.manifest: The embedded XML application manifest, which controls privileges, dependencies, and UI settings.
-
Debug directory (
debug): one block per image, decoded inblint/lib/pe_debug.py. Added with the W1.1 packet (plan 01/A.4-A.5); an image with no debug directory reports nothing, and the security-properties gaps carry the absence.entries: one row perIMAGE_DEBUG_DIRECTORYentry;type(named from blint's winnt.h table inpe_constants,UNKNOWN(<value>)for untabled values),type_value,timestamp,size, plus the raw addresses. More than 64 entries are counted underentries_truncatedrather than decoded.codeview: the PDB lookup key;signature(RSDSor the legacyNB10),guid(canonical dumpbin form for RSDS),age,pdb_pathandpdb_filename(split on both\and/). This block is the single source feedingsecurity_properties.debug_info/debug_info_pdb_path.repro:{"present": true, "hash": "<hex>"}(the length-prefixed image hash, 32 bytes on current/Breprobuilds) when the image carries anIMAGE_DEBUG_TYPE_REPROentry,{"present": false}when a directory exists without one. This is the authoritative reproducibility answer;is_reproducible_build(LIEF, timestamp-based) stays alongside as the legacy heuristic; when they disagree,reprowins.vc_feature: theIMAGE_DEBUG_TYPE_VC_FEATUREcounters (c_cpp,gs,guards,sdl,pre_vcpp).pogo: the PGO section list (sections, capped at 256 withsections_truncated/sections_total) and thesignature(PGO/PGU).ex_dllcharacteristics: theIMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICSpayload decoded through blint'sEX_DLL_CHARACTERISTICSbit table (CET_COMPAT, ...).
-
Rich header (
rich_header): decoded and validated at the byte level inpe_debug.py; replaces the raw LIEF entry dump when the header can be read (the LIEF shape stays as the fallback). Entries are in file order.key: the XOR key, rendered as0x....checksum_valid: the stored key is the checksum of the DOS region plus the header itself (algorithm per the RichHeaderResearch RichPE tooling, validated on real MSVC images); afalsehere is a repacking/tampering signal, never a parse error.entries: the raw records,{id, build_id, count}in file order.decoded: each record named through the generated comp.id tables inblint/data/pe_rich_compids.yml(provenance in the file header; regenerate withtests/scripts/generate_pe_rich_compids.py):{product_id, tool, label, build_id, count}; the label names the exact Visual Studio drop, e.g. python313.dll'sLNK VS2022 v17.14.9 build 35213. Unknown pairs still name the tool from the 16-bit product id; wholly unknown products renderUNKNOWN(<id>).toolchain:linker_build_idandlinker_labelfrom the linker record,comp_id_builds, andmixed_toolchain(objects built by more than one drop; vendored static libraries are the benign case, a rewritten header the interesting one). This also feeds thetoolchainblock'smsvccompiler entry.
-
VERSIONINFO (
version_info, top level): the full VERSIONINFO decode, added with W1.3 (plan 01/A.7); the string tables are parsed from the raw resource by blint because LIEF 1.0 drops valid tables and merges adjacent language tables. This block is the input for the Windows SBOM identity work (03/D) and the tier 0-1 presence gate.present,languages: sorted language keys (040904b0, ...).strings:{language: {KeyName: value}}; every key of every language table.fixed: theVS_FIXEDFILEINFOblock decoded from the raw resource (LIEF 1.0 does not expose it):file_version/product_versioninMS.LSdotted form,file_flagsthrough the mask (DEBUG,PRERELEASE,PATCHED,PRIVATEBUILD,INFOINFERRED,SPECIALBUILD),file_os,file_type,file_subtype. Absent when the resource carries strings only.mismatches: which ofFileVersion/ProductVersiondisagree between the fixed block and the string table; the classic tampering tell. The comparison is semantic (first two numeric components), so a fixed3.13.7150.1013beside a marketing string3.13.7is agreement, not a finding.
-
Resources depth (
resourcesextensions): the legacyhas_*booleans, the rawmanifestXML and the flattenedversion_metadataare unchanged; W1.3 adds:manifest_parsed: the manifest decoded;requestedExecutionLevel,uiAccess,dpiAware/dpiAwareness,longPathAware,activeCodePage,supportedOSGUIDs mapped to Windows names, andassembly_identities(side-by-side dependencies). A present-but-unparseable manifest recordsparse_status: failedrather than reading as "no elevation requested".tree_summary: resource type →{count, bytes, entropy}. Type names come from blint's RT_* table; entropy samples two 64 KiB windows (start and end) per resource so a multi-MB blob is summarized without being read in full.hashes: per-resource SHA-256 (type,id,lang,size,sha256), truncated at 1 MiB per resource withhash_truncatedand a recorded degradation beyond a 32 MiB total budget.icon_hash: deterministic SHA-256 over the sorted per-icon digests; the cluster key for "same icon" identity.embedded_pe: resources whose data starts with a structurally valid PE image (MZ, e_lfanew inside the data,PE\0\0there), each with itstype,id,file_offsetandsize; a dropper indicator reported as a fact, not a finding.degradations: every limit the tree hit (node count, depth, hash budgets, embedded-PE report cap) is recorded here, never silently skipped (ground rule 30: the resource tree is attacker-controlled input).
-
Imports and Exports (
imports,exports):imports: A list of all functions imported from external DLLs, grouped by library. Forms the basis of theimphash.exports: A list of all functions this binary provides to other executables.- W1.2 import depth (
blint/lib/pe_imports.py):- Ordinal-only imports are resolved through the generated ordinal snapshot (
blint/data/pe_ordinals.yml, sourced from the six DLLs whose ordinals are stable and common:ws2_32,mpr,oleaut32,shlwapi,netapi32,wsock32; the snapshot records the Windows build and the source files' sha256). A resolved entry carriesordinalplusresolution: "ordinal_table"; an ordinal outside the snapshot stays an ordinal; itsshort_namerenders#<ordinal>andresolution: "unresolved". The two outcomes are never conflated. - API set contracts (
api-ms-win-*,ext-ms-*) resolve through the generated snapshot ofapisetschema.dll(blint/data/pe_apisets.yml, which records the Windows build it came from; contract versions the schema dropped are restored from theSystem32\downlevelstub DLLs' forwarder exports). A resolved entry's dependency identity is the host DLL, with the original contract name kept inapiset(per entry) andapisets(per dependency-list entry); a contract missing from the snapshot keeps its own name. Regenerators live intests/scripts/. delay_imports[]: the delay-load import table in the same shape asimports(never merged with it, the distinction is the signal) plusdelay_import_hash, the same normalization asimport_hashover the delay table only.import_resolution: exact counts for the image (ordinals_resolved,ordinals_unresolved,apisets_resolved,apisets_unresolved) with capped (ordinals_unresolved_capped,apisets_unresolved_samples) samples naming what fell back.dynamic_entriesgains atagvocabulary besideNEEDED:DELAYLOADfor a DLL reached only through the delay-load table andFORWARDERfor an export-forwarder target (see below), so the SBOM sees the dependency without conflating the tables. An apiset-resolved NEEDED entry carriesapisets: [...]listing the contract names it stands in for.
- Ordinal-only imports are resolved through the generated ordinal snapshot (
- Export forwarders: an export whose target lives in another DLL records
forwarded_toin dumpbin's form ("NTDLL.RtlAllocHeap") beside the existingfwd_library/fwd_function, and the target DLL joinsdynamic_entries(tagFORWARDER) and theimport_dependenciesgraph (library typeforwarder_target); the loader maps it even though no import-table entry names it.
-
Layout forensics (
layout): the section-table and header facts of B.5, each a named field, computed from the section table, optional header and file size. Fields that would be vacuous are omitted, not defaulted (a zero entry point computes no placement fields; a single-section image omitsentry_point_in_last_section, which is vacuous there):entry_point_section,entry_point_outside_text(computed only when a.textexists to be outside of),entry_point_in_last_section,entry_point_outside_any_section.sizeof_image_expectedandsizeof_image_mismatch: what the section table adds up to versus what the optional header claims.zero_raw_size_sections,large_virtual_zero_raw_sections(no file bytes, a page or more of mapped space: the unpacker-stub shape; the ordinary.bssappears here too, it is a fact list, not a verdict),raw_exceeds_virtual_sections.non_standard_sectionsandsection_naming_toolchain: names outside the named toolchain's section set (MSVC's documented.00cfg,.hexpthk,.a64xrm/fothkARM64X/EC sections count as standard;/nnnCOFF string-table long names are the spec's encoding, not custom names). The toolchain comes from the rich header (W1.1), the Go and .NET markers, elseunknown(key omitted; the generic prefix set applies).truncated_last_section: the last section's raw bytes run past end of file.timestamp(headerTimeDateStamp),timestamp_epoch_zero(the/Breproshape),timestamp_in_future. A future stamp on a/Breprobuild is expected: correlate withdebug.repro.present.
-
Pre-main execution (
pre_main_execution): the one summary of what runs beforemain(B.1); the legacy top-leveltls_callbacksaddress list keeps its shape.tls_callbacks: one row per callback;address, andfunction/resolved: truewhen the address matched a discovered function.tls_directory_writableandtls_callback_array_writable(withtls_callback_array_section): the writability of the TLS directory struct and of the callback array the loader walks; a writable array is the runtime-patchable shape. Both are stated either way; the array pair is omitted only when no section covers the array.ctor_functions(static initializers) or, when LIEF's PE initializers are exactly the TLS callbacks (it derives them from the callback array),initializers_are_tls_callbacks: true: one fact, not a duplicate list.callback_count,initializer_count: exact counts, never the size of the capped listing;tls_callbacks_truncatedandinitializers_truncatedsay when the listings stop short of them.anti_debug_reachable_functions: only when disassembly ran and a resolved callback's call targets reach one of a fixed set of debugger-awareness imports (IsDebuggerPresent,CheckRemoteDebuggerPresent,NtSetInformationThread,NtQueryInformationProcess).
-
Thread Local Storage (TLS) Callbacks:
tls_callbacks: List of the callbacks associated with the current TLS. These functions are called before any other functions.tls_address_index: The location to receive the TLS index assigned by the loader. This location should be located in a writable section like .data.tls_sizeof_zero_fill: Size in bytes of the zeros to be padded after the data specified by data_template.tls_data_template_len: Length of the initial content used to initialize TLS data.tls_characteristics: The four bits [23:20] describe alignment info. Possible values are those defined as LIEF.IMAGESCN_ALIGN*, which are also used to describe alignment of section in object files. The other 28 bits are reserved for future use.tls_section_name: Section associated with the TLS object (or absent if not linked)tls_directory_type: Name of the DataDirectory associated with the TLS object (or absent if not linked)
-
Exceptions (exceptions): For x86-64 and ARM64 PE binaries, this section provides detailed stack unwinding information extracted from the IMAGE_DIRECTORY_ENTRY_EXCEPTION.
- Attributes:
rva_startandrva_end: The memory boundaries of the function code.unwind_info: metadata including sizeof_prologue, frame_reg (frame pointer register), and flags.opcodes: The specific machine instructions (e.g., PUSH_NONVOL, ALLOC_SMALL) used to set up the stack frame.handler_rva: The address of the language-specific exception handler (e.g., __C_specific_handler).
- Use Cases:
- Function Discovery in Stripped Binaries: Even if the symbol table is removed, the Exception Directory must remain valid for the OS to handle crashes. This makes rva_start and rva_end the most reliable way to discover function boundaries in stripped malware or commercial software.
- Stack Frame Reconstruction: By analyzing the opcodes and prologue_size, analysts can reconstruct exactly how the stack is manipulated. This is vital for understanding where local variables are stored and identifying potential buffer overflow conditions.
- Anti-Analysis Detection: Malware sometimes employs custom exception handlers (handler_rva) to obscure control flow or detect debuggers. Identifying non-standard handlers is a key indicator of obfuscation.
- Attributes:
-
Embedded cryptography (
crypto_material): Recovered from the raw bytes of.rdata,.data,.textand.rodata, so it is available without--disassemble. Complements the behaviouralCRYPTO_BEHAVIORfunction review, which can say a routine looks like a cipher but not which one.algorithms: Sorted list of algorithm names identified from specification-fixed constants.constants: One entry per matched constant, withalgorithm,constant(what the bytes are),section,offsetandconfidence. A 16-byte round-constant table ishighconfidence; a 4-byte polynomial that could occur as an unrelated immediate ismedium.permutation_tables: 256-byte tables holding every byte value exactly once; a substitution box, including a custom or modified one that matches no published constant.opaque_regions: Contiguous high-entropy regions that match no known table, withsize,entropy,section_sizeandsection_fraction. The fraction is what separates an embedded encrypted payload from the certificates and compressed assets that fill a large program's.rdata.- Note that absence of a constant is not absence of the algorithm: an AES built against AES-NI carries no tables, and mbedTLS with runtime table generation leaves only zero-filled BSS.
-
Stack-built strings (
stack_strings, requires--disassemble): String literals the binary assembles on its stack from arithmetic rather than storing in a data section. These appear in no other string channel, so without this they are invisible to string scanning, to YARA rules written against literals, and to a reviewer reading the file.- Each entry carries
value,encoding(utf-16leorascii), thefunctionandaddressit was built in, and theframeslot. - Recovery is a fixed-point dataflow over the function's CFG: a register or frame byte survives a control-flow merge only when every incoming path agrees on its value, and blocks unreachable from the entry contribute nothing. A value assembled on one branch of a conditional is therefore not reported; treat entries as paths that certainly execute, and confirm the reconstruction before acting on it.
stack_strings_coveragerecords how the pass covered the binary:functions_total,functions_dataflow(CFG fixed point),functions_fallback(no usable CFG; straight-line pass), andfunctions_iteration_cap_hit(the loop iteration cap was reached; those functions contribute no entries, because a half-converged state is residue rather than evidence).
- Each entry carries
-
Call-site constant arguments (
call_site_arguments, requires--disassemble): The integer constants an image holds in the ABI's argument registers at calls the disassembler resolved to a named callee. A capability review can only say an image is linked to an API; this block says what it passes to it, which control code reachesDeviceIoControl, which algorithm constant reachesCCCrypt, which path string reachesCreateFilewhen the constant points into a string section.- One entry per distinct
(callee, argument, value)triple, sorted, each withargument(0-based position in the callee's integer argument list),site_count(how many call sites were collapsed), up to three citingfunctions, and anexamplecitation (function,line,instruction) so every value can be checked against the disassembly. When a constant points at a mapped section and decodes as text, the string rides along asstring. A plain integer is offered for decoding only from 0x10000 up (below that it is a flag, size or count); a pointer folded from a pc-relative form is offered wherever it lands, since shared objects map read-only data from address zero. In ELF images only allocated, non-executablePROGBITSsections are read, never symbol or string tables, notes or code. Both encodings a pointed-at literal uses are read: ASCII and UTF-16LE little-endian (the wide form theWWindows APIs take), under the stack-string decoder's character filter and a four-character minimum that counts decoded characters, not bytes. - A constant is reported only where every CFG path reaching the call agrees on it; unresolved call sites contribute a coverage counter, never an entry: an integer with no resolved destination stays out of the block. Callee names come from the disassembler's resolved call targets; for ELF shared libraries the PLT stubs are decoded into those names (arm64
adrp x16/ldr x17, x86-64jmp [rip+disp32]and arm32add ip, pc, #A; add ip, ip, #B; ldr pc, [ip, #C]!stub forms, resolved through the JUMP_SLOT relocations), and each recorded target carries the emitting instruction'ssite_index; nyxstone prints ARM branch targets pc-relative, so two sites can share an operand text while reaching different callees, and resolution is by site with operand text as the fallback (an operand resolving to more than one callee still refuses rather than guessing). x86 tailjmp immoperands read end-relative, likecall's. When the resolved target is a local pure thunk (at most four register-move or constant-load instructions, then one unconditional tail branch to a named callee: the machine outliner'sOUTLINED_FUNCTION_*and hand-written forwarders), the entry names the thunk's final callee and carries the thunk incallee_via_thunk. Its arguments are read after the thunk's instructions are stepped over the caller's state, so a constant the thunk loads replaces the caller's. - The block is bounded:
BLINT_MAX_CALLSITE_ARGUMENTS(default 4096,0disables the block) caps entries per binary, one function contributes at most 256 distinct entries, and every tripped bound is named incall_site_arguments_coverage(entries_truncated,functions_entries_capped,functions_entries_capped_names). call_site_arguments_coveragealso records the per-function analysis method counts (functions_total,functions_dataflow,functions_no_cfg,functions_cfg_mismatch,functions_cap_hit,functions_no_abi,functions_skipped) andrecords_unresolved_callee, so what the block cannot say is a number, not a silence.- Position-independent code materialises string addresses in two instructions (ARM64
adrp+add, x86-64 rip-relativelea). When the disassembly can locate each instruction, those pairs are folded into the absolute address they compute and reported as the constant, so astringin this block is the text actually living at the address a call received. Each disassembled function therefore also carriesinstruction_lengths(per-line instruction sizes aligned withassembly; reconstruct a line's address by prefix-summing lengths from its CFG block's start VA; except on 32-bit ARM functions that carrydata_spans, where each span's size must be added once the running offset passes its address). Materialisation failures are named, never silent:functions_no_line_addresses(blocks without usable VAs or without exported lengths),functions_extent_mismatch(a block's extent contradicts the strides its lines claim),functions_unmodelled_pc_relative(anadrthe model deliberately does not fold),arguments_materialised/pointer_values_page_only(how many argument values were folded pointers, and how many of those are bare uncompleted pages), andstrings_resolved(entries whose constant resolved to text). The same reasons surface inanalysis_coverage.degradationsascallsite_*, together withcallsite_abi_not_modelled(the dataflow does not model this ABI (only arm64 and x86_64 are) so the block is empty whatever the code does) andcallsite_entries_truncated(the block hit its entry bound, so an absent constant proves nothing). - Three per-function fields exist only on 32-bit ARM (armeabi-v7a) disassembly and are absent everywhere else:
instruction_mode("thumb"or"arm", the instruction set state the bytes were decoded in; from$a/$tmapping symbols, the symbol's Thumb bit, call or data-pointer interworking evidence, or the scored both-state decode arbiter; seedocs/DISASSEMBLE.md),instruction_mode_source(what decided it:mapping_symbol,symbol_parity,call,data_pointerorarbiter) and, when$ddata islands were skipped inside the function's extent,data_spans(a list of{address, size}ranges that sit between the instruction lines).
- One entry per distinct
-
Privileged-host plugin surface (
host_plugin, PE only): Present only when the binary's export set (plus, for the two COM contracts, an in-binary registration reference) satisfies a documented Windows extension-point contract; absent otherwise, and absence reads as "no plugin contract evidenced", never as "verified not a plugin". A binary whose export directory is declared but unreadable carriesexports_read_status: "failed"beside the emptyexportslist and anexport_table_unreadabledegradation inanalysis_coverage, so a detection gap is never mistaken for a clean result.contracts: one entry per satisfied contract, in table order, each withid,title,host_process(the process the contract loads the DLL into;lsass.exe,spoolsv.exe, the W32Timesvchost.exe,winlogon.exeand WNet callers,LogonUI.exe,audiodg.exe, or the set of calling processes for the Winsock/AppCert contracts),host_privilege(system,local_service,protectedorinherited), andmatched_exports(the exact names that satisfied the contract).protected_process: trueappears on contracts whose host runs protected (LSA under PPL,audiodg.exefor protected content);credential_exposurenames what a contract sees in the clear (lsa_password_filter: account passwords on every change;network_provider: logon credentials viaNPLogonNotifyunder winlogon);registration_hintnames the registry key the host registers the DLL under.- The export-keyed contracts are exact-name matches against the table in
blint/data/pe_host_plugin_contracts.yml, which is pinned against Microsoft's documentation with each contract's full-System32 hit count recorded in its header. The two COM contracts (credential_provider,audio_processing_object) additionally require an in-binary reference to their registration path (Authentication\Credential Providers,AudioEngine\AudioProcessingObjects), scanned from section bytes in ASCII and UTF-16LE: not from thestringslist, whose gates and fallback cap must not bound detection. Their entries carryevidence.registration_strings(the readable spans around each reference, listed up to 4; a listing bound no rule reads past the first entry of). Recall on these two is partial by construction: only the registry knows which CLSID is registered, so the finding text qualifies with "when registered". - For ARM64X images whose primary and nested export listings disagree on the contract set,
host_plugin_slice_variancenames the differing contracts; when the primary listing is unreadable but the nested one read cleanly,host_plugin_scope: "nested_binary"says which listing speaks. Both are mirrored inanalysis_coverage. - Ordinal-only exports cannot satisfy any contract here, which is sound rather than a gap: every host in the table resolves its entry points by name.
-
Driver identity (
driver, PE only): Present for every Windows image whose subsystem is NATIVE or WINDOWS_BOOT_APPLICATION, that importsntoskrnl.exe, or that imports the UMDF framework (the one addition to the plan's gate sentence: a UMDF driver is a user-mode DLL and would otherwise never reach the block, yetumdfis a kind the model names). Identity, not verdicts; thedriver_ioctlsblock (below) stays the IOCTL surface of record.input_length_constraints(W5.3, requires--disassemble): per handler function, theInputBufferLengthconstants the code compares against (the IRP stack-location load at+0xB8/+0x60followed by an access of the length field at+0x0C), andinput_length_checkedrides each recovereddriver_ioctlsentry whose handler function contains such a comparison. The verdict is function-level and says so; it is a correlation with the function the code was recovered from, not a proof the check guards that branch; a handler blint's function discovery split into fragments may hide its check.device_acl(W5.3): the device-object security descriptor, from SDDL strings scanned in.rdata/.datain both ASCII and UTF-16LE; theDefaultSDDLStringanIoCreateDeviceSecurecall installs.world_accessible: truenames grants to Everyone/Anonymous/Users/Authenticated Users (WD/AN/BU/AU); the canonical restricted shapeD:P(A;;GA;;;SY)(A;;GA;;;BA)is listed but never flagged. Absent when no descriptor exists; the no-descriptor shape is carried by the W5.1 device-creation facts, not by an empty block here. Measured: 53 of 330 benign VM drivers install a descriptor, 18 grant something to a world SID (typically read-only), so the ACL fact is evidence for the reviewer and for the review rules, not a finding by itself.vulnerable_driver(W5.3, every PE): exact-digest (SHA-256/MD5) matching against the shipped snapshot of the loldrivers.io dataset and the Microsoft recommended driver block rules (blint/data/pe_vulnerable_drivers.json, with both sources' URLs, fetch dates and the blocklist policy version; refreshing it is a data PR; no network call at scan time, ever).lookup_statusis one ofmatched/no_match(hashes checked, none listed; the only state from which "not in the snapshot" may be read) /no_hash/snapshot_unavailable. Filename or signer matches never fire: driver filenames collide across vendors, and this accusation is specific enough that a name match would be a false positive by construction.kindis determined from the signal tableblint/data/pe_driver_kinds.yml(wdm/kmdf/umdf/wdf_static/minifilter/ndis/storport/wfp) and never defaulted: an image for which no signal matches carrieskind: "unknown"as a value. Every matched signal is listed inkind_evidence(kind + the establishing import) even when precedence picked another kind, so a KMDF minifilter shows both. When a driver matches several, the reported kind follows the table's precedence; the specific I/O model (filter, storage, network) says more about what the IOCTL surface means than the framework wrapping it.wdf(when a WDF binding is imported):library(WdfLdr.sysfor the dynamic binding, the statically linked runtime DLL otherwise) andstatic.wdf.versionis deliberatelyNonewithversion_note: "not_decoded_statically"; the version lives in the WDF_BIND_INFO struct behind a pointer argument, and decoding it would mean guessing a non-public layout, i.e. fabricating metadata.device_names,symbolic_links,client_device_paths,registry_paths: the kernel objects the image names, scanned from section bytes in ASCII and UTF-16LE (so thestringslist's gates never bound identity evidence;object_paths_source: "section_scan"says so). The empty case is stated (device_names: []), not left ambiguous with "not scanned".object_paths_truncatednames any per-bucket matches past the listing bound of 16; a listing bound only, no rule reads past the first entries.wdm_callbacks(requires--disassemble): the WDM callbacks the driver's code registers (DriverUnload,DriverStartIo,AddDevice) recovered from stores at the DRIVER_OBJECT field offsets (both layout widths, gated by the image's machine type, in Intel and AArch64 syntax), plusfast_io_dispatch. Two refusal shapes keep it honest: a NULL store (str xzr/ a zeroed register) is the driver stating "no callback", not a registration, and a store counts only in a function that also stores a MajorFunction slot; the shared context proving the base register holds a DRIVER_OBJECT (a KMDF driver's runtime context structs collide with these offsets; cdrom.sys stores non-NULL pointers at +0x60/+0x68 into exactly such a struct).callback_evidencenames the registering functions. Absent when no disassembly ran; absence of a run is not absence of callbacks.dispatch_routines(requires--disassemble): the IRP_MJ_* handler summary, mirrored fromdriver_ioctls.dispatch_handlerswithdispatch_routines_sourcenaming the block of record.signing: the W2.4signing_class(and its anchor) carried in fromcode_signaturewithsource: "code_signature"; summarised, never re-derived, so the two blocks cannot drift.hvci_compatibility(W5.2): Microsoft's documented HVCI (memory integrity) ruleset as individually evidenced conditions,machine_64_bit,no_writable_executable_sections,section_alignment_page_sized(≥ 0x1000),relocations_present, each with averdictofpass/fail/undeterminedand its evidence (compatibleistrueonly when every condition passes; a condition whose header source is unreadable isundetermined, never failed).CHECK_HVCI_COMPATIBLEfires naming each failed condition. Measured: all 330 VMSystem32\driverspass every condition.kernel_hardening(W5.2):kernel_cfg(GuardFlagsCFW_INSTRUMENTED, the kernel-mode CFG bit),cfg_instrumented,retpoline,xfg,rfg(all fromload_configuration.guard_flagsvia blint's winnt.h table),force_integrity(fromsecurity_properties),enclave_config; each key omitted when its source is absent, neverFalseby omission.boot_startis determined only from theWINDOWS_BOOT_APPLICATIONsubsystem (boot_start_basisnames the basis; for every other driver StartType is registry state blint does not read, so the fact isnull). The plan's "import optimization" has no GuardFlags bit of its own in the SDK table; retpoline presence is the flag that carries it.dangerous_imports(W5.2, requires--disassemblefor themsr_port_iofamily): the BYOVD primitive families;physical_memory(MmMapIoSpace, ZwMapViewOfSection, …),process_tampering(KeStackAttachProcess, ZwOpenProcess, …),pci_config(Hal{Get,Set}BusDataByOffset, …),callback_registration(ObRegisterCallbacks, PsSet*NotifyRoutine, …),msr_port_io(rdmsr/wrmsr/… instructions found in disassembly); each with its matched imports (listed up to 8, a listing bound;import_countstates the rest), and acapability_scoresumming family weights. This block is context, not a finding: measured over the 330-driver benign sub-tier, MmMapLockedPagesSpecifyCache is imported by 138 benign drivers (42%), MmMapIoSpace by 3, and 72 drivers carry ≥ 2 families, so no rule fires on family counts alone at any severity (ground rule 34). The findings that need this capability live in the review rules that also require missing access checks (DRIVER_INSECURE_DEVICE_OBJECTand siblings).
-
Kernel-adjacent user-mode surface (W5.4, PE only):
com_registration(CLSIDs/AppIDs the image references, fromCLSID\{...}/AppID\{...}registry-path strings scanned in section bytes, both encodings;clsid_count/appid_countexact withlisting_truncatednaming the listing bound),persistence_surfaces(references to Run keys, IFEO, AppInit/AppCert, WMI event consumers, scheduled-task XML, and the Services control set; exact counts, bounded evidence snippets, "a reference is not a write" stated in band),amsi_references(AmsiScanBuffer references), andrpc_interfaces(requires--disassemble: interface UUIDs read from theRPC_SERVER_INTERFACEstructures anRpcServerRegisterIf*call site receives, GUID at +0x04 per rpcdce.h, plausibility-gated; call-site anchored only, with the recall limit stated, ARM64 images resolve few import callees, so MIDL servers there are mostly invisible). The review rules that judge these surfaces (review_usermode_win.yml; direct syscalls closing issue #2, the injection chain, ETW/AMSI tampering, persistence references) carry their ATT&CK (issue #1) and D3FEND (issue #126) tags asattack/d3fendfields, mirrored into the capability catalog; D3FEND mappings are stated only where unambiguous. -
Windows posture summary (
windows_posture, W5.5, PE only): the at-a-glance answer to "what am I looking at", built last so every block it summarises already exists.signing_class(the W2.4 class, only when determined),hardeningsplit intopresent/absent/unknown(computed-and-off vs source-unreadable, fromsecurity_propertiesandsecurity_properties_gaps; absent and unknown are different values and are never merged;security_properties_scope/slice_variancepass through when an ARM64X image's slices disagree),driver_kind(from thedriverblock),container_origin(the W4 package kind),managed_shape(the W3.3 shape kind; an IL assembly without shape evidence readsil_assembly, and a file with no evidence is not stamped "native").sourcesnames the block of record behind each fact: the posture reads, it never recomputes (rule 21), so it cannot drift from the blocks it summarises. -
Windows-aware
blint diff(W5.5): the diff gains awindowslayer for update triage;signer_changed(signer CN/serial of the class-deciding signature),signing_class_changed,driver_kind_changed,ioctl_surface_changed(recovered control codes gained/lost, counts exact, names capped),pinvoke_changed(the managed/native boundary moved),manifest_execution_level_changed(an update quietly asking for more privilege), andvulnerable_driver_status_changed(the blocklist caught up with (or dropped) this exact binary). Lost hardening flags are deliberately not restated here: the hardening layer owns and classifies those (rule 21).
In the case of ARM64X, a single PE file encapsulates ARM64 and ARM64EC architectures. For ARM64EC nested PE binaries, an additional attribute nested_binary would contain the information such as exports, exceptions, functions, ctor_functions, and dotnet_dependencies.
Mach-O files are the standard for macOS, iOS, and other Apple operating systems.
-
Header (
header):cpu_type: The target architecture (e.g.,ARM64).file_type: Identifies the binary as anEXECUTABLE,DYLIB(shared library), etc.magic: The Mach-O magic as a name (observed"MAGIC_64"on an arm64 macOS executable). A top-level key, like the other header fields here.is_neural_model:trueonly when the header magic is Apple's neural-model file magic (0xbeefface, LIEF'sMACHO_TYPES.NEURAL_MODEL);falseon ordinary Mach-O images (observedfalseon/bin/ls,gh, and Go toolchain binaries). Present on every Mach-O metadata; it names the file type the header declares, not an analysis verdict, sofalseis the expected value for binaries and libraries.
-
Load Commands: Mach-O uses load commands instead of a dynamic section.
libraries: A list of required dylibs, equivalent toNEEDEDentries in ELF.uuid: A unique identifier for the binary, used by debuggers and crash report symbolication tools.rpath: A runtime search path for libraries.code_signature: The embedded code-signature SuperBlob, parsed into semantic detail (see below).
-
Code signature (
code_signature): The SuperBlob thatLC_CODE_SIGNATUREpoints at, parsed byblint/lib/codesign_macho.py; what the binary claims about itself, never a trust judgment.trust_validationfields saynot_performedin band, so "signed" is never read as "trusted"; blint does not validate certificate chains, revocation, or notarization.available:truewhen the slice carries an embedded signature blob.parse_status:parsed,parse_failed, orabsent. A present-but-corrupt blob isparse_failed; never folded into a confident "unsigned" or an empty entitlements answer; it lands inanalysis_coverage(security_properties_gapsgainscode_signature_detail, anddegradationsgainscode_signature_parse_failed).size,data_size: legacy keys (strings); the load-command size and the blob size in the file.data_offset: the load command'sdataoff, which is slice-relative;file_offset: the absolute position of the SuperBlob in the file (fat_offset + dataoff; the two differ for every slice of a universal binary).blob_source:lief_contentorfile_range, i.e. which read path produced the bytes. The formerdatakey (removed, an explicit additive-only exception) held the hex of the 16-byte load command itself under a name claiming signature content: it had never held signature data. Its entire information content (cmd, cmdsize, dataoff, datasize) is preserved bysize,data_offset/file_offsetanddata_size.superblob.blobs: the blob index;slot_type,offset,size,magicper member.superblob.code_directories[]: every CodeDirectory (primary and alternates) withversion,identifier,team_id,flags(named booleans:adhoc,hard,kill,restrict,runtime(hardened runtime),library_validation,get_task_allow,linker_signed, …),flags_raw,hash_type/hash_size,page_size,code_slots/special_slots,code_limit,platform_id,runtime_version(v0x20500+),exec_seg_flags(v0x20400+), and the cdhash;cdhash(20-byte truncated, the formcodesign -dvvvdisplays and the identity the system keys on) pluscdhash_fullandcdhash_algorithm.superblob.effective_cdhash/effective_cdhash_slot: the cdhash this signature is known by, the onecodesign -dvvvprints asCDHash. It comes from the strongest CodeDirectory, which is notcode_directories[0]when a SHA-1 directory is kept for old systems beside a SHA-256 alternate. The per-slicecode_signature.cdhashuses the same rule, andcdhash_slotnames the directory it came from.superblob.entitlements: parsed key/value pairs from the XML-plist slot;superblob.entitlements_der: the same from the DER slot (modern Apple binaries). A payload that fails to decode yields{"decode_error": ...}rather than an empty dict, so "no entitlements" and "undecodable entitlements" stay distinct.superblob.requirements: internal-requirements inventory (count,types).superblob.cms: the CMS (PKCS#7) signature;content_type,signer_cn(the signing certificate, identified by SignerInfo issuerAndSerialNumber, not blob position),certificates[]withsubject_cn/subject_organization/issuer_cn/serial, andtrust_validation: "not_performed".superblob.provenance:adhoc,linker_signed,cms_signed, orunknown; a claim read off the blob's flags and CMS presence.
-
Signature-derived security properties: when the SuperBlob parsed,
security_propertiesgains explicithardened_runtime,library_validationandget_task_allowbooleans from the primary CodeDirectory flags (Falseis reported too; for hardening properties the negative is the finding).is_signedremains presence-only (the blob exists), so a corrupt blob still readsis_signed: truewith the failure recorded as a gap. -
Encryption (
is_encrypted,encryption_info): Derived fromLC_ENCRYPTION_INFO(_64).is_encryptedistruewhencrypt_idis non-zero (FairPlay-protected App Store binaries); developer, ad-hoc, and enterprise builds reportfalse.encryption_infocarriescrypt_id,crypt_offset, andcrypt_size. An encrypted__TEXTsegment cannot be meaningfully disassembled without on-device decryption. -
Objective-C metadata (
objc_metadata): Recovered by walking the raw__objc_*sections (LIEF's community build does not expose this). Internal pointers are resolved via relocation targets, with a chained-fixup fallback that decodes rawdyldchained pointers (validated against mapped section ranges) when no relocation map is present; external class pointers are resolved via the dyld binding table. Present only when the binary contains 64-bit Objective-C metadata (32-bit armv7/i386 layouts are skipped).class_count,protocol_count,selector_count: summary counters.classes: each entry hasname,superclass(internal class name or external framework class),method_count,methods(selector names), and optionalprotocols.protocols: declared protocols with theirnameandmethods.selectors: distinct selectors referenced at message-send sites (__objc_selrefs).external_classes: framework/runtime classes the binary links against (e.g.CLLocationManager,CTTelephonyNetworkInfo). Selectors and external classes feed the capability review, surfacing iOS privacy capabilities.method_imps: recovered method implementations as{name, address}, wherenameis the readable-[Class selector]form andaddressis the implementation's virtual address. These seed and label thefunctionslist (see below) so message handlers are disassembled and named even in stripped binaries.category_count,categories: categories from__objc_catlist; named extensions that patch existing (usually framework) classes. Each carriesname,class_name(from the dyld binding table for external classes, otherwise the internalclass_ro_tname), instance/classmethods,protocolsandproperties. Category methods are seeded like class methods, spelled-[Class(Category) selector].nonlazy_class_count,nonlazy_classes: classes in__objc_nlclslist; the runtime runs their+loadbeforemain. Each entry names the class (runs_before_main: true) and, when recovered, the+loadimplementation address; these seed+[Name load]functions.nonlazy_category_count/nonlazy_categoriescover__objc_nlcatlist. RuleCHECK_OBJC_LOAD_METHODS(info) names the classes.classes[].ivars/classes[].properties: declared ivars (name,type) and properties (name, type-encodingattributessuch asT@"NSString",R,N) from the class's read-only data, in both the legacy pointer and relative small encodings.parse_degradations,parse_degradation_count: counted tokens for every unresolvable pointer or unparsable list; partial reads are reported, never silent.
-
Function recovery (
functions): For stripped release builds (the common case for shipped iOS/macOS apps) the symbol table exposes little beyond__mh_execute_header. blint augments the function list from theLC_FUNCTION_STARTStable; every entry point is recovered, reusing a surviving symbol name when one exists and synthesising asub_<address>name otherwise. Recovered Objective-C implementations then upgrade matchingsub_<address>entries to their-[Class selector]names. This is what allows disassembly and callgraph construction to work on stripped apps. -
One address space for Mach-O functions: every address in
functions,ctor_functions,unwind_functions,discovered_functionsand thedisassembled_functionskeys is a virtual address (executables are typically0x100000000-based; dylibs are typically0x0-based, where the virtual and file-relative spaces coincide). LIEF's aggregate mixes symtab virtual addresses with file-relativeLC_FUNCTION_STARTSoffsets; blint classifies each entry against the image's segment ranges (an address inside a content-bearing segment's virtual range is virtual, one inside its file range is file-relative and is rebased byimagebase) so a reader never has to guess which space an address is in. Subtractimagebaseto recover the file-relative offset. Entries that collapsed onto one address after rebasing are merged (a real symbol name wins over the syntheticsub_<addr>twin, the largest known size survives). Themacho_function_address_spaceblock records what happened:normalized_to("virtual"), theimagebaseanchor,rebased_entries,duplicates_merged,names_recovered, andambiguous_entries/unresolved_entriesfor addresses the segment ranges could not place (left in their raw space, never guessed; also mirrored intoanalysis_coverage.degradations). Zero-file-size segments such as__PAGEZEROare excluded from classification; their virtual range covers the whole low half of the address space and would swallow every file-relative address. -
Skipped disassembly (
disassembly_skipped): When--disassembleis requested for a FairPlay-encrypted binary (is_encryptedistrue), disassembly is skipped and this field is set tofairplay_encryptedrather than producing meaningless instructions from the encrypted__TEXT. -
Universal binaries (
is_universal,slices): A fat (universal) Mach-O contains one image per architecture. blint summarizes every slice, not just the one the generic parser auto-selects. The existing top-level keys (cpu_type,functions,security_properties, …) keep describing the primary slice (the first fat entry; the same slice that was analyzed before this field existed, so consumers of those keys see no change);is_universalistrueonly for fat inputs, andslicescarries one lean entry per slice in fat order:index,cpu_type,cpu_subtype,arch,is_primary: slice identity.archuses the names lipo andcodesign --archuse, soarm64,arm64eandarm64e.x1(the extra slice macOS 27 system binaries carry) stay distinct. It matters because only the arm64e-family slices get pointer authentication. An arm64 subtype blint does not know is namedarm64.subtype<N>rather than folded intoarm64.security_properties: the same property set as the top-level block, computed from this slice's bytes. Hardening that differs between slices (a signature present on one slice but not another, PAC only on the arm64e slice) is reported per slice and is never merged into a single optimistic or pessimistic answer.code_signature: the slice's own parsed signature summary (parse_status,provenance,identifier,team_id,cdhash,hash_type,flags,entitlements,entitlements_der). Signatures are per slice (every slice has its own CodeDirectory and its own cdhash, and entitlements can differ between slices) so a single top-level cdhash for a fat binary would be a wrong answer.functions,symbols,imports: counters evidencing the slice was really parsed.is_encrypted: per-slice FairPlay state (set when the slice'scrypt_idis non-zero). Because the top-level block speaks for the primary slice, a fat input also carriessecurity_properties_scope: "primary_slice"and, when the slices do not agree,security_properties_slice_variancenaming every property they differ on (both mirrored intoanalysis_coverage)./usr/bin/gitis the case in point: PAC is on its arm64e slice, so the top level has nopackey and would otherwise read exactly like a binary checked and found to lack it. Nothing is merged across slices (a merge would have to pick between an optimistic and a pessimistic lie) so the per-slice truth stays inslicesand the summary states its own scope. The same rule applies to signatures: the top-levelcode_signatureblock declarescode_signature_scope: "primary_slice"and, when slices disagree,code_signature_slice_variancenames the aspects (cdhash,entitlements,flags, …); also mirrored intoanalysis_coverage. cdhash differs by construction across slices; differing entitlements are the signal to look for.
Disassembly, entropy and string-based reviews run on the primary slice only; slice summaries are metadata-level. A slice whose summary fails is isolated and recorded in
analysis_coverageunderslices: the file is not aborted.
Swift symbols are demangled automatically (e.g.
Foundation.URL.appendingPathComponent(...)), including the Mach-O underscore-prefixed manglings (_$s…/_$S…/_T0…) which the bundled demangler recognises directly. When--disassembleis enabled, Mach-O imported calls made through__stubsand the GOT are resolved to their demangled symbol names, so call sites reference real Foundation/libswiftCore/libc APIs rather than anonymous stubs.
An .ipa is a zip archive containing a Payload/<App>.app/ bundle. blint unpacks it and analyzes every Mach-O it contains, the main executable, embedded frameworks and dylibs (Frameworks/), and app extensions (PlugIns/*.appex), each producing its own *-metadata.json. The application context from the bundle's Info.plist is attached to each binary's metadata.
| Attribute | Description |
|---|---|
ios_bundle |
Bundle context: bundle_identifier, bundle_name, bundle_version, bundle_build, minimum_os_version, platform_name, application_category, role (main/framework/dylib/plugin), bundle_path, and the optional app_transport_security, url_schemes, and privacy keys described below. |
bundle_identifier |
Convenience top-level copy of the app's CFBundleIdentifier. |
bundle_version |
Convenience top-level copy of CFBundleShortVersionString. |
minimum_os_version |
Minimum supported OS version from the bundle. |
The bundle context can also carry two security-relevant keys parsed from the Info.plist:
app_transport_security: present only when the App Transport Security policy weakens the secure default. Carriesallows_arbitrary_loads,allows_arbitrary_loads_media,allows_arbitrary_loads_web, and aninsecure_exception_domainslist. For the main executable these are also projected intoinformative_stringsasATS_*tokens so the rule engine can flag a weakened transport posture (ruleIOS_INSECURE_TRANSPORT_ATS).url_schemes: the custom URL schemes the app registers (CFBundleURLTypes), useful as deep-link / inter-app entry points.
The bundle context also carries the app's privacy posture, parsed from the Info.plist and the PrivacyInfo.xcprivacy privacy manifest(s):
privacy_usage_descriptions: theNS...UsageDescriptionconsent-string keys the app declares (e.g.NSCameraUsageDescription), indicating the sensitive resources it is provisioned to access.query_schemes: theLSApplicationQueriesSchemesthe app can probe viacanOpenURLto detect other installed apps.bonjour_services: theNSBonjourServicesthe app browses for on the local network.privacy_manifest: present when any component ships aPrivacyInfo.xcprivacy. Aggregated across the app, embedded frameworks and extensions, it carriespresent,manifest_count,tracking,tracking_domains,collected_data_types, andaccessed_api_categories(the declared "required reason" API categories).
For the main executable these are projected into informative_strings as PRIV_* tokens (mirroring the ATS_* tokens above) so the rule engine can flag the privacy surface: for example PRIV_NSCameraUsageDescription, PRIV_LSApplicationQueriesSchemes, PRIV_NSPrivacyTracking, PRIV_PrivacyManifestMissing, and PRIV_UNDECLARED_<category> for a required-reason API referenced by the binary without a matching manifest declaration.
Embedded framework and app-extension binaries are additionally enriched with their own Info.plist identity (bundle_identifier, bundle_version) so the SBOM can report the real product version of a bundled dependency rather than inheriting the host app's version.
A macOS bundle is a directory (unlike an .ipa, there is nothing to unpack). blint walks it: the main executable under Contents/MacOS (named by CFBundleExecutable), embedded frameworks (Contents/Frameworks), app extensions (Contents/PlugIns/*.appex), XPC services (Contents/XPCServices/*.xpc), login-item and LaunchServices helper apps under Contents/Library, a framework's own Versions/ tree, and a .dSYM's DWARF slices, and analyzes every Mach-O it finds. The plugin bundle kinds are recognized and walked the same way: .driver, .plugin, .component, .qlgenerator, .mdimporter, .systemextension and .dext (all measured to use the app Contents/Info.plist + Contents/MacOS layout). Auxiliary executables beside the main binary (installer tools, privileged helpers) are collected with role tool/helper so a bundle scan never loses them relative to a plain directory scan. Members are deduplicated through Versions/Current symlink aliasing, and a 2000-binary walk cap records binary_walk_truncated instead of stopping silently. In SBOM output the bundle is the parent application component and each binary a component with pkg:macos purls keyed by bundle-relative path.
| Attribute | Description |
|---|---|
macos_bundle |
The same bundle-context shape as ios_bundle below (identity, role, bundle_path, ATS/URL-scheme/privacy keys) for binaries analyzed through a macOS bundle directory. |
Present on every binary entry of a plugin-kind bundle when the kind names a host: .driver (Audio Server Plug-In), .plugin under a CoreMediaIO/Plug-Ins/DAL path (camera DAL plugin; a .plugin anywhere else names no host and gets no block at all, because the suffix alone does not determine one), .component (Audio Unit), .qlgenerator (QuickLook), .mdimporter (Spotlight), .systemextension and .dext. Absent otherwise; never an empty block that reads as "not a plugin".
kind,title,host,host_privilege,activation: the plugin kind, the process it loads into, that host's privilege, and how the plugin is activated. The activation difference is stated in band because it is the point of the block: a.driveris installed with an administrator password once and loaded on every boot with no consent surface after; a.systemextension/.dextactivates through a one-time user-approval dialog from its containing application. The table is data (blint/data/macos_host_plugin_kinds.yml) with its provenance and the per-key measurements in the header.install_scope:system(/System/..., Apple's own),machine(/Library/..., admin install, loaded for every user) oruser(~/Library/...). Absent when blint is looking at the bundle out of context; a.driverin a downloads folder has no install scope yet. A.systemextension/.dextfound inside an application also gets none, because the copy that runs is the activated one under/Library/SystemExtensions, which blint is not looking at.declarations(Audio Server Plug-Ins only): theInfo.plistcapability declarations, worded as declarations and never as observed behaviour;network(AudioServerPlugIn_Network, reported exactly as declared; the rule fires only on a real booleantrue),mach_services(AudioServerPlugIn_MachServices, a list of Mach service names) andiokit_user_clients(AudioServerPlugIn_IOKitUserClients, a list of IOKit user-client class names). Each name list is capped at 64 entries, a listing bound no rule reads past, and 4x the largest list measured on a stock system (16), withmach_services_listing_capped/iokit_user_clients_listing_cappedrecording the cap.AudioServerPlugIn_LoadingConditionsandAudioServerPlugIn_Createare not reported: they govern when a plugin loads, not what it may do. When the plist could not be read there is nodeclarationskey anddeclarations_status(info_plist_absent,info_plist_unreadable,info_plist_not_a_dict) names why, so "declared nothing" and "could not be read" stay distinguishable.containing_app(.systemextension/.dextonly): the bundle identifier of the.appthe extension ships inside, when one is in the ancestry. A DriverKit extension's process cannot open sockets, but its containing application can, with data flowing between them, so the block either names the containing application or says nothing; it never carries any field that reads as "this software cannot reach the network".
Measured on the machine this shipped on (macOS 15.8, 12 Apple HAL plugins, no third-party plugins): AudioServerPlugIn_Network 0 of 12 (including Apple's own network-audio AirPlay.driver/AppleAVBAudio.driver, which hand off to companion processes over Mach IPC), _MachServices 7 of 12, _IOKitUserClients 6 of 12, _LoadingConditions 5 of 12, _Create 0 of 12. That is why CHECK_AUDIO_PLUGIN_MACH_SERVICES does not exist: bare _MachServices presence fires on the operating system's own plugins, so the service names are recorded and no rule fires on them.
Rules: CHECK_MACOS_HOST_PLUGIN (info, context, names the host and activation), CHECK_AUDIO_PLUGIN_NETWORK (medium: the network declaration, explicitly not observed behaviour), CHECK_UNSIGNED_HOST_PLUGIN (high: a plugin bundle in a system or machine-wide install scope that is neither an Apple platform binary (any CodeDirectory platform_id, what codesign -dv prints as Platform identifier) nor Developer-ID-signed (the CMS signer_cn, what spctl -a -vv prints as the origin)). The unsigned verdict follows the primary slice's parsed signature and stays silent on undetermined ones. There is no macOS plugin corpus in the blint corpus plan: the third-party plugin directories on a stock development machine are typically empty (they were on this one), CHECK_AUDIO_PLUGIN_NETWORK has no positive example on any stock system, and the positive coverage is synthetic fixtures built to the measured bundle layout.
An MSIX/Appx package is a zip whose AppxManifest.xml states identity and capabilities; a bundle is a zip of packages. blint walks the container (blint/lib/msix.py through the shared bounded framework in blint/lib/container.py), analyzes every exe/dll member through the normal PE path, and attributes each member to its place in the package (container_path, e.g. CascadiaPackage_1.22.12111.0_ARM64.msix/wt.exe). The container itself is an analyzed unit whose metadata carries:
| Attribute | Description |
|---|---|
container.kind |
msix, appx, msixbundle or appxbundle: also the metadata exe_type, so container-scoped rules gate on it. |
container.identities |
Bundle identity plus each package's AppxManifest facts: identity (name, version, publisher, architecture), display_name, publisher_display_name, target_device_families, package_dependencies, applications (entry executables). |
container.capabilities |
{general, restricted, device} capability names from the manifest. Restricted capabilities are recognised by their rescap namespace, not a name list, so capabilities Microsoft adds later still class correctly. Listed per class up to 128; restricted_capability_count is the counted total the rule reads, so a past-the-cap manifest still fires. |
container.signature |
Facts from AppxSignature.p7x (a PKCX-prefixed PKCS#7) parsed through the same signature walker the PE certificate table uses: signer_cn, signer_o, digest_algorithm, signature_count. No trust validation is performed. |
container.blockmap |
The AppxBlockMap.xml declaration: hash_method (SHA-256 only; anything else records blockmap_hash_method_unsupported), file_count, total_size. |
container.blockmap_verification |
Block hashes recomputed for every extracted member: verified_files, verified_blocks, and mismatches (member + mismatched-block count). Unextracted files are declared, not verified; the scope is stated by the counts, never implied. |
container.package_count / member_binary_count |
How many nested packages were walked and how many member binaries were analyzed. |
container.refusals |
Every named refusal (ground rule 30): member_path_unsafe, member_is_symlink, member_size_exceeds_cap, member_count_exceeds_cap, total_uncompressed_exceeds_cap, member_compression_ratio_exceeds_cap, appx_manifest_missing, manifest_xml_malformed, nested_package_unreadable, archive_unreadable and friends. Sorted, deliberately not de-duplicated: a package with ten unsafe members reports ten. |
Caps are measured (module docstring, blint/lib/msix.py): ≥2x the Windows Terminal 1.22 reference bundle; 2048 members, 512 MiB total, 64 MiB per member, depth 16, compression ratio 128, 64 nested packages. CHECK_MSIX_RESTRICTED_CAPABILITY (info, by measurement; 17 of 56 ordinary Store packages declare a restricted capability) names the declared surface. In SBOM output the package is the parent component (pkg:appx/<name>@<version> from the manifest identity) and each member a component keyed by container path with a SHA-256 hash; container refusals reach the BOM as internal:container_refusals.
Bundles signed for direct distribution carry a provisioning profile: embedded.mobileprovision (iOS shape) or embedded.provisionprofile (macOS, under Contents/). The profile is a CMS envelope around a plist; blint decodes it (blint/lib/provisioning.py) into:
| Attribute | Description |
|---|---|
parse_status |
parsed or parse_failed (with parse_error). A failed CMS or plist decode is reported, never guessed. |
name, team_name, team_identifiers |
The profile's identity and signing team. |
created, expires |
ISO-8601 validity window. Validity is evaluated by the checks at scan time, keeping parse output deterministic. |
entitlements |
Every entitlement key the profile grants, with its value, spelled exactly as signed: no allow-list, the same policy as the code-signature superblob.entitlements block. iOS profiles sign application-identifier; macOS profiles sign com.apple.application-identifier; both spellings are the same entitlement and are reported verbatim (consumers should read either). |
provisioned_device_count |
Count only: device UDIDs never enter metadata. |
provisions_all_devices |
Enterprise (in-house) distribution marker. |
signer_cn |
Common name of the CMS signer chain (names only; no trust validation). |
Three rules consume the block: CHECK_PROFILE_EXPIRED (high), CHECK_PROFILE_DEVELOPMENT (medium, a get-task-allow profile in a shipping bundle) and CHECK_PROFILE_WILDCARD (medium, a team-id.* application identifier).
WASM (WebAssembly) binaries are parsed via wasm_tools and then normalized into blint's common metadata model.
- Detection: blint treats a file as WASM when the extension is
.wasmor the magic bytes are00 61 73 6d. - Normalization: blint preserves common cross-format keys (
imports,dynamic_entries,functions,symtab_symbols,dynamic_symbols) so dependency and review workflows continue to work. - Raw passthrough export: the complete parser output is written as a separate report artifact (
*-wasm-report.json) in the reports directory.
| Attribute | Description | Notes |
|---|---|---|
binary_type |
Set to WASM. |
Distinguishes the format in downstream logic. |
exe_type |
Set to wasmbinary. |
Format hint used by review logic. |
machine_type |
WASM architecture class: WASM32 or WASM64. |
Derived from memory limits (memories[].limits.is_64) or the isa.memory64 capability when the 64-bit memory is imported. |
module_version |
WebAssembly module version from header. | Usually 1 for core wasm modules. |
section_count |
Number of parsed sections. | Mirrors parser report. |
sections |
Parsed section records (id, name, size, offset, etc.). |
Format-specific detail for structural analysis. |
wasm_imports |
Detailed WASM imports with module, name, kind, type_index. |
Use this for function-level import analysis. |
imports |
Compatibility dependency list. | For WASM this is normalized to module-level {name, tag:"NEEDED"} entries. |
dynamic_entries |
Same dependency-style list as imports. |
Keeps dependency processing consistent with ELF/PE/Mach-O. |
exports |
WASM exports with name, kind, ref_index. |
Export-level capability and surface analysis. |
functions |
Parsed functions mapped to blint shape (index, name, address, size, instruction_count). |
address reflects instruction/body offset in wasm bytes. |
dynamic_symbols |
Synthetic imported symbol list used by dependency graph logic. | Built from WASM imports; includes is_imported markers. |
symtab_symbols |
Synthetic exported symbol list used by review logic. | Built from WASM exports; includes is_exported markers. |
wasm_analysis |
Structured analysis from wasm_tools (detections, capabilities, profiles, findings). |
Preserved as provided by parser API. |
wasm_errors |
Parser-reported errors, if any. | Non-fatal parse warnings/errors can appear here. |
is_component |
true when the binary is a Component Model artifact. |
Added by blint from the wasm-tools 2.0 report. |
wasm_toolchain |
Toolchain fingerprint (languages, processed_by, sdks, target_features). |
Decoded from producers / target_features custom sections. |
wasm_strings_summary |
Strings/IoC digest: detected, string_count, signals, counts, masked samples. |
Summary only; the full string list stays in *-wasm-report.json. |
wasm_call_graph_summary |
Call graph digest: node_count, edge_count, truncated, edge_kinds. |
Edge kinds: direct, indirect-approx, typed-approx. Full graph stays in *-wasm-report.json. |
wasm_unknown_opcodes |
Distinct unknown instruction mnemonics from the analysis summary. | Empty when the decoder recognizes every instruction. |
wasm_isa_capabilities |
Sorted isa.* instruction-set capability tokens (e.g. isa.simd, isa.gc, isa.memory64). |
Added with wasm-tools 2.1; derived from decoded opcodes, type definitions, and memory limits. |
wasm_types_summary |
Type-section digest: total plus per-kind counts (func, struct, array). |
Added with wasm-tools 2.1; GC composite types decoded from plain and rec-group entries. |
wasm_debug_info_present |
true when the module still carries DWARF (.debug_*) custom sections. |
Added with wasm-tools 2.1; mirrors the debug_info_present format signal. |
build_info |
WASI runtime/variant and JS-interface hints (see below), plus toolchain and component version. | wasi_variants use preview1/preview2/preview3/legacy. |
errors |
blint-level error list for WASM parsing. | Set when parser reports issues or parse operation fails. |
The build_info block for WASM binaries is assembled from the parser detections and report blocks:
runtime: "WASI"andwasi_variantswhen WASI imports are detected. Variant naming follows wasm-tools 2.0:preview1,preview2(renamed frompreview2-like),preview3(WASI 0.3 / async components), andlegacy.host_interface: "JavaScript"when JS-interface modules are detected.languagesandprocessed_bywhen a toolchain fingerprint was decoded fromproducerscustom sections.component_versionandlayer_versionfor Component Model binaries.
For Component Model binaries, module_version carries the raw 32-bit header value (for example 65549 = component version 13, layer 1) rather than the core-module version 1, and the section lists in both the metadata and *-wasm-report.json are aggregations across all nested core modules, with each entry carrying a core_module index.
The findings computed by the wasm_tools analysis layer (WASM-CAP-001 through WASM-STR-007, plus WASM-ISA-008) are passed through into blint's findings output (findings.json and the console/HTML report). Each finding keeps its stable WASM-* id and severity (they top out at high, so CI builds that fail only on critical findings are unaffected), maps the upstream remediation text to description, and carries confidence plus the parser evidence dict. These findings are checks, not reviews: they appear even when runs are invoked with --no-reviews.
With wasm-tools 2.1, WASM-DOS-003 fires only when a memory.grow executes inside a loop body (allocator startup growth alone no longer triggers it) and its evidence reports loop_memory_grow_ops plus the responsible functions. The new WASM-ISA-008 advisory (severity low) reports relaxed-SIMD instructions, the principal source of cross-engine numeric non-determinism.
blint requires wasm-tools 2.1 and surfaces its new report attributes as follows:
- Instruction-set capability tokens. The analysis
capabilitieslist gainsisa.*tokens derived from decoded opcodes, type definitions, and memory limits:isa.simd,isa.relaxed-simd,isa.atomics,isa.gc,isa.function-references,isa.tail-call,isa.memory64,isa.wide-arithmetic,isa.legacy-exceptions, andisa.exceptions. blint re-exports theisa.*subset aswasm_isa_capabilitiesand also usesisa.memory64to classifymachine_typewhen a module imports its 64-bit memory (Emscripten-style) instead of defining it in the memory section. - GC type decoding. Type-section entries now decode GC composite types, including rec groups that expand one entry per member of the group's type-index slots. Each
types[]entry in*-wasm-report.jsoncarries akind(func,struct, orarray);struct/arrayentries carry no signature. blint summarizes this aswasm_types_summary(total,func,struct,array, plus a key for any further kind a newer wasm-tools decodes, so the per-kind counts always sum tototal), and the mere presence of composite types raises theisa.gccapability. - Legacy exception handling. The pre-renumbering
try/catch/catch_all/rethrow/delegateopcodes still emitted by older toolchains now decode (raisingisa.legacy-exceptions) instead of surfacing as unknown opcodes; the currenttry_table/throw/throw_refform raisesisa.exceptions. - DWARF awareness.
.debug_*custom sections are detected and raise adebug_info_presentsignal inanalysis.detections.format.signals; printable strings from a.debug_strsection are appended to the reportstringswith asource: "custom:.debug_str"label and a separate 250-entry budget. Secret/IoC detection (and thereforeWASM-STR-007andwasm_strings_summary) still considers data-segment strings only. blint surfaces the signal as thewasm_debug_info_presentboolean.
WASM binaries also receive common derived fields like hashes, import_dependencies, llvm_target_tuple (for example wasm32-unknown-unknown), security_properties, and binary_composition.
blint writes the raw parser payload to a companion file named *-wasm-report.json alongside *-metadata.json. With wasm-tools 2.0 this payload additionally includes is_component, the extracted strings with linear-memory provenance (capped), the labeled call_graph, the toolchain fingerprint, and (for components) the component block with interfaces, interface packages, and nested core modules. With wasm-tools 2.1, types[] entries carry a kind attribute, the strings list may include .debug_str entries labeled with a source field, and the analysis block carries the isa.* capabilities and the loop_memory_grow_ops memory profile metric.
Two CLI opt-outs and one default guard bound the wasm report size:
-
--no-wasm-stringsskips string extraction.stringsbecomes empty in the report,wasm_strings_summaryreportsdetected: false, and the string-derived findings (e.g.WASM-STR-007) disappear along with their evidence source. -
--no-wasm-call-graphskips call-graph construction.call_graphbecomes empty,wasm_call_graph_summaryzeroes out, and wasm callgraph exports (--export-callgraph-*) produce no artifacts even with--disassemble. -
Function instruction streams are capped at
BLINT_MAX_WASM_INSTRUCTIONSinstructions per report (default50000,0disables). This is the only unbounded part of the parser output (strings and graph edges are already capped by wasm-tools) and for large modules it dominates the report size (a 1 MB module produced a 33 MB report, 98% instructions).The budget covers every function in the report, including those inside core modules and nested components. It is divided max-min fair rather than first-come: each function is offered an equal share, and a function needing less than its share releases the remainder to the longer ones. This keeps a trimmed report a sample of the whole module instead of the first few functions in section order; at the default budget a 3000-function module leaves every function with at least some of its body, where a greedy split would empty all but the first few dozen.
Trimmed functions keep a truthful
instruction_countand gain aninstructions_truncatedcount. When anything was dropped the report gains a top-levelblint_truncationblock (instruction_budget,instructions_dropped,functions_truncated), so the artifact is self-describing and a consumer can distinguish a small module from a trimmed large one without the log line. A log hint also names the environment variable.
The wasm callgraph is converted from the wasm_tools static call graph rather than from disassembly; see Callgraph Analysis for the edge kinds and confidence semantics. For SBOM generation, blint sbom --wasm-sbom turns a Component Model binary's imported WIT interface packages into components; see Custom Properties for the properties involved.
Representative WASM fixtures used by tests are available under tests/data/*.wasm.
This collection of attributes describes the functions and data within the binary, providing insight into its structure and capabilities.
Symbols are names for locations in memory, typically corresponding to functions or global variables. BLint extracts symbols from two primary sources, which serve different purposes.
+------------------------------------+
| Your Executable File |
| |
| +-----------------+ (for static |
| | .symtab | linking & |
| | (symtab_symbols)| debugging) |
| +-----------------+ |
| ^ |
| | (often stripped) |
| |
| +-----------------+ (for dynamic |
| | .dynsym | linking at |
| |(dynamic_symbols)| runtime) |
| +-----------------+ |
| |
+------------------------------------+
-
symtab_symbols: This is the full symbol table (.symtabin ELF), containing names for all functions and global variables, including internal, non-exported ones.- Purpose: Provides a comprehensive map of the binary's internal structure.
- Use Case: Invaluable for reverse engineering, as it gives names to internal functions.
- Limitation: This table is often stripped from production binaries to reduce size and hinder reverse engineering. Its absence is a key indicator (
"stripped": trueinsecurity_properties).
-
dynamic_symbols: This is the smaller symbol table (.dynsymin ELF) used by the dynamic linker at runtime. It only contains symbols that are imported from or exported to other shared libraries.- Purpose: To resolve dependencies between shared libraries.
- Use Case: Understanding the binary's public API (what it exports) and its direct dependencies on functions from other libraries (what it imports).
- Strength: This table is almost never stripped from dynamically linked executables, as it is essential for the program to run.
Each symbol entry contains details like its name, type (FUNC or OBJECT), binding (GLOBAL, LOCAL, WEAK), and whether it is is_imported or is_exported.
nameis the demangled form where one exists, which is what a reader wants to see.raw_nameis the linkage name, present only when demangling changed it. This is the name that appears in another object's export table, so it is the only key that matches a C++ or Rust symbol across binaries. It is what symbol attribution and version-aware database lookups match on.versionis the symbol version node the symbol binds to, such asGLIBC_2.28. Seeabi_analysis.
While symbol tables provide the names, these lists represent a curated set of functions that LIEF identifies as code entry points.
-
functions: A list of general functions identified by the parser, often corresponding to exported symbols or entries in specific sections. -
ctor_functions: A list of constructors. These are special functions that are executed before the program's main entry point (mainorWinMain).- Purpose: To initialize the program's state or set up runtime environments.
- Use Case for Analysts: Malware and legitimate programs alike use constructors for early initialization. Examining these functions can reveal anti-debugging checks, environment setup, or other critical startup logic that occurs before the main code path.
-
dtor_functions: A list of destructors. These are special functions that are executed when the program exits cleanly.- Purpose: To perform cleanup tasks like flushing files or releasing resources.
- Use Case for Analysts: Malware may use destructors to cover its tracks, delete files, or send a final beacon upon exit. These are important to check for cleanup or anti-forensic activities.
Recovered function starts for binaries whose symbol tables are stripped or incomplete. Two structures are additive to the symbol-driven lists:
discovered_functions: every function start recovered from the structures the runtime itself depends on, which survivestrip:- Mach-O
__TEXT,__unwind_info(compact unwind):source: "unwind", with exact function sizes derived from the sorted offset table and its sentinel entry. - ELF
.eh_frame_hdr/.eh_frame:source: "eh_frame". Starts come from the binary-search table when present; sizes come from the exact FDEpc_rangevalues. A CIE/FDE walk covers binaries whose header table is missing or malformed. - ELF
.ARM.exidx(ARM EHABI, 32-bit ARM only):source: "arm_exidx". One row per function the runtime can unwind, decoded from PREL31 offsets; the table replaces.eh_frameon armeabi-v7a and survivesstrip. EHABI stores no extents, so sizes are 0 and disassembly bounds these entries with the next-known-start window. - Each entry carries
name(the real symbol name when one exists,sub_<address>otherwise),address,sizeandsource. Addresses already claimed by a symbol bucket enrich the existing entry with the exact unwind size when its size was unknown.
- Mach-O
function_discovery: summary of the merge;sources(per-source counts) andmerged_count(addresses that were genuinely new, i.e. not claimed by any symbol bucket).
Entries whose addresses already appear in the symbol-driven buckets never replace or duplicate them; only genuinely new addresses are appended to functions (with "discovered": true, size 0). Call-site promotion (see the disassembly docs) records its additions in discovered_functions with source: "callsite".
On ELF, function_extent records the size reconciliation that follows the merge (see docs/DISASSEMBLE.md, "Function extents"): sizes_rejected (impossible sizes dropped), sizes_corrected (sizes replaced by a higher-precedence source) and entries_checked. The first two are also surfaced in analysis_coverage.functions.
One record per identified framework or runtime, emitted only when evidence
named in blint/lib/framework_ident.py matches (rule 38): a file name alone
is a hint, never a component. The key is absent when no evidence matched
(ELF) or the pass never ran (non-ELF) - both read as "no framework
evidence", never as "verified not a framework":
{
"framework": "ndk-libcxx",
"version": "r26-canary",
"evidence": [
{
"what": ".note.android.ident NDK version",
"where": "notes",
"value": "r26-canary"
}
],
"hints": [],
"static": false
}versionis present only when the named evidence pins one; hashes that no published table maps to a version are reported as hashes inevidence.static: truemarks a framework found inside a host library (a nested identification, carried in the host record'snestedlist).- TLS grades (H4/J2): OpenSSL is identified by the
OPENSSL_VERSION_TEXTbanner (versioned; replace-grade only under alibcrypto.soSONAME, a static copy elsewhere), or banner-less by exportedossl_*names (static, versionless). OpenSSL's shared libcrypto hides that internal namespace, so exporting it means a static copy (realm-core's insidelibrealm-jni.so); BoringSSL has none. BoringSSL keeps its three H4 grades (provider replace, re-exported static copy, hint-only strings). - In
blint sbomon Android apps, a replace-grade identification takes over the library file's component slot (pkg:generic/android-ndk/libcxx@for the NDK C++ runtime, for example), keeping the file's provenance properties and adding oneblint:identification:evidenceproperty per evidence entry plusblint:hint:file_name. Files that merely bundle a framework statically keep their own component: a copy with exact evidence (the Dart VM and BoringSSL insidelibflutter.so, or a re-exportedBORINGSSL_*surface) is a child component whose bom-ref is scoped by the host's, and weaker evidence is ablint:identification:<framework>property. App-level facts ride the parent application component as properties (blint:ndk_versions: the distinct NDK note versions per ABI, per ground rule 36;blint:hermes_bytecode_version: the Hermes bytecode header version of each bundle).
The committed evidence fixtures (tests/data/android/<framework>-evidence.json)
carry the corpus side of every detector, with the extracting command and the
llvm oracle read recorded in each file.
With --use-blintdb, every native library of an Android app (one call per
unique sha256) is matched against the local blintdb through the same
detect_binaries_utilized the standalone binary path uses
(blint/lib/android_blintdb.py, A6.3). A match is symbol evidence, not
identity, and lands in the SBOM in one of three shapes:
- Nested (the default): a static copy inside a host library becomes a
child component of the host's, bom-ref scoped by the host's
(
<host purl>|<child purl>), purl from the database row's project identity with the artifact-derived version and the ABI qualifier. - Replace: only when the host structurally is the project's library;
its declared
DT_SONAMEequals one of the project's own library names in the database (the same SONAME rule the NDK libc++ and BoringSSL provider identifications use). A file name alone is a hint. - Dropped: a framework identification for the same bytes wins. A
BoringSSL record contradicts an
opensslmatch; an OpenSSL or NSS record duplicates the match for its own project, which the APK path already emits. Drops are counted in the host'sblint:blintdb:superseded_by_frameworkproperty (<framework>:<project>). The standalone binary path emits no framework components, so there only a contradiction drops a match (internal:blintdb_superseded_by_framework). - Refused: an
opensslmatch on a host whose SONAME islibcrypto.soorlibssl.so(vendor-prefixed ones included) needs one of OpenSSL 3's own names among its dynamic symbols, defined or imported: the publicOSSL_*API or the internalossl_*namespace. BoringSSL shares the rest of theSSL_*/EVP_*API, and none of the 78 tier-0 BoringSSLlibcrypto.so/libssl.sofiles carries either namespace; OpenSSL 3.6.2'slibssl.sodefines 5OSSL_*names and imports 45. The refusal is counted inblint:blintdb:refused_provider_shape(internal:blintdb_refused_provider_shapeon the standalone path).
Component versions come from the artifact, never from the database row (the
row carries the vcpkg port's version; the corpus OsmAnd ships PROJ 8.2.0
against a 9.8.1 row). The strings are read under one rule table,
blint.lib.banners.VERSION_RULES, shared with the standalone vendored-banner
layer; each rule names the upstream file that defines its string. The
banner rules (zlib's deflate_copyright/inflate_copyright, Lua's
LUA_COPYRIGHT, OpenSSL's OPENSSL_VERSION_TEXT, curl's curl_version(),
expat's XML_ExpatVersion(), libpng's PNG_HEADER_VERSION_STRING) name the
library in the string. The others only date a project the match already
identified: libopus x.y.z (libopus celt/celt.c), PROJ's pj_release
(src/release.cpp), SQLite's SQLITE_SOURCE_ID date mapped through
sqlite.org's chronology.html, and the bare ZSTD_VERSION_STRING,
PNG_LIBPNG_VER_STRING and SENTRY_SDK_VERSION, each accepted only as the
artifact's single distinct bare version. Named strings are read before bare
ones, and named strings that disagree leave the component versionless, as
do several bare candidates. The database row's purl is recorded as the
blint:blintdb:project_purl property.
Matched components carry blint:identification:evidence entries naming the
symbol match (count and sample) and the version evidence, plus
blint:blintdb:score, and on a replace blint:blintdb:soname_match (the
host SONAME and the project's library names it equals). A child's abi
qualifier lists only the ABIs whose copy matched. The committed evidence fixture
(tests/data/android/blintdb-zstd-evidence.json) carries both sides of the
R1 match: the vcpkg build's symbol names (llvm-nm) and the corpus library's
exported symbols (llvm-nm -D), with the extracting commands recorded.
These attributes provide insight into the toolchain, programming language, and third-party libraries used to create the binary. This is critical for Supply Chain Security and vulnerability analysis.
This object summarizes key information about the toolchain and primary language used to compile the binary.
| Property | Description | Use Case |
|---|---|---|
language |
The primary programming language detected (e.g., Go, Rust, .NET). This is inferred from language-specific sections or symbols. |
Guides the reverse engineering process by setting expectations for runtime behavior, calling conventions, and data structures. |
go_version |
If the language is Go, the Go toolchain version copied from go_formulation.go_version (e.g. go1.27.1), read length-prefixed from the raw buildinfo blob. Absent when the version cannot be recovered (e.g. a pre-1.18 pointer-layout buildinfo or a truncated blob) rather than set to a placeholder. |
Checking against known vulnerabilities in specific versions of the Go compiler or standard library. |
linker_version |
The version of the linker program (e.g., from ld or link.exe) that produced the final executable, if this information is present in the binary. |
Can help fingerprint the build environment (e.g., a specific Linux distribution or version of Visual Studio). |
compiler_version |
The compiler identification string, often extracted from the .comment section in ELF files (e.g., GCC: (Ubuntu 11.2.0-19ubuntu1) 11.2.0). |
Precisely identifies the compiler and its version, which is useful for tracking toolchain vulnerabilities. |
These attributes provide detailed lists of third-party libraries and packages compiled into the binary.
| Attribute | Description | Use Case |
|---|---|---|
go_dependencies |
A list of Go packages used to build the binary, extracted from the embedded .go.buildinfo section. Includes package names, exact versions, and checksums (h1: hashes). |
Gold Standard for SCA. Allows for precise identification of Go libraries and their versions, enabling direct mapping to known vulnerabilities (CVEs) in those packages. |
rust_dependencies |
A list of Rust crates used to build the binary, extracted from the .dep-v0 section created by the cargo-auditable feature. Includes crate name, version, and kind. |
Similar to Go, this enables precise SCA for Rust applications, mapping crates to known CVEs. Limitation: This section is only present if the developer explicitly enables the cargo-auditable feature during compilation. |
dotnet_dependencies |
A structured list of NuGet packages and their versions, extracted from the deps.json file embedded in the PE overlay of self-contained .NET applications. |
Provides precise SCA for .NET applications, allowing for vulnerability mapping. Limitation: This is only available for .NET Core/5+ applications published in "self-contained" mode and is not present in framework-dependent deployments. |
import_dependencies |
A structured graph detailing which shared libraries (.dll, .so, .dylib) are imported by the main binary and which specific symbols are used from each library. See Symbol attribution for what the attribution is based on per format. |
Provides a clear, high-level view of runtime dependencies. Helps identify the use of sensitive APIs (e.g., crypto, networking) and from which library they originate. This is a foundational element for behavior analysis. |
Companion to go_dependencies: the Go build's own description of itself, parsed from the embedded buildinfo (.go.buildinfo on ELF, __go_buildinfo on Mach-O, a .data section scan on PE). A flat object:
go_versionis the Go toolchain version (e.g."go1.27.1",devel …strings keep their full shape, e.g."go1.26.4-X:jsonv2"). It is read at the byte level from the raw buildinfo blob: the 32-byte\xff Go buildinf:header, then the uvarint-length-prefixed version string exactly as laid out bygo/src/debug/buildinfo(inline since Go 1.18; observed on go1.26.4/1.26.5/1.27.0/1.27.1 builds, on ELF, Mach-O and PE alike), so it cannot be confused with module paths that merely contain agotoken. On pre-1.18 pointer-layout buildinfo the version lives outside the blob andgo_versionstays absent; absent, not a placeholder, is the contract for "not determined".pathandmodulecome from the same byte-level blob read, not from the NUL-stripped text: after the version, the blob carries the modinfo string framed by 16-byte sentinels (cmd/go/internal/modload'sinfoStart/infoEnd), and inside that frame the lines are tab-separated exactly asgo/src/runtime/debug.ParseBuildInfoexpects.pathis the single field of thepathline;moduleis the name column only of themodline; the version andh1:hash columns are real data, not part of the module identity, and never appear in the value. A dependency whose name ends inpath(e.g.…/jsonpath) cannot leak intopath, because lines are anchored on theirkeyword\tprefix rather than matched as substrings. When the blob cannot be read (pre-1.18 layout, truncation, failed sentinel check), both keys are absent; never a partial or concatenated string. A module-less build (go build main.go) reports"path": "command-line-arguments"and nomodulekey at all.- One entry per
build KEY=VALUEsetting with-stripped from the key (e.g.-buildmode=exe→"buildmode": "exe",-compiler=gc→"compiler": "gc", while environment-style keys keep their spelling:"CGO_ENABLED": "1","GOARCH": "arm64"). The value is cut at the first=, so values that themselves contain=survive whole;DefaultGODEBUG=tracebacklabels=0,x509sslcertoverrideplatform=0is stored in full (observed ongh2.100.0). - The key is present on every binary parsed by the ELF/PE/Mach-O readers,
{}for non-Go binaries (observed{}on/bin/ls), so an empty object means "not a Go binary or no readable buildinfo", not "Go with no build settings". On PE the blob (magic, version, framed modinfo) is located by searching the whole.datasection, since the modinfo string can be larger than the text window used by the fallback;go_dependenciesandbuildsettings are also recovered from that full-section blob read.
This is where blint provides the most value, by interpreting low-level data and presenting high-level security and compositional insights.
When disassembly output is available, blint derives a deterministic top-level callgraph:
version: Schema version for downstream compatibility.node_count/edge_count: Number of internal nodes and internal edges.nodes: Stable list of functions with{id, key, name, address, aliases}wherealiasesincludes other names sharing the same entry address.edges: Internal call edges as{src, dst, count, kind, confidence}wherekindis one ofdirect,tailcall,indirect_hint.external: Unresolved or ambiguous call targets as{src, target, count, reason, confidence}, pluslibrarywhen the target can be attributed to the library that supplies it.external_attribution_sources/attributed_external_count: What the library attribution was based on, and how many external edges carry one.
Notes:
- The graph includes direct edges, tail-call approximations, and register-tracked indirect hints.
confidenceindicates edge trust level (high,medium,low) for analyst triage.- Duplicate call instructions are preserved via edge
count. - Address/name collisions and misses are surfaced in
externalwith reason buckets such asambiguous_address,ambiguous_name, andaddress_space_miss. - An external edge carries a
libraryonly when the resolver recovered a symbol name for the target and that name is a known import. Edges whose target is a register-indirect operand cannot be attributed, solibraryis absent on most of them. Where it is present,confidenceis raised fromlowtomedium: the target is still unresolved as an internal edge, but the library it reaches is evidence rather than a guess. - Same-address symbol aliases are collapsed into a canonical node to reduce false ambiguity while preserving alias visibility.
For WASM binaries the same payload is produced by converting the wasm_tools static call graph instead of disassembly, so the callgraph export flags work for .wasm inputs under --disassemble too. Differences from the native graph:
- Imported host functions become
externaltargets with reasonimport(e.g.wasi_snapshot_preview1.fd_write) rather than nodes; the nodes are the locally defined functions. - Edge
kindkeeps the upstream semantics:directedges are exact,indirect-approx(element-segment over-approximation forcall_indirect) andtyped-approx(signature-based approximation forcall_ref) are candidates, and onlydirectedges carryhighconfidence. - Call sites to the same target collapse into edge
count; nodekeyuses the function body offset as its address.
A top-level dict mapping Mach-O import landing addresses to the imported symbol's name, built once when disassembly starts (blint/lib/disassembler.py::build_macho_import_address_map). Mach-O has no ELF-style PLT/GOT relocations, so imported calls would otherwise land on anonymous sub_* stub nodes; this table is what lets the callgraph builder classify such calls as external import edges instead of misattributing them to whichever internal function happens to span the slot address (the mechanism the Swift-symbols note above refers to).
- Two address families are covered: dyld binding-table slots (
__gotand the lazy/non-lazy symbol-pointer sections) and__stubsentries (indexed through the indirect symbol table via the section'sreserved1/reserved2). - Keys are hex virtual-address strings; values are demangled symbol names. Observed on
/bin/ls(175 entries), e.g."0x100005000": "__DefaultRuneLocale", and on/usr/bin/log(793 entries), where the C++ mangled binding__ZNSt3__112__next_primeEmappears as"0x100037340": "std::__1::__next_prime(unsigned long)". - The key appears only for Mach-O inputs and only when disassembly runs; without
--disassembleit is absent (not empty). An empty dict would mean a Mach-O with no bindings and no stubs: distinct from the key never having been built. ELF needs no such table: imported names are resolved from GOT/PLT relocations directly. - The callgraph's
externaledges consume it: a call target that lands on a known slot is a call out to a dynamic library, never an internal edge, and the recovered name is what makes the edge'slibraryattribution (and its confidence raise fromlowtomedium) possible.
ELF only. Describes the runtime the binary requires and the ABI features that constrain where it can be deployed. Every value is derived from the imported symbols, not from the version definition table, because a version node appearing in .gnu.version_r does not mean any symbol binds to it.
| Property | Description |
|---|---|
libc |
The C library the binary is linked against: glibc, musl, bionic, or empty when it cannot be determined. Inferred from the interpreter first, then from the version providers and DT_NEEDED. |
min_glibc_version |
The minimum glibc the binary can run on, as a dotted version. Empty when no glibc version node is bound. |
requirements |
One entry per version provider. See the table below. |
features |
Symbol-level ABI features: ifunc_symbols, imported_ifunc_symbols, tls_symbols, unique_symbols, and implementation_specific_imports split by measured libc availability into glibc_specific_imports, musl_specific_imports, shared_libc_imports (both glibc 2.41 and musl 1.2.6 export them) and non_libc_runtime_imports (neither libc exports them; libgcc/loader interfaces). Each is a capped list of symbol names. |
uses_symbol_versioning |
True when any imported symbol carries a version node. |
uses_ifunc |
True when the binary defines or imports an indirect function, which requires a loader that runs IFUNC resolvers. |
uses_tls |
True when thread-local storage symbols are present. |
uses_unique_symbols |
True when the binary defines STB_GNU_UNIQUE symbols, which prevent the object from being unloaded. |
uses_private_symbol_versions |
True when a symbol binds to a private version node such as GLIBC_PRIVATE. These are internal interfaces with no stability promise. |
private_version_providers |
The private providers bound, e.g. ["GLIBC_PRIVATE"]. |
uses_implementation_specific_interfaces |
True when the binary imports any interface outside the portable libc surface (loader introspection, allocator internals, backtrace support, non-portable pthread extensions), classified by measured availability in features. |
is_statically_linked |
True when there is no interpreter and no DT_NEEDED entry. |
portability_notes |
Human-readable sentences summarizing the above, suitable for direct display. |
Each entry in requirements describes one version provider:
| Property | Description |
|---|---|
provider |
The version node provider, e.g. GLIBC, GLIBCXX, LIBPAM_EXTENSION, GLIBC_PRIVATE. |
min_version |
The highest version node any imported symbol binds to, that is, the minimum the runtime must supply. Empty for providers whose nodes carry no version. |
determining_symbols |
The imported symbols that set min_version. Use these to trace an unexpectedly high floor back to the single import responsible. |
symbol_count |
How many imported symbols bind to this provider. |
versions |
Every distinct version bound for this provider, sorted numerically. |
package_name / package_group |
Package coordinates for the provider where known, e.g. libc / gnu for GLIBC. Used to build the SBOM component. |
Libraries opened through dlopen never appear in DT_NEEDED, so a dependency list built from the dynamic table alone omits them. dlopen_dependencies covers the case where the project embeds a declarative note; these two attributes cover everything else by recovering the information from the image.
runtime_loading describes the behaviour:
| Property | Description |
|---|---|
entry_points |
The runtime-loading functions the binary imports (dlopen, dlmopen, android_dlopen_ext, LoadLibraryW, dlsym, …). A function the binary merely defines does not count: the dynamic loader exports dlopen itself, and its own libc.so string is its job, not a dlopen call. |
loads_libraries |
True when at least one entry point actually opens a library. dlsym alone operates on a handle the caller already has and does not count. |
call_sites |
Maps each entry point to the functions that call it. Populated only with --disassemble. |
call_site_count |
Total number of call sites across all entry points. |
recovered_dependencies lists the libraries themselves. Names already present in DT_NEEDED, libraries, or dlopen_dependencies are excluded, since those are not gaps.
| Property | Description |
|---|---|
name |
The soname as it appears in the binary, e.g. libgpm.so.2. |
confidence |
high when the name is a literal in a read-only data section and looks like a library, medium for weaker placement, low otherwise. |
evidence |
Why the candidate was accepted: which loading entry point is imported, which section the literal is in, and any absolute path found. |
paths |
Absolute paths found for the library, when the binary hardcodes one. |
sections |
The sections the name was found in. |
Format templates such as %s/libfoo.so are excluded: they are assembled at runtime and are not themselves names. This keeps the pass conservative; it under-reports rather than inventing dependencies.
Opt-in. Resolving the closure reads the filesystem the scan runs on, which is only meaningful when that filesystem is the binary's intended runtime. Enable it with:
| Environment variable | Description |
|---|---|
BLINT_RESOLVE_LINK_CLOSURE |
Set to 1, true, or yes to run the resolution. |
BLINT_LINK_ROOT |
Filesystem root to resolve against. Point this at an unpacked image or sysroot rather than the scanning host. |
BLINT_LINK_SEARCH_PATH |
Extra directories treated as if they were in LD_LIBRARY_PATH, separated by the platform path separator. |
Resolution follows the loader's documented search order: DT_RPATH (ignored when DT_RUNPATH is present), then LD_LIBRARY_PATH, then DT_RUNPATH, then the default directories for the machine type plus anything configured in /etc/ld.so.conf. The $ORIGIN, $LIB and $PLATFORM tokens are expanded as the loader expands them.
| Property | Description |
|---|---|
resolved |
Each object that would be mapped, with path, found_via (which kind of search path answered), needed_by, relation (direct or transitive), and export_count. |
missing |
Sonames nothing on the search path supplies. Each is a load-time failure and usually indicates an undeclared packaging dependency. |
unresolved_symbols |
Imported symbols no object in the closure defines, capped at 64 entries. Weak imports are excluded, since going unresolved is their intended behaviour. |
unresolved_symbol_count |
The full count, before the cap. |
symbol_providers |
Maps each resolved symbol name to the soname that supplies it. This is the edge-level dependency data dynamic_entries cannot give you. |
risky_search_paths |
DT_RPATH / DT_RUNPATH entries that are relative, empty (which the loader reads as the working directory), or under a world-writable directory. Each carries path, kind, and issue. |
complete |
True only when nothing is missing and no symbol is unresolved. |
root |
The filesystem root the result describes. |
The closure is capped at 256 objects so a pathological dependency graph cannot stall a run.
import_dependencies maps each imported symbol to the library that supplies it. How much evidence exists for that depends entirely on the format:
| Format | Evidence | Available |
|---|---|---|
| PE | The import table is organised by DLL, so every import names its library. | Always |
| Mach-O | Each symbol is bound to a dylib, recorded as library::symbol. The is_imported flag on each symtab entry marks the undefined symbols that are the imports. |
Always |
| ELF | The dynamic symbol table and the DT_NEEDED list are unrelated flat lists. The connection only exists once the dependency closure is resolved and each library's exports are read. |
Only with link_closure enabled |
Two fields record what the result rests on:
| Property | Description |
|---|---|
attribution_sources |
Which evidence was used: import_table, load_commands, sdk_tbd, link_closure. Empty means none was available. |
unattributed_symbol_count |
How many imported symbols could not be tied to a library. These are collected under a synthetic unattributed entry in libraries. |
An ELF binary analysed without closure resolution attributes nothing. That is deliberate. Assigning a symbol to an arbitrary declared library produces a dependency edge that is indistinguishable downstream from a correct one, and a wrong edge is worse than an honest gap.
C++ and Rust symbols are matched on their linkage name, which blint records as raw_name on the symbol whenever demangling changed it. A provider's export table holds mangled names, so the demangled name alone never matches.
Note that :: is a library separator only in Mach-O, and only when the prefix looks like a library. Everywhere else it separates namespace components, and APT::PackageContainer::begin is one symbol rather than a dependency on APT.
On a running macOS install the system libraries a binary links against are absent from disk: they live only in the dyld shared cache. --sdk-path <dir> (off by default) points blint at an Apple SDK whose .tbd text stubs describe what every system library exports, and is the only static oracle for confirming Mach-O dependency questions. The path must contain .tbd files; a run given a path with none aborts with an error rather than serving an empty index.
When the option is on, Mach-O imports the binary's own evidence could not pin to a library (flat binds, missing binding info) are attributed to the first declared library (in load-command order, matching dyld's search order) whose export surface provides the symbol. A symbol's surface is the library's own exports/reexports sections plus, transitively, the exports of everything it names under reexported-libraries (v4) or re-exports (v2/v3).
A re-exported symbol is attributed to the declaring library, never the implementing one. A Mach-O two-level bind records the ordinal of the library in the binary's own load-command list; dyld resolves through that library's re-export edges at runtime, but the bind still names the declared library: dyld_info -imports /usr/bin/git reports _dispatch_once (from libSystem) although the implementation lives in libdispatch.dylib. Substituting the implementing library would contradict the binary's own bind and re-flag declared umbrellas as unused.
The evidence is marked sdk_tbd in attribution_sources, always distinct from load_commands: a .tbd describes what the SDK ships, not what the machine under the binary ships. The block records no SDK identity (no paths, versions, or totals of the analyst's environment) only these binary-relative facts:
| Property | Description |
|---|---|
attributed_symbol_count |
Imports without a dylib::symbol prefix that the index pinned to a declared library. |
attributed_symbols |
Capped, sorted sample of the same, as symbol: install-name. |
confirmed_symbol_count |
Load-command binds the SDK surface confirms. |
reexport_confirmed_symbol_count |
How many of those confirmations needed the re-export closure. |
unconfirmed_symbol_count |
Load-command binds the SDK surface cannot back: private or otherwise noteworthy. |
unconfirmed_symbols |
Capped, sorted sample of the same. |
Every _count above is exact; only the _symbols samples are capped, so a large binary's metadata does not grow with its import table. A count that saturated at the sample cap would understate exactly the binaries whose gaps matter most.
The index is built once per run and cached on disk (under the parse-cache directory, keyed by a fingerprint of the SDK's .tbd tree), so --jobs N workers each pay one load rather than one build.
Reports declared dependencies that are never used and used libraries that are never declared. Requires symbol attribution, so for ELF it needs closure resolution; the whole block is absent when no attribution evidence was collected at all, because with no evidence every dependency looks unused. A separate shape covers the case where the binary imports symbols but none of them could be pinned to any library:
| Property | Description |
|---|---|
attribution_status |
"unresolved": the binary imports symbols, but none were attributed, so unused/undeclared answers would be manufactured rather than observed. No unused_dependencies or undeclared_dependencies keys are present in this state, and analysis_coverage.degradations carries dependency_attribution_unresolved. |
| Property | Description |
|---|---|
unused_dependencies |
Declared libraries from which no symbol is imported, each with a name and a reason. Linking with --as-needed removes them. |
undeclared_dependencies |
Libraries supplying symbols without being declared, with symbol_count and a sample of symbols. These work only for as long as some other dependency keeps them mapped. |
attribution_sources |
The evidence the result is based on, as above. |
unattributed_symbol_count |
Imports not tied to any library. A high count means the findings are based on partial evidence. |
imported_symbol_count |
Present only in the unresolved state: how many imports existed for the (failed) attribution to consider. |
declared_count |
How many direct dependencies were declared. |
unused_dependencies answers a similar question to ldd -u, but not an identical one. ldd -u relocates the whole closure and counts a library as used if anything in it binds to the library, so a library this binary never calls still counts as used when some other dependency calls it. blint reports direct use, which is what --as-needed acts on. Everything ldd -u reports unused will also be reported here; the reverse does not hold.
Over-linking is largely a property of the distribution rather than the project: builds that pass --as-needed are almost free of it, while those that do not accumulate it as link lines are inherited between libraries.
Present for every ELF, PE and Mach-O image. Lists the loadable segments or sections the loader maps both writable and executable, each with its name, normalized permissions (for example rwx) and virtual_address. An empty list means the image maintains Write XOR Execute: no code mapping is writable and no data mapping is executable.
What counts as a mapping follows what each platform loader actually enforces:
- ELF:
PT_LOADprogram headers carrying bothPF_WandPF_X, namedPT_LOAD[<index>]by program-header position. An executablePT_GNU_STACKis deliberately not listed here; it is a stack-executability defect and is reported by the NX signal (has_nx) instead. - PE: Sections whose characteristics include both
MEM_WRITEandMEM_EXECUTE. - Mach-O: Segments whose
init_protectionincludes both write and execute.max_protectionis ignored: it describes what a segment may later be remapped to, not what it is mapped with, so a permissive maximum alone does not mean writable code ever existed.
The CHECK_WX_SEGMENTS security check turns each entry into a finding naming the segment.
ELF only. Three related additions record how an ELF is laid out and where that layout contradicts itself:
entry_point_section: the name of the section containinge_entry, the ELF counterpart of the field PE metadata has always carried. An empty string means the entry address falls in no section at all, which is the strongest form of the anomaly below, not a missing computation.segments_summary: every program header in table order, each withindex,type, normalizedpermissions,file_offset,file_size,virtual_addressandvirtual_size. ELF metadata previously recorded onlynumberof_segments, which is exactly the field that stays constant when a spare program header is retyped in place; exporting the table makes that change visible to anything diffing two builds of the same binary.layout_anomalies: a list of structural contradictions, each with akind, the addresses and names needed to check it by hand, and adetailsentence. An empty list is the normal result.
The anomaly kinds:
kind |
What it means |
|---|---|
entry_point_outside_any_section |
e_entry is covered by no section header. Toolchain-produced entry points always lie inside a section. |
entry_point_in_non_executable_section |
e_entry is inside a section not marked SHF_EXECINSTR. The section table says this is not code; the header says it is. |
entry_point_in_unexpected_section |
e_entry is inside an executable section whose name is outside the small set toolchains emit entry stubs into. |
note_section_without_note_segment |
The image maps .note.* sections but carries no PT_NOTE at all, so nothing at run time can reach them. |
executable_mapping_at_eof |
An executable PT_LOAD's file range ends on the last byte of the file, where a linker would have placed data and symbols. |
These exist because an implant can be added to a finished ELF without changing one original byte: append the payload at EOF, retype a spare PT_NOTE program header into an executable PT_LOAD covering it, and redirect e_entry and e_shoff (arXiv 2607.24888, which carries exactly this through GNU strip across the NixOS bootstrap). Nothing is packed, nothing becomes writable-and-executable, and the result is internally consistent enough that readelf reports no problem, so neither wx_segments nor entropy sees it. What the result cannot hide is the disagreement between its parts.
The ELF_ENTRY_POINT_OUTSIDE_CODE, ELF_NOTE_SECTION_WITHOUT_SEGMENT, ELF_APPENDED_EXECUTABLE_MAPPING and ELF_BUILD_SANDBOX_EVASION_GATE reviews turn these into findings. Note that note_section_without_note_segment requires the absence of any PT_NOTE rather than per-section coverage: the Go linker legitimately emits a PT_NOTE spanning only .note.go.buildid and leaves the adjacent .note.gnu.build-id outside it, so per-section coverage fires on every Go binary.
Per-section Shannon entropy plus packing evidence, collected for every ELF, PE and Mach-O image whether or not disassembly is requested.
sections: one entry per non-empty section withname,size,entropy(bits per byte, 0-8),executable,writable, andsampled(true when entropy was computed over the first 8 MB of a larger section; the sample is always the section head, so results stay deterministic).packing: the derived signals:packed_likelihood:high/medium/lowsummary of the evidence below. On PE, an overlay counts as evidence only when its residue isunknown_high_entropyand an independent packing signal agrees; a lone overlay (installer payload, appended archive, bundle) is reported without raising the likelihood.packers: packer section-name signatures found (UPX, Themida, VMProtect, ASPack, MPRESS and others).writable_executable_sections: section-level W+X evidence; the loadable-segment view lives inwx_segments.executable_section_entropy/max_executable_section_entropy: per-executable-section entropy values.overlay_size: bytes past the last section's file content (ELF/PE only; the Mach-O file tail is the code-signature SuperBlob and is excluded). On PE this is the overlay residue; the Authenticode certificate table (IMAGE_DIRECTORY_ENTRY_SECURITY) is subtracted first, so a signed stock binary reportsoverlay_size: 0rather than counting its signature as packing evidence.overlay_classification: PE only; the magic-based label of the overlay residue (zip,cab,msi,nsis,inno,installshield,sfx_7z,dotnet_single_file_bundle,go_buildinfo,unknown_high_entropy,unknown_low_entropy; seeoverlay_info).findings: the individual evidence strings (packer_section:,writable_executable_section:,high_entropy_exec_with_few_imports,entrypoint_outside_executable_sections,virtual_size_mismatch:,file_overlay).
The CHECK_PACKED security check turns high/medium likelihood into a finding naming the evidence.
PE only. The classified overlay: the region past the last section's raw bytes, minus the Authenticode certificate table.
| Attribute | Description |
|---|---|
offset, size |
File offset and size of the residue: the overlay that is left once the certificate table is subtracted. size is 0 for a stock signed binary, whose entire overlay was the signature. |
security_directory |
{offset, size} of the IMAGE_DIRECTORY_ENTRY_SECURITY region (the PE specification defines the certificate table's "RVA" as a file offset), or null when absent. |
entropy |
Shannon entropy of the residue, sampled from its head/tail windows. |
classification |
Magic-based label: zip, cab, msi, nsis, inno, installshield, sfx_7z, dotnet_single_file_bundle, go_buildinfo, unknown_high_entropy, unknown_low_entropy. The classifier (blint/lib/pe_overlay.py) is shared with the installer/container work. |
Compiler and runtime attribution built from binary evidence rather than declared metadata. Every signal carries source and confidence:
compilers: from ELF.commentsections (gcc, clang, rustc, lld with versions), Mach-OLC_BUILD_VERSIONtool entries, and the PE linker version fields.runtimes: Go (buildinfo orruntime.*symbols), Rust (buildinfo or_ZN/_Rmangling), Swift (swift_stdlib symbols), Objective-C (objc_*trampolines), MSVC/MinGW CRT fingerprints, .NET.libc:glibc,muslorbionicfrom symbol-version requirements, the ELF interpreter and the Android target flag.
Empty lists mean the format carries no such evidence: attribution is never padded with guesses.
A stable digest over the normalized import-name set (ELF dynamic symbols marked as imports, or the imports list for PE/Mach-O). Normalization strips ELF @@VERSION suffixes, PE __imp_/_imp_ thunks and common leading-underscore decoration, so the same dependency set hashes identically across formats and minor version bumps. Empty for binaries that import nothing (fully static images).
Swift reflection metadata, parsed by blint/lib/swift_metadata.py from __swift5_* (Mach-O) or .swift5_* (ELF) sections: the same parser serves both formats (issue #109). The sections survive stripping and carry every Swift type's name, fields and metadata access functions.
| Attribute | Description |
|---|---|
type_count, types |
Every Swift type: name, kind (class/struct/enum), the metadata access_function address, and the declared fields (property names). |
kind_counts |
Per-kind totals. |
protocol_count, protocols |
Protocol names from __swift5_protos. |
access_function_count, access_functions |
Named code addresses (metadata_access_function_for_<Type>) seeded into functions so disassembly and callgraph work reaches Swift entry points on stripped binaries. |
Swift type and field names also join the ObjC selectors and symbols in the privacy-marker haystack (_symbol_haystack), so a Swift property creationDate matches the required-reason API categories like the C symbol spellings do.
Accounting for what was analyzed versus what was discovered, so a run that disassembled 3 of 400 functions is never indistinguishable from a clean run of 400:
functions:symbolic(from symbol buckets),discovered(recovered from unwind tables, prologues and call sites),discovered_merged_into_function_list,disassembled. On ELF binaries the extent repair is visible here too:function_sizes_rejected(entries whose declared size was impossible (wrapped or past the containing section) and was dropped, leaving the disassembler's next-known-start rule) andfunction_sizes_corrected(entries whose size was replaced by a higher-precedence source: a symbol'sst_size, then the unwind-table FDE range, over LIEF'sfunctionssize).degradations: reasons parts of the binary were not analyzed, e.g.fairplay_encrypted,disassembly_unavailable,slice_summary_failed.sections_analyzed: sections the entropy pass examined.slices(universal Mach-O binaries only):total,summarizedandfailedslice counts. A slice whose summary failed is isolated; the remaining slices are still reported, anderrorscarries one record per failed slice (index,exception_type,message).security_properties_gaps: properties the format could carry but blint does not compute yet, or whose source could not be read. Their absence fromsecurity_propertiesmeans "not implemented" or "source unreadable", never "checked and clean". Mach-O records its granularhas_nx_stack/has_nx_heapand unreadable signature blobs here; PE records an unreadable load configuration, an absent debug directory, an unresolved Authenticode scope (catalog signing, W2.3) and unparsed page hashes (nocode_signatureblock was parsed).
Alongside findings.json/reviews.json, default-mode runs write an analysis-coverage.json summarizing the run's units (a top-level file, or one binary contained in an .ipa). A binary that fails to parse no longer aborts the scan (issues #122, #188); the failure lands here instead:
units:attempted/succeeded/failed/skipped. Totals mix granularities: an.ipaarchive counts as a unit beside the member units it contains.units_by_role: the same four counters per unit role (top-level, plus container member roles such asipa-member,msix-member,cab-member,sfx-member,msg-attachmentandapk-so-member), so a consumer can compute a success rate over just the member binaries or just the top-level inputs.failures: one record per failed unit withfile_path,unit_role(top-leveloripa-member),stage,exception_typeandmessage.skipped: one record per recognized-but-unanalyzed unit withfile_path,unit_roleand a machine-readablereason(e.g.extract_failed,no_dex_bytecode).cache: parse-cache accounting.enabledtells a fast run from a cached one;hits/misses/storedcount what was served from the content-addressed parse cache;caches_failuresis alwaysfalse; parse failures are never cached, so every record infailuresis a fresh failure;by_rolecarries the same three counters per unit role, since not every role can hit the cache (the android app unit itself never goes throughparse(); itsapk-so-memberunits do, and dedupe through the same content-addressed keys).
This file is exported even when the scan produced no findings, so a caller can always tell "clean" from "blind" without reading stderr.
Default-mode runs can cache parse metadata in a content-addressed SQLite store keyed on (sha256(file bytes), blint version, options digest), separate from blintdb (which is a shipped read-only artifact). A warm run replays byte-identical metadata, including the cross-path case, where the stored path is rewritten to the current one exactly where parse() embeds it. The cache is off by default; --cache opts a run into it, since caching writes to the user's disk and is a caller's choice; blint cache stats reports entry count and actual size on disk, and blint cache clear deletes the store. Entries are zlib-compressed and bounded by BLINT_CACHE_MAX_BYTES (default 1 GiB; 0 disables the bound) with least-recently-used eviction; the store lives at BLINT_CACHE_DIR (default: the user cache directory, e.g. ~/.cache/blint on Linux), in parse-cache.db.
--jobs N analyzes up to N binaries in parallel worker processes (default 1, which is the unchanged sequential loop; 0 or auto means one worker per CPU). Both the default mode and blint sbom accept the flag; the unit of work is one binary, and there is no parallelism within a binary. Output is byte-identical to the sequential run for any N: every worker result carries its input position and the parent merges strictly in that order, which also holds for analysis-coverage.json and the cache counters. A worker that dies hard (e.g. a segfault inside LIEF) is recorded in analysis-coverage.json as a failure with stage: "worker" and exception_type: "WorkerDied" naming the file it was analyzing; the remaining files are still analyzed. If the pool cannot start at all, the run falls back to the sequential path with an error logged. With --cache --jobs N, every worker opens its own SQLite connection to the same store (WAL mode); hit/miss/stored totals match the equivalent sequential run.
This object provides a quick, at-a-glance summary of the most important security mitigations compiled into the binary.
Properties are format-aware: a property the format has no concept of is omitted rather than reported as a negative finding (relro never appears for Mach-O, for example), and a property blint does not compute for the format is omitted and listed in analysis_coverage under security_properties_gaps.
| Property | Description | Security Implication |
|---|---|---|
nx |
Non-eXecutable. True if data regions (stack/heap) are not executable. | Mitigates code injection attacks. |
w_xor_x |
Write XOR Execute. True when no loadable segment is mapped both writable and executable. | Keeps code pages unmodifiable at runtime; violations are listed in wx_segments. |
pie / aslr |
Address Space Layout Randomization. | Makes memory corruption exploits harder by randomizing locations. |
canary |
Stack Cookie. Confirmed via Load Config or symbols. | Mitigates stack-based buffer overflows. |
control_flow_guard |
CFG (Forward-Edge). Validates indirect call targets. | Mitigates function pointer corruption (e.g., vtable hijacking). |
xfg |
Extended Flow Guard. A stricter version of CFG that validates function signatures (types) at indirect call sites. | significantly reduces the number of valid targets for an attacker compared to standard CFG. |
cfg_export_suppression |
CFG Export Suppression. Prevents valid exported functions from being called indirectly unless explicitly permitted. | Reduces the attack surface by limiting available gadgets in exported APIs. |
cet_shadow_stack |
Intel CET / Shadow Stack. PE: the CET_COMPAT bit of the debug directory's EX_DLLCHARACTERISTICS entry. |
Hardware-enforced protection against ROP by maintaining a secondary, immutable stack for return addresses. |
retpoline |
Retpoline. Use of return trampolines. | Mitigates Spectre Variant 2 (Branch Target Injection) side-channel attacks. |
cast_guard |
CastGuard. Validates virtual function calls. | Mitigates C++ type confusion and vtable hijacking attacks. |
safe_seh |
Safe SEH. (x86) Registers exception handlers at compile time. | Prevents attackers from overwriting SEH chains on the stack to gain execution. |
safe_delay_load |
Protected Delay-Load IAT. Marks delay-load tables read-only after initialization. | Prevents hooking of APIs that are loaded lazily during execution. |
enclave |
Enclave Support. Binary contains configuration for SGX/VBS. | Indicates the application uses TEE (Trusted Execution Environment) features for high-security operations. |
packed |
Packing evidence present. Derived from the entropy block. |
Strings, symbols and disassembly-derived findings may be incomplete until the binary is unpacked. |
For PE, the block is computed from named sources (PE-lane packet W0.3): every property answers from the specific header field that defines it, and is omitted rather than guessed when that source is absent; the omission is recorded in security_properties_gaps, never reported as a thin false. A load configuration that failed to read is a gap; a load configuration that read and lacks a bit is a computed false.
PE-specific properties and their sources:
| Property | Source | Notes |
|---|---|---|
aslr / high_entropy_va / dep / force_integrity |
optional header DLLCharacteristics bitfield, decoded through blint's PE-spec table (pe_constants) |
seh on x86 from the same field's NO_SEH bit. |
cfg / control_flow_guard / xfg / rfg / retpoline / cast_guard / safe_delay_load / cfg_export_suppression |
load configuration GuardFlags, decoded bit by bit through the winnt.h-derived table |
cfg/control_flow_guard are the CF_INSTRUMENTED bit. The EH_CONTINUATION_TABLE_PRESENT bit is /guard:ehcont metadata in load_configuration, deliberately not read as CET. |
gs_canary / canary |
load configuration SecurityCookie != 0 and not SECURITY_COOKIE_UNUSED |
one value under the PE name and the cross-format name. |
safe_seh |
load configuration SEHandlerCount (x86 machines) |
|
cet_shadow_stack / cet_shadow_stack_strict |
debug directory IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS entry (winnt.h type 20), CET_COMPAT bits |
user-mode CET is a debug-directory claim, not a GuardFlags one. |
debug_info |
debug directory: full when a CodeView entry carries a PDB path, codeview_only when entries exist without one, none for an empty directory |
replaces the COFF-symbol-table stripped guess, which modern MSVC images made meaningless; debug_info_pdb_path carries the path when present. |
arm64ec / arm64x |
PE header machine type | |
authenticode_scope |
embedded when a signature table exists |
catalog signing is not resolved yet (W2.3), so a non-embedded scope is recorded as a gap rather than guessed as none. |
signed_page_hashes |
code_signature.signatures[].page_hashes |
stated either way when a signature was parsed (true when any signature carries SpcPeImageData page hashes); the gaps list carries it when no block was parsed. |
enclave |
load configuration EnclaveConfigurationPointer |
presence-only: true when an enclave configuration exists, omitted otherwise; absence is the Windows norm, not a computed negative. |
pac/pac_strict are deliberately absent for PE: no PE field records ARM64 pointer authentication (the GuardFlags RF_* bits are Return Flow Guard per the Windows SDK headers), so there is no honest source to compute from.
An .msi is a CFBF storage whose streams are database tables; a .cab is the payload container MSI, drivers and update packages ship. Both parse with pure struct code (blint/lib/msi.py, blint/lib/cab.py) over the shared CFBF reader (blint/lib/cfbf.py): no OLE library, no Windows API.
| Attribute | Description |
|---|---|
msi (exe_type msi) |
parse_status, table_count/tables (decoded names), identity (product_code, upgrade_code, package_code, product_name, product_version, manufacturer), summary (the \x05SummaryInformation property set: title, author, template, revision number (the PackageCode), timestamps, application name), file_count, component_count, custom_action_count/custom_actions (each with the Type field decoded: kind dll/exe/jscript/vbscript, source, deferred_in_script, no_impersonate, rollback, async, continue_on_error, terminal_server_aware), binaries (Binary-table stream names and sizes; stream bytes are never read), embedded_cabinets (Media table's Cabinet column: name, embedded, size), digital_signature_present, refusals, degradations. |
cab / cab_members (exe_type cab) |
parse_status, version, folder_count, methods (none/mszip/lzx/quantum), member_count, total_uncompressed, extracted_member_count, extraction_refusals, plus the member listing (name, size, unsafe_path). Members in stored/MSZIP folders extract and their PE members (.exe/.dll/.sys) analyze as cab-member units attributed to the member path; LZX/Quantum folders refuse by name (member_compression_unsupported): a stdlib-only constraint, stated rather than worked around. |
CFBF chain sanity is a first-class fact (a malformed chain is a finding, not a swallowed error): fat_chain_loop, minifat_chain_loop, sector_out_of_range, chain_terminated_early, directory_tree_loop, stream_chain_broken are named degradations beside whatever was read. Caps are measured (module docstrings): CFBF 4,096 directory entries / 256 MiB per stream / 512 MiB total read budget; CAB 16,384 members / 512 MiB total / 256 MiB per member. In SBOM output the .msi parent carries the product identity and codes (internal:msiProductCode etc.) and a .cab lists its members as components keyed by member path; refusals and degradations reach the BOM as internal:msi_refusals / internal:cab_refusals (rule 32).
A PE whose overlay residue classifies as an installer family (the W0.2 overlay classifier) carries an installer block in its metadata; ClickOnce manifests (.application, and .manifest files whose namespace is ClickOnce's asm.v2) parse as their own units.
| Attribute | Description |
|---|---|
installer.family |
nsis, sfx_7z, inno or installshield: what the overlay classifier matched. |
installer.extraction |
States the honesty of the block: detection_only (NSIS, Inno, InstallShield, no member extraction) or members (7z-SFX, member listing and bounded extraction). Detection-only is stated in the block, never implied. |
installer.nsis_firstheader |
The documented NSIS firstheader: flags, offset, length_of_header, length_of_all_following_data. NSIS member extraction is deliberately not implemented; the data block is a compiled install-script database resolved by emulating the script VM, which is a decompiler, not a container reader. Detection-only, stated. |
installer.sfx_payload |
The appended 7z archive (signature-header CRC verified before use): offset, version, member_count, members (name, size; exact for LZMA/LZMA2/copy folders), total_unpacked. BCJ2-encoded folders (x86-filtered SFX modules) refuse by name (member_compression_unsupported, folder_unpack_sizes_unresolved) rather than listing as analyzable. |
clickonce (exe_type clickonce) |
kind (deployment/application), identity (name, version, publicKeyToken, culture, architecture), publisher, product, update_url (the <deploymentProvider>), requested_execution_level, permission_set_unrestricted, compatible_frameworks, files (application manifests), signature_present (XML-DSig), refusals. |
The runner analyzes a 7z-SFX's decodable members (.exe/.dll/.sys) as sfx-member units attributed to their member path, beside the stub executable's own top-level unit. The installer block rides parse() output, so it changes the stored cache shape; CACHE_SCHEMA_VERSION stays frozen at 10 for the rest of v4 pre-release and blint cache clear is what a checkout running with --cache needs. No rule consumes the installer block yet: a "packed installer" rule needs a measured benign population (software installers are overwhelmingly legitimate), which is not yet measured.