Lion Cipher Lab
Write an ELF loader from scratch (map a real binary into memory and jump to its entry point)
- Foundations
- Advanced
- ~120 min
- Free · no account required
Why it matters
Every time you type a command, the kernel performs a small miracle you have never seen: it reads a file off disk, recognizes it as an ELF binary, carves it into memory at exactly the right addresses with exactly the right permissions, builds a stack out of thin air with your arguments and a secret data structure called the auxiliary vector, and jumps into it. That machinery — binfmt_elf in the kernel, and ld.so for dynamic binaries — is the most load-bearing black box in the whole system, and almost nobody has looked inside it. This lab has you REBUILD it in userspace: a ~130-line C program…
What you'll learn
- reading the ELF64 header by hand: the \x7fELF magic, EI_CLASS (ELF64), e_type (ET_EXEC vs ET_DYN), e_entry, e_phoff/e_phnum/e_phentsize
- walking the program header table (Elf64_Phdr) and telling PT_LOAD segments apart from PT_INTERP, PT_TLS, PT_NOTE, PT_GNU_STACK
- mapping a segment with mmap(2): page-aligning p_vaddr, MAP_FIXED to force the exact address, copying p_filesz bytes and letting the anonymous map zero the .bss gap up to p_memsz
- translating p_flags (PF_R/PF_W/PF_X) into mmap/mprotect permissions and respecting W^X (never map a page writable AND executable)
- why even a STATIC binary needs a stack the kernel builds: argc, argv, envp, and the auxiliary vector (AT_ENTRY, AT_PHDR, AT_PHNUM, AT_PAGESZ, AT_RANDOM, AT_NULL)
- handing control to a loaded image with a tiny asm stub: install the new %rsp, zero %rdx, jmp to e_entry
- the difference between a freestanding raw-syscall binary (needs nothing) and a glibc static binary (dies without a correct auxv) — proved by running both through your loader
- reproducing and naming five loader failure modes: missing MAP_FIXED, forgotten BSS zeroing, W^X violation, a missing/incorrect auxv, and a wrong AT_ENTRY
- the judgment: when hand-rolling a loader is the right tool (reflective/in-memory loading, unpacker analysis, sandboxing, embedded/no-OS) vs just running the binary
- the next rung named precisely: ET_DYN/PIE load bias, PT_INTERP -> ld.so, and dynamic relocations (R_X86_64_RELATIVE, GLOB_DAT/JUMP_SLOT) — why a dynamic binary is much harder
Steps in this lab
- The job you're about to steal from the kernel
- Set up a scratch box and build two target binaries
- Read the ELF with your eyes before you read it with code
- Parse the ELF64 header in C (and validate it)
- Map each PT_LOAD segment: mmap at the vaddr, copy filesz, zero the BSS
- Why even a static binary needs a stack you build by hand
- Hand over control: install the stack and jmp to e_entry
- Break it #1: forget MAP_FIXED (the segment lands at the wrong address)
- Break it #2: map the code W but not X (a W^X violation)
- Break it #3: delete the auxv (glibc dies, freestanding survives)
- The judgment: when to hand-roll a loader, and the next rung (PIE + ld.so)