15 KiB
Binary Formats: PE, ELF, and Mach-O
Reference for identifying and interpreting PE, ELF, and Mach-O binary format
characteristics. Load this when the binary metadata output shows an
unfamiliar format, when you need to interpret section flags or entry point
conventions, or when format-specific quirks affect your analysis.
Format Detection
The CLI detects the format during import and reports it in binary metadata:
binary metadata --project <proj> --json
# data.format: "PE", "ELF", "Mach-O", or "RAW"
The data.architecture and data.endianness fields provide additional context
for interpreting format-specific structures.
Detection Heuristics
| Magic Bytes | Format | Notes |
|---|---|---|
MZ (0x4D 0x5A) |
PE (Portable Executable) | DOS stub followed by PE signature at offset from 0x3C |
\x7fELF (0x7F 0x45 0x4C 0x46) |
ELF (Executable and Linkable Format) | Linux, BSD, Solaris, embedded systems |
\xFE\xED\xFA\xCE or \xFE\xED\xFA\xCF |
Mach-O (32-bit) | macOS, iOS — big-endian and little-endian variants |
\xCA\xFE\xBA\xBE or \xCF\xFA\xED\xFE |
Mach-O (64-bit / Universal) | macOS, iOS — fat/universal binary or 64-bit |
| None of the above | RAW | Unrecognized — may be firmware, packed, or a non-standard format |
Quick Format Identification
Before importing, you can use standard tools to identify the format:
file /path/to/binary
# PE32+ executable (GUI) x86-64, for MS Windows
# ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux)
# Mach-O 64-bit executable x86_64
PE (Portable Executable)
Used on Windows. PEs have a DOS header, a PE signature, a COFF file header, an optional header (not optional for executables), section headers, and data directories.
Key Characteristics
| Field | Location | Meaning |
|---|---|---|
| Machine | COFF header | Target architecture: 0x014C (x86), 0x8664 (x64), 0xAA64 (ARM64) |
| NumberOfSections | COFF header | Count of section headers |
| TimeDateStamp | COFF header | Compile timestamp (may be zeroed by some compilers) |
| AddressOfEntryPoint | Optional header | RVA of the first instruction executed |
| ImageBase | Optional header | Preferred load address (typically 0x400000 for x86, 0x140000000 for x64) |
| Subsystem | Optional header | 2 (GUI), 3 (Console), 1 (Native/driver) |
| DllCharacteristics | Optional header | Bitfield: DYNAMIC_BASE (ASLR), NX_COMPAT (DEP), NO_SEH, etc. |
PE Sections
Common PE sections and what they contain:
| Section | Typical Contents | Flags |
|---|---|---|
.text |
Executable code | rx |
.rdata |
Read-only data (imports, exports, debug) | r |
.data |
Initialized read-write data | rw |
.bss |
Uninitialized data (zero-filled at load) | rw |
.rsrc |
Resources (icons, dialogs, strings, manifests) | r |
.reloc |
Base relocations (for ASLR) | r |
.pdata |
Exception handling data (x64/ARM64) | r |
.tls |
Thread-local storage | See flags |
Red flags in section analysis:
- Missing
.textsection: The binary may be packed; the real code is in a different section or unpacked at runtime. - Writable and executable section (flags
rwx): Uncommon in normal binaries. Common in packed or self-modifying code. Flag this in triage. - High entropy in any section (> 7.5): Indicates compression, encryption, or packing. See packed-and-obfuscated.md.
- Unusually named sections: Names like
.upx0,.upx1indicate a specific packer. Names like.xxxor random strings may indicate a custom packer/protector.
PE Data Directories
The optional header contains data directories pointing to important tables:
| Directory | Index | What It Points To |
|---|---|---|
| Export Table | 0 | Exported functions (DLLs) |
| Import Table | 1 | Imported functions |
| Resource Table | 2 | Embedded resources |
| Exception Table | 3 | Exception handlers (x64) |
| Certificate Table | 4 | Authenticode certificate |
| Base Relocation Table | 5 | Relocations for ASLR |
| Debug | 6 | Debug information |
| TLS Directory | 9 | Thread-local storage callbacks |
| Load Config | 10 | Load configuration (security settings) |
| IAT | 12 | Import Address Table |
| Delay Import | 13 | Delay-loaded imports |
Red flags:
- TLS callbacks present: TLS callbacks execute before the entry point. Flag in triage — common in both legitimate initialization and anti-debugging.
- Large or missing import table: Large tables are normal for dependency-heavy software. Very small tables (< 5 imports) combined with high entropy suggest packing.
- Empty export table on non-DLL: Suspicious if the binary is an EXE. Some legitimate tools export functions for plugins, but it's worth noting.
PE Import Tracking
Use binary imports to examine the import table:
binary imports --project <proj> --json
Focus on:
- Resolution status:
UNRESOLVEDimports indicate missing dependencies or obfuscation (imports resolved at runtime via GetProcAddress). - Module grouping: Which system DLLs are imported (kernel32.dll, advapi32.dll, ws2_32.dll, wininet.dll, etc.) maps to capabilities.
- Suspicious APIs: Use
binary suspicious-apisfor automated risk scoring based on import patterns.
ELF (Executable and Linkable Format)
Used on Linux, BSD, Solaris, and many embedded systems. ELFs have a header, a program header table (runtime view), and a section header table (link-time view).
Key Characteristics
| Field | Location | Meaning |
|---|---|---|
| e_ident[EI_CLASS] | ELF header | 1 = 32-bit, 2 = 64-bit |
| e_ident[EI_DATA] | ELF header | 1 = little-endian, 2 = big-endian |
| e_type | ELF header | 2 = executable, 3 = shared object (.so), 4 = core dump |
| e_machine | ELF header | 0x03 (x86), 0x3E (x86-64), 0x28 (ARM), 0xB7 (AArch64) |
| e_entry | ELF header | Virtual address of the entry point |
| e_phoff | ELF header | Offset to program header table (runtime segments) |
| e_shoff | ELF header | Offset to section header table (link-time sections) |
ELF Segments vs Sections
ELFs have two overlapping views:
-
Segments (program headers): Runtime view. Define memory mappings. Types:
PT_LOAD(load into memory),PT_DYNAMIC(dynamic linking info),PT_INTERP(interpreter path),PT_NOTE(auxiliary info),PT_GNU_STACK(stack executability),PT_GNU_RELRO(read-only relocations). -
Sections (section headers): Link-time view. Types:
.text(code),.rodata(read-only data),.data(writable data),.bss(zeroed data),.plt(procedure linkage table),.got(global offset table),.init_array/.fini_array(constructor/destructor arrays).
binary sections reports the section view. For segment analysis, use
binary metadata and examine the binary's load layout contextually.
Common ELF Sections
| Section | Contents | Notes |
|---|---|---|
.text |
Executable code | |
.rodata |
Read-only data (strings, constants) | |
.data |
Initialized writable data | |
.bss |
Zero-initialized writable data | No file data; zero-filled at load |
.plt |
Procedure Linkage Table | Lazy-binding trampolines for dynamic linking |
.plt.got |
PLT entries resolved at load time | Full RELRO binaries |
.got |
Global Offset Table | Pointers to dynamically resolved symbols |
.init_array |
Initialization function pointers | Called before main — analogous to PE TLS callbacks |
.fini_array |
Termination function pointers | Called at exit |
.dynsym |
Dynamic symbol table | Symbols for dynamic linking |
.dynstr |
Dynamic string table | Symbol names |
.interp |
Interpreter path | Typically /lib64/ld-linux-x86-64.so.2 |
.note |
Vendor/OS notes | ABI tags, build IDs, Go build info |
.comment |
Compiler version info | Often identifies the toolchain |
Red flags:
- Writable and executable segment (PT_LOAD with PF_W|PF_X): Suspicious. Normal binaries have W^X separation.
.init_arrayentries: Like TLS callbacks on PE, these run beforemain. Flag in triage if entries point to unusual functions.- Missing section header table: Stripped binaries are common in production. The program header table still defines the runtime layout.
- Static binary (no
.interp, no.dynamic): Does not use the dynamic linker. Larger binary but harder to interpose at load time.
ELF Import/Export Tracking
ELF uses dynamic symbols (.dynsym) rather than separate import/export tables.
binary imports reports symbols with UND binding. binary exports reports
symbols with GLOBAL binding and FUNC or OBJECT type.
binary imports --project <proj> --json
binary exports --project <proj> --json
Mach-O
Used on macOS, iOS, watchOS, and tvOS. Mach-O files have a header, a sequence of load commands, and segments containing sections.
Key Characteristics
| Field | Location | Meaning |
|---|---|---|
| magic | Mach-O header | MH_MAGIC (32-bit), MH_MAGIC_64 (64-bit), FAT_MAGIC (universal) |
| cputype | Mach-O header | CPU_TYPE_X86, CPU_TYPE_X86_64, CPU_TYPE_ARM, CPU_TYPE_ARM64 |
| filetype | Mach-O header | MH_EXECUTE, MH_DYLIB, MH_BUNDLE, MH_OBJECT, MH_DYLINKER |
| ncmds | Mach-O header | Number of load commands |
| sizeofcmds | Mach-O header | Total size of load commands |
Universal (fat) binaries contain slices for multiple architectures. The CLI handles this transparently and reports the native slice.
Mach-O Load Commands
Load commands define the structure and runtime behavior:
| Command | Purpose |
|---|---|
LC_SEGMENT / LC_SEGMENT_64 |
Define a memory segment (sections within) |
LC_SYMTAB |
Symbol table location |
LC_DYSYMTAB |
Dynamic symbol table indices |
LC_LOAD_DYLIB |
Linked dynamic library |
LC_MAIN |
Entry point (replaces LC_UNIXTHREAD) |
LC_UUID |
Unique build identifier |
LC_VERSION_MIN_MACOSX / LC_VERSION_MIN_IPHONEOS |
Minimum deployment target |
LC_SOURCE_VERSION |
Build version string |
LC_CODE_SIGNATURE |
Code signature location |
LC_SEGMENT_SPLIT_INFO |
Sub-range code signing info |
LC_DYLIB_CODE_SIGN_DRS |
Designated requirement for library validation |
Common Mach-O Sections
| Section | Typical Contents |
|---|---|
__TEXT,__text |
Executable code |
__TEXT,__cstring |
C string constants |
__TEXT,__const |
Read-only constants |
__TEXT,__objc_methname |
Objective-C method names |
__TEXT,__objc_classname |
Objective-C class names |
__TEXT,__objc_methtype |
Objective-C method type signatures |
__DATA,__data |
Writable data |
__DATA,__bss |
Zero-initialized data |
__DATA,__la_symbol_ptr |
Lazy symbol pointers (PLT equivalent) |
__DATA,__objc_classlist |
Objective-C class list |
__DATA,__objc_catlist |
Objective-C category list |
__DATA,__mod_init_func |
Initialization function pointers (pre-main) |
__DATA,__mod_term_func |
Termination function pointers |
__LINKEDIT |
Symbol table, string table, code signature |
Red flags:
__mod_init_func: Function pointers called beforemain. Flag in triage.- Unsigned or ad-hoc signed: macOS may refuse to run unsigned binaries. An unsigned binary is suspicious unless it's a development build.
LC_MAINmissing on modern binary: Older binaries useLC_UNIXTHREAD, but modern macOS binaries should haveLC_MAIN. Its absence on a recent minimum-deployment-target binary is odd.
Mach-O Import/Export Tracking
Mach-O uses two-level namespaces: imports reference both the library and the
symbol. binary imports reports module.library and symbol. Exports are
tracked via the symbol table with N_EXT flag.
binary imports --project <proj> --json
binary exports --project <proj> --json
Cross-Format Comparison
| Characteristic | PE | ELF | Mach-O |
|---|---|---|---|
| Entry point indicator | AddressOfEntryPoint | e_entry | LC_MAIN or LC_UNIXTHREAD |
| Import mechanism | Import Directory / IAT | .dynsym + .plt / .got | LC_LOAD_DYLIB + lazy symbol ptrs |
| Export mechanism | Export Directory | .dynsym (GLOBAL + FUNC) | Symbol table (N_EXT) |
| Pre-main execution | TLS callbacks | .init_array | __mod_init_func |
| ASLR support | DYNAMIC_BASE DllCharacteristic | PIE (ET_DYN) + ASLR | Default on, PIE required |
| Code signing | Authenticode (Certificate Table) | None built-in | LC_CODE_SIGNATURE |
| Resource storage | .rsrc section | No standard format | No standard format |
| Debug info | .pdb reference or embedded | .debug_* sections | DWARF in __DWARF segment or dSYM bundle |
Unknown or RAW Format
If binary metadata reports format: "RAW", the binary does not match PE,
ELF, or Mach-O magic bytes. This does not necessarily mean the file is
malicious — it could be:
- Firmware: Flat binary loaded at a fixed address. See firmware.md.
- Packed/compressed: An executable packed with a custom loader. See packed-and-obfuscated.md.
- Proprietary container: Vendor-specific format (e.g., game engine archives, database files).
- Corrupted: Truncated or damaged binary.
What to Do with RAW Format
- Run
binary strings --project <proj> --min-length 8 --jsonand scan for identifying strings (compiler names, error messages, format signatures). - Check entropy via
binary sections(if sections were identified) or viabinary metadatafor size and any heuristic format guesses. - If the file is firmware, follow the workflows in firmware.md.
- If the file appears packed, follow the workflows in packed-and-obfuscated.md.
- If you cannot determine the format, flag as
unknownin triage and note the file size, any identifiable strings, and entropy characteristics.
Using Metadata to Guide Analysis
The binary metadata command is your first stop after import:
binary metadata --project <proj> --json
Use the output to decide your next steps:
| Metadata Field | Tells You | Guides You To |
|---|---|---|
format |
File format | Which format section above to reference |
architecture |
Target CPU | Which instruction set the disassembly will use |
endianness |
Byte order | How to interpret multi-byte values |
size_bytes |
File size | Whether the binary is small (micro-loader) or large (full application) |
entry_point |
Start address | Where to begin focused analysis |
After metadata, run binary sections to understand the layout:
binary sections --project <proj> --json
Check for:
- RWX sections (flag as suspicious)
- High-entropy sections (flag as possibly packed)
- Missing expected sections (
.text,.data,.rdata— may indicate packing or a non-standard linker) - Section count: Too few sections (1-2) is suspicious. Too many (50+) is unusual but not inherently suspicious.