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,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]](初勘)配合使用