4、ELF文件格式解析:ELF头部、节区表、程序头表、动态链接与重定位、常见段(.text、.data、.bss、.got、.plt)
在CTF的Pwn与逆向工程中,绝大多数题目都基于Linux下的ELF(Executable and Linkable Format)文件。无论是分析栈溢出、格式化字符串漏洞,还是手工构造ROP链、劫持GOT表,都离不开对ELF文件结构的深刻理解。ELF不仅是一种文件格式,更是操作系统加载、链接器解析、调试器定位符号的“契约”。本章将从ELF头部出发,逐层拆解节区表(Section Header Table)与程序头表(Program Header Table),并深入动态链接与重定位机制,最后结合CTF实战场景剖析.text、.data、.bss、.got、.plt等关键段的作用与利用价值。
上图展示了ELF文件在静态与动态两个维度上的核心结构。左侧的ELF头部是文件的“总索引”,通过e_phoff和e_shoff分别指向程序头表和节区表;右侧的常见段则是链接器与加载器实际操作的“内容单元”。理解这张图,就掌握了ELF分析的全局脉络。
4.1 ELF头部:一切解析的起点
核心原理:ELF头部(Elf64_Ehdr)固定位于文件起始位置,大小为64字节(64位系统)。它定义了文件的类型(可重定位文件、可执行文件、共享对象)、目标架构、程序入口地址、程序头表和节区表的偏移及条目数量。在CTF中,我们常用readelf -h或xxd快速查看头部信息,以确认文件是32位还是64位、是否开启PIE(地址随机化)、入口点在哪里。
判断方法:使用readelf -h ./pwn命令,重点关注以下字段:
- Type:EXEC(固定地址)或DYN(PIE/共享对象),决定能否直接使用固定地址构造ROP。
- Machine:Advanced Micro Devices X86-64,确认指令集。
- Entry point address:程序入口,通常是
_start,但PIE开启时该地址是相对偏移。 - Start of section headers:节区表偏移,用于解析节区。
4.2 程序头表:进程内存布局的蓝图
核心原理:程序头表(Program Header Table)是内核加载ELF时唯一参考的结构。每个条目(Elf64_Phdr)描述一个段(Segment),常见类型包括PT_LOAD(可加载段)、PT_DYNAMIC(动态链接信息)、PT_INTERP(解释器路径)、PT_GNU_STACK(栈可执行性)。PT_LOAD段指定了文件中的某段内容应映射到进程虚拟地址空间的哪个位置,以及映射后的权限(R/E/W)。
操作流程:使用readelf -l ./pwn查看程序头表,重点关注:
- Offset 与 VirtAddr:文件偏移与虚拟地址的对应关系,用于计算ASLR偏移。
- Flags:R(读)、E(执行)、W(写)。通常.text所在段为R+E,.data和.bss所在段为R+W。
- PT_GNU_STACK:若该段Flags包含E,则栈可执行,可考虑直接注入shellcode;否则需要ROP。
案例拆解:假设一个64位Pwn题,readelf -l显示有两个PT_LOAD段。第一段包含ELF头部和.text,权限为R+E;第二段包含.data和.bss,权限为R+W。当程序开启PIE时,内核会将整个文件按页映射到随机基址,但两段之间的相对偏移不变。因此,如果我们泄露了puts@got(位于第二段)的地址,就可以通过固定偏移计算出_start(位于第一段)的真实地址,从而绕过PIE。
4.3 节区表与常见段:.text、.data、.bss
核心原理:节区表(Section Header Table)是ELF文件的“目录”,每个条目描述一个节(Section)的名称、类型、地址、偏移、大小等信息。对于CTF逆向,最常关注的节如下:
| 节名 | 内容 | 权限 | CTF关注点 |
|---|---|---|---|
| .text | 程序机器码 | 只读+执行 | 反汇编分析漏洞函数,查找gadget |
| .data | 已初始化的全局变量 | 读写 | 存放flag或关键指针,可被覆写 |
| .bss | 未初始化的全局变量 | 读写 | 不占文件空间,适合存放伪造结构或二次payload |
| .got | 全局偏移表(外部函数地址) | 读写(延迟绑定后) | GOT覆写攻击,泄露libc地址 |
| .plt | 过程链接表(跳板) | 只读+执行 | 调用外部函数,ret2plt |
判断方法:使用readelf -S ./pwn查看节区表,注意每个节的Addr(虚拟地址)、Off(文件偏移)、Size。在IDA或Ghidra中,这些节会以不同颜色区分,便于快速定位。
实战场景:在格式化字符串漏洞中,我们常利用%n向任意地址写入数据。如果目标地址在.bss段(无文件内容),我们需要先通过readelf -S确认.bss的虚拟地址范围。例如,某题.bss基址为0x404060,我们可将payload写入.bss+0x100处,避免破坏全局变量。
read@plt,将伪造的“结构体”写入.bss,其中包含system("/bin/sh")的地址。最终在程序退出时,该指针被调用,成功getshell。此案例说明,理解.bss的用途和地址布局,是构造复杂利用链的基础。
4.4 动态链接与重定位:GOT与PLT的协作机制
核心原理:动态链接允许可执行文件在运行时加载共享库(如libc.so)。为了实现这一点,ELF引入了两个关键结构:GOT(Global Offset Table)和PLT(Procedure Linkage Table)。PLT中的每个条目对应一个外部函数(如puts),它是一段短小的跳转代码。首次调用时,PLT跳转到GOT中记录的地址,该地址初始指向PLT的下一条指令(即解析代码);解析器(通常是_dl_runtime_resolve)找到真实函数地址后,将其写入GOT。后续调用直接通过GOT跳转,无需再次解析。重定位表(.rela.plt / .rela.dyn)记录了哪些GOT条目需要被修正,以及修正类型(如R_X86_64_JUMP_SLOT)。
操作流程:在CTF中,我们通常使用objdump -R ./pwn查看重定位表,或使用readelf -r。关键信息包括:
- Offset:GOT条目的虚拟地址,即写入真实地址的位置。
- Type:R_X86_64_JUMP_SLOT(函数地址)、R_X86_64_GLOB_DAT(全局变量地址)。
- Symbol Name:对应的函数或变量名。
案例拆解:假设我们想要泄露libc地址。首先通过objdump -R找到puts@GOT的地址(例如0x404018)。然后利用格式化字符串漏洞,用%7$s读取该地址处存储的内容(即puts的真实地址)。由于puts在libc中的偏移是固定的,我们通过libc-database或在线工具匹配偏移,计算出libc基址,进而找到system和/bin/sh的地址。
.got(全局变量)和.got.plt(函数地址)。.got.plt的前三项是保留项(分别指向动态段、链接映射和解析器),从第四项开始才是函数地址。覆写GOT时,务必确认目标地址属于.got.plt且对应正确的函数。
总结复盘:ELF格式是Pwn与逆向的基石。掌握ELF头部、程序头表、节区表、动态链接与重定位,意味着你能够:
- 通过
readelf快速定位关键段和地址,判断防护机制(PIE、RELRO、NX)。 - 理解程序加载后的内存布局,为ROP、栈迁移、堆利用提供地址基础。
- 利用GOT/PLT机制进行地址泄露、函数劫持,甚至构造ret2dl-resolve攻击。
- 在逆向分析时,通过节区表快速定位用户代码、全局变量和外部依赖。
checksec脚本(pwntools自带)一键查看所有安全属性,再结合readelf -a输出完整信息。对于复杂题目,可以先用readelf -S和-l画出内存布局图,再开始漏洞分析。