Engine
A compiled program is a sealed box. The source is gone, the structure is implicit, and the only artifact left is the bytes the CPU runs. The engine opens that box.
It reads the binary, recovers what the program is and what it does, and writes the result in two forms a human and a machine can both work with: a structured set of facts about the program, and a Rust source project that mirrors it closely enough to compile.
The engine is what sits underneath openbinary. The platform is the surface that searches, ranks, and presents the engine's output; the engine is the part that produces the output in the first place. The same engine is designed to sit underneath any other product that needs the same kind of binary truth.
The pipeline
READ → LIFT → REASON → IDENTIFY → JUDGE → EMIT
| Phase | What happens |
|---|---|
| READ | Open the file, recognize the format from its bytes, reach inside containers, lay out the structure |
| LIFT | Translate CPU instructions into a portable IL, build the control-flow graph, convert to SSA |
| REASON | Track values, calls, syscalls, jump targets, and runtime metadata until every function is named and every call is resolved |
| IDENTIFY | Match the recovered structure against curated knowledge: types, runtimes, libraries, debug info |
| JUDGE | Compose the cross-cutting views — capabilities, hardening, consistency, behavioral aspects — and the four security surfaces |
| EMIT | Write the JSON facts, the DNA fingerprint, the reconstructed Rust project, the inspection views, the diffs |
The rest of the brief walks each phase. For the full coverage matrix (98 spec targets, 3 executable formats, the platform-syscall depth), see Architectures.
READ — Reading binaries
The engine recognises its input by reading the first few bytes. Mach-O announces itself with a magic word like 0xFEEDFACF. PE32+ begins with MZ followed by a PE\0\0 header. ELF begins with \x7FELF.
The three executable formats
Each format is parsed into a structured object that the rest of the pipeline can ask questions of: sections and segments, imports and exports, fixups, embedded signatures, and embedded application manifests.
- Mach-O — chained fixups (the modern
LC_DYLD_CHAINED_FIXUPSformat Apple uses on arm64e) are walked through bind ordinals into concrete imported symbol names. - PE32+ — imports, exports, the
.pdataand.xdataexception directories, the Authenticode certificate directory, the application manifest resource, and the PDB CodeView debug-directory record (theRSDSGUID + age + path that names the external symbol file) are read out. - ELF — dynamic sections,
.rela.pltand.rela.dynrelocations, BTI-aware arm64 PLT trampolines, IBT-aware x86_64.plt.sec, IRELATIVE entries for IFUNC resolvers, the.gnu.versionversioning tables, and the GNU build identifier are read out.
Imports carry a kind tag — function, data, or ifunc — so cross-format consumers can distinguish a JUMP_SLOT function pointer from a R_*_GLOB_DAT data-pointer entry (__stack_chk_guard, _environ, _optarg). Same surface across all three formats.
See Architectures for the full per-format spec coverage (Mach-O 632 items across 51 dimensions, PE 249 items across 16 dimensions, ELF 895 items across 22 dimensions).
Containers
Four container formats unwrap transparently into the same pipeline:
- DMG disk images (
.dmg, UDIF format) — walked through the HFS+ catalog; embedded Mach-O binaries extracted. - dyld shared cache — the multi-gigabyte file holding every macOS system framework as one packed blob across a base file and subcaches. Memory-mapped, reconstructed image-by-image into synthetic Mach-O buffers.
- Apple kernel collections (
MH_FILESETMach-O files) — split into per-kext analyses and merged. - Fat binaries — Mach-O universals carrying more than one architecture are sliced automatically.
A wider unpack layer covers 91 container formats — 85 of them with a working extractor, 6 identification-only — across filesystems, archives, compression, and firmware carriers: squashfs, JFFS2, UBI / UBIFS, ext4, U-Boot, UImage, TRX, UEFI FFS, Android boot and sparse images, tar / zip / xar / 7-zip / ar / cpio archives, and gzip / xz / zstd / bzip2 / lz4 / lzfse compression. The container coverage and the cross-tool comparison are in Architectures.
Identity
Every binary carries a structural identity. The fields differ per format; the surface is symmetric:
| Format | Unique ID | Version metadata |
|---|---|---|
| Mach-O | LC_UUID | Min OS version, SDK version, source version, bundle identifier |
| PE32+ | PDB filename (CodeView RSDS debug-directory record) — GUID parsing not yet surfaced; SHA-256 is the de-facto unique ID | OS major / minor, image checksum, manifest version |
| ELF | NT_GNU_BUILD_ID (20-byte SHA-1 or 16-byte MD5 in .note.gnu.build-id) | DT_SONAME, PT_INTERP |
Plus the full-binary SHA-256 and the slice-only SHA-256 for fat binaries — and alongside them MD5, SHA-1, and an ssdeep fuzzy hash computed on the same byte slice. All four hash families are first-class on every analysis so a binary joins cleanly into VirusTotal, MISP, threat-intel feeds, and any other corpus that keys on a particular algorithm. Mach-O additionally surfaces the CDHash, team identifier, and arm64e flag (see Code signature and identity); the full set of fat-binary arch slices is enumerated in available_arches.
LIFT — Disassembly and IL
Decoded instructions are translated into the engine's intermediate language, a single representation that does not depend on which CPU produced it. The IL exists in two altitudes: a low-level form close to the machine, and a medium-level form once analysis has pulled meaning out of the low form.
The arm64 disassembler covers the full Apple Silicon instruction set. The x86-64 disassembler covers roughly eighty opcode families across the System V AMD64 ABI on macOS and Linux and the Microsoft x64 ABI on Windows — SSE scalar floating-point, SSE/AVX vector basics, LOCK-prefixed atomics, REP-prefixed string operations (STOS / MOVS / LODS / CMPS / SCAS at byte / word / dword / qword widths), and PIC jump-table dispatchers recovered in three shapes: the cross-format jmp [rip+disp], MSVC Forms A and B (lea Rbase, table; mov Ridx, [Rbase + i*4] and the two-level byte-index movzx Rslot, byte [idx_tbl + i]; jmp [jmp_tbl + Rslot*8]), and the Mach-O LC_DATA_IN_CODE-hinted form. Sixty-four more ISA decoders sit under the same dispatch — see Architectures.
Control flow and SSA
Around the IL, the engine builds the program's control flow graph (the map of how a program moves between instructions: branches, loops, calls, returns) and then converts function bodies into single static assignment form (SSA). SSA is an arrangement in which each value is written exactly once and every later use of that value points back to a single producer. A pass that wants to know where did this value come from walks one edge instead of searching the whole function. Every later analysis depends on it.
Dominators are computed using Lengauer-Tarjan. SSA is constructed using the Cytron et al. pruned-φ algorithm.
Calling conventions
Function boundaries carry typed parameters and typed returns from the first analysis pass onward. The convention is selected automatically from the (architecture, image format) pair — the five most common shown here; see Architectures for the full set:
| Convention | Where | Distinguishing feature |
|---|---|---|
| AAPCS64-Apple | macOS, iOS arm64 | X18 platform-reserved; ObjC variadic calls bypass registers |
| AAPCS64-SysV | Linux/BSD arm64 | X18 caller-saved; no ObjC promotion path |
| SysVAmd64 | macOS, Linux x86-64 | Standard System V |
| MS_X64 | Windows x86-64 | Microsoft x64 ABI |
| GoRegabi | Go binaries (any arch) | Go's register-based sequence |
REASON — Analysis and resolution
Analysis passes
Multiple SSA-form passes run in three families.
Value tracking. Constant propagation tracks literal values forward through arithmetic. Copy propagation collapses chains of register-to-register moves. Common-subexpression elimination removes redundant computations. Type propagation infers Rust-shaped types from compare instructions, mask widths, signed-versus-unsigned operations, and pointer arithmetic. Non-zero bit-mask propagation tracks which bits of each value can possibly be set, allowing later passes to narrow types from u64 down to u8 / u16 / u32 based on AND-mask evidence.
Runtime-noise removal. ARC lowering recognises Apple's automatic-reference-counting runtime calls — objc_retain, objc_release, objc_storeStrong, and the rest — and lowers them to semantic operations, removing thirty to fifty percent of the call-graph noise typical Objective-C produces. Dead code elimination removes unreachable and unused work. Peephole rules apply roughly a dozen ARM64 idiom rewrites driven by a function-pointer rule registry with fixpoint iteration.
Source structure recovery. Stack-slot recovery reconstructs the function's local variable layout. Stack-string detection identifies compiler-generated stack-allocated string constants from sequences of consecutive byte stores into a single stack slot — the pattern a compiler emits when a literal cannot be placed in __cstring, often because it is being assembled or decoded at runtime. ConstSeq recovers literal sequences — printable ASCII strings, byte arrays, GUIDs, short structs — that the compiler split across several byte stores into a single source-level literal. Var-merge cover unifies SSA values that share storage origin, using interval-based merging instead of union-find so cross-origin merges cannot leak.
A naming pass passes through up to three sources of human-meaningful names — argument-taint analysis, Objective-C ivar layouts, and DWARF debug information when present — and applies them at print time without modifying the IL itself, so the diagnostic stage views (asm, il, cfg, ssa, pseudo) stay raw.
Interprocedural argument access
A separate slice closes the call-boundary recall gap. When a function passes a parameter into a callee that dereferences or frees it, the callee's recovered access pattern is reflected back into the caller's taint state. Three callee-aware rule IDs fire on shapes the intraprocedural lattice cannot see: null-deref.callee-dereferences (CWE-476), use-after-free.callee-uses (CWE-415/416), and taint.flow.callee-derefs.<source>. The propagation walks bottom-up over the call-graph SCC condensation with a fixpoint only on recursion; opt-in behind OPENBINARY_INTERPROC_ARG_ACCESS=1. Measured impact on the four-fixture corpus: curl 4/8/5, jq 16/3/4, airportd 0/0/1, ripgrep 0/0/0.
Bounded emulation
Constant propagation has limits. When a value depends on a memory load, a non-trivial loop, or a chain of arithmetic the compiler optimised into a non-obvious form, propagation alone cannot pin it. For these cases the engine has a bounded interpreter — a small PCode emulator that walks a slice of the IL with an operation budget of 4,096 ops and returns one of four results: resolved (the value was pinned), budget (the budget ran out), unknown-load (an unmodeled memory location was read), or diverged (a syscall, an external call, or floating-point hit a feature outside the integer-only model).
The emulator is opt-in per call site; nothing auto-runs during a scan. Three consumers probe it on constant-propagation misses:
- Jump-table lifter — resolves indirect-branch dispatcher tables, including the dispatchers in macOS XPC daemons that walk multi-tier
cmp/b.gtrange checks - Syscall pass — resolves direct
svcimmediates that did not survive the simpler constant-propagation pass - Dynload pass — resolves string arguments to
dlopen,dlsym,NSClassFromString,objc_getClass, andsel_registerName
The result: values the compiler buried come back out without execution and without dynamic instrumentation.
Call resolution
Every bl, blr, and call instruction is mapped to its real target. Calls hidden by the runtime are followed through.
On Apple binaries, Objective-C objc_msgSend stub trampolines from __objc_stubs are decoded so selectors like objc_msgSend$initWithMetricName:options: appear directly. arm64e __auth_stubs are resolved through GOT entries via chained-fixup bind ordinals. Mach-O chained fixups are walked through bind ordinals into concrete symbol names. On Windows, the position-independent call qword ptr [rip+disp32] form resolves through the import address table to imported DLL function names without lifter changes. On Linux, PLT/GOT trampolines (including BTI-aware variants on arm64 and IBT-aware .plt.sec on x86-64) are walked, with R_*_JUMP_SLOT relocations binding each stub to its import name and .gnu.version resolving the library ordinal.
Tail calls (b to a function start) are distinguished from local branches. Compiler-inserted ARC runtime calls are filtered from the call graph by default. Every bl, blr, and unconditional b instruction in the disassembly view carries an inline comment naming the resolved target. Resolution rate on Apple binaries reaches roughly 98%.
The unified symbol table merges nlist symbols, imports, Objective-C stub trampolines, auth-stub targets, recognised library functions, and synthetic addresses for resolved syscalls into one space the rest of the pipeline queries.
Jump-table resolution
Compilers turn switch statements over dense integer keys into jump tables: a base address and an array of offsets, dispatched by an indirect branch. Until the table is resolved, an indirect branch is a dead end — the call graph stops, cross-references stop, audit walks stop. Apple's XPC dispatchers in system daemons are jump tables, so resolving them is what makes audits work on the binaries customers actually ship.
When the binary preserves the LC_DATA_IN_CODE load command (the Mach-O hint that tells you a region of bytes is data, not code), the engine reads the table base and entry size directly. Apple has stripped that load command from every late-stage shipped daemon — airportd, wifid, bluetoothd all ship with datasize 0 — so a hint-only resolver would resolve nothing on the binaries the tool is most often run against.
The fallback is to detect the canonical instruction idiom directly: cmp plus adrp+add plus ldrsw[… lsl #0x2] plus adr+add+br. Multi-tier range checks of up to three nested cmp+b.gt pairs (the form used in macOS daemon XPC dispatchers) collapse into a single switch structure. The dispatcher block's predecessor cmp/csel chain carries the bounds; the load-dispatch sequence carries the entry size; the emulator supplies the table base when constant propagation alone cannot. The resolved handler addresses become first-class call-graph edges — cross-references on a handler virtual address find its dispatcher, audit walks pass through dispatchers instead of dead-ending, and reconstruction emits the dispatcher as a Rust match expression with one arm per resolved handler.
Cross-references
The engine indexes every reference between code and data, both forward and reverse. ADRP+ADD/LDR data-access instruction pairs on arm64 are resolved to their target virtual addresses and recorded in two indexes: per-function (which data each function references) and per-data (which functions reference each data address). String constants resolve through these indexes and are inlined as comments on the disassembly view, so a load of 0x1000d2480 reads as ; "com.apple.wifi.set_channel" next to the instruction.
The same index powers query-by-needle. The --xref query accepts an imported symbol (dlopen), a virtual address (0x1000de200), a string literal ("com.apple.wifi.set_channel"), or an Objective-C class name (SFAuthorization), and lists every site that references it. Each site is tagged with its kind: bl-call, adrp-load, classref, strref, jump-table.
System calls
System calls are the points where a program asks the operating system for a privileged operation. Most syscalls go through the standard library, but some go directly via instructions like svc or syscall — the direct route is a common obfuscation choice for binaries that want to bypass library-level instrumentation.
The engine resolves both, with a separate path per OS because the syscall ABIs diverge.
| OS | Mechanism | Tables |
|---|---|---|
| Apple | svc #0x80 + x16 immediate | BSD + Mach trap (457 BSD + 67 Mach-trap entries; from XNU bsd/kern/syscalls.master + osfmk/kern/syscall_sw.c) |
| Linux x86_64 | syscall + rax | 385 entries (syscall_64.tbl) |
| Linux arm64 | svc #0 + x8 | 328 entries (asm-generic/unistd.h) |
| Linux ARM32 | svc 0 + r7 | 424 entries |
| Linux MIPS o32 | syscall + $v0 | 445 entries |
| Linux PowerPC | sc | 425 entries |
| FreeBSD / OpenBSD / NetBSD / DragonFly | per-family | 496 / 231 / 360 / 316 entries |
| Solaris/illumos | per-family | 246 entries |
| Windows | (usually Win32 imports; rarely raw syscall) | 143 NT SSDT + 59 versioned across 6 Win10/11 releases + 21 win32k.sys |
Resolved syscalls are placed in a synthetic address-space range so they appear as first-class edges in the call graph and in the cross-reference index alongside imports. A computed-syscall counter tracks the number of direct-syscall sites where the number could not be pinned — a spike in that counter is itself an obfuscation signal.
When the binary under analysis is libsystem_kernel.dylib, the engine statically carves the Mach-trap number table out of the SSA IL and writes it as JSON keyed by library and dyld-cache UUID, so downstream consumers can join trap numbers to names without running the kernel.
Resolved syscalls aren't just counted. They are partitioned into the twelve-axis behavioral aspect taxonomy (see Behavioral aspects below), so open and openat register on file_io, socket and connect on network, posix_spawn and execve on process_control, and so on.
Dynamic symbol resolution
Programs that load code by name at runtime — dlopen("/usr/lib/libobjc.A.dylib"), dlsym(h, "objc_msgSend"), NSClassFromString(@"NSXPCConnection"), objc_getClass("CLLocationManager"), sel_registerName("init") — are doing the dynamic-loader equivalent of an indirect call. Until the string argument is recovered, the call graph at that site is empty.
The engine tracks the string argument through data-flow analysis at each call site. Sites split into three buckets:
- Resolved — the string was pinned to a literal; the call site is annotated with the exact path, symbol, or class name
- Computed — the string was constructed from a computation the engine could trace but not constant-fold (a concatenation of two literals, a
sprintfwith a constant format and a variable) - Unresolved — the string came from somewhere the engine cannot reach
A binary with a high computed count is almost always doing string obfuscation. The count appears on the DNA fingerprint as a first-class field.
Behavioral signatures
A behavioral signature is one step beyond "this binary calls open": it is the resolved arguments at a specific call site. Opens a path under /Users/*/Library/Keychains/login.keychain-db. Execs /usr/bin/osascript. Connects to a Unix socket at /var/run/....
The engine produces these for a curated table of ~60 primitives by extending the dynload pass with per-argument schemas (Str, Int, Ignored). Coverage groups: filesystem (open / openat / open_nocancel / stat / lstat / fstatat / access / readlink / getattrlist / getattrlistat / chmod / fchmod / fchmodat / mkdir / mkdirat), process spawn and identity (execve, posix_spawn, the execl family, the setuid / seteuid / setgid / setegid family), dynamic loading (dlopen, dlsym), kernel-state queries (sysctlbyname), BSD networking (socket / connect / bind / listen / accept / getaddrinfo / gethostbyname / gethostbyname2 / res_query), Apple CFNetwork URL/HTTP/Host/Stream APIs, and the Mach-XPC surface (xpc_connection_*, xpc_dictionary_*, and the initWithMachServiceName: objc_msgSend stubs).
Two transforms make signatures stable across hosts. Path normalization rewrites /Users/<name>/ to /Users/*/ and /var/folders/<hash>/T/<f> to /var/folders/*/T/*, while leaving Apple-system paths (/System/Library/, /Library/Apple/) verbatim. Flag decoding turns numeric flags into named bit sets (OPEN_FLAGS, ACCESS_MODE, SOCKET_DOMAIN, SOCKET_TYPE).
Each signature carries a resolved field — full when every required argument resolved, partial when at least one. The product surface is search and similarity: ask the corpus for every binary that opens a path under /Users/*/Library/Keychains/ and the signatures answer.
IDENTIFY — Curated knowledge
The Apple type library
The Apple SDK is large, and most of what an Apple binary does is load fields out of Apple framework types. A memory access at offset 16 of an NSString is self.length. At offset 24 of an NSData, it is self.bytes. Without the type information, the same access reads as *(self + 16) — accurate but unreadable.
The engine ships with a curated database of 3,162 Apple framework classes and 18,633 fields drawn from Foundation, AppKit, CoreData, CoreFoundation, Security, QuartzCore, CoreLocation, and CoreText. Known struct types like CGRect, CGPoint, and NSRange resolve their offsets to named fields. Known classes resolve their ivar offsets to named fields. Known enums resolve integer constants to named variants. Loaded on first access, zero startup cost when not consulted.
The result is that disassembly views, the IL stage views, and the reconstructed Rust source all read in terms of the names a programmer would have written, not the offsets the compiler emitted.
Library function recognition
A real-world binary is mostly not the application's own code. It is OpenSSL, libcurl, sqlite, and several hundred other open-source libraries linked in. Without recognising them, every analysis spends most of its time reading library code, and reconstruction emits library internals instead of use imports.
The engine recognises library functions in three confidence tiers:
| Tier | Method | Coverage |
|---|---|---|
| 1 — Hand-built signatures | Strings + constants the function references plus its size range — high confidence on hit | 71 signatures (zlib, sqlite, OpenSSL, BoringSSL, libcurl, lz4, zstd, libpng, mbedTLS, xxHash, …) |
| 2 — Prologue hash index | Function prologue hashes against statically-linked open-source libraries | 87,828 hashes from 219 libraries (OpenSSL, FFmpeg, Boost, SDL2, zstd, lz4, libpng, harfbuzz, libuv, ZeroMQ, …); built-in 6.5 MB binary index |
| 3 — Apple kernel signatures | Byte-identical fingerprints mined from on-disk arm64e kernels | 34,219 fingerprints, 3,797 unique names across 11 Apple Silicon kernel variants — M1, M2, M3, M4, M5 base chips plus M1 Pro, M2 Pro, M3 Pro+Max, M4 Max, and the virtualization kernel |
A three-layer override (environment variable > user cache > bundled) means earlier sources win on hash collision. The user cache is extensible from three miners: a kernel-specific miner refreshes Tier-3 after a macOS upgrade; a general dyld-cache miner extracts every named function from any cache (a vendor SDK, an iOS firmware cache, an alternate macOS version); and a single-binary miner does the same for any standalone Mach-O dylib, daemon, or app. Each miner writes a JSON signature map of (hash, name, image, size) rows that append-merge with existing entries.
Two FLIRT (Fast Library Identification and Recognition Technology) corpora supplement the tiers above. A vendored Mandiant FLARE pack covers PE / x86–x64 MSVC runtime libraries (Apache-2.0, ~1.2M parsed entries — the Windows arm of library recognition; lazy-loaded, fires only on PE binaries). An openbinary-generated .pat.gz corpus covers ~27 statically-linked libraries on macOS arm64 (libcrypto, libsodium, libonig, libpcre2, libnghttp2, libzstd, libopus, libwebpdecoder, libevent, libcares, libcjson, libmpdec, libarm64decode, …) plus an ELF MIPS entry; each (format, arch, libname) blob decompresses on first use and parses once per process.
The match set is also rolled up into a build-provenance record — a consensus compiler (Apple clang, MinGW, MSVC, GCC), a consensus optimisation level, and an agreement ratio — plus a statically-linked-library inventory with per-library name, version, and matched-function count. Same data feeds the SBOM/CVE layer (see CVEs & SBOM).
Runtime metadata
Every binary publishes some amount of metadata that the runtime itself uses. The engine reads it.
| Language | Sections / data | What comes out |
|---|---|---|
| Objective-C | __objc_classlist | Classes, instance + class methods, ivars, protocol conformances. Type encoding strings (v24@0:8@16) decoded into readable signatures. Both arm64e relative method-list layout and legacy absolute pointer layout. dyld-shared-cache images additionally consume Apple's libobjc optimization tables for selectors and class names the per-image path can't reach |
| Swift | __swift5_types, __swift5_fieldmd, __swift5_proto, __swift5_protos | Type descriptors via relative i32 pointer resolution. Field metadata: names, types, mutability, indirect enum cases. Protocol descriptors and conformance records with retroactive/synthesized flags. Pure-Rust demangling. Module names inferred from prefixes |
| C++ | __const (vtables), __data_const (RTTI typeinfo) | Itanium ABI vtable parsing. Typeinfo classified as leaf, single-inheritance, or multiple-inheritance. Full class hierarchy reconstructed. Pure-virtual slot detection. cpp_demangle-driven symbol names. Vtable function pointers merge into the global symbol table |
| Go | GoBuildInfo, pclntab, runtime type descriptors, itab records | Go version, module path, build settings. Function names + boundaries + source-line mappings even when stripped. Bounded RTTI walk enumerates concrete types with fields. itab parsing resolves interface dispatch sites to concrete type-method pairs. Module hash to DNA for supply-chain comparison |
Each binary is classified as Objective-C, Swift, C++, Rust, Go, Mixed, or Unknown based on which metadata sections are present and which symbol patterns appear. The runtime kind is a first-class field on the analysis and on the DNA, so similarity searches can scope to a single runtime.
DWARF debug information
When DWARF debug information is present, the engine reads it. On Mach-O that means embedded __DWARF sections or an external .dSYM bundle whose UUID matches the binary. On ELF that means inline .debug_info / .debug_abbrev / .debug_str / .debug_line sections, or a separate .debug file resolved through two independent channels: a .gnu_debuglink chain walking <binary_dir>/<filename>, <binary_dir>/.debug/<filename>, and /usr/lib/debug/<binary_dir>/<filename> with CRC32 validation against the link record, and a build-id-keyed lookup at /usr/lib/debug/.build-id/<XX>/<YYY...>.debug (independent of .gnu_debuglink; the build-id digest is its own integrity check). Mismatched sidecars are refused. Function signatures come back with named parameters and typed returns. Struct field names, types, and sizes come back. Enum variant names come back. Source-file paths from the line-info table come back. DWARF 2/3/4/5 are all supported.
The connection to reconstruction is direct. The variable-rename pass uses DWARF locals as one of three rename sources, alongside argument-taint analysis and Objective-C ivar layouts; the precedence is argument > ivar > DWARF > mechanical, so DWARF supplements the others rather than overwriting them. A mismatched dSYM or a CRC-mismatched .gnu_debuglink sidecar is silently ignored. On debuggable binaries, the reconstructed Rust reads close to source.
Function rarity scoring
Once library code is filtered out, the remaining functions are the application's own. Most of those are unsurprising — the same scaffolding every binary of its kind contains. A small number are unusual: a function whose shape does not look like the shape of functions in similar binaries.
The engine scores every function by Mahalanobis distance against a reference distribution of centroids. Distance is computed per binary type — daemons, apps, extensions, kernel binaries — so the scoring is comparable within a class of binaries. The reference distribution ships embedded inside the engine as a memory-mapped blob. There is no setup, no cluster fit, no model serving.
The output is an ordering: the most-unusual functions surface first. This is the engine's only built-in ML — heavier classifiers and learned embeddings live on the consumer side, fed off the DNA fingerprint and a per-function feature vector (41 dimensions per function — 34 numeric, log-transformed, plus 7 boolean) that ships on every analysis.
JUDGE — Cross-cutting views
Capabilities
A capability is what a binary can do to the system. The engine maps capabilities into platform-specific domains by combining entitlement evidence, framework evidence, and resolved syscall evidence.
| Platform | Domains | Evidence streams |
|---|---|---|
| Apple | 10 — Location, Keychain, Network, Storage, Hardware, IPC, Process, Analytics, Security, System | Entitlements from LC_CODE_SIGNATURE plist (~41 mappings — 34 specific keys + 7 prefix patterns), linked frameworks (~44 mappings), syscalls + libSystem wrappers via the aspect taxonomy |
| Linux | 10 — Network, Process, Filesystem, Memory, IPC, Crypto, DynamicCode, Time, SystemInfo, Threading | Curated libc table (~321 symbols), 7 per-arch syscall tables (x86_64 / x86_32 / arm64 / arm32 / mips32 / mips64 / ppc), TLS-base / fs-segment intrinsic detection |
| Windows | 5 — ProcessControl, Network, Crypto, HostInteraction, AntiAnalysis | Imported DLL classification (kernel32 / ws2_32 / bcrypt / crypt32 / ole32); TLS-callback presence; CFG / CFW / RF / EH-continuation entries |
Unmapped entitlements are reported, never silently dropped. Each domain hit carries the raw evidence source — the entitlement string, the framework name, or the syscall — and an optional sub-capability tag. Every hit is a fact, with severity left to whoever is reading the facts.
Code signature and identity
On Apple binaries, the engine parses the CodeDirectory blob inside LC_CODE_SIGNATURE for the CDHash (a SHA-256 of the binary's signed structure), the team identifier, the platform identifier, and the signing flags. Flag bits decode into human-readable names — hardened runtime, library validation, restrict, and the rest. Each binary is classified as Apple-signed, developer-signed, ad-hoc, or unsigned.
On Windows binaries, the engine parses the Authenticode PKCS#7 SignedData blob in the WIN_CERTIFICATE attribute-certificate directory. The first signer's Common Name comes out. The SignerInfo digest OID resolves to sha1, sha256, sha384, or sha512. A SHA-256 over the full signature blob fingerprints the certificate-directory payload — the PE counterpart to Mach-O's CDHash. The PKCS#9 signingTime signed attribute decodes from UTC GeneralizedTime or UTCTime. Malformed-but-present signatures still surface whatever fields could be recovered; absent certificate directories return "unsigned".
PE binaries carry an embedded application manifest as a RT_MANIFEST resource — the Win32 equivalent of an Apple entitlements plist. The engine surfaces it as a WindowsEntitlements record with the requested execution level (asInvoker, highestAvailable, requireAdministrator — the UAC elevation policy), the UI-access flag, and the DPI-awareness declaration.
On Linux binaries, the cross-OS unique-binary identifier is NT_GNU_BUILD_ID — a 20-byte SHA-1 (the default) or 16-byte MD5 written by the linker into a .note.gnu.build-id section and the corresponding PT_NOTE segment. ELF counterpart to Mach-O's LC_UUID and PE's PDB GUID — one symmetric identifier surface that downstream consumers can join across.
Build hardening
A binary's hardening posture is the set of protections it was compiled with — the same surface that tools like checksec, BinSkim, and jtool2-Mitigations report. The engine emits hardening as part of every analysis, not behind a separate subcommand, so two builds of the same source with one mitigation flipped show the difference in DNA and in diff automatically.
| Platform | Findings | Feature keys |
|---|---|---|
| Apple | 19 (PIE, RWX segment, allow-stack-exec, no-heap-NX, text-writable, stack canaries, FORTIFY, signing posture [unsigned + ad-hoc], hardened runtime, library validation, restrict, FairPlay encryption, plus 6 dangerous-entitlement findings [get-task-allow, disable-library-validation, allow-unsigned-mem, disable-page-protection, allow-dyld-env, allow-jit]) | 14 (pie, nx_heap, stack_canaries, fortify, rwx_segment, data_const, hardened_runtime, library_validation, restrict, pac, min_os_modern, fairplay_encrypted, signed, adhoc_signed) |
| Windows | 9 (no-ASLR, no-high-entropy-VA, no-NX, no-CFG, no-CET-shadow-stack, no-force-integrity, no-/GS, unsigned, signature-invalid) | 9 (aslr, high_entropy_va, nx_compat, cfg, cet_compat, force_integrity, gs_cookie, safeseh, authenticode_signed) |
| Linux | — | 9 (elf_pie, elf_nx_stack, elf_relro, elf_fortify, elf_stack_canaries, elf_cet_ibt, elf_cet_shadow_stack, elf_arm64_bti, elf_arm64_pac_returns) |
The Linux classifier reads architecture-specific control-flow-integrity bits out of PT_GNU_PROPERTY. When the toolchain emits the property record only as a .note.gnu.property section without a corresponding program header — the shape checksec and pwntools miss — the engine falls back to a section walk and surfaces the protections regardless.
Each finding carries a per-finding severity (Info, Low, Medium, High, Critical) inheriting industry-tool conventions. The engine assigns no global hardening score — cross-format aggregation is dishonest and policy belongs in the consumer. A hardening hash and an on/off feature summary contribute to the DNA; the binary diff carries hardening-regression and hardening-improvement sections so mitigation flips across versions surface as facts.
Surface consistency
A binary declares what it needs through entitlements, application manifests, and code signatures. It also actually does things — resolves imports, calls syscalls, follows dynamic loads, exposes XPC services. The two should match. When they do not, the divergence is a fact worth surfacing.
Six divergence kinds:
| Kind | Meaning |
|---|---|
DeclaredUnused | A capability is declared but never exercised |
UsedUndeclared | A capability is exercised without being declared |
ViaDynload | A capability is reached only through dlopen or dlsym and would be hidden from any consumer that only reads static linkage |
PrivateEntitlementNonApple | An Apple-private entitlement (com.apple.private.*) is held by a binary that is not Apple-signed |
AdhocPrivileged | An ad-hoc-signed binary holds privileged entitlements |
BundleIdImpersonation | The bundle identifier is constructed to impersonate a trusted identity |
Each divergence carries the MITRE ATT&CK technique IDs the consumer can pivot off. The output is a list of facts — each divergence is a record, not a verdict.
The BundleIdImpersonation and PrivateEntitlementNonApple rules consult a curated Apple team-ID allowlist. The allowlist holds three classes of entry: legacy display strings (Apple Inc., Software Signing — the identity forms codesign -dv printed before team IDs existed), public ten-character Apple team IDs observed on shipped Apple binaries (each row cites the system binary it was observed on, so the provenance survives audit), and short prefix sentinels Apple uses internally.
Behavioral aspects
The behavioral aspect taxonomy is the engine's canonical way of describing what a binary does, partitioning its activity along twelve axes: file_io, network, process_control, ipc, memory, crypto, time, signals, threading, dynamic_code, system_info, hardware.
Three evidence streams contribute to each aspect: direct system calls from the syscall pass, libSystem (or libc) wrapper calls from the wrapper-detection pass, and framework or Win32 API calls from the call graph. An aspect's evidence count reflects the full surface, not just raw syscalls.
The mapping from operations to aspects is a curated table per platform. On Apple, open / openat / read / write / stat / unlink feed file_io; socket / connect / bind / sendto plus NSURLSession and CFNetwork feed network; posix_spawn / execve / fork / kill feed process_control; mach_msg, XPC bootstrap APIs, and Unix-domain-socket primitives feed ipc; mmap / mprotect / madvise and allocator calls feed memory. Linux and Windows have parallel mappings into the same twelve axes.
Each aspect contributes to the DNA: an aspect hash slot, a vector dimension, a Jaccard set, and a similarity weight. The older 7-bit action profile (opens_files, opens_network, spawns_processes, loads_code_dynamically, sends_mach_messages, has_direct_svc, has_computed_svc) is retained as a derived view so existing consumers do not break.
A small summary accompanies the per-aspect detail: a taxonomy version (cache-invalidation key for consumers when the engine ships a new axis), a dominant aspect (whichever axis carries the most evidence), and a hardening ratio reporting the share of behavioral evidence that came from direct svc sites rather than wrappers — a high ratio is the marker of a binary that talks to the kernel directly.
Network surface
The engine extracts every URL, IPv4 address, IPv6 address, and hostname from the binary's strings using regex with false-positive rejection. Endpoints that contain format specifiers (%@, %s, %d) are flagged as runtime-constructed.
Networking APIs are detected against 23 known Objective-C and Swift networking classes and 10 networking selector patterns. The call graph cross-references each networking-API call to the application method that invokes it, so the report shows not just what networking APIs are used but which application methods invoke them. Network entitlements and linked networking frameworks are correlated against the API calls so the surface is internally consistent.
XPC service identifiers (com.apple.*) are extracted from embedded strings, and XPC protocol traits are reconstructed from the discovered service interfaces.
Strings and telemetry
Printable ASCII runs of four or more characters are extracted across the entire binary, plus structured CFString constants from the __cfstring section (with chained-fixup pointer resolution).
Strings are categorised into telemetry endpoints, URLs and endpoints, file paths, bundle IDs, and IOKit constants — so a report can answer what does this binary collect and what does this binary access without reading the call graph. ADRP+LDR/ADD instruction pairs that load string constants are annotated inline on the disassembly view, so a load reads as ; "com.apple.wifi.set_channel" next to the instruction.
Telemetry strings are highlighted in the report, because they answer the most-asked customer question: what does this binary phone home about? On airportd, the surface comes out as 120 telemetry strings against 23 submission methods.
Packing, entropy, embedded payloads
Packed and obfuscated binaries leave traces in their entropy distribution. The engine reports per-section Shannon entropy on every Mach-O section. A heuristic fires when imports are unusually sparse and text-segment entropy is high — the canonical pattern for a compressed or encrypted executable. Overlay detection reports bytes present past the end of the Mach-O load commands, the classic appended-payload indicator.
Embedded format identification scans the overlay and high-entropy sections for 27 carrier formats with two confidence tiers:
- Compression — gzip, zstd, xz, bzip2, lzfse, lz4
- Containers — tar, zip, xar, 7-zip, cab, ar, AppleArchive
- Filesystems — squashfs, JFFS2, CramFS, UBI, UBIFS, ROMFS
- Firmware — U-Boot images, UImage, TRX, Seama, Android
boot.img, Android sparse, UEFI FFS, DTB
Each match carries the section name, the offset, and the confidence tier. Embedded matches attach to the same records the entropy surface produces, so a consumer reading the entropy block gets the format facts in the same place.
Secrets discovery
A binary can carry credentials and cryptographic material it should not. The engine scans for eleven kinds: PEM private keys, PEM public keys, PEM certificates, OpenSSH private keys, X.509 DER certificates, JWTs, AWS access keys (AKIA / ASIA), GitHub personal access tokens (ghp_ / gho_ / ghs_ / ghr_ / ghu_), Slack tokens (xoxb- / xoxp- / xoxa- / xoxs-), GCP service-account markers, and crypt(3) password hashes ($1$, $2[abxy]$, $5$, $6$, $y$, $argon2*$).
Four scan sites: the overlay (bytes past the end of the load commands), high-entropy sections, __TEXT.__cstring, and __TEXT.__objc_methname. Two confidence tiers — MagicOnly and HeaderValid — distinguish a magic-byte match from a structurally validated finding. X.509 DER is walked with a bounded ASN.1 iterator capped at depth 8 and 1 KB, iterative, no recursion. Strict-alphabet validators on API tokens emit HeaderValid or nothing — the false-positive rate of magic-only AKIA matches in random __cstring data is too high to admit the lower tier.
Every finding is redacted by default — middle-elision for tokens, header-only for PEM, sixteen-byte hex prefix for X.509 DER. The offset and the location are the load-bearing facts; the matched text travels through artifacts (CI logs, GitHub issues, downstream indexes) only in redacted form. Findings carry MITRE ATT&CK tags — T1552.001 (Credentials In Files), T1552.004 (Private Keys), T1552.005 (Cloud Instance Metadata API). A hard cap of 256 findings per binary prevents pathological scans.
IOKit attack surface
For Apple kernel collections specifically, the engine maps the IOKit driver attack surface. IOUserClient subclasses are extracted from the C++ RTTI hierarchy — those are the kernel objects user-space processes can open and call. Entitlement string references inside driver code identify the access-control requirements gating each driver. The result is a map from entitlements to drivers and from drivers to user-callable operations.
This is the surface a kernel security review walks first. A single query answers which drivers does this entitlement gate and which entitlements does this driver require.
The audit matrix
A privileged operation reachable from an untrusted entry point without an authorization check on every path is the canonical authorization-gap bug. The audit matrix is the engine's static-analysis answer to which of those exist in this binary.
The matrix walks every entry point — XPC entry points especially — forward through the call graph, including jump-table edges, and records two things per reachable function. First, which authorization verifiers it calls: __verifyEntitlement:, __verifyRequiredEntitlement:, __verifyEntitlementForEventType:, didUserConfirmRequestType:auditToken:, didUserConfirmRequestType:timestamp:, plus Authorization Services entry points like AuthorizationCopyRights. Second, which privileged operations it can reach: keychain writes, NVRAM writes, SFAuthorization setters, wifi channel and association changes, and similar high-impact APIs.
A row is flagged with missing_gate: true when a privileged operation is reachable but no verifier dominates every path to it. The dominance check is a documented under-approximation of dynamic coverage — a false negative is possible when a verifier is conditionally skipped inside an intermediate function; a false positive is possible when a privileged call is guarded by a runtime predicate invisible to static analysis. The engine never claims "verifier coverage" as a safety guarantee.
Both verifier and privileged-call lists are curated tables, auditable in git, extensible per macOS release. There is no severity scoring. The matrix output is a list of facts; the consumer decides what is acceptable.
The four security surfaces
Findings (CWE), indicators (capability + ATT&CK), malware verdict, and CVE/SBOM matches are four parallel security read-outs produced from the same analysis. Each has its own taxonomy, its own confidence model, its own dedicated brief:
- Findings — 70+ CWE-tagged detector modules. What is wrong with the code?
- Indicators — 9 capa-aligned behavior categories mapped to 430 macOS-relevant MITRE ATT&CK techniques. What does this binary do?
- Malware — Four-tier classifier (Benign / Suspicious / LikelyMalicious / Malicious) composed over 19 semantic detectors plus aggregation rules. Is this binary malicious?
- CVEs & SBOM — 3-layer stack (path patterns / version banners / firmware-version lookup) for firmware audits. Does it contain a known-vulnerable version of a known component?
Security is the orientation map across the four.
EMIT — Outputs
The behavioral fingerprint (DNA)
The DNA is the engine's compact, comparable representation of a binary's behavior. It lets two binaries be compared at a glance, lets a fleet be searched for similarity, and lets behavioral drift across versions be detected without re-reading the underlying code.
Eleven structural hashes describe different facets of the binary, each content-addressed so identical content produces identical hashes regardless of analysis host or order:
| Hash | Over |
|---|---|
sha256_text | Instruction bytes only — a re-signed binary with identical code matches a previous build |
imphash | Imported-symbol set — the standard import-table fingerprint |
frameworkhash | Linked frameworks |
classhash | Objective-C and Swift class set |
symbolhash | Unified symbol table |
entitlementhash | Parsed entitlements |
cpp_classhash | C++ class hierarchy |
syscallhash | Resolved syscalls + libSystem wrappers |
aspecthash | Twelve-axis behavioral aspect profile |
indicator_hash | Indicator-rule-hit set |
tlsh | Trend Micro Locality-Sensitive Hash over full binary bytes — T1... fuzzy hash for cross-corpus lookup |
A numeric vector of more than 100 dimensions captures structural quantities — class count, method count, entitlement count, framework count, syscall counts per aspect, indicator counts per category, ATT&CK technique counts per tactic, embedded-format counts, hardening signal counts, telemetry density.
A set of more than 40 named sets captures categorical content — private frameworks, entitlements, XPC services, protocol conformances, syscall wrappers, ATT&CK techniques covered, indicator rules matched, embedded formats present, secret kinds present, hardening features on, hardening features off, components, and more.
Similarity is a weighted Jaccard sum over the named sets plus cosine similarity over the numeric vector. Weights are calibrated against measured signal: indicator overlap weights 6.0 (behavioral coincidence is a stronger signal than structural coincidence), ATT&CK technique coverage and consistency-flag overlap weight 5.0, structural sets weight lower. TLSH is deliberately not folded into in-corpus similarity — that's weighted Jaccard and cosine; TLSH is the cross-corpus identifier you paste into VirusTotal, Mandiant, or a public threat report.
Rendered for human reading, the DNA is a one-page bar chart that summarises capabilities, class and method surface, framework usage, telemetry density, syscall surface, action profile, consistency flags, aspect signatures, and indicator hits at a glance. Telemetry and analytics are highlighted in pink.
The DNA is structured as a feature vector by design. Every numeric and categorical field can be ingested directly by downstream classifiers and similarity indexes. See Platform for how the engine's vector is consumed downstream.
Reconstruction to Rust
The reconstruction pipeline takes the analyzed program and rewrites it as readable, compilable Rust.
The control flow graph is structured into a region tree: if/else, while, switch with one arm per resolved jump-table handler, break and continue instead of goto. The structuring pass is a "no more gotos" walk that turns the graph into a tree the emitter can render linearly. The emitter walks the region tree and produces Rust source.
Project assembly is one command. The output is a buildable Cargo project with one file per Objective-C, Swift, or C++ class; grouped C functions; framework stubs typed against the Apple SDK so the project compiles without an Apple SDK dependency; a Cargo.toml whose dependencies are inferred from recognised statically-linked libraries and frameworks; and a lib.rs. Class and method filtering lets the consumer reconstruct a single class or method instead of the entire binary.
The Rust output preserves real source structure. Objective-C class-reference loads emit as class!(NSData) macro calls. Objective-C BOOL property pairs reconstruct into Rust that compiles. Multi-way dispatchers reconstruct as match expressions with one arm per resolved handler, including the XPC request dispatchers in system daemons. XPC protocol interfaces reconstruct as Rust trait definitions — typed API contracts pulled out of the binary.
Type recovery is layered. Objective-C method signatures decode from the type encoding in __objc_classlist and apply at the function boundary. Swift method signatures decode from __swift5_types. DWARF parameter and local variable names apply when DWARF is present. Argument-taint analysis names parameters when no other source applies.
Parameter direction is recovered too. A pointer argument the function only reads from emits as &T; one the function writes through emits as &mut T; one the function never dereferences emits by value. The classification is driven by an intraprocedural walk of every load and store against the parameter slot, then propagated interprocedurally bottom-up over the call-graph SCC condensation so a caller that forwards an argument to a callee that stores through it ends up correctly typed &mut T even though the caller's body never touches the value. Across the four-fixture corpus (airportd / ripgrep / jq / curl), the inference changed 520 parameter signatures from raw *mut T to typed references (airportd 103, ripgrep 396, jq 20, curl 1). Parameters that are never read or dereferenced render as _argN. Objective-C self gets a propagation carve-out so the receiver is never promoted to &mut Self by parameter-access inference alone.
Every emitted method carries a confidence score from 0.0 to 1.0 and an unknown_count for residual unrecovered operations. A manifest.json records per-method confidence, unknown count, an irreducible-CFG flag, resolved-versus-unknown classref counts, and a compile_failure_reason enum when the project did not compile end-to-end. The full-binary path on a real-world binary like airportd reaches a 99.7% method compile rate.
A verify step runs cargo check against the reconstructed project, reports the result, computes class and method coverage relative to the original binary, and counts the residual // UNKNOWN markers so reconstruction quality can be measured over time.
Inspection
The inspection surface answers questions about a single class or method without re-running the analysis.
A class view shows ivars with decoded types, methods with their call targets, and the framework APIs the class touches. A method view shows the typed signature, the methods it calls, the methods that call it (cross-references), and the annotated disassembly with inline call-target names matching the call list. Fuzzy search lets a partial name match with suggestions when the query does not match exactly. Dot-notation queries (ClassName.selectorName) scope a method search to a specific class. A single query searches Objective-C classes, Swift types, and C++ classes simultaneously.
A pipeline-stage view dumps any internal stage of the analysis for any matched method:
| Stage | What it shows |
|---|---|
--show asm | Annotated arm64 |
--show il | Pre-SSA lift |
--show cfg | Edge list |
--show ssa | Full SSA decompile |
--show pseudo | Structured region tree with real condition expressions in if, while, and switch headers |
--show all | Everything plus a summary footer |
Stages can be combined (--show cfg,ssa) and serialized as JSON for tooling. This is what makes diagnosing a low-confidence reconstruction tractable — when manifest.json shows a method below threshold, the pipeline view exposes exactly which stage lost information.
Cross-binary outputs
Beyond the per-binary surface, the engine produces three cross-binary outputs.
diff — function-level matching across two binary versions in three phases. Phase one pairs functions by symbol name. Phase two pairs unnamed functions by instruction hash. Phase three pairs the rest by structural Jaccard over their call-target sets. Each pair is classified as size-only, body-changed, calls-changed, or fully-changed. New methods, removed methods, and changed methods come out as a list. A hardening-diff section reports mitigation regressions and improvements as facts. Use case: see what a macOS update added in telemetry, what changed in encryption behavior, what new XPC endpoints appeared.
compare — side-by-side DNA similarity between two binaries. Weighted Jaccard runs across 40+ content sets — private frameworks, entitlements, API calls, XPC services, protocol conformances, syscall wrappers, ATT&CK technique coverage, indicator rule hits, hardening features, embedded formats, components. Indicator rule hits carry the highest weight (6.0); ATT&CK coverage and consistency flags carry 5.0; structural sets carry less. Concrete: nc returns iperf3 (60.6%), traceroute (57.8%), rarpd (56.3%) as nearest neighbours — three independent networking tools cluster together by behavior alone. Use cases: find related binaries, attribute malware families, track behavioral drift across versions of the same service.
Corpus aggregates — measure-aspects, measure-constseq, measure-overrides-phase1, and measure-overrides-phase2 walk a directory of binaries and emit JSON reports of population counts and pairwise similarity stats. They double as engineering-quality gates: a regression in aspect signal, literal recovery, or override-populator coverage fails the build before merge.
Where it sits
The engine is the part that produces truth about a binary. It does not present that truth, rank it, alert on it, or assign meaning to it.
Platform is the surface that does. It indexes the engine's facts into a programmable corpus, computes similarity over the DNA fingerprint, runs full-text search and a compositional query language across 50+ indexed fields, ranks results, presents them in a UI, and adds the policy and product layer that turns facts into something an analyst, a compliance team, or an enterprise SOC can act on.
Other surfaces can sit on the same engine — partner products, internal tools, regulator-facing reports — without re-implementing the parser, the lifter, or the curated knowledge. The fence is deliberate. Anything dependent on judgment lives in the surface. Anything dependent on truth lives in the engine.