SuperAPI AI Directory

re-format-elf

Security & auditing

View repository

Updated 10-15 21:34

About this skill

ELF format parsing

When to use / when not to use

  • Use: the target is a Linux ELF executable or .so shared library, and you need to understand the structure, inspect .init_array, GOT/PLT and symbols, and assess security properties
  • Do not use: PE (use [[re-format-pe]]), Mach-O (use [[re-format-macho]]); bare-metal firmware binary (non-standard ELF, use the [[re-firmware]] domain)
  • Do not use: when you only need function logic (use the decompilation skill directly)

Tool preparation

See [[re-analyze/platform-tips]]—ELF targets are mostly analyzed locally on Linux; for cross-architecture (ARM/MIPS firmware ELF) you can use QEMU user-mode emulation, and static analysis needs no sandbox.

readelf(binutils)

  • Linux: apt install binutils / dnf install binutils / pacman -S binutils
  • macOS: brew install binutils(greadelf)
  • WSL: Linux version
  • Verify: readelf --version

objdump(binutils)

  • Same as above (on macOS use gobjdump)
  • Verify: objdump -V

elfutils (the eu-* tool family)

  • Linux: apt install elfutils / dnf install elfutils / pacman -S elfutils
  • macOS: brew install elfutils(eu-readelf , etc.)
  • Verify: eu-readelf -h /bin/ls

patchelf (modify ELF header/loader)

  • Linux: apt install patchelf / dnf install patchelf / pacman -S patchelf
  • macOS: brew install patchelf
  • Verify: patchelf --version
  • Purpose: analysis aid (change RPATH/interpreter) and repair; during analysis, read-only first

Procedure

  1. The three readelf tables (ehdr/phdr/shdr):

    readelf -h sample        # ELF 头: Class/Machine/Entry/Type
    readelf -l sample        # program headers(段→装载偏移,动态加载与 GNU_STACK 在这里)
    readelf -S sample        # section headers(节→地址/大小/标志)
    

    Three-table comparison: -h gives the entry point,-l gives the runtime memory layout,-S gives the static section view.

  2. Structure overview (ehdr/phdr/shdr layout): An ELF file = header + program header table + sections + section header table; in 64-bit, ehdr is 64 bytes, phdr is 56 bytes each, and shdr is 64 bytes each. Key ehdr fields and offsets:

    0x00  e_ident[16]  magic(7F 45 4C 46)+class(0x04:1=32位/2=64位)+data(0x05)+OSABI(0x07)
    0x10  e_type        1=REL 2=EXEC 3=DYN(PIE/so) 4=CORE
    0x12  e_machine     62=x86-64 3=i386 183=AArch64 243=RISC-V 8=MIPS
    0x18  e_entry       入口点
    0x20  e_phoff       程序头表偏移
    0x28  e_shoff       节头表偏移
    0x34  e_ehsize      64(32 位为 52)
    0x38  e_phnum | 0x3A e_shentsize | 0x3C e_shnum | 0x3E e_shstrndx
    

    Three-table relationship: readelf -h every field output can be manually checked in the first 64 bytes of the file (xxd -l 64); the program headers describe "which file segments are mapped to memory with what permissions/alignment", and the section headers describe "named regions in the file such as symbols/strings/code". For the full field table and layout diagram, see [[layout]].

  3. Initialization and termination callbacks (.init_array / .fini_array):

    readelf -S sample | grep -iE 'init_array|fini_array'
    readelf -a sample | grep -A5 -i 'init_array'
    objdump -s -j .init_array sample     # .init_array 是函数指针数组(数据节),用 -s 打印内容;-d 只反汇编代码节,实际不输出
    # 取到指针后逐个 `objdump -d --start-address=<ptr> --stop-address=<ptr+len> sample` 看回调函数
    

    The two execute at different times, so do not conflate them:.init_array The function pointers in execute in forward array order during the startup initialization phase (before main)—initialization/anti-debugging/decryption are often hidden here and must be checked first;.fini_array The pointers in execute during the exit/dlclose cleanup phase, in reverse array order, and do not immediately follow main (see the pitfalls section for details).

  4. GOT/PLT and dynamic symbols:

    objdump -d -j .plt sample            # PLT 桩(外部函数调用入口)
    readelf -r sample | head -40         # 重定位表(含 GOT 条目)
    readelf -s sample | grep FUNC        # 符号表(动态符号在 .dynsym)
    objdump -T sample | grep UND         # 未定义符号 = 导入
    
  5. Key points for parsing dynamic linking structures(mandatory for .so and dynamically linked executables):

    readelf -d sample                    # dynamic section: DT_* 标签
    readelf -s sample | head -20         # .dynsym 动态符号(导入/导出)
    readelf -r sample | grep -E 'JUMP_SLOT|GLOB_DAT|RELATIVE'
    

    Association chain: DT_STRTAB/DT_SYMTAB tags point to dynstr/dynsym; iterate the symbol table with the fixed length given by the DT_SYMENT entry size (standard ELF32 Elf32_Sym is 16 bytes, ELF64 is 24 bytes; read DT_SYMENT and step by that size, do not hardcode 24);DT_GNU_HASH(new) replaces DT_HASH(old) for symbol lookup; the relocation type determines GOT slot behavior—R_X86_64_JUMP_SLOT(PLT jump),GLOB_DAT(global variable),RELATIVE(base-relative).DT_BIND_NOW(or FLAGS' DF_BIND_NOW) present = all binding is done at startup, no lazy binding (default on modern distributions);but RELRO is an independent condition—full RELRO = PT_GNU_RELRO segment and BIND_NOW both hold (GOT turned read-only); only if PT_GNU_RELRO is Partial RELRO, BIND_NOW alone cannot imply RELRO. For dynamic section parsing details see [[layout]].

  6. Approach to symbol recovery in stripped binaries:

    readelf -s sample | wc -l            # 如果只剩 .dynsym(几十个),说明被 strip
    strings -n 6 sample | grep -iE 'error|usage|\.so'   # 错误消息泄露内部函数名
    

    Recovery workflow: string cross-references (strings -t x take offset → locate references in Ghidra/radare2) → signature matching against common library functions (Ghidra FLIRT / rizin z signatures) → trace call relationships backward from the main entry point.

  7. Security property checks:

    readelf -l sample | grep -E 'GNU_STACK|GNU_RELRO'
    # GNU_STACK 无 E 标志 = 不可执行栈(NX)
    # GNU_RELRO 存在 + BIND_NOW = 全 RELRO;GOT 只读
    readelf -s sample | grep -c __stack_chk_fail   # >0 仅为 SSP 线索(静态链接会因 libc 自带 SSP 误报;SSP 按函数施加,不代表全部函数受保护)
    

    RELRO/Canary/NX status determines subsequent dynamic analysis (e.g., whether the GOT is writable) and the hijack surface assessment for [[re-imports]].

  8. Manual parsing and verification: when readelf output is anomalous or header fields are forged, use xxd + Python struct to parse ehdr/phdr/shdr directly by offset (for a minimal runnable example and byte samples see [[examples]]); don't discard a parsing failure as a "corrupted file".

Cross-domain combinations

  • [[re-binary-core]]: workflow step 3, ELF target format parsing
  • [[re-mobile]]: Android native .so libraries (run this skill first before analyzing the app)
  • [[re-firmware]]: ELF components in embedded Linux firmware
  • [[re-ctf]]: common ELF targets in pwn/reverse challenges
  • When a packer/obfuscation is detected, switch to [[re-anti-analysis]]

Common pitfalls and traps

  • init_array hides initialization/anti-debugging: executes earlier than main; looking only at main will miss pre-installed logic

  • After stripping, symbols remain only in dynsym:readelf -s the list shrinks drastically; recovery relies on string cross-references + signature matching, don't expect complete symbols

  • GOT overwrite is a common attack point: when RELRO is not full, the GOT is writable—both reverse engineering and exploitation analysis must confirm GNU_RELRO and BIND_NOW

  • When checking cross-architecture ELF (ARM/MIPS), the local objdump reports "unknown format" → use the corresponding cross toolchain or QEMU emulation (see the Linux branch of [[re-analyze/platform-tips]])

  • The early initialization chain is more than just init_array: symptom—you checked .init_array but still missed earlier-executing logic (e.g., strcmp@GOT was hooked but not reproduced at the main breakpoint); cause—during early dynamic linker initialization, .preinit_arrayruns first (earlier than .init_array), and a malicious constructor can overwrite the GOT/install hooks before main; countermeasure—disassemble both.preinit_array and .init_array , set a breakpoint at the __libc_start_main init call site, and verify whether the GOT entries have already been modified before main

  • Forged section headers make readelf report errors: symptom—readelf -S/-l errors out or output is interrupted (abnormal e_shentsize, absurd program header count, missing dynamic section); cause—obfuscation/adversarial samples forge header fields to make tool parsing fail; countermeasure—xxd manually verify the key ehdr fields (e_shoff/e_shnum/e_shentsize/e_phnum), correct them to the real values, and re-parse; don't discard it as a "corrupted file"

  • fini does not execute immediately after main: symptom—you can't find the "wrap-up" logic at a breakpoint on main's return; cause—fini/.fini_array executes during the exit cleanup phase (together with rtld_fini, atexit, and destructors), not right after main; countermeasure—set breakpoints for wrap-up logic on the exit path (exit_group / rtld_fini), don't look for it at the end of main

  • R_X86_64_RELATIVE addend must be self-consistent with the vaddr system:*slot = B + addend(B = load bias). If the artifact's vaddr = ImageBase + RVA (PE conversion scenario), the value stored in the file slot is the target vaddr → addend = stored value; only the "vaddr = pure RVA" system uses 存储值 − ImageBase—mixing the two formulas is a final-review-level bug (the deviation is constantly one base, and "being dlopen-able" does not expose it)

  • SHF_ALLOC sections must be covered by PT_LOAD: the dynamic area (.dynsym/.dynstr/.hash) is marked SHF_ALLOC but is not inside any segment → the loader does not map it and symbol resolution fails—when hand-writing an ELF generator, give the dynamic area its own PT_LOAD (p_offset and p_vaddr can be decoupled)

  • DSO requires an executable stack but the process has not enabled it → dlopen fails: symptom—dlopen reports cannot enable executable stack as shared object requires: Invalid argument(glibc directly rejects the dlopen path; this is not kernel behavior); cause—the DSO requires an executable stack but the executable stack was not enabled when the process started, and whether "the absence of PT_GNU_STACK counts as a requirement" depends on the target ABI's default stack permissions (x86/x86-64/arm32 etc. are executable by default, aarch64/riscv etc. are non-executable by default); countermeasure—the correct fix is to add a non-X PT_GNU_STACKto the target (PF_R|PF_W, no X, align 16); only when compatibility is truly needed may you temporarily set glibc.rtld.execstack=2(=1 has been found ineffective in practice; this value relaxes restrictions and reduces security)

  • The segment targeted by a relocation must be writable: if the segment containing the r_offset of GLOB_DAT/RELATIVE is read-only (PF_R), ld.so writing to it causes SIGSEGV—force PF_W on sections containing relocation targets (v1 may give up RELRO and add PT_GNU_RELRO later)

  • Do not use strlen to get the length of shstrtab: the string table starts with \0 , so strlen truncates at the first byte to 1—use an explicit length/sizeof; likewise this will trap .dynstr indexing

  • filesz > memsz is a readelf error:p_memsz = max(vsize, raw_size) ensures filesz≤memsz; the semantics of the BSS zeroing area are handled by the loader

  • ELF wrapped by gzexe (disguised as .sh): symptom—dragging the target file into IDA reports "not an ELF format", but the file is indeed an executable program; cause—gzexe gzip-compresses the ELF and wraps it in a shell script (a common compression method on Linux); countermeasure—use hexdump to check the header and confirm (script header + compressed data at the end),gzexe -d decompress to restore the real ELF before analysis

  • ELF with OLLVM obfuscation + string encryption: intercept at the GOT exit: symptom—static patching is not feasible (51642 -like key strings are only decrypted at runtime and cannot be found in the binary); cause—OLLVM string encryption makes strings appear in memory only at runtime; countermeasure—do not work at the data source, intercept at the data exit: the program's final send must go through the GOT to call sendto/send/write/SSL_write → hook the GOT slot, and in buf search for the needle and, after replacement, call the real function as-is; first use debug mode to confirm that the target string indeed appears in the send buffer before patching;OLLVM may obfuscate the GOT value itself(MOVZ+MOVK×3 64-bit constant assembled from got_addend)—when installing the hook, stay consistent with the obfuscation method (modify MOVZ+MOVK immediate rather than directly writing the address)

  • code cave injection + sentinel placeholder (no recompilation needed): Phenomenon — the address of the shellcode to be injected depends on the specific target layout, requiring a rewrite for each target; Cause — directly hardcoding addresses is not reusable; Countermeasure — use sentinel values for all addresses within the shellcode (such as 0xCAFEBABE as a prefix) as placeholders, and the patcher scans for the sentinels during injection to replace them with actual addresses (cave address/offset/original init pointer can all be automatically detected); use .init_arrayfor the injection point (automatically called at program startup, earlier than main)