reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
---
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 早)

View File

@@ -0,0 +1,162 @@
# ELF 最小解析示例
以下示例以本机 `/bin/ls`(x86-64 PIE)为标本,readelf 输出与 Python/字节样例可逐字段对照(不同发行版构建参数有差异,以样本自身输出为准)。结构定义见 [[layout]]。
## 1. readelf 输出对照
### `readelf -h /bin/ls`(ELF 头)
```
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Entry point address: 0x5540
Start of program headers: 64 (bytes into file)
Start of section headers: 164784 (bytes into file)
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 15
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
```
对照要点:magic 第 5 字节 `02` = ELF64、第 6 字节 `01` = 小端;e_type=DYN 说明是 PIE(Address 全为相对基址偏移);e_phoff=64、e_phentsize=56、e_phnum=15 → 程序头表占文件 `64..64+15*56`。
### `readelf -l /bin/ls`(程序头,节选)
```
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 ...
0x0000000000000348 0x0000000000000348 R 0x8
INTERP 0x00000000000003ac 0x00000000000003ac ...
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 ...
0x00000000000024a0 0x00000000000024a0 R 0x1000
LOAD 0x0000000000003000 0x0000000000003000 ...
0x0000000000019ba1 0x0000000000019ba1 R E 0x1000
LOAD 0x000000000001d000 0x000000000001d000 ...
0x0000000000008e00 0x0000000000008e00 R 0x1000
LOAD 0x0000000000026d10 0x0000000000026d10 ...
0x0000000000001550 0x0000000000002868 RW 0x1000
DYNAMIC 0x0000000000027a58 0x0000000000027a58 ...
0x00000000000001f0 0x00000000000001f0 RW 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 ...
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000026d10 0x0000000000026d10 ...
0x00000000000012f0 0x00000000000012f0 R 0x1
```
对照要点:4 个 PT_LOAD 按 R / R E / R / RW 权限分段(页对齐 0x1000);最后一个 LOAD 的 `FileSiz 0x1550 < MemSiz 0x2868`——差值即 .bss 零填充区;GNU_STACK 无 X = NX;GNU_RELRO 存在 + 后面 `readelf -d` 的 BIND_NOW = 全 RELRO(GOT 只读)。
### `readelf -S /bin/ls`(节头,节选)
```
[ 2] .interp PROGBITS 00000000000003ac 000003ac
[ 3] .gnu.hash GNU_HASH 00000000000003c8 000003c8
[ 4] .dynsym DYNSYM 0000000000000468 00000468
[ 5] .dynstr STRTAB 00000000000010c8 000010c8
[ 8] .rela.dyn RELA 00000000000018d8 000018d8
[11] .text PROGBITS 0000000000003040 00003040
[13] .rodata PROGBITS 000000000001d000 0001d000
[19] .init_array INIT_ARRAY 0000000000026d10 00026d10
[20] .fini_array FINI_ARRAY 0000000000026d18 00026d18
[22] .dynamic DYNAMIC 0000000000027a58 00027a58
[23] .got PROGBITS 0000000000027c48 00027c48
[24] .data PROGBITS 0000000000028000 00028000
[25] .bss NOBITS 0000000000028260 00028260
```
对照要点:`.dynsym`(节 4)与 `.dynstr`(节 5)的 sh_link 关系可从 `readelf -S` 完整输出的 Link 列看到;本机构建为 BIND_NOW,节表无 .plt/.rela.plt(惰性绑定二进制才有,见 [[layout]]);.init_array 与 .fini_array 都在数据段(RW),构造函数指针数组。
### `readelf -d /bin/ls`(dynamic section,节选)
```
Dynamic section at offset 0x27a58 contains 26 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libcap.so.2]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x3000
0x0000000000000019 (INIT_ARRAY) 0x26d10
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3c8
0x0000000000000005 (STRTAB) 0x10c8
0x0000000000000006 (SYMTAB) 0x468
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000007 (RELA) 0x18d8
0x000000000000001e (FLAGS) BIND_NOW
```
对照要点:NEEDED 给依赖链(libc.so.6 等);STRTAB/SYMTAB/SYMENT(24) 定位动态符号表;GNU_HASH 是新式符号查找;FLAGS=BIND_NOW 说明无惰性绑定。
## 2. Python struct 解析 ehdr(最小可运行)
```python
import struct
def parse_ehdr(path):
data = open(path, 'rb').read(64)
if data[:4] != b'\x7fELF':
raise ValueError('not an ELF')
cls = data[4] # 1=ELF32 2=ELF64
endian = '<' if data[5] == 1 else '>' # 1=LE 2=BE
if cls == 2: # ELF64: 16sHHIQQQIHHHHHH
fmt = endian + '16sHHIQQQIHHHHHH'
f = struct.unpack_from(fmt, data, 0)
return dict(e_type=f[1], e_machine=f[2], e_entry=f[4],
e_phoff=f[5], e_shoff=f[6], e_ehsize=f[8],
e_phentsize=f[9], e_phnum=f[10],
e_shentsize=f[11], e_shnum=f[12], e_shstrndx=f[13])
else: # ELF32: 16sHHIIIIIHHHHHH
fmt = endian + '16sHHIIIIIHHHHHH'
f = struct.unpack_from(fmt, data, 0)
return dict(e_type=f[1], e_machine=f[2], e_entry=f[4],
e_phoff=f[5], e_shoff=f[6], e_ehsize=f[8],
e_phentsize=f[9], e_phnum=f[10],
e_shentsize=f[11], e_shnum=f[12], e_shstrndx=f[13])
print(parse_ehdr('/bin/ls'))
# {'e_type': 3, 'e_machine': 62, 'e_entry': 21824(0x5540), 'e_phoff': 64,
# 'e_shoff': 164784, 'e_ehsize': 64, 'e_phentsize': 56, 'e_phnum': 15,
# 'e_shentsize': 64, 'e_shnum': 29, 'e_shstrndx': 28} —— 与 readelf -h 逐字段一致
```
按 ehdr 给的 e_phoff/e_phentsize/e_phnum 可同样解析 phdr:`struct.unpack_from('<IIQQQQQQ', data, e_phoff + i*56)`;shdr 同理 `'<IIQQQQIIQQ'`。注意先按 e_ident 判 class 与字节序再选格式([[layout]] 字段表)。
## 3. 字节样例(/bin/ls 前 64 字节,逐字段标注)
```
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
magic──┘ ┆┆┆ └┬┘ └┬┘ └── e_ident 填充 7 字节
class=02(ELF64) data=01(LE) ver=01 osabi=00(SystemV)
00000010: 0300 3e00 0100 0000 4055 0000 0000 0000 ..>.....@U......
e_type=03(DYN)─┘ └e_machine=003e(62) e_version=1
e_entry=0x5540──────────┘
00000020: 4000 0000 0000 0000 b083 0200 0000 0000 @...............
e_phoff=0x40───────────────────┘ e_shoff=0x283b0──────┘
00000030: 0000 0000 4000 3800 0f00 4000 1d00 1c00 ....@.8...@.....
e_flags=0 e_ehsize=64(0x40) e_phentsize=56(0x38)
e_phnum=15(0x0f) e_shentsize=64(0x40)
e_shnum=29(0x1d) e_shstrndx=28(0x1c)
```
手工核对流程:`xxd -l 64 /bin/ls` 与 readelf -h 输出对照;魔数不符先查是不是 gzexe 包裹/自解压壳(见 SKILL.md 坑);伪造头字段时按真实值修正后重解析(见 SKILL.md 坑)。
## 实现教训(内化)
- 所有解析先读 e_ident[4](class)与 e_ident[5](字节序),再选 struct 格式——32/64、LE/BE 混用是解析器最常见的错
- 动态链接目标以 `readelf -d` 的 SYMTAB/STRTAB/SYMENT 为索引锚点,别硬编码节偏移
- readelf 输出与文件字节必须能互相印证:对不上说明头被修改或工具解析歧义,先手工核对再下结论
## 使用注意
- 静态分析无需沙箱(见 [[re-analyze/platform-tips]]);跨架构 ELF 的 `readelf -h/-l/-S` 本机可直接跑,反汇编才需交叉工具
- 样例取自系统二进制仅作对照;分析真实样本时以样本自身三表为准,别套模板

View File

@@ -0,0 +1,139 @@
# ELF 布局:ehdr / phdr / shdr 字段表与典型布局
ELF 文件 = ELF 头(ehdr)+ 程序头表(phdr,供加载器)+ 节区(sections)+ 节头表(shdr,供工具/链接器)。三张表的偏移与条数都在 ehdr 里给出(e_phoff/e_phnum、e_shoff/e_shnum),文件里可以按索引线性遍历。32 位与 64 位字段相同、大小不同(本文件以 64 位为主,32 位差异单独标注)。
## ELF 头(ehdr)
### e_ident 前 16 字节(0x00-0x0F)
| 偏移 | 名称 | 长度 | 典型值 |
|---|---|---|---|
| 0x00 | EI_MAG | 4 | `7F 45 4C 46`(`\x7fELF`),前 4 字节不符即非 ELF |
| 0x04 | EI_CLASS | 1 | 1=ELFCLASS32,2=ELFCLASS64 |
| 0x05 | EI_DATA | 1 | 1=小端,2=大端 |
| 0x06 | EI_VERSION | 1 | 恒 1 |
| 0x07 | EI_OSABI | 1 | 0=System V,3=Linux,9=FreeBSD;固件常见 0(裸机) |
| 0x08 | EI_ABIVERSION | 1 | 0 |
| 0x09-0x0F | 填充 | 7 | 0 |
### 字段表(64 位,总 64 字节;32 位总 52 字节)
| 偏移(64) | 名称 | 类型 | 含义 |
|---|---|---|---|
| 0x10 | e_type | u16 | 1=REL(可重定位 .o) 2=EXEC(固定基址) 3=DYN(PIE/共享库) 4=CORE |
| 0x12 | e_machine | u16 | 62=x86-64 3=i386 40=ARM 183=AArch64 8=MIPS 20=PPC 243=RISC-V |
| 0x14 | e_version | u32 | 1 |
| 0x18 | e_entry | u64 | 入口点(DYN 下为相对基址的偏移) |
| 0x20 | e_phoff | u64 | 程序头表文件偏移 |
| 0x28 | e_shoff | u64 | 节头表文件偏移 |
| 0x30 | e_flags | u32 | 架构相关(如 MIPS 的 ABI/ISA/PIC/NaN 等位,见 EF_MIPS_*;**不含字节序**——字节序始终看 e_ident[EI_DATA]) |
| 0x34 | e_ehsize | u16 | 64(32 位为 52) |
| 0x36 | e_phentsize | u16 | 56(32 位为 32) |
| 0x38 | e_phnum | u16 | 程序头条数(=PN_XNUM 0xffff 时真实值在 shdr[0].sh_info) |
| 0x3A | e_shentsize | u16 | 64(32 位为 40) |
| 0x3C | e_shnum | u16 | 节头条数(=0 时真实值在 shdr[0].sh_size——与 e_phnum 不是同一套规则) |
| 0x3E | e_shstrndx | u16 | 节名表(.shstrtab)在节头表里的索引(=SHN_XINDEX 0xffff 时真实索引在 shdr[0].sh_link) |
32 位偏移变化:e_entry=0x18(4B)、e_phoff=0x1C、e_shoff=0x20、e_flags=0x24、e_ehsize=0x28、e_phentsize=0x2A、e_phnum=0x2C、e_shentsize=0x2E、e_shnum=0x30、e_shstrndx=0x32。
## 程序头(phdr)——加载视图
64 位每条 56 字节;32 位 32 字节。
| 偏移(64) | 名称 | 类型 | 含义 |
|---|---|---|---|
| 0x00 | p_type | u32 | 1=PT_LOAD 2=PT_DYNAMIC 3=PT_INTERP 4=PT_NOTE 6=PT_PHDR 7=PT_TLS;GNU 私有:0x6474e550=GNU_EH_FRAME 0x6474e551=GNU_STACK 0x6474e552=GNU_RELRO |
| 0x04 | p_flags | u32 | 1=PF_X 2=PF_W 4=PF_R |
| 0x08 | p_offset | u64 | 段在文件中的偏移 |
| 0x10 | p_vaddr | u64 | 段装载虚拟地址(DYN 为相对基址偏移) |
| 0x18 | p_paddr | u64 | 物理地址(多数平台忽略) |
| 0x20 | p_filesz | u64 | 文件内大小(readelf 要求 ≤ memsz) |
| 0x28 | p_memsz | u64 | 内存大小(> filesz 部分为零填充,如 .bss) |
| 0x30 | p_align | u64 | 对齐(页 0x1000;p_vaddr≡p_offset mod align) |
32 位顺序不同:p_type、p_offset、p_vaddr、p_paddr、p_filesz、p_memsz、p_flags、p_align(p_flags 在第 7 个而不是第 2 个)。
- PT_LOAD:定义需要装载进进程内存映像的 loadable segment(按 p_filesz/p_memsz 建立常规文件→内存映射);可执行文件至少 1 个,通常 2-4 个(R、R X、R W)。**不要表述成"唯一被映射的段类型"**——PT_PHDR 在存在时同时描述程序头表在文件与内存映像中的位置,PT_TLS 等也具运行时内存语义
- PT_DYNAMIC:指向 .dynamic 段(DT_* 标签数组),动态链接入口
- PT_INTERP:解释器路径字符串(`/lib64/ld-linux-x86-64.so.2`)
- PT_GNU_STACK:**存在时**由 PF_X 决定是否请求可执行栈(内核按该 header 的 p_flags 控制用户栈权限)。**缺失时不能一律推断**,且分两个层面:内核装载主程序时按目标 ABI/实现决定(x86-64 上缺失按不可执行处理,ia32 上缺少 PT_GNU_STACK 会走 READ_IMPLIES_EXEC 使映射实际可执行);glibc 的 `dlopen` 判据不同——缺 header 时按该 ABI 的默认栈权限处理(x86/x86-64/arm32 等视为要求可执行栈、aarch64/riscv 等不要求),"要求可执行栈而进程未启用"即报 `cannot enable executable stack`(见 SKILL.md 坑)
- PT_GNU_RELRO:该范围映射为只读(配合 BIND_NOW 为全 RELRO)
## 节头(shdr)——静态视图
64 位每条 64 字节;32 位 40 字节。
| 偏移(64) | 名称 | 类型 | 含义 |
|---|---|---|---|
| 0x00 | sh_name | u32 | 节名在 .shstrtab 内的偏移(0=无名) |
| 0x04 | sh_type | u32 | 1=PROGBITS 2=SYMTAB 3=STRTAB 4=RELA 5=HASH 6=DYNAMIC 8=NOBITS(.bss) 9=REL 11=DYNSYM 14=INIT_ARRAY 15=FINI_ARRAY;0x6ffffff6=GNU_HASH |
| 0x08 | sh_flags | u64 | 1=SHF_WRITE 2=SHF_ALLOC 4=SHF_EXECINSTR(A 标志 = 需装载进内存) |
| 0x10 | sh_addr | u64 | 装载后的虚拟地址(非 ALLOC 节为 0) |
| 0x18 | sh_offset | u64 | 文件偏移(NOBITS 节无内容) |
| 0x20 | sh_size | u64 | 节大小 |
| 0x28 | sh_link | u32 | 关联节索引(符号节→字符串节;RELA→符号表) |
| 0x2C | sh_info | u32 | 附加信息(RELA:重定位的节索引) |
| 0x30 | sh_addralign | u64 | 对齐 |
| 0x38 | sh_entsize | u64 | 定长条目大小(符号 24、重定位 24、DT_ 16),0=不定长 |
## 常见 section 布局
典型非 PIE x86-64 可执行文件(地址从左到右递增):
```
文件偏移 节 内存(vaddr) 属性
0x000000 ELF header (64B)
0x000040 Program headers (e_phnum×56B)
0x0001a8 .interp 0x4001a8 R (解释器路径)
.note.gnu.property .note.ABI-tag
.gnu.hash 符号哈希(新式查找)
.dynsym 动态符号表(24 字节/条) A
.dynstr 动态符号字符串表 A
.rela.dyn 重定位(DATA/RELATIVE) A
.rela.plt 重定位(JUMP_SLOT) A
.text 代码 AX
.rodata 只读数据 A
.eh_frame_hdr/.eh_frame
.init_array/.fini_array 构造/析构指针数组 AW
.dynamic DT_* 标签数组 AW
.got/.got.plt GOT AW
.data 已初始化数据 AW
.bss 零初始化(NOBITS) AW
文件尾 .symtab/.strtab(调试符号,可被 strip)
.shstrtab 节名表
Section headers(e_shoff 指向)
```
布局要点:
- 程序头(加载视图)与节头(静态视图)是两套并行的视图:节可以不在任何段里(.comment、.shstrtab),段也可以不覆盖节头表(多数二进制如此)
- 动态区(.dynsym/.dynstr/.gnu.hash/.rela.*/.dynamic/.got)全部带 SHF_ALLOC,因为运行时需要;.symtab/.strtab 不带——被 strip 后只剩动态区
- 惰性绑定二进制有 .plt/.plt.got + .rela.plt;`-z now`(全 RELRO)二进制 GOT 装载时填好,只读
- 现代发行版默认 `-z now` + PIE(e_type=DYN),部分构建无 .plt(GOT 直调)
## 动态链接对象链(符号解析路径)
```
.dynamic (DT_* 标签)
├─ DT_SYMTAB → .dynsym(每 24 字节一个 Elf64_Sym: st_name→dynstr 偏移, st_value, st_size, st_info(bind+type), st_shndx)
├─ DT_STRTAB/DT_STRSZ → .dynstr(符号名字符串池)
├─ DT_GNU_HASH / DT_HASH → 符号查找表(GNU_HASH 新格式:bloom 过滤器 + 桶 + 链;DT_HASH 旧格式仅桶链)
├─ DT_JMPREL + DT_PLTRELSZ → .rela.plt(R_X86_64_JUMP_SLOT: GOT 槽 → 实际函数地址)
├─ DT_RELA + DT_RELASZ → .rela.dyn(GLOB_DAT/RELATIVE/64)
└─ DT_INIT_ARRAY/DT_INIT_ARRAYSZ → 构造函数指针数组(main 前执行)
```
- R_X86_64_GLOB_DAT(6):GOT 槽 = 符号地址;R_X86_64_JUMP_SLOT(7):PLT/GOT 跳转槽;R_X86_64_RELATIVE(8):槽 = 基址 + addend
- DT_BIND_NOW(或 DT_FLAGS 含 BIND_NOW):装载时完成全部重定位,GOT 只读(全 RELRO 前提)
## 实现教训(内化)
- 一切偏移以 e_ident[4](class)分派:32/64 位结构大小不同,解析器先读 class 再选格式
- **扩展编号(extended numbering)是三套独立规则**,手写解析器必须分别实现,不能共用一套逻辑:`e_phnum==PN_XNUM(0xffff)` → 真实程序头数在 `shdr[0].sh_info`;`e_shnum==0` → 真实节头数在 `shdr[0].sh_size`;`e_shstrndx==SHN_XINDEX(0xffff)` → 真实索引在 `shdr[0].sh_link`。三者都以"节 0 存在"为前提——建议配 parser fixture 验证这三条路径(工具一般已处理)
- 大端目标(MIPS 固件常见)解析前按 EI_DATA 换字节序,别默认小端
- readelf 能正常解析 ≠ 结构"正确":工具解析器有容错,关键结论(GOT 可写性、加载范围)对照 phdr 手工核一遍
## 使用注意
- 静态分析无需沙箱;跨架构目标(ARM/MIPS/RISC-V)用 `readelf -h -l` 不依赖本机架构,反汇编才需要交叉工具
- 与 [[re-imports]](导入导出/劫持面)、[[re-triage]](初勘)配合使用