Chris Collins

Tools · systems

Binary inspector: Mach-O, ELF and PE

Drop an executable or library — Mach-O (thin or universal), ELF or Windows PE — and see its load commands or headers, libraries and run paths, symbols demangled, fixups, the code signature with entitlements and requirements, a checksec-style hardening list, Go build info, Rust crates, strings, a hex view of any section and a size treemap.

Added

Works with

How to use it

The questions I ask of a binary are always the same: what is it built for, what does it link against, is it signed and how, what’s in it, and why is it so big. On a Mac that’s otool -l -L, codesign -dvvv, vtool and dyld_info; on Linux readelf, objdump and checksec; for Go go version -m. This page answers all of them for one file at once, in the browser. It opens on a small universal C program; the Examples menu has Go, Rust, C++ and Windows ones I built for it.

How to use it

Drop a file on the box, click it to pick one, or drop an .app folder (or use Open .app folder…): the main executable is opened, and the bundle’s Info.plist and stapled ticket are checked. Gzipped files are unpacked first. For a universal binary, pick the architecture at the top; every tab follows it.

  • Overview — format, CPU, platform and minimum OS (from LC_BUILD_VERSION), UUID or build ID, entry point, the language it was written in (from what the other tabs find), and a card per topic below.

  • Load commands (Mach-O) — every command with what it says, and the header flags explained (PIE, TWOLEVEL, NO_HEAP_EXECUTION…). Headers (ELF, PE) — program headers, dynamic section, notes (build ID, ABI tag, GNU properties), GNU version needs — with the newest glibc version it needs — or the PE data directories and exports.

  • Sections — per segment, with addresses, sizes and types. Click one to see its bytes.

  • Libraries — LC_LOAD_DYLIB, weak, re-exported and upward dylibs with their compatibility and current versions, LC_RPATH entries and where @rpath/ names would be looked for; or ELF’s NEEDED, RPATH, RUNPATH, SONAME and interpreter; or the DLLs a PE imports and from each, which functions.

  • Symbols — exported, imported and local symbols with the library each import comes from (two-level namespace on macOS, GNU symbol versions on Linux), searchable by text or /regex/, with C and Rust names demangled. C follows LLVM’s c++filt (>> rather than GNU’s > >); Rust handles both the legacy _ZN…17h…E scheme and v0 (_R…) as rustfilt prints it.

  • Fixups (Mach-O) — chained fixups (macOS 12 and later): each segment’s pointer format and how many binds and rebases it holds, the import table, and every bind with its target, or the classic LC_DYLD_INFO bind opcodes for older binaries. The same list dyld_info -fixups prints.

  • Signature (Mach-O) — who signed it (ad hoc, Developer ID, Apple, development, App Store) and what that means; a checklist of what notarization needs (Developer ID, hardened runtime, secure timestamp, no get-task-allow); the CodeDirectory as codesign -dvvv prints it, with the cdhash; the flags explained; entitlements from both the XML and the DER blob (and a warning if they differ); the requirements decompiled to text (designated ⇒ identifier "…" and anchor apple generic and …); the certificate chain from the CMS signature; and Verify page hashes, which hashes every page of the code and compares it with the signature.

  • Hardening (ELF, PE) — the checks checksec makes: RELRO, stack canary, NX, PIE, FORTIFY_SOURCE (which calls were fortified and which could have been), RPATH and RUNPATH, plus Intel CET (IBT, shadow stack) or Arm BTI and PAC from the GNU property note, text relocations and stripping. For PE: ASLR, high-entropy ASLR, DEP, Control Flow Guard and whether it carries an Authenticode signature.

  • Go — the build info every Go binary since 1.18 carries: Go version, main package and module, every dependency module with its version, go.sum hash and replacement, and the build settings (-trimpath, CGO_ENABLED, GOARCH, -ldflags, vcs.revision, vcs.modified), also as the exact text go version -m prints.

  • Rust — the compiler version from .comment (ELF) or the commit in /rustc/<hash>/ paths; the crate list from cargo-auditable’s .dep-v0 section if the binary was built with cargo auditable build; and, even in a stripped binary, every crate and version that appears in a panic location’s registry path.

  • Strings — printable strings by section, with a minimum length, a text or regex filter, and quick filters for paths, URLs, format strings, versions and environment-style names.

  • Hex — any section or the whole file, a page at a time, jump to an offset.

  • Sizes — a treemap of the file by segment and section, with __LINKEDIT split into its symbol table, strings, fixups, exports and code signature, and a table of the largest parts.

Open in dependency tree ↗ on the Go and Rust cards sends the modules or crates to the dependency tree analyser: Go modules as a go.mod and go.sum, Rust crates as a Cargo.lock (with the dependency edges when cargo-auditable recorded them), where you can check them against OSV.dev for known vulnerabilities.

For Java bytecode use the class file and JAR inspector; for how a struct is laid out in memory, memory layout; for protobuf, MessagePack and other data formats, binary formats.

Notarization and stapled tickets

Notarization happens at Apple, so a page can only tell you whether a binary is ready for it and whether a ticket is stapled. A ticket can be stapled to an .app, .dmg or .pkg, never to a bare Mach-O file: for a lone command-line tool Gatekeeper looks the ticket up online the first time it runs. Open the .app folder to see whether Contents/CodeResources holds a stapled ticket.

How I checked it

I compiled small programs for the examples — C as a universal arm64 and x86_64 binary, ad-hoc signed with entitlements; Go 1.23 and Rust 1.99 (with cargo-auditable) for macOS and Linux; C and C for Linux with and without hardening; a shared library; and a Windows program with MinGW — and the tests compare this page with what the real tools say about them: `otool -l -L`, `vtool`, `dyld_info -fixups -exports`, `codesign -dvvv --entitlements - -r-`, `csreq -t`, `go version -m`, `rust-audit-info`, `readelf`, `nm -DC`, `checksec` and `objdump -p` (the Linux ones in a Debian container). I also checked the signature parsing against signed apps on my Mac (Chrome, Slack and Rancher Desktop with Developer ID or App Store signatures and secure timestamps, and Apple's own `/bin/ls`), and the demangler against `cfilt` over 160,000 symbols from LLVM’s libraries (97–99% identical; the rest are rare template-expression forms) and against rustfilt.

The Go examples have their machine code and read-only data zeroed to keep them small, so Verify page hashes reports changed pages on the macOS one. That’s the check working.

Not covered: Swift metadata beyond class names, DWARF debug info, PE resources and Authenticode certificate details, static libraries (.a), and kernel or firmware file sets.

Browser support

Any current browser. Gzipped files are unpacked with DecompressionStream, and cdhashes and page hashes use the Web Crypto API (both in Chrome, Edge, Firefox and Safari 16.4+). Files over 64 MB are read a megabyte at a time with Blob.slice, so only the parts a view needs are loaded. Dropping an .app folder works in Chrome, Edge, Firefox and Safari; Open .app folder… uses the directory picker.

Privacy

Everything happens in this page: files and folders you open are read by your browser and never uploaded. Open in dependency tree writes the module list into this browser’s local storage for that page to read; the dependency tree page only contacts OSV.dev if you press its vulnerability button. The share link carries the chosen example and tab, not your file.

Runs entirely in your browser; nothing you type leaves this page.