Hacktricks-skills elf-binary-analysis
Analyze ELF binary files for reverse engineering, security research, and exploitation. Use this skill whenever the user needs to understand ELF structure, analyze program headers, section headers, symbols, relocations, GOT/PLT, or identify binary protections like RELRO, stack canaries, and PIE. Trigger on any request involving ELF files, binary analysis, readelf output interpretation, or exploitation reconnaissance.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/binary-exploitation/basic-stack-binary-exploitation-methodology/elf-tricks/SKILL.MDELF Binary Analysis
A comprehensive guide for analyzing ELF (Executable and Linkable Format) binaries for reverse engineering, security research, and exploitation.
Quick Start
When analyzing an ELF binary, start with this reconnaissance sequence:
# Basic file info file <binary> # Program headers (memory layout, protections) readelf -lW <binary> # Section headers (detailed structure) objdump -h <binary> # Symbols (functions, variables) readelf -sW <binary> # Dynamic section (dependencies, relocations) readelf -d <binary> # Relocations (GOT/PLT entries) readelf -r <binary>
Program Headers
Program headers tell the loader how to map the binary into memory. Use
readelf -lW to view them.
Key Program Header Types
| Type | Purpose | Exploitation Relevance |
|---|---|---|
| PHDR | Program header table itself | Contains metadata about the binary structure |
| INTERP | Dynamic loader path | Missing in static binaries; affects ret2dlresolve feasibility |
| LOAD | Memory segments to load | Shows memory layout, permissions (R/W/X), and sizes |
| DYNAMIC | Dynamic linking info | Contains NEEDED libraries, relocations, flags |
| NOTE | Metadata (build-id, properties) | May contain CPU features like CET (IBT/SHSTK) |
| GNU_EH_FRAME | Stack unwind tables | Used by debuggers, C++ exceptions |
| GNU_STACK | Stack execution flag | = executable stack (rare), = non-executable |
| GNU_RELRO | Relocation read-only | Partial vs Full RELRO affects GOT overwrite attacks |
| TLS | Thread-local storage | Per-thread variable storage |
LOAD Segment Analysis
Each LOAD segment specifies:
- Offset: Where in the file to read from
- VirtAddr: Virtual address in memory
- FileSiz: Bytes to copy from file
- MemSiz: Total memory size (may be larger for .bss)
- Flg: Permissions (R=read, W=write, E=execute)
Example interpretation:
LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x003f7c 0x003f7c R E 0x10000
This segment loads 0x3f7c bytes from file offset 0 to virtual address 0x0 with read+execute permissions.
GNU_STACK - Stack Executability
Check if the stack is executable:
readelf -l ./binary | grep GNU_STACK
= executable stack (vulnerable to shellcode on stack)RW
= non-executable stack (NX/DEP enabled)R
Toggle for testing:
execstack -c ./binary # Clear executable flag execstack -s ./binary # Set executable flag
GNU_RELRO - Relocation Read-Only
RELRO marks certain memory regions as read-only after dynamic linking:
- Partial RELRO:
remains writable (GOT overwrite possible).plt.got - Full RELRO: All GOT entries are read-only (GOT overwrite blocked)
Check RELRO status:
readelf -l ./binary | grep GNU_RELRO readelf -d ./binary | grep FLAGS
Look for
BIND_NOW or NOW flag for Full RELRO.
Section Headers
Section headers provide detailed information about binary contents. Use
objdump -h or readelf -S.
Important Sections
| Section | Contents | Exploitation Relevance |
|---|---|---|
| .text | Executable code | ROP gadget hunting, shellcode placement |
| .data | Initialized globals | Data structures, potential targets |
| .bss | Uninitialized globals | Zero-initialized, writable |
| .rodata | Read-only data | Strings, constants |
| .got | Global Offset Table | Function addresses (writable if Partial RELRO) |
| .got.plt | PLT GOT entries | Lazy binding targets |
| .plt | Procedure Linkage Table | Lazy function call stubs |
| .init/.fini | Init/fini functions | Early/late execution hooks |
| .init_array/.fini_array | Constructor/destructor arrays | Hijackable under Partial RELRO |
| .dynamic | Dynamic linking info | NEEDED libs, relocations, flags |
| .tdata/.tbss | Thread-local data | Per-thread variables |
Section Flags
- ALLOC: Section occupies memory at runtime
- LOAD: Section is loaded from file
- READONLY: Read-only at runtime
- CODE: Contains executable code
- DATA: Contains data
Symbols
Symbols are named locations (functions, variables) in the binary. Use
readelf -sW.
Symbol Table Structure
Num: Value Size Type Bind Vis Ndx Name 3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.17
| Field | Meaning |
|---|---|
| Num | Symbol index |
| Value | Address (or 0 for undefined) |
| Size | Symbol size in bytes |
| Type | FUNC, OBJECT, SECTION, NOTYPE, TLS, GNU_IFUNC |
| Bind | LOCAL, GLOBAL, WEAK |
| Vis | DEFAULT, HIDDEN, PROTECTED |
| Ndx | Section index (UND = undefined/external) |
| Name | Symbol name (may include version) |
Symbol Binding
- LOCAL: Visible only within the binary
- GLOBAL: Visible to other binaries/libraries
- WEAK: Can be overridden by strong symbols
GNU IFUNC (Indirect Functions)
IFUNC symbols have resolvers that select implementations at load time:
readelf -sW ./binary | rg -i "IFUNC"
Common for CPU dispatch (e.g., optimized string functions).
Symbol Versioning
Modern glibc uses versioned symbols:
strlen@GLIBC_2.17
For manual relocations (ret2dlresolve), you must supply the correct version index.
Dynamic Section
The dynamic section contains information for the dynamic linker. Use
readelf -d.
Key Dynamic Entries
| Tag | Purpose |
|---|---|
| NEEDED | Required shared libraries |
| INIT/FINI | Init/fini function addresses |
| INIT_ARRAY/FINI_ARRAY | Constructor/destructor arrays |
| INIT_ARRAYSZ/FINI_ARRAYSZ | Array sizes |
| PLTGOT | GOT/PLT base address |
| PLTRELSZ/PLTREL/JMPREL | PLT relocation info |
| RELA/RELASZ/RELAENT | Relocation table info |
| FLAGS | BIND_NOW, NOW (Full RELRO) |
| VERNEED/VERNEEDNUM/VERSYM | Symbol versioning |
| RPATH/RUNPATH | Library search paths |
Library Search Order
The dynamic linker searches for libraries in this order:
(ignored for setuid/secure-execution)LD_LIBRARY_PATH
(only ifDT_RPATH
absent)DT_RUNPATHDT_RUNPATHld.so.cache- Default directories (
,/lib64
, etc.)/usr/lib64
Check RPATH/RUNPATH:
readelf -d ./binary | egrep -i 'r(path|unpath)'
Test library resolution:
LD_DEBUG=libs ./binary 2>&1 | grep -i find
Relocations
Relocations adjust addresses after the binary is loaded. Use
readelf -r.
Relocation Types
| Type | Purpose |
|---|---|
| RELATIVE | Adjust addresses based on load bias |
| GLOB_DAT | Global data symbol (write address to GOT) |
| JUMP_SLOT | Function symbol (PLT lazy binding) |
| IRELATIVE | Runtime relocation with resolver |
GOT (Global Offset Table)
The GOT stores addresses of external functions and variables. Key points:
- Lazy binding: First call to a function resolves it via PLT, then updates GOT
- GOT overwrite: If Partial RELRO, you can overwrite GOT entries to redirect calls
- Full RELRO: GOT is read-only, GOT overwrite attacks blocked
PLT (Procedure Linkage Table)
The PLT enables lazy function binding:
- Call
→ jumps to PLT stubfunc@plt - First call: PLT stub resolves address via GOT, updates GOT, calls real function
- Subsequent calls: Direct call via GOT entry
Modern Linking Behaviors
| Flag | Effect |
|---|---|
| Full RELRO, disables lazy binding |
| Direct GOT calls instead of PLT stubs |
| Compact relative relocations (DT_RELR) |
Check for packed relocations:
readelf -d ./binary | egrep -i "DT_RELR|RELRSZ|RELRENT"
Program Initialization
The program doesn't always start at
main. Initialization order:
- Load binary into memory, initialize
and zero.data.bss - Initialize dependencies and perform dynamic linking
- Execute
functions (if present)PREINIT_ARRAY - Execute
functionsINIT_ARRAY - Call
function (if present)INIT - Call entry point (
for programs)main
Constructor/Destructor Attributes
__attribute__((constructor)) // Runs before main __attribute__((destructor)) // Runs after main
These functions are added to
INIT_ARRAY and FINI_ARRAY.
Exploitation Note
Under Partial RELRO,
INIT_ARRAY and FINI_ARRAY are writable before ld.so marks them read-only. You can hijack control flow by overwriting entries with your function addresses.
Thread-Local Storage (TLS)
TLS variables have per-thread storage:
__thread int my_var; // C __thread_local int my_var; // C++
- Stored in
(initialized) and.tdata
(uninitialized).tbss - Each thread has its own copy
points to the TLS base address__TLS_MODULE_BASE
Auxiliary Vector (auxv)
The kernel passes an auxiliary vector with runtime information:
| Entry | Purpose |
|---|---|
| AT_RANDOM | 16 random bytes (stack canary seed) |
| AT_SYSINFO_EHDR | vDSO base address |
| AT_EXECFN | Executable path |
| AT_BASE | Dynamic linker base |
| AT_PAGESZ | System page size |
Access from code:
#include <sys/auxv.h> printf("AT_RANDOM=%p\n", (void*)getauxval(AT_RANDOM)); printf("AT_SYSINFO_EHDR=%p\n", (void*)getauxval(AT_SYSINFO_EHDR));
From
/proc:
cat /proc/$(pidof target)/auxv | xxd
Common Analysis Commands
Full Reconnaissance
# File type and architecture file ./binary # Check protections checksec --file=./binary # If available # Program headers readelf -lW ./binary # Section headers objdump -h ./binary readelf -S ./binary # Symbols readelf -sW ./binary nm ./binary # Dynamic section readelf -d ./binary # Relocations readelf -r ./binary # Disassembly objdump -d ./binary
Protection Detection
# PIE readelf -h ./binary | grep Type # DYN = PIE, EXEC = non-PIE # Stack executable readelf -l ./binary | grep GNU_STACK # RELRO readelf -l ./binary | grep GNU_RELRO readelf -d ./binary | grep -E "BIND_NOW|NOW" # Canary (check for __stack_chk_fail) readelf -sW ./binary | grep stack_chk # FORTIFY_SOURCE (check for _chk functions) readelf -sW ./binary | grep _chk # CET (Control-flow Enforcement Technology) readelf -n ./binary | grep -i "ibt\|shstk"
Finding Useful Symbols
# System call functions readelf -sW ./binary | grep -E "sys_|__kernel_" # Memory functions readelf -sW ./binary | grep -E "malloc|free|memcpy|strcpy" # String functions readelf -sW ./binary | grep -E "str|printf|scanf" # File I/O readelf -sW ./binary | grep -E "open|read|write|close" # Process functions readelf -sW ./binary | grep -E "fork|exec|exit"
Exploitation Considerations
When GOT/PLT is Available (Partial RELRO)
- GOT overwrite: Redirect function calls to shellcode or ROP chain
- ret2dlresolve: Force dynamic linker to resolve arbitrary symbols
- PLT stubs: Use for ROP gadgets (call + ret sequences)
When GOT/PLT is Not Available (Full RELRO)
- ROP/SROP: Use existing code gadgets
- Format string: Leak addresses, overwrite memory
- Heap exploitation: Use heap corruption techniques
- Other writable pointers: Look for function pointers in data structures
PIE Considerations
- Address randomization: Need to leak an address to calculate offsets
- Common leak sources: Format strings, heap metadata, infoleaks
- vDSO:
provides a stable base for gadgetsAT_SYSINFO_EHDR
Static vs Dynamic Binaries
| Type | Characteristics |
|---|---|
| Dynamic (ET_DYN + INTERP) | Uses loader, has PLT/GOT, standard exploitation |
| Static-PIE (ET_DYN, no INTERP) | No loader, relocations applied by kernel, no PLT resolution |
| Static (ET_EXEC) | Fixed addresses, no ASLR, no dynamic linking |