ELF Header Forensics
Walk a raw ELF64 header byte-by-byte and pull out the fields a loader relies on to map and run the file.
Investigate
Hints
Hint 1
e_ident (the first 16 bytes) is fixed-size and always precedes the rest of the header — everything after it is a field defined by struct offset, not a search.
Hint 2
Every Elf64_* field is little-endian: bytes 0x40 0x00 0x00 0x00 0x00 0x00 0x00 0x00 read as the value 0x40, not 0x4000000000000000.
Hint 3
e_phnum sits right after e_phentsize in the fixed Elf64_Ehdr layout — both are 2-byte fields.
Hint 4
ET_DYN binaries are position-independent — that's what lets ASLR randomize their base address at load time. Modern Linux binaries default to this.
Submit Findings
Solution
ELF Header Forensics — Solution
Unlike the PE format's MZ → e_lfanew indirection, the ELF64 header (Elf64_Ehdr) is a single fixed 64-byte struct — every field lives at a constant offset:
| Offset | Field | Value |
|---|---|---|
| 0x00 | e_ident |
\x7fELF, class=64-bit, data=LSB |
| 0x10 | e_type |
0x0003 (ET_DYN) |
| 0x12 | e_machine |
0x003e (EM_X86_64) |
| 0x18 | e_entry |
0x1020 |
| 0x20 | e_phoff |
0x40 |
| 0x38 | e_phnum |
4 |
- e_phoff = 0x40 — the program header table starts immediately after the 64-byte header.
- 4 program headers — in this sample: PT_PHDR, PT_INTERP, and two PT_LOAD segments (one
r-xfor code, onerw-for data). - e_entry = 0x1020 — where
_startbegins once the file is mapped. - PIE (Position-Independent Executable) —
ET_DYNmeans this is really a shared-object-shaped executable, letting the kernel map it anywhere and randomize its base for ASLR.
Prerequisites
- l1-elf-format-deep-dive
- f1-binary-anatomy
- ELF64 header structure (e_ident, e_type, e_machine, e_entry, e_phoff, e_phnum)
- Little-endian byte order
- PIE vs ET_EXEC
Methodology
Investigation Methodology
1. ELF64 Header Layout (Fixed 64 bytes)
Unlike PE's indirection via e_lfanew, ELF header is at fixed offset 0:
typedef struct {
unsigned char e_ident[16]; // 0x00-0x0F
uint16_t e_type; // 0x10-0x11
uint16_t e_machine; // 0x12-0x13
uint32_t e_version; // 0x14-0x17
uint64_t e_entry; // 0x18-0x1F
uint64_t e_phoff; // 0x20-0x27
uint64_t e_shoff; // 0x28-0x2F
uint32_t e_flags; // 0x30-0x33
uint16_t e_ehsize; // 0x34-0x35
uint16_t e_phentsize; // 0x36-0x37
uint16_t e_phnum; // 0x38-0x39
uint16_t e_shentsize; // 0x3A-0x3B
uint16_t e_shnum; // 0x3C-0x3D
uint16_t e_shstrndx; // 0x3E-0x3F
} Elf64_Ehdr;
2. e_ident (0x00-0x0F) - Magic + Meta
| Offset | Field | Value |
|---|---|---|
| 0x00 | EI_MAG0-3 | 0x7F 'E' 'L' 'F' |
| 0x04 | EI_CLASS | 0x02 = 64-bit |
| 0x05 | EI_DATA | 0x01 = LSB (little-endian) |
| 0x06 | EI_VERSION | 0x01 = current |
| 0x07 | EI_OSABI | 0x00 = System V |
| 0x08 | EI_ABIVERSION | 0x00 |
| 0x09-0x0F | EI_PAD | Zero |
3. Key Fields (Little-Endian!)
| Field | Offset | Size | Example Bytes | Value |
|---|---|---|---|---|
| e_type | 0x10 | 2 | 03 00 | 0x0003 (ET_DYN) |
| e_machine | 0x12 | 2 | 3E 00 | 0x003E (x86_64) |
| e_version | 0x14 | 4 | 01 00 00 00 | 1 |
| e_entry | 0x18 | 8 | 20 10 00 00 00 00 00 00 | 0x1020 |
| e_phoff | 0x20 | 8 | 40 00 00 00 00 00 00 00 | 0x40 |
| e_shoff | 0x28 | 8 | ... | ... |
| e_phentsize | 0x36 | 2 | 38 00 | 0x38 (56 bytes) |
| e_phnum | 0x38 | 2 | 04 00 | 4 |
4. e_type Values
| Value | Constant | Meaning |
|---|---|---|
| 0x01 | ET_REL | Relocatable (.o) |
| 0x02 | ET_EXEC | Executable (fixed base) |
| 0x03 | ET_DYN | Shared object / PIE |
| 0x04 | ET_CORE | Core dump |
PIE (ET_DYN) = Position Independent Executable = ASLR-friendly.
5. Program Headers (at e_phoff)
Each 56 bytes (e_phentsize):
typedef struct {
uint32_t p_type; // PT_LOAD, PT_INTERP, PT_PHDR, PT_DYNAMIC, PT_TLS
uint32_t p_flags; // PF_R, PF_W, PF_X
uint64_t p_offset; // File offset
uint64_t p_vaddr; // Virtual address
uint64_t p_paddr; // Physical (ignored)
uint64_t p_filesz; // Size in file
uint64_t p_memsz; // Size in memory
uint64_t p_align; // Alignment
} Elf64_Phdr;
6. PT_LOAD Segments = Memory Mappings
| p_type | p_flags | Purpose |
|---|---|---|
| PT_LOAD | R+X | Code segment (.text) |
| PT_LOAD | R+W | Data segment (.data, .bss) |
| PT_INTERP | - | Dynamic linker path (e.g., /lib64/ld-linux-x86-64.so.2) |
| PT_DYNAMIC | - | .dynamic section (relocations, needed libs) |
| PT_TLS | - | Thread-local storage |
7. Dynamic Section (.dynamic)
Entries: DT_NEEDED (libraries), DT_STRTAB/DT_SYMTAB (symbols), DT_RELA/DT_REL (relocations), DT_INIT/DT_FINI (constructors/destructors).
Common Pitfalls
- Reading multi-byte fields as big-endian (ELF is little-endian on x86_64)
- Confusing e_shoff (section header table) with e_phoff (program header table)
- Missing that ET_DYN = PIE = ASLR-enabled (modern default)
- Not verifying e_ident magic (0x7F 'ELF') first
- Assuming section headers exist (stripped binaries have e_shoff=0)
Tools
- readelf -h sample.elf (header)
- readelf -l sample.elf (program headers/segments)
- readelf -d sample.elf (dynamic section)
- objdump -x sample.elf (all headers)
- Ghidra / Binary Ninja (ELF support)
- pefile equivalent: pyelftools (Python)
- Built-in Hex Viewer
References
- Lesson L1: ELF Format Deep Dive
- Lesson F1: Binary Anatomy (PE vs ELF comparison)
- ELF Specification (System V ABI)
- man 5 elf
- How programs get run (execve, ld-linux)
Extensions & Challenges
- Parse the program headers manually from the hex dump
- Identify which PT_LOAD is code (R+X) and which is data (R+W)
- Find the PT_INTERP path — the dynamic linker
- Research: How does the dynamic linker resolve DT_NEEDED libraries?
- Analyze a stripped vs unstripped ELF — what's missing?