Files
reverse-skill/skills/reverse-engineering/elf-analysis.md
T
2026-08-08 18:32:33 +08:00

8.4 KiB
Raw Blame History

ELF 二进制深度分析参考

逆向 Linux/Android ELF 文件时的结构解析、反分析对抗识别和分析技巧。


ELF 结构速查

文件头 (ELF Header)

偏移  大小  字段              说明
0x00  4    e_ident[EI_MAG]   Magic: 7f 45 4c 46 ("\x7fELF")
0x04  1    e_ident[EI_CLASS] 1=32bit, 2=64bit
0x05  1    e_ident[EI_DATA]  1=LE, 2=BE
0x10  2    e_type            2=EXEC, 3=DYN(PIE/SO), 4=CORE
0x12  2    e_machine         0x03=x86, 0x3E=x86_64, 0xB7=AArch64, 0x28=ARM
0x18  8    e_entry           入口点虚拟地址
0x20  8    e_phoff           程序头表偏移
0x28  8    e_shoff           节头表偏移(strip 后可能为 0)
0x38  2    e_phnum           程序头数量
0x3C  2    e_shnum           节头数量

程序头 (Program Header)

类型值  名称       说明
0x01   PT_LOAD    可加载段(代码/数据)
0x02   PT_DYNAMIC 动态链接信息
0x03   PT_INTERP  解释器路径(/lib/ld-linux.so)
0x04   PT_NOTE    辅助信息
0x06   PT_PHDR    程序头表自身
0x6474e550 PT_GNU_EH_FRAME  异常处理
0x6474e551 PT_GNU_STACK     栈可执行标记
0x6474e552 PT_GNU_RELRO     只读重定位

常见节 (Sections)

节名 说明
.text 代码段
.rodata 只读数据(字符串常量)
.data 已初始化全局变量
.bss 未初始化全局变量
.plt / .got 动态链接跳转表
.init_array 构造函数指针数组
.fini_array 析构函数指针数组
.dynamic 动态链接信息
.symtab / .dynsym 符号表
.strtab / .dynstr 字符串表

反分析手法识别

常见 ELF 反分析技术

手法 特征 对抗方式
损坏程序头 PHDR 填充垃圾数据(如 0x0a) 手动修复或忽略损坏的 PHDR
无 section header e_shoff = 0, e_shnum = 0 只依赖程序头分析,不依赖 section
去符号 (strip) 无 .symtab,函数名全丢 GoReSym(Go) / 签名匹配 / FLIRT
静态链接 无 .dynamic,体积巨大 用 FLIRT/Lumina 识别库函数
伪装文件类型 后缀 .sh/.txt/.jpg 用 file 命令 / magic bytes 判断
UPX 加壳 包含 UPX! 标记 upx -d 脱壳
自定义壳 入口点跳转到解压代码 动态运行到 OEP 后 dump
反调试 ptrace(TRACEME) LD_PRELOAD hook / patch
反虚拟机 检查 /proc/cpuinfo 修改 cpuinfo 或 hook 读取
代码加密 运行时解密 .text 断点在解密后 dump

识别自解压/自修改代码

特征:
1. 入口点附近有 mmap(PROT_READ|PROT_WRITE|PROT_EXEC) 调用
2. 紧接着有 memcpy 或循环拷贝
3. 然后 mprotect 改权限
4. 最后 br/jmp 到新映射的地址

分析策略:
1. 找到 mmap 调用 → 记录返回的地址
2. 在 mprotect(PROT_EXEC) 后下断点
3. dump 解压后的内存区域
4. 作为新的二进制分析

ARM64 (AArch64) 逆向速查

寄存器

寄存器 用途
x0-x7 参数/返回值
x8 间接结果(syscall 号)
x9-x15 临时寄存器
x16-x17 IP0/IP1(PLT 跳转)
x18 平台寄存器(Android: shadow call stack)
x19-x28 被调用者保存
x29 (FP) 帧指针
x30 (LR) 链接寄存器(返回地址)
SP 栈指针
PC 程序计数器

常见指令模式

函数序言:
  stp x29, x30, [sp, #-N]!    # 保存 FP 和 LR
  mov x29, sp                  # 设置帧指针

函数尾声:
  ldp x29, x30, [sp], #N      # 恢复 FP 和 LR
  ret                          # 返回(br x30)

系统调用:
  mov x8, #NR                  # syscall 号
  svc #0                       # 触发 syscall

条件分支:
  cmp x0, #0
  b.eq label                   # 等于跳转
  b.ne label                   # 不等于跳转
  cbz x0, label                # x0 == 0 跳转
  cbnz x0, label               # x0 != 0 跳转

地址加载:
  adrp x0, page                # 加载页地址高位
  add x0, x0, #offset          # 加低 12 位偏移
  ldr x0, [x1, #offset]        # 从内存加载

Linux ARM64 系统调用号

号码 名称 说明
56 openat 打开文件
63 read 读取
64 write 写入
57 close 关闭
222 mmap 内存映射
226 mprotect 修改内存权限
117 ptrace 进程跟踪
220 clone 创建进程/线程
221 execve 执行程序
93 exit 退出
94 exit_group 退出进程组

常见压缩/打包算法识别

算法 识别特征 解压方式
LZSS 位流 + 字面量/匹配标记 自定义解压器(如本报告)
ZLIB/Deflate Magic: 78 01/78 9C/78 DA zlib.decompress()
GZIP Magic: 1F 8B gzip -d / gunzip
LZ4 Magic: 04 22 4D 18 lz4 -d
LZMA/XZ Magic: FD 37 7A 58 5A 00 (XZ) xz -d / lzma -d
Brotli 无固定 magic,看上下文 brotli -d
Zstandard Magic: 28 B5 2F FD zstd -d
UPX 字符串 UPX! upx -d
自定义 入口点有解压循环 逆向算法后写解压器

识别自定义压缩的线索

1. 入口点附近有循环 + 位操作(移位、AND、OR)
2. 有"滑动窗口"回拷(从输出缓冲区往回读)→ LZ 系列
3. 有频率表/霍夫曼树构建 → Deflate/Huffman
4. 有固定大小块处理 → 块压缩(LZ4/Snappy)
5. 有算术编码特征(区间缩小)→ LZMA/ANS

Linux 进程注入技术

mmap + 代码注入

流程:
1. mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0)
2. 将 shellcode/payload 写入映射区域
3. mprotect(addr, size, PROT_READ|PROT_EXEC)  # 改为可执行
4. 跳转到映射地址执行

特征:
- mmap 返回值被保存
- 紧接着有 memcpy 或循环写入
- 然后 mprotect 改权限
- 最后 br/blr 到该地址

ptrace 注入

流程:
1. ptrace(PTRACE_ATTACH, target_pid)
2. waitpid(target_pid)
3. ptrace(PTRACE_GETREGS, target_pid, &regs)
4. 修改 regs.pc 指向注入代码
5. ptrace(PTRACE_SETREGS, target_pid, &regs)
6. ptrace(PTRACE_CONT, target_pid)

特征:
- 打开 /proc/<pid>/mem 或使用 ptrace
- 读取/修改目标进程寄存器
- 写入 shellcode 到目标进程空间

/proc/self/mem 自修改

流程:
1. open("/proc/self/mem", O_RDWR)
2. lseek(fd, target_addr, SEEK_SET)
3. write(fd, new_code, size)

用途:
- 绕过 W^X 保护(mmap 的页不能同时 W+X)
- 修改自身代码段(.text 通常是只读的)
- 运行时 patch 指令

分析大型 ELF 的策略

对于 5MB+ 的大型二进制:

1. 快速侦察(5 分钟)
   - file / rabin2 -I → 架构、类型、保护
   - strings | grep -i "error\|fail\|http\|/proc\|/dev" → 关键字符串
   - rabin2 -i → 导入函数(如果有)
   - rabin2 -E → 导出函数

2. 结构分析(10 分钟)
   - readelf -l → 程序头(LOAD 段布局)
   - 入口点附近代码 → 是否有解压/解密
   - 找 .init_array → 构造函数(可能有反调试)

3. 定位关键逻辑
   - 从字符串交叉引用入手
   - 从系统调用(mmap/ptrace/open)入手
   - 从网络函数(connect/send/recv)入手

4. 分而治之
   - 如果是自解压 → 先解压,分析 payload
   - 如果是多模块 → 按功能分块分析
   - 用 binary-diff 对比不同版本

工具命令速查

# 基本信息
file binary
readelf -h binary          # ELF 头
readelf -l binary          # 程序头
readelf -S binary          # 节头(如果有)
rabin2 -I binary           # 综合信息

# 字符串
strings -a binary | less
rabin2 -z binary           # 数据段字符串
rabin2 -zz binary          # 全文件字符串

# 反汇编
r2 -A binary               # radare2 分析
objdump -d binary          # GNU 反汇编
aarch64-linux-gnu-objdump -d binary  # ARM64 交叉反汇编

# 动态分析
strace -f ./binary         # 系统调用跟踪
ltrace -f ./binary         # 库函数跟踪
qemu-aarch64 -strace ./binary  # ARM64 模拟执行

# 内存 dump
gdb -p <pid> -ex "dump memory out.bin 0xADDR 0xADDR+SIZE" -ex quit

# 修复损坏的 ELF
# 手动修改 e_phnum 或 patch 损坏的 PHDR
python -c "
import struct
with open('binary', 'r+b') as f:
    f.seek(0x38)  # e_phnum offset (64-bit)
    f.write(struct.pack('<H', 2))  # 修改为正确的 PHDR 数量
"