写在前面:进入 Week2,我们需要把目光从宏观的保护机制下沉到微观的文件结构中。在平时用
readelf或objdump分析二进制时,我们经常会看到“节区”和“段”这两个词。它们到底有啥区别?我们天天喊的“打 GOT 表”,GOT 表到底在文件结构的哪个位置?本文将为你彻底讲透。
📑 目录
- ELF 双视图:节区 与 段 的本质区别
- 核心节区速览
- 动态链接的枢纽:.plt 与 .got 详解
- PWN 视角:为什么要区分 .got 和 .got.plt?
1. ELF 双视图:节区 与 段 的本质区别
ELF (Executable and Linkable Format) 文件可以从两个不同的维度来看待:链接视图和执行视图。这也是节区和段的最根本区别。
1.1 链接视图(节区 Section)
- 服务对象:链接器。
- 作用:链接器需要知道代码放在哪、数据放在哪、符号表在哪。因此 ELF 被划分成了无数个细分的“节区”。
- 特点:粒度极细。比如
.text(代码)、.data(已初始化全局变量)、.bss(未初始化全局变量)都是独立的节区。
1.2 执行视图(段 Segment)
- 服务对象:操作系统的程序加载器。
- 作用:加载器在把程序载入内存时,不需要知道
.text和.rodata的区别,它只关心“这块内存需要可读可执行,那块内存需要可读可写”。为了节约内存页的开销,加载器会将多个属性相同的节区合并成一个“段”来加载。 - 特点:粒度较粗。
💡 假设性说明(模拟readelf输出):
假设我们在终端输入readelf -l vuln(查看段信息),你会在输出中看到类似这样的对照关系:
Section to Segment mapping: Segment Sections... 00 .interp 01 .interp .note.gnu.property .note.ABI-tag ... 02 .init .plt .plt.got .text .fini <-- 合并成了可执行段 03 .rodata .eh_frame_hdr .eh_frame <-- 合并成了只读段 04 .init_array .fini_array .dynamic .got .data .bss <-- 合并成了可读写段从上面的模拟输出可以直观看到:.plt、.text被打包在一起执行;而.got、.data被打包在一起读写。节区是逻辑划分,段是物理装载。
2. 核心节区速览
在 PWN 中,我们最常打交道的几个节区如下:
| 节区名称 | 说明 | PWN 关注度 |
|---|---|---|
.text | 存放程序编译后的机器指令。通常只读可执行。 | ⭐⭐⭐ (找 ROP Gadget、后门函数) |
.bss | 存放未初始化的全局/静态变量。运行时在内存中分配并清零。 | ⭐⭐ (ret2shellcode 或 堆利用占位) |
.data | 存放已初始化的全局/静态变量。 | ⭐⭐ (查数据) |
.rodata | 只读数据,比如字符串常量"/bin/sh"。 | ⭐⭐⭐ (找/bin/sh地址) |
.got/.plt | 动态链接跳板与地址表。 | ⭐⭐⭐⭐⭐ (核心攻击目标) |
3. 动态链接的枢纽:.plt 与 .got 详解
在 Week1 我们讲过“懒绑定”机制,程序在第一次调用外部函数时才去寻找真实地址。这个过程就是靠.plt和.got配合完成的。
3.1 .plt (过程链接表)
- 属性:属于代码段,可读可执行(通常不可写)。
- 作用:它是一段段极其精简的跳板代码。程序中
call printf,实际上汇编是call printf@plt。 - 内部结构:每个外部函数在
.plt中都有一个小项(比如 16 字节)。它的核心逻辑是:“去.got表里查地址并跳转过去”。
3.2 .got (全局偏移表)
- 属性:属于数据段,可读可写(除非开启了 Full RELRO)。
- 作用:它本质上是一个指针数组。每个表项占 4 字节(32位)或 8 字节(64位),用来存放外部函数在 libc 中的真实绝对地址。
4. PWN 视角:为什么要区分 .got 和 .got.plt?
用readelf -S查看节区时,你可能会发现有两个长得像 GOT 的东西:.got和.got.plt。它们有什么区别?
4.1.got
这个表主要用于存放全局变量的地址(比如引用其他动态库中的全局变量)。在 PWN 中较少直接作为主要攻击目标。
4.2.got.plt(重点中的重点)
这个表专门用于存放外部函数的真实地址(如puts,system,gets)。这就是我们常说的“打 GOT 表”的那个表。
.got.plt表的特殊结构(前 3 项是特殊数据):
- 第一项:存放
.dynamic节的地址(动态链接器使用)。 - 第二项:存放
link_map结构的地址(动态链接器使用)。 - 第三项:存放动态链接解析函数
_dl_runtime_resolve的地址。 - 第四项及以后:才是我们熟悉的
puts@got、gets@got等。
4.3 攻击逻辑复盘
在 Partial RELRO(部分保护)下,.got.plt是可写的。
- 程序调用
puts,第一次执行时,动态链接器找到puts在 libc 的地址,比如0x7ffff7a649c0,然后把这个地址写入.got.plt表中puts对应的表项里。 - 下次再调用
puts,程序直接从.got.plt读出0x7ffff7a649c0并跳转。 - 攻击发生:如果我们通过栈溢出或格式化字符串漏洞(任意地址写),把
.got.plt表中puts的表项内容,篡改成system函数的地址0x7ffff7a4c440。 - 程序再次执行
puts("hello")时,去.got.plt读地址,读出的是system的地址,于是实际执行了system("hello")。
这就是经典的GOT Table Overwrite(GOT 表覆写)攻击。而开启 Full RELRO 的原理,就是在程序启动时一口气解析完所有函数地址填入.got.plt,然后直接把这块内存设为只读,从根源上掐断覆写可能。
5. 结语
理解了 ELF 的双视图以及.got.plt表的精确结构,我们就能在后续实战中精准地算出目标函数 GOT 表的绝对地址。下一步,我们将学习如何利用泄露出来的地址,反查libc版本并计算system函数的偏移量。
如果本篇文章对您有帮助,请点赞收藏支持一下,感谢阅读!我们下一部分见!🙏