ELF 重定位深度实战:从符号表解析到二进制修补的完整指南
在计算机系统底层开发中,理解 ELF 文件的重定位机制是掌握程序链接过程的关键。本文将带您深入探索 LinkLab 实验的五个阶段,通过实际操作演示如何解析符号表、计算节偏移以及修改重定位条目,最终形成一套完整的 ELF 文件分析与修改方法论。
1. ELF 重定位基础与实验环境搭建
ELF(Executable and Linkable Format)是现代 Linux 系统中最常用的二进制文件格式,它包含了程序运行所需的代码、数据以及元信息。理解 ELF 重定位,首先要掌握几个核心概念:
- 可重定位目标文件(.o 文件):包含未链接的机器代码,其中的符号引用尚未解析
- 符号表(.symtab):记录所有定义的符号及其属性
- 重定位条目(.rel.text/.rel.data):指示链接器如何修改符号引用
实验环境准备:
# 基础工具安装 sudo apt-get update sudo apt-get install -y binutils hexedit gcc # 验证工具版本 readelf --version | head -1 objdump --version | head -1关键工具说明:
| 工具 | 用途 | 常用参数示例 |
|---|---|---|
| readelf | 查看ELF文件结构信息 | -a(显示全部信息) |
| objdump | 反汇编和查看目标文件信息 | -d(反汇编代码段) |
| hexedit | 二进制文件编辑 | 直接编辑文件 |
| gcc | 编译和链接 | -o(指定输出文件名) |
2. 阶段一:全局变量与数据节修改
第一阶段的核心任务是修改全局变量在.data节中的内容。以下是详细操作步骤:
- 使用readelf分析符号表:
readelf -a phase1.o | grep -A5 "Symbol table"典型输出示例:
Symbol table '.symtab' contains 18 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS phase1.c ... 8: 0000000000000000 16 OBJECT GLOBAL DEFAULT 3 g_data- 定位.data节信息:
readelf -S phase1.o | grep -A3 ".data"输出示例:
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 3] .data PROGBITS 0000000000000000 00000060 0000000000000010 0000000000000000 WA 0 0 4- 计算修改位置:
.data节偏移 = 0x60 g_data在.data节中的偏移 = 0x11 实际修改位置 = 0x60 + 0x11 = 0x71- 使用hexedit修改二进制:
hexedit phase1.o在0x71位置输入学号的ASCII码(如"23215150438"对应32 33 32 31 35 31 35 30 34 33 38)
- 验证修改结果:
gcc -o linkbomb main.o phase1.o -no-pie ./linkbomb关键提示:在修改.data节时,务必注意数据的字节序和对齐要求。x86架构采用小端字节序,但ELF文件中的偏移量总是以大端格式表示。
3. 阶段二:强弱符号解析与覆盖
第二阶段重点在于理解强弱符号的交互规则:
- 强符号:已初始化的全局变量/函数
- 弱符号:未初始化的全局变量(COMMON符号)
操作流程:
- 分析弱符号特征:
readelf -a phase2.o | grep -A3 "g_myCharArray"输出示例:
10: 0000000000000000 256 OBJECT WEAK DEFAULT COM g_myCharArray- 创建强符号覆盖: 创建
phase2_patch.c文件:
char g_myCharArray[256] = { 0x00, 0x00, /* 前0x11个字节填充0 */ /* 学号ASCII码 */ 0x32, 0x33, 0x32, 0x31, 0x35, 0x31, 0x35, 0x30, 0x34, 0x33, 0x38 };- 编译与链接:
gcc -c phase2_patch.c gcc -o linkbomb2 main.o phase2.o phase2_patch.o -no-pie- 处理偏移问题: 当出现字符偏移时,可通过辅助程序计算实际偏移量:
// hack.c #include <stdio.h> int main() { char buf[256]; fgets(buf, sizeof(buf), stdin); printf("Actual output: %s\n", buf); return 0; }技术细节:弱符号的Size字段表示其最小分配空间,而强符号的实际大小必须至少达到这个值,否则可能导致内存越界。
4. 阶段三:代码节注入与函数调用重定位
第三阶段涉及代码节的直接修改和函数调用重定位,这是最复杂的环节之一。以下是关键步骤:
- 分析函数调用关系:
objdump -d linkbomb3输出示例:
0000000000001149 <do_phase>: 1149: e8 d2 ff ff ff call 1120 <myFunc2> 114e: 48 89 c7 mov %rax,%rdi 1151: e8 ba ff ff ff call 1110 <myFunc1>- 计算注入位置:
readelf -S phase3.o | grep -A1 ".text"输出示例:
[ 1] .text PROGBITS 0000000000000000 00000040 0000000000000031 0000000000000000 AX 0 0 1注入起始地址 = 0x40(.text节偏移) + 0x31(do_phase函数偏移) = 0x71
- 构造机器指令:
call myFunc2:相对调用,计算偏移量目标地址 = myFunc2地址 = 0x1b 当前指令结束地址 = 0x31 + 5 = 0x36 相对偏移 = 0x1b - 0x36 = -0x1b → 补码表示:0xff ff ff e5 完整指令:e8 e5 ff ff ffmov %rax,%rdi:机器码为48 89 c7call myFunc1:类似方法计算
- 二进制修补:
hexedit phase3.o在0x71位置依次写入构造的指令:
e8 e5 ff ff ff 48 89 c7 e8 c2 ff ff ff- 修改数据节内容:
readelf -S phase3.o | grep -A1 ".data".data节偏移 = 0x1a0 学号位置 = 0x1a0 + 0x11 = 0x1b1
深度解析:call指令使用PC相对寻址,其机器码格式为E8后跟4字节的相对偏移(目标地址 - 下条指令地址)。理解这一点对正确构造调用指令至关重要。
5. 阶段四与五:重定位表修复与符号交换
最后两个阶段涉及更复杂的重定位表操作:
阶段四关键步骤
- 识别异常重定位条目:
readelf -r phase4.o输出示例:
Relocation section '.rela.text' at offset 0x250 contains 3 entries: Offset Info Type Sym. Value Sym. Name + Addend 0000000000000000 000300000001 R_X86_64_64 0000000000000000 .data + 0 0000000000000000 000300000001 R_X86_64_64 0000000000000000 .data + 0 0000000000000000 000300000001 R_X86_64_64 0000000000000000 .data + 0- 修正重定位偏移量: 根据objdump输出确定正确偏移量(0x6, 0x11, 0x19),在hexedit中修改0x250位置的数据。
阶段五关键步骤
- 调试分析符号引用:
gdb ./linkbomb5 (gdb) b do_phase (gdb) run (gdb) x/s 0x601040- 交换重定位条目:
readelf -r phase5.o找到两个相关条目的偏移量(通常在0x340附近),交换它们的符号引用。
6. 重定位类型速查与实战总结
常见重定位类型对比:
| 类型 | 计算方式 | 用途 | 字节数 |
|---|---|---|---|
| R_X86_64_32 | S + A | 32位绝对地址 | 4 |
| R_X86_64_PC32 | S + A - P | 32位PC相对地址 | 4 |
| R_X86_64_64 | S + A | 64位绝对地址 | 8 |
| R_X86_64_PLT32 | L + A - P | 过程链接表相对地址 | 4 |
实战经验总结:
系统化分析方法:
- 始终从readelf -a开始全面分析ELF结构
- 使用objdump -d交叉验证代码逻辑
- 通过gdb动态调试确认运行时行为
常见问题解决:
- 段错误:通常由重定位地址计算错误导致
- 输出乱码:检查数据节的偏移和编码
- 链接失败:确认符号可见性和强弱关系
进阶技巧:
# 使用Python辅助计算重定位值 def calc_reloc(S, A, P, type): if type == 'R_X86_64_32': return S + A elif type == 'R_X86_64_PC32': return S + A - P # 其他类型...
通过这五个阶段的系统实践,我们不仅掌握了ELF重定位的核心机制,还建立了一套完整的二进制分析与修改方法论。这种底层技能在逆向工程、安全分析和系统编程等领域都具有极高的实用价值。