Step 1 of 5 · Reading · ~3 min
Read
File Systems & Beyond
ELF Loading & Process Image
exec is the system call that turns a file on disk into a running program. The kernel must read the file, decide whether it is something it can run, and build an address space from it, without trusting a single byte, because the file is attacker-supplied. In this lesson your program is the validation and planning half of Linux's ELF loader: given the bytes of an ELF64 executable, it checks the header and the program headers and reports the memory image the kernel would build.
The ELF header
An ELF file starts with a fixed 64-byte header (little-endian on x86-64):
| Offset | Size | Field | Typical value |
|---|---|---|---|
| 0 | 4 | magic | 7f 45 4c 46 ("\x7fELF") |
| 4 | 1 | class | 2 = 64-bit |
| 5 | 1 | data | 1 = little-endian |
| 16 | 2 | type | 2 = executable, 3 = shared object / PIE |
| 18 | 2 | machine | 0x3e = x86-64 |
| 24 | 8 | entry | virtual address of the first instruction |
| 32 | 8 | phoff | file offset of the program header table |
| 54 | 2 | phentsize | 56 |
| 56 | 2 | phnum | number of program headers |
The first bytes of a real binary read 7f 45 4c 46 02 01 01 00 ...: magic, 64-bit, little-endian, version 1.
Program headers: what to map
The loader ignores sections (those are for linkers and debuggers) and walks the program headers, 56 bytes each:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | p_type: 1 LOAD, 3 INTERP, 6 PHDR, 0x6474e551 GNU_STACK |
| 4 | 4 | p_flags: 4 = R, 2 = W, 1 = X |
| 8 | 8 | p_offset: where the bytes are in the file |
| 16 | 8 | p_vaddr: where they go in memory |
| 32 | 8 | p_filesz: bytes to copy from the file |
| 40 | 8 | p_memsz: bytes of memory to provide |
| 48 | 8 | p_align: usually 0x1000 |
For each LOAD segment the kernel allocates pages covering p_vaddr to p_vaddr + p_memsz, maps them with permissions from the flags (R-X for code, RW- for data), copies p_filesz bytes from the file, and zero-fills the rest. That tail is the .bss: variables initialised to zero take no space on disk, only p_memsz - p_filesz bytes in memory. Flags map directly to page-table bits: a segment without W is a read-only page, one without X gets NX. Security-conscious loaders refuse W+X.
What can be wrong
A loader must check each of these before using the file:
p_fileszlarger thanp_memsz, orp_offset + p_fileszpast the end of the file (it would read garbage or beyond the buffer);- alignment:
p_vaddrandp_offsetmust agree modulop_align, because the kernel maps whole file pages at whole memory pages; - the segment lies in user space: it must not reach kernel addresses, and
p_vaddr + p_memszmust not wrap around 64 bits; - segments must not overlap in pages;
- the entry point must lie in an executable segment.
Interpreters and the stack
A dynamically linked program has a PT_INTERP segment holding a path such as /lib64/ld-linux-x86-64.so.2: the kernel loads that interpreter too and starts it, and it loads the shared libraries. PT_GNU_STACK states whether the stack may be executable (it should not be). After mapping, exec builds the user stack with argc, argv and envp, sets the stack pointer and jumps to the entry point in ring 3.
Your exercise
The starter turns the hex input into bytes and prints your report. You write the analysis: the header checks, the program header walk, the segment checks and the page count.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…