Back to skills
SKILL.md
Re Format Elf
ASecurityELF 格式解析:ehdr/phdr/shdr、GOT/PLT、init_array、符号恢复。 触发词:ELF、解析so、dynamic section
- 112 stars
- 0 votes
- 0 copies
- 2 views
- Added September 19, 2026
Security analysis
100/100Pro scans all 3 files and shows the line behind each finding
npx -y skills add dslsdzc/rev-skills --skill re-format-elf --agent claude-codeAre you the author of Re Format Elf?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/dslsdzc-re-format-elf)---
name: re-format-elf
description: >
ELF 格式解析:ehdr/phdr/shdr、GOT/PLT、init_array、符号恢复。
触发词:ELF、解析so、dynamic section
capabilities: [elf-parser]
---
# ELF 格式解析
## 何时使用 / 何时不用
- 用:目标是 Linux ELF 可执行文件或 .so 共享库,需要理解结构、看 .init_array、GOT/PLT 与符号、评估安全属性
- 不用:PE(走 [[re-format-pe]])、Mach-O(走 [[re-format-macho]]);裸二进制固件(非标准 ELF,走 [[re-firmware]] 域)
- 不用:只需函数逻辑(直接反编译技能)
## 工具准备
参考 [[re-analyze/platform-tips]]——ELF 目标多为 Linux 本地分析;跨架构(ARM/MIPS 固件 ELF)可用 QEMU 用户态仿真,静态分析无需沙箱。
### readelf(binutils)
- Linux: `apt install binutils` / `dnf install binutils` / `pacman -S binutils`
- macOS: `brew install binutils`(`greadelf`)
- WSL: Linux 版
- 验证: `readelf --version`
### objdump(binutils)
- 同上(macOS 为 `gobjdump`)
- 验证: `objdump -V`
### elfutils(eu-* 工具族)
- Linux: `apt install elfutils` / `dnf install elfutils` / `pacman -S elfutils`
- macOS: `brew install elfutils`(`eu-readelf` 等)
- 验证: `eu-readelf -h /bin/ls`
### patchelf(修改 ELF 头/加载器)
- Linux: `apt install patchelf` / `dnf install patchelf` / `pacman -S patchelf`
- macOS: `brew install patchelf`
- 验证: `patchelf --version`
- 用途: 分析辅助(改 RPATH/解释器)与修复,分析时先只读
## 操作步骤
1. **readelf 三表(ehdr/phdr/shdr)**:
```sh
readelf -h sample # ELF 头: Class/Machine/Entry/Type
readelf -l sample # program headers(段→装载偏移,动态加载与 GNU_STACK 在这里)
readelf -S sample # section headers(节→地址/大小/标志)
```
三表对照: `-h` 给入口点,`-l` 给运行时内存布局,`-S` 给静态节视图。
2. **结构速览(ehdr/phdr/shdr 布局)**:ELF 文件 = 头 + 程序头表 + 节区 + 节头表;64 位下 ehdr 64 字节、phdr 56 字节/条、shdr 64 字节/条。ehdr 关键字段与偏移:
```
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
```
三表关系: `readelf -h` 输出的每个字段都能在文件前 64 字节里手工核对(`xxd -l 64`);程序头描述"哪些文件区段按什么权限/对齐映射到内存",节头描述"文件里的符号/字符串/代码等命名区段"。字段全表与布局图见 [[layout]]。
3. **初始化与终止回调(.init_array / .fini_array)**:
```sh
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` 看回调函数
```
两者的执行时机不同,别混为一谈:`.init_array` 中的函数指针在启动初始化阶段(main 之前)按数组正序执行——初始化/反调试/解密常藏在这里,必须最先查;`.fini_array` 中的指针在退出/dlclose 清理阶段执行、按数组逆序,不紧跟 main(详见坑项)。
4. **GOT/PLT 与动态符号**:
```sh
objdump -d -j .plt sample # PLT 桩(外部函数调用入口)
readelf -r sample | head -40 # 重定位表(含 GOT 条目)
readelf -s sample | grep FUNC # 符号表(动态符号在 .dynsym)
objdump -T sample | grep UND # 未定义符号 = 导入
```
5. **动态链接结构解析要点**(.so 与动态可执行文件必查):
```sh
readelf -d sample # dynamic section: DT_* 标签
readelf -s sample | head -20 # .dynsym 动态符号(导入/导出)
readelf -r sample | grep -E 'JUMP_SLOT|GLOB_DAT|RELATIVE'
```
关联链: `DT_STRTAB`/`DT_SYMTAB` 标签指向 dynstr/dynsym,符号表按 `DT_SYMENT` 给出的 entry size 定长遍历(标准 ELF32 的 `Elf32_Sym` 为 16 字节、ELF64 为 24 字节;读 `DT_SYMENT` 并按该步长走,勿硬编码 24);`DT_GNU_HASH`(新)替代 `DT_HASH`(旧)做符号查找;重定位类型决定 GOT 槽行为——`R_X86_64_JUMP_SLOT`(PLT 跳转)、`GLOB_DAT`(全局变量)、`RELATIVE`(基址相对)。`DT_BIND_NOW`(或 FLAGS 的 `DF_BIND_NOW`)出现 = 启动时完成全部绑定、无惰性绑定(现代发行版默认);**但 RELRO 是独立条件**——全 RELRO = `PT_GNU_RELRO` 段 + BIND_NOW 同时成立(GOT 转只读);只有 `PT_GNU_RELRO` 是 Partial RELRO,只有 BIND_NOW 推不出 RELRO。动态区解析细节见 [[layout]]。
6. **stripped 二进制符号恢复思路**:
```sh
readelf -s sample | wc -l # 如果只剩 .dynsym(几十个),说明被 strip
strings -n 6 sample | grep -iE 'error|usage|\.so' # 错误消息泄露内部函数名
```
恢复流程: 字符串交叉引用(`strings -t x` 取偏移 → 在 Ghidra/radare2 中定位引用)→ 对常见库函数做签名匹配(Ghidra FLIRT / rizin `z` 签名)→ 从 main 入口逆推调用关系。
7. **安全属性检查**:
```sh
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 情况决定后续动态分析(如 GOT 是否可写)与 [[re-imports]] 的劫持面判断。
8. **手工解析与验证**:readelf 输出异常/头字段被伪造时,用 `xxd` + Python `struct` 按偏移直接解析 ehdr/phdr/shdr(最小可运行示例与字节样例见 [[examples]]),别把解析失败当"损坏文件"丢弃。
## 跨域联合
- [[re-binary-core]]:工作流第 3 步,ELF 目标格式解析
- [[re-mobile]]:Android 原生 .so 库(分析 App 前先走本技能)
- [[re-firmware]]:嵌入式 Linux 固件中的 ELF 组件
- [[re-ctf]]:pwn/逆向题常见 ELF 目标
- 发现壳/混淆时转 [[re-anti-analysis]]
## 常见坑与陷阱
- **init_array 藏初始化/反调试**:比 main 更早执行,只看 main 会漏掉预置逻辑
- **stripped 后符号只剩 dynsym**:`readelf -s` 列表骤减,恢复靠字符串交叉引用 + 签名匹配,别期待完整符号
- **GOT 覆盖是常见攻击点**:非全 RELRO 时 GOT 可写——逆向/利用分析都要确认 `GNU_RELRO` 与 `BIND_NOW`
- 检查跨架构 ELF(ARM/MIPS)时本机 objdump 报 "unknown format" → 用对应交叉工具或 QEMU 仿真(见 [[re-analyze/platform-tips]] Linux 分支)
- **早期初始化链不止 init_array**:现象——查过 `.init_array` 却仍漏掉更早执行的逻辑(如 `strcmp@GOT` 被 hook 但 main 断点处未复现);原因——动态链接器初始化早期会先跑 `.preinit_array`(比 .init_array 更早),恶意构造器可在 main 之前覆写 GOT/装钩子;对策——`.preinit_array` 与 `.init_array` 都反汇编,在 `__libc_start_main` 调用 init 处断点,核对 GOT 条目在 main 前是否已被改写
- **伪造节头使 readelf 报错**:现象——`readelf -S`/`-l` 报错或输出中断(e_shentsize 异常、程序头计数离谱、dynamic section 缺失);原因——混淆/对抗样本伪造头字段使工具解析失败;对策——`xxd` 手工核对 ehdr 关键字段(e_shoff/e_shnum/e_shentsize/e_phnum),按真实值修正后重解析,别当"损坏文件"丢弃
- **fini 不在 main 后立即执行**:现象——在 main 返回处断点找不到"收尾"逻辑;原因——`fini`/`.fini_array` 在退出清理阶段执行(与 rtld_fini、atexit、析构函数一起),不紧跟 main;对策——收尾逻辑在 exit 路径(exit_group / rtld_fini)上断点,别在 main 尾部找
- **R_X86_64_RELATIVE addend 必须与 vaddr 体系自洽**:`*slot = B + addend`(B=加载 bias)。若产物 vaddr = ImageBase + RVA(PE 转换场景),文件槽内存储值即目标 vaddr → **addend = 存储值**;只有"vaddr = 纯 RVA"体系才用 `存储值 − ImageBase`——混用两套公式是终审级 bug(偏差恒定一个 base,且"能 dlopen"不暴露)
- **SHF_ALLOC 节必须被 PT_LOAD 覆盖**:动态区(.dynsym/.dynstr/.hash)标记 SHF_ALLOC 但不在任何段内 → 加载器不映射,符号解析失败——手写 ELF 生成器时给动态区单独 PT_LOAD(p_offset 与 p_vaddr 可解耦)
- **DSO 要求可执行栈而进程未启用 → dlopen 失败**:现象——`dlopen` 报 `cannot enable executable stack as shared object requires: Invalid argument`(glibc 对 dlopen 路径直接拒绝,非内核行为);原因——该 DSO 要求可执行栈而进程启动时未启用可执行栈,而"缺 `PT_GNU_STACK` 是否算要求"取决于目标 ABI 默认栈权限(x86/x86-64/arm32 等默认可执行、aarch64/riscv 等默认不可执行);对策——正解是给目标补非 X 的 `PT_GNU_STACK`(PF_R|PF_W、无 X、align 16);仅在确需兼容时可临时设 `glibc.rtld.execstack=2`(`=1` 实测无效;该值放宽限制、降低安全性)
- **重定位目标段必须可写**:GLOB_DAT/RELATIVE 的 r_offset 所在段若只读(PF_R),ld.so 写入即 SIGSEGV——含重定位目标的节强制 PF_W(v1 可放弃 RELRO,后续再上 PT_GNU_RELRO)
- **shstrtab 别用 strlen 取长**:字符串表以 `\0` 开头,strlen 在首字节截断为 1——用显式长度/sizeof;同理会坑 .dynstr 索引
- **filesz > memsz 是 readelf 报错**:`p_memsz = max(vsize, raw_size)` 保证 filesz≤memsz,BSS 清零区语义由 loader 处理
- **gzexe 包裹的 ELF(伪装 .sh)**:现象——目标文件拖进 IDA 报"不是 ELF 格式",但文件确实是可执行程序;原因——gzexe 把 ELF gzip 压缩后包在 shell 脚本里(Linux 常见压缩方式);对策——hexdump 看头确认(脚本头 + 尾部压缩数据),`gzexe -d` 解压还原真正的 ELF 再分析
- **OLLVM 混淆 + 字符串加密的 ELF:GOT 出口拦截**:现象——静态补丁不可行(`51642` 类关键串运行时才解密,二进制里找不到);原因——OLLVM 字符串加密使字符串仅运行时出现在内存;对策——不在数据源头动手,在数据出口拦截:程序最终发送必然经 GOT 调 `sendto`/`send`/`write`/`SSL_write` → hook GOT 槽位,在 `buf` 中搜索 needle 替换后原样调用真函数;先用 debug 模式确认目标串确实出现在发送缓冲区再 patch;**OLLVM 可能混淆 GOT 值本身**(`MOVZ+MOVK×3` 拼出的 64 位常量 `got_addend`)——hook 安装时保持与混淆方式一致(改 `MOVZ+MOVK` 立即数而非直接写地址)
- **code cave 注入 + 哨兵占位(免重编译)**:现象——要注入的 shellcode 地址依赖目标具体布局,每目标重写一次;原因——直接硬编码地址不可复用;对策——shellcode 内所有地址用哨兵值(如 `0xCAFEBABE` 开头)占位,patcher 注入时扫描哨兵替换为实际地址(cave 地址/偏移/原 init 指针均可自动检测);注入点用 `.init_array`(程序启动自动调用,比 main 早)
Files in this skill
- SKILL.md
- references/examples.md
- references/layout.md
Attribution
Comments
Loading comments…