6.3 KiB
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).01would 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.
.textis AX (code),.rodatais A (read-only data),.datais WA,.bssis WA. .bsstype 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
.textAddress, and the symbol table shows_startat 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:
.bssis 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
gccbuild is PIE (ET_DYN) with load-relative addresses. The challenge binaries are compiled-no-pieon purpose so addresses inreadelf,objdump, and the engine all agree with what a learner sees.