Cybersecurity-Projects/PROJECTS/advanced/rveng/docs/research/01-elf-format.md

141 lines
6.3 KiB
Markdown

<!-- ©AngelaMos | 2026 -->
<!-- 01-elf-format.md -->
# 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.