第11节-ELF文件格式
第 11 节:ELF 文件格式
所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 6 周
课程主题:可执行文件与程序装载
本节目标:理解 Linux 下 ELF 可执行文件的基本结构,掌握 ELF Header、Program Header、Section Header、.text、.data、.bss、.rodata、.plt、.got等概念,并能使用readelf、objdump、file等工具观察 ELF 文件。
1. 本节课你要学会什么
学完这一节,你应该能回答下面几个问题:
- ELF 是什么?
- Linux 为什么能识别一个文件是可执行文件?
- ELF Header 里保存了什么?
- Program Header 和 Section Header 有什么区别?
.text、.data、.bss、.rodata分别放什么?- 动态链接和静态链接有什么区别?
.plt和.got是什么?- 为什么逆向工程必须理解 ELF?
- 为什么漏洞利用经常关注 ELF 保护机制和段权限?
- 如何用
readelf、objdump、nm、ldd查看 ELF 信息?
本节课的主线是:
1 | |
前面你已经学习了程序如何运行、汇编、系统调用;这一节开始具体观察 Linux 可执行文件本身的结构。
2. ELF 是什么
ELF 全称是:
1 | |
中文常译为:
1 | |
ELF 是 Linux、Unix-like 系统中常见的二进制文件格式。
常见 ELF 文件包括:
- 可执行程序
- 目标文件
.o - 共享库
.so - core dump 文件
例如:
1 | |
它们都可能是 ELF 格式。
3. ELF 为什么重要
ELF 很重要,因为它连接了几个关键领域。
3.1 对操作系统
操作系统需要根据 ELF 文件中的信息知道:
- 这是不是可执行文件
- 面向什么 CPU 架构
- 程序入口点在哪里
- 哪些内容需要加载到内存
- 每个内存区域权限是什么
- 是否需要动态链接器
- 依赖哪些共享库
3.2 对逆向工程
逆向 ELF 程序时,你要看:
- 入口点
- 函数代码
- 字符串
- 符号表
- 动态库依赖
- 导入函数
- PLT/GOT
- 段权限
这些都来自 ELF 结构。
3.3 对漏洞利用
漏洞利用中常关注:
- 是否开启 PIE
- 是否开启 NX
- 是否有 Canary
- RELRO 状态
- GOT 是否可写
- libc 是否动态链接
- 程序基址是否固定
- 可执行段权限
这些都和 ELF 装载、链接和保护机制有关。
4. 准备一个示例程序
创建 elf_demo.c:
1 | |
编译:
1 | |
后面所有实验都可以围绕这个程序展开。
5. 用 file 识别 ELF
执行:
1 | |
你可能看到类似:
1 | |
逐段解释:
| 字段 | 含义 |
|---|---|
| ELF | 文件格式是 ELF |
| 64-bit | 64 位程序 |
| LSB | 小端序 |
| pie executable | 位置无关可执行文件,支持地址随机化 |
| x86-64 | 目标 CPU 架构 |
| dynamically linked | 动态链接 |
| interpreter | 动态链接器路径 |
| with debug_info | 包含调试信息 |
| not stripped | 没有去除符号 |
这一条命令能快速告诉你二进制的基本属性。
逆向拿到陌生 Linux 文件时,第一步通常就是:
1 | |
6. ELF 文件的整体结构
一个 ELF 文件可以从两个角度看。
1 | |
简化图:
1 | |
注意:
这个图只是简化。
真实 ELF 中各部分位置和数量会根据编译选项、链接方式、是否 strip、是否 PIE 等发生变化。
7. ELF Header 是什么
ELF Header 是 ELF 文件开头的一段头部信息。
它告诉系统:
- 这是 ELF 文件
- 32 位还是 64 位
- 大端还是小端
- 目标架构是什么
- 文件类型是什么
- 程序入口点在哪里
- Program Header Table 在哪里
- Section Header Table 在哪里
查看:
1 | |
你会看到类似:
1 | |
8. ELF Magic Number
ELF 文件开头有魔数。
1 | |
其中:
1 | |
所以开头可以理解为:
1 | |
操作系统和工具可以通过这个魔数识别 ELF 文件。
你可以用:
1 | |
查看前 16 字节。
你会看到类似:
1 | |
这就是 ELF 标识。
9. Entry point address 是什么
readelf -h 中有一项:
1 | |
它表示程序入口点地址。
注意:
入口点通常不是
main。
程序真正开始执行的位置通常是运行时启动代码。
大致流程:
1 | |
所以你不能简单认为入口点就是业务逻辑开始处。
逆向时,需要从入口点追踪到 main,或者让工具帮你识别 main。
10. Program Header 是什么
Program Header Table 描述程序装载时需要的段。
它是给操作系统加载器看的。
查看:
1 | |
你会看到多个 Program Header,例如:
1 | |
重点看:
| 字段 | 含义 |
|---|---|
| Type | 段类型 |
| Offset | 文件中偏移 |
| VirtAddr | 加载到内存后的虚拟地址 |
| FileSiz | 文件中占用大小 |
| MemSiz | 内存中占用大小 |
| Flags | 权限,如 R/W/E |
| Align | 对齐 |
11. LOAD Segment
LOAD 类型表示这个段需要被加载到内存。
常见权限:
1 | |
例如:
1 | |
这和内存保护有关。
正常情况下:
1 | |
这就是 W^X 思想的一部分:
1 | |
12. Section Header 是什么
Section Header Table 描述 ELF 中的节。
它更多是给链接器、调试器、逆向工具看的。
查看:
1 | |
你会看到很多 section,例如:
1 | |
节更细,段更粗。
一个 Segment 可能包含多个 Section。
可以这样理解:
1 | |
13. Program Header 和 Section Header 的区别
这是 ELF 学习中的重点。
| 对比 | Program Header | Section Header |
|---|---|---|
| 面向对象 | 操作系统加载器 | 链接器、调试器、分析工具 |
| 关注问题 | 如何加载到内存 | 文件内部如何组织内容 |
| 单位 | Segment | Section |
| 命令 | readelf -l |
readelf -S |
| 运行是否必须 | 可执行装载通常需要 | 运行时不一定必须 |
一个被 strip 的 ELF 仍然能运行,甚至 Section Header 也可能被去掉或破坏,但只要必要的 Program Header 和装载信息存在,系统仍可能执行它。
逆向工具则非常依赖 Section 信息来帮助分析。
14. .text 节
.text 通常存放程序机器指令。
例如:
mainhelper- 编译器生成的代码
- 运行时相关代码
查看反汇编:
1 | |
只看 .text:
1 | |
.text 通常属于可执行段。
权限一般是:
1 | |
即:
1 | |
15. .rodata 节
.rodata 是只读数据节。
常见内容:
- 字符串常量
- const 数据
- switch jump table 等只读表
例如程序中的:
1 | |
这些字符串通常会放到 .rodata。
查看:
1 | |
或者:
1 | |
逆向中,.rodata 很重要,因为字符串常常是定位关键逻辑的入口。
16. .data 节
.data 存放已初始化的全局变量和静态变量。
例如:
1 | |
因为它有初始值,所以需要在文件中保存这个初始值。
查看 section:
1 | |
查看符号:
1 | |
你可能看到 global_var 类型为 D,表示它在已初始化数据区。
17. .bss 节
.bss 存放未初始化或初始化为 0 的全局变量和静态变量。
例如:
1 | |
它初始值为 0。
.bss 的特点是:
1 | |
所以 .bss 在文件中占用可能很小,但在内存中会占空间。
这就是 Program Header 中 FileSiz 和 MemSiz 可能不同的原因之一。
18. .symtab 和 .strtab
.symtab 是普通符号表。
.strtab 是普通字符串表,保存符号名。
如果程序没有被 strip,你可以用:
1 | |
看到:
1 | |
如果执行:
1 | |
可能看到:
1 | |
这说明普通符号表被移除了。
但程序仍然可以运行。
19. .dynsym 和 .dynstr
动态链接程序还需要动态符号表。
.dynsym 保存动态链接相关符号。
.dynstr 保存动态符号名字符串。
查看动态符号:
1 | |
或:
1 | |
你可能看到:
1 | |
这些动态符号用于运行时动态链接。
即使程序被 strip,动态符号往往仍然保留一部分,因为动态链接器需要它们。
20. 动态链接是什么
动态链接表示程序运行时依赖共享库。
例如:
1 | |
可能看到:
1 | |
说明程序依赖 libc 和动态链接器。
动态链接优点:
- 可执行文件较小
- 多个程序共享同一份库
- 库可以单独更新
缺点:
- 运行时需要依赖库存在
- 分析时需要考虑外部库调用
- 地址可能受 ASLR 影响
21. 静态链接是什么
静态链接会把库代码尽量打包进可执行文件。
编译示例:
1 | |
可能会生成较大的文件。
查看:
1 | |
你可能看到:
1 | |
静态链接优点:
- 依赖少
- 部署方便
缺点:
- 文件大
- 逆向时库代码混入程序,函数更多
- 安全更新不如动态库灵活
22. .plt 是什么
.plt 全称:
1 | |
可以理解为动态函数调用跳板。
当程序调用外部库函数,比如:
1 | |
反汇编中可能看到:
1 | |
这表示程序不是直接调用 libc 中最终地址,而是先调用 PLT 跳板。
PLT 会配合 GOT 和动态链接器找到真实函数地址。
入门理解:
1 | |
23. .got 是什么
.got 全称:
1 | |
GOT 中保存一些运行时需要解析的地址。
对于动态链接函数,GOT 可能保存最终解析出的 libc 函数地址。
简化理解:
1 | |
这对漏洞利用很重要。
早期或特定保护较弱情况下,如果攻击者能改写 GOT 表项,就可能把某个函数调用重定向到别的位置。
例如把 printf 的 GOT 项改成 system。
现代系统通过 RELRO 等机制增加 GOT 篡改难度。
24. 观察 PLT 和 GOT
查看 PLT:
1 | |
也可以看反汇编中外部函数调用:
1 | |
你可能看到:
1 | |
查看重定位信息:
1 | |
你可能看到和外部函数相关的重定位项。
这些信息和动态链接密切相关。
25. 重定位是什么
重定位可以理解为:
某些地址在编译链接时还不能最终确定,需要在链接或加载时修正。
例如动态库函数地址运行时才知道。
因为 ASLR 会让 libc 每次加载地址不同。
所以程序不能简单写死:
1 | |
动态链接器需要在运行时解析地址,并把相关位置修正好。
查看重定位:
1 | |
你可能看到:
1 | |
这些通常和动态函数调用有关。
26. PIE 是什么
PIE 全称:
1 | |
中文可理解为:
1 | |
启用 PIE 后,程序代码可以加载到随机基址。
这配合 ASLR 提高漏洞利用难度。
查看:
1 | |
如果看到:
1 | |
说明它是 PIE。
编译非 PIE:
1 | |
对比:
1 | |
非 PIE 程序代码地址通常更固定。
27. NX 是什么
NX 表示:
1 | |
即某些内存区域不可执行。
例如栈和堆通常不应该执行代码。
查看 GNU_STACK:
1 | |
你可能看到权限类似:
1 | |
如果没有 E,说明栈不可执行。
NX 对缓冲区溢出利用影响很大。
早期攻击可能把 shellcode 放在栈上执行。
NX 开启后,栈上数据不能直接作为代码执行。
28. RELRO 是什么
RELRO 全称:
1 | |
它和重定位、GOT 保护有关。
简化理解:
1 | |
常见状态:
- No RELRO
- Partial RELRO
- Full RELRO
如果安装了 checksec,可以查看:
1 | |
如果没有 checksec,可以先记住概念,后面漏洞防护章节会再讲。
29. Canary 和 ELF
Stack Canary 是编译器插入的栈保护机制。
它不只是 ELF 格式本身的字段,但会反映在二进制中。
如果程序启用 Canary,可能会导入:
1 | |
查看:
1 | |
如果看到 __stack_chk_fail,说明可能启用了栈保护。
编译关闭 Canary:
1 | |
后面漏洞防护章节会更深入分析。
30. ELF 和逆向工程
逆向 ELF 程序时,可以按这个顺序观察:
file看文件类型readelf -h看 ELF Headerreadelf -l看装载段和权限readelf -S看节表strings看字符串nm看符号objdump -d看反汇编ldd看动态库依赖readelf -r看重定位- Ghidra / IDA 深入分析函数逻辑
这些工具不是互相替代,而是从不同角度观察同一个 ELF。
31. ELF 和漏洞利用
漏洞利用中,ELF 信息能回答很多关键问题:
| 问题 | 相关信息 |
|---|---|
| 程序代码地址是否固定 | PIE |
| 栈是否可执行 | NX / GNU_STACK |
| GOT 是否容易改写 | RELRO |
| 是否有栈保护 | Canary / __stack_chk_fail |
| 是否动态链接 libc | ldd / dynamic section |
| 外部函数怎么调用 | PLT/GOT |
| 字符串在哪里 | .rodata |
| 函数名是否保留 | .symtab / strip |
所以做 Pwn 题或分析崩溃时,第一步经常是检查二进制保护和 ELF 结构。
32. ELF 和恶意代码分析
Linux 恶意样本也可能是 ELF。
分析时关注:
- 是否静态链接
- 是否 strip
- 是否 UPX 加壳
- 是否包含可疑字符串
- 是否调用网络相关函数
- 是否调用
execve、fork、ptrace - 是否使用
mprotect修改内存权限 - 是否使用
dlopen/dlsym动态解析函数 - 是否有异常节名或异常权限
例如:
1 | |
这些命令能快速建立样本画像。
33. Linux 实验:完整观察 ELF
围绕 elf_demo 执行:
1 | |
你要记录:
- ELF 类型
- 是否 64 位
- 是否 PIE
- 入口点地址
- LOAD 段权限
.text、.rodata、.data、.bss是否存在- 动态库依赖
- 是否能看到
main/helper - 是否能看到字符串
hello elf - 是否有
printf@plt、malloc@plt、free@plt
34. Linux 实验:strip 前后对比
执行:
1 | |
对比:
1 | |
思考:
- strip 后程序还能运行吗?
- strip 后
nm是否还能看到普通符号? - 字符串是否还在?
- 反汇编是否还能看到代码?
- 逆向难度增加在哪里?
35. Linux 实验:PIE 对地址的影响
编译两个版本:
1 | |
修改程序或用 GDB 打印 main 地址。
也可以运行:
1 | |
GDB 中:
1 | |
多运行几次。
再对 elf_demo_no_pie 做同样操作。
观察:
- PIE 版本
main地址是否变化 - no-PIE 版本
main地址是否更固定 - 这和 ASLR 有什么关系
36. 本节重点总结
你需要记住这些核心结论:
- ELF 是 Linux 常见的可执行、目标文件、共享库格式。
- ELF Header 描述文件基本属性和入口点等信息。
- Program Header 面向装载,描述哪些段加载到内存及其权限。
- Section Header 面向链接和分析,描述
.text、.data、.bss等节。 .text通常存放代码,.rodata存放只读数据和字符串。.data存放已初始化全局变量,.bss存放未初始化或零初始化全局变量。- 动态链接程序依赖共享库,通常通过 PLT/GOT 调用外部函数。
- PIE、NX、RELRO、Canary 等保护机制会影响漏洞利用难度。
readelf、objdump、nm、ldd、strings是分析 ELF 的基础工具。- 理解 ELF 是 Linux 逆向、Pwn、恶意代码分析的重要基础。
37. 本节课后作业
作业 1:ELF Header 分析
对 elf_demo 执行:
1 | |
提交内容:
1 | |
作业 2:Program Header 和权限分析
执行:
1 | |
提交内容:
1 | |
作业 3:Section 分析
执行:
1 | |
提交内容:
1 | |
作业 4:符号和 strip 对比
执行:
1 | |
提交内容:
1 | |
作业 5:PLT/GOT 和动态链接观察
执行:
1 | |
提交内容:
1 | |
作业 6:PIE 对比
编译:
1 | |
对比:
1 | |
用 GDB 多次查看 main 地址。
提交内容:
1 | |
38. 自测题
题 1
ELF 的全称是什么?常见于哪些类型文件?
题 2
ELF Header 中有哪些重要信息?
题 3
Program Header 和 Section Header 有什么区别?
题 4
.text、.rodata、.data、.bss 分别通常存放什么?
题 5
为什么 .bss 在文件中可能不真正保存大量 0?
题 6
动态链接和静态链接有什么区别?
题 7
PLT 和 GOT 的直觉作用是什么?
题 8
PIE、NX、RELRO 分别大致和什么安全目标有关?
题 9
为什么 strip 后程序还能运行,但逆向更难?
39. 自测题参考答案
答 1
ELF 全称是 Executable and Linkable Format。它常见于 Linux/Unix-like 系统中的可执行文件、目标文件 .o、共享库 .so 和 core dump 文件。
答 2
ELF Header 包含魔数、32/64 位、大小端、目标架构、文件类型、入口点地址、Program Header Table 位置、Section Header Table 位置等信息。
答 3
Program Header 面向操作系统加载器,描述哪些段要加载到内存及权限;Section Header 面向链接器、调试器和分析工具,描述文件内部各个节如 .text、.data 等。
答 4
.text 通常存放机器指令;.rodata 存放只读数据和字符串常量;.data 存放已初始化全局/静态变量;.bss 存放未初始化或零初始化全局/静态变量。
答 5
因为 .bss 中的数据初始值为 0,文件中只需记录需要多少空间,加载时由系统把这块内存清零即可,不需要在文件中保存大量 0 字节。
答 6
动态链接在运行时依赖共享库,文件较小且库可共享;静态链接把库代码打入可执行文件,依赖少但文件更大,库代码也会混入程序中。
答 7
PLT 可以理解为调用外部函数的跳板;GOT 保存运行时解析出来的外部函数或全局地址。二者配合动态链接器完成外部函数调用。
答 8
PIE 让程序基址可随机化,配合 ASLR 增加地址预测难度;NX 让栈/堆等数据区域不可执行;RELRO 保护重定位后的区域,降低 GOT 等位置被篡改的风险。
答 9
因为 CPU 执行程序主要需要机器指令、入口点、装载信息和必要动态链接信息,不需要普通符号名和调试信息。strip 去掉这些人类友好的信息后,程序仍能运行,但函数名、变量名、调试信息缺失,逆向更困难。
40. 下一节预告
下一节课会讲:
1 | |
你会学习:
- DOS Header
- PE Header
- Section Table
.text、.rdata、.data、.rsrc- Import Table 和 IAT
- DLL 加载
- PE 与 ELF 的对照
- 如何用 PE-bear、Detect It Easy、Ghidra 和 x64dbg 分析 Windows 可执行文件