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.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/binary-exploitation/basic-stack-binary-exploitation-methodology/elf-tricks/SKILL.MD
source content

ELF 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

TypePurposeExploitation Relevance
PHDRProgram header table itselfContains metadata about the binary structure
INTERPDynamic loader pathMissing in static binaries; affects ret2dlresolve feasibility
LOADMemory segments to loadShows memory layout, permissions (R/W/X), and sizes
DYNAMICDynamic linking infoContains NEEDED libraries, relocations, flags
NOTEMetadata (build-id, properties)May contain CPU features like CET (IBT/SHSTK)
GNU_EH_FRAMEStack unwind tablesUsed by debuggers, C++ exceptions
GNU_STACKStack execution flag
RW
= executable stack (rare),
R
= non-executable
GNU_RELRORelocation read-onlyPartial vs Full RELRO affects GOT overwrite attacks
TLSThread-local storagePer-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
  • RW
    = executable stack (vulnerable to shellcode on stack)
  • R
    = non-executable stack (NX/DEP enabled)

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:
    .plt.got
    remains writable (GOT overwrite possible)
  • 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

SectionContentsExploitation Relevance
.textExecutable codeROP gadget hunting, shellcode placement
.dataInitialized globalsData structures, potential targets
.bssUninitialized globalsZero-initialized, writable
.rodataRead-only dataStrings, constants
.gotGlobal Offset TableFunction addresses (writable if Partial RELRO)
.got.pltPLT GOT entriesLazy binding targets
.pltProcedure Linkage TableLazy function call stubs
.init/.finiInit/fini functionsEarly/late execution hooks
.init_array/.fini_arrayConstructor/destructor arraysHijackable under Partial RELRO
.dynamicDynamic linking infoNEEDED libs, relocations, flags
.tdata/.tbssThread-local dataPer-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
FieldMeaning
NumSymbol index
ValueAddress (or 0 for undefined)
SizeSymbol size in bytes
TypeFUNC, OBJECT, SECTION, NOTYPE, TLS, GNU_IFUNC
BindLOCAL, GLOBAL, WEAK
VisDEFAULT, HIDDEN, PROTECTED
NdxSection index (UND = undefined/external)
NameSymbol 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

TagPurpose
NEEDEDRequired shared libraries
INIT/FINIInit/fini function addresses
INIT_ARRAY/FINI_ARRAYConstructor/destructor arrays
INIT_ARRAYSZ/FINI_ARRAYSZArray sizes
PLTGOTGOT/PLT base address
PLTRELSZ/PLTREL/JMPRELPLT relocation info
RELA/RELASZ/RELAENTRelocation table info
FLAGSBIND_NOW, NOW (Full RELRO)
VERNEED/VERNEEDNUM/VERSYMSymbol versioning
RPATH/RUNPATHLibrary search paths

Library Search Order

The dynamic linker searches for libraries in this order:

  1. LD_LIBRARY_PATH
    (ignored for setuid/secure-execution)
  2. DT_RPATH
    (only if
    DT_RUNPATH
    absent)
  3. DT_RUNPATH
  4. ld.so.cache
  5. Default directories (
    /lib64
    ,
    /usr/lib64
    , etc.)

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

TypePurpose
RELATIVEAdjust addresses based on load bias
GLOB_DATGlobal data symbol (write address to GOT)
JUMP_SLOTFunction symbol (PLT lazy binding)
IRELATIVERuntime 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:

  1. Call
    func@plt
    → jumps to PLT stub
  2. First call: PLT stub resolves address via GOT, updates GOT, calls real function
  3. Subsequent calls: Direct call via GOT entry

Modern Linking Behaviors

FlagEffect
-z now
Full RELRO, disables lazy binding
-fno-plt
Direct GOT calls instead of PLT stubs
-z pack-relative-relocs
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:

  1. Load binary into memory, initialize
    .data
    and zero
    .bss
  2. Initialize dependencies and perform dynamic linking
  3. Execute
    PREINIT_ARRAY
    functions (if present)
  4. Execute
    INIT_ARRAY
    functions
  5. Call
    INIT
    function (if present)
  6. Call entry point (
    main
    for programs)

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
    .tdata
    (initialized) and
    .tbss
    (uninitialized)
  • Each thread has its own copy
  • __TLS_MODULE_BASE
    points to the TLS base address

Auxiliary Vector (auxv)

The kernel passes an auxiliary vector with runtime information:

EntryPurpose
AT_RANDOM16 random bytes (stack canary seed)
AT_SYSINFO_EHDRvDSO base address
AT_EXECFNExecutable path
AT_BASEDynamic linker base
AT_PAGESZSystem 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:
    AT_SYSINFO_EHDR
    provides a stable base for gadgets

Static vs Dynamic Binaries

TypeCharacteristics
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

References