# ELF Format Research Primary source: the System V Application Binary Interface and the ELF-64 Object File Format specification. Every field size and offset below is cross-checked against a real binary compiled locally. ## Ground-truth sample ``` gate.c compiled with: gcc -no-pie -fno-stack-protector -O0 -o gate gate.c file: ELF 64-bit LSB executable, x86-64, EXEC, dynamically linked, not stripped ``` Chosen `-no-pie` so the load addresses are fixed and readable (EXEC type, not a PIE/DYN shared object with load-time relocation). This is the shape the curated challenge binaries use so a learner sees stable addresses. ## ELF header (Elf64_Ehdr) `readelf -h gate` reported, and the first 64 raw bytes confirm, this layout. Offsets are into the file from byte 0. | Off | Size | Field | Value in sample | Raw bytes | |-----|------|-------|-----------------|-----------| | 0x00 | 16 | e_ident | magic + class + data + version | `7f 45 4c 46 02 01 01 00` then padding | | 0x10 | 2 | e_type | EXEC (2) | `02 00` | | 0x12 | 2 | e_machine | x86-64 (62) | `3e 00` | | 0x14 | 4 | e_version | 1 | `01 00 00 00` | | 0x18 | 8 | e_entry | 0x401060 | `60 10 40 00 00 00 00 00` | | 0x20 | 8 | e_phoff | 64 | `40 00 00 00 00 00 00 00` | | 0x28 | 8 | e_shoff | 13984 (0x36a0) | `a0 36 00 00 00 00 00 00` | | 0x30 | 4 | e_flags | 0 | `00 00 00 00` | | 0x34 | 2 | e_ehsize | 64 | `40 00` | | 0x36 | 2 | e_phentsize | 56 | `38 00` | | 0x38 | 2 | e_phnum | 14 | `0e 00` | | 0x3a | 2 | e_shentsize | 64 | `40 00` | | 0x3c | 2 | e_shnum | 30 | `1e 00` | | 0x3e | 2 | e_shstrndx | 29 | `1d 00` | e_ident breakdown (the first 16 bytes): - bytes 0-3: magic `7f 45 4c 46` = `\x7f E L F`. Every ELF starts with these. - byte 4 (EI_CLASS): `02` = ELFCLASS64 (64-bit). `01` would be 32-bit. - byte 5 (EI_DATA): `01` = little-endian (ELFDATA2LSB). - byte 6 (EI_VERSION): `01`. - bytes 7-15: OS/ABI + padding, zero here. The raw hex cross-check is exact: the entry point bytes at file offset 0x18 are `60 10 40 00` little-endian = 0x401060, matching both `readelf -h` and the entry point of `.text`. The section header table offset at 0x28 is `a0 36 00 00` = 0x36a0 = 13984, matching e_shoff. ## Section header table (Elf64_Shdr) Located at e_shoff (0x36a0), e_shnum (30) entries of e_shentsize (64) bytes each. Section names are indices into the section named by e_shstrndx (29, `.shstrtab`). Sections relevant to the learning modules, from `readelf -S`: | Nr | Name | Type | Address | Offset | Size | Flags | |----|------|------|---------|--------|------|-------| | 11 | .init | PROGBITS | 0x401000 | 0x1000 | 0x17 | AX | | 12 | .plt | PROGBITS | 0x401020 | 0x1020 | 0x40 | AX | | 13 | .text | PROGBITS | 0x401060 | 0x1060 | 0x182 | AX | | 15 | .rodata | PROGBITS | 0x402000 | 0x2000 | 0x30 | A | | 24 | .data | PROGBITS | 0x404018 | 0x3018 | 0x10 | WA | | 25 | .bss | NOBITS | 0x404028 | 0x3028 | 0x8 | WA | | 27 | .symtab | SYMTAB | 0 | 0x3048 | 0x378 | | | 28 | .strtab | STRTAB | 0 | 0x33c0 | 0x1ca | | Key facts the engine must model: - Flags: A = allocated into memory, X = executable, W = writable. `.text` is AX (code), `.rodata` is A (read-only data), `.data` is WA, `.bss` is WA. - `.bss` type is NOBITS: it occupies memory at runtime but has zero size in the file (Size 0x8 but no file bytes). The engine must not read file bytes for a NOBITS section. - A section with Address 0 is not loaded into the process image (`.symtab`, `.strtab`, `.shstrtab`). These exist only in the file. - The entry point 0x401060 equals `.text` Address, and the symbol table shows `_start` at 0x401060. Entry is the first instruction the loader jumps to. ## Program header table (Elf64_Phdr) Located at e_phoff (64), e_phnum (14) entries of e_phentsize (56) bytes. These describe segments: how the file maps into memory at load time. From `readelf -l`, the LOAD segments and their flags: | Offset | VirtAddr | FileSiz | Flags | Holds | |--------|----------|---------|-------|-------| | 0x0000 | 0x400000 | 0x568 | R | ELF header + read-only metadata | | 0x1000 | 0x401000 | 0x1ed | R E | .init .plt .text .fini (code) | | 0x2000 | 0x402000 | 0x14c | R | .rodata + eh_frame | | 0x2df8 | 0x403df8 | 0x230 | RW | .data .bss and relro | Sections are the linker/analysis view. Segments are the loader view. The engine parses sections for the learning modules (naming, layout) and can show segments for the elf-anatomy module to explain how file bytes become a running process. ## Symbol table (Elf64_Sym) `.symtab` at file offset 0x3048, entry size 24 (0x18). Names index into `.strtab`. Sample function symbols from `readelf -s`: | Value | Size | Type | Bind | Name | |-------|------|------|------|------| | 0x401060 | 34 | FUNC | GLOBAL | _start | | 0x401146 | 30 | FUNC | GLOBAL | check | | 0x401164 | 126 | FUNC | GLOBAL | main | Each symbol carries value (address), size, a type (FUNC/OBJECT/etc.), a binding (LOCAL/GLOBAL/WEAK), and a section index. `check` at 0x401146 with size 30 is the target of the strings-symbols and disassembly modules. A stripped binary has no `.symtab`, which is why later challenges can strip symbols to force learners into raw disassembly. ## What we hand-roll vs delegate Hand-roll (this is the ELF-format learning surface): - e_ident + Elf64_Ehdr field parse from raw bytes (the table above). - Section header table walk: read e_shoff/e_shnum/e_shentsize, iterate entries, resolve names via e_shstrndx. Delegate to pyelftools where hand-rolling adds no pedagogical value: - Program header details, relocation tables, dynamic section, DWARF. pyelftools 0.33 is the installed reference and is used to cross-check the hand-rolled parser in the M1 known-answer tests: the hand-rolled header and section fields must equal what pyelftools reports for the same binary. ## Facts an adversarial check confirmed or corrected - Confirmed: entry point, e_shoff, e_shnum, and all e_ident bytes match the raw file bytes exactly (not taken from memory or a generic ELF diagram). - Confirmed: `.bss` is NOBITS with a nonzero memory size and no file content; the parser must special-case it. - Corrected assumption: an EXEC (non-PIE) binary has fixed addresses; a default `gcc` build is PIE (ET_DYN) with load-relative addresses. The challenge binaries are compiled `-no-pie` on purpose so addresses in `readelf`, `objdump`, and the engine all agree with what a learner sees.