PLT & GOT
PLT(过程链接表)属于代码段(.text 相关节),GOT(全局偏移表)属于数据段(.data/.bss 相关节) —— 这个分段设计是动态链接机制的核心基础,也直接决定 Pwn 攻击的核心思路(比如为什么能劫持 GOT 而不能直接劫持 PLT)。
结合 CTF Pwn 视角,我们深入拆解这个设计的「本质原因」和「攻击关联」:
一、先明确:代码段 vs 数据段的核心差异(Pwn 关键)
ELF 文件的段(Segment)是操作系统加载内存时的权限控制单位,核心差异在于 权限属性 和 用途,这直接影响漏洞利用的可行性:
| 段类型 | 核心权限 | 典型节(Section) | 用途 | Pwn 相关性 |
|---|---|---|---|---|
| 代码段(Text Segment) | 只读 + 可执行(RX) | .text(程序自身代码)、.plt(PLT 表) | 存储可执行指令(CPU 能直接运行的代码) | 不可写 → 无法直接修改 PLT 指令,只能通过「跳转目标劫持」(如 GOT) |
| 数据段(Data Segment) | 可读 + 可写(RW) | .data(初始化全局变量)、.bss(未初始化全局变量)、.got.plt(GOT 表) | 存储变量、指针等可修改数据 | 可写 → 漏洞利用的核心目标(任意写漏洞修改 GOT 指针) |
⚠️ 关键提醒:x86-64 系统中,代码段绝对禁止「可写」(否则会触发内存保护机制,如 NX 保护),而数据段绝对禁止「可执行」(否则会触发 DEP 保护)—— 这是现代系统的基础安全策略,也倒逼 Pwn 攻击只能围绕「修改数据段的指针」来实现。
二、PLT 为什么在代码段?
PLT 的本质是「跳板指令集合」,每个 PLT 条目都是一段固定的汇编代码(比如之前提到的 jmp [GOT地址] + push 符号索引 + jmp 通用入口),其设计必须满足「可执行」和「不可修改」:
- 需要可执行:PLT 是函数调用的「跳板」,CPU 必须能直接执行其中的
jmp、push指令 —— 只有代码段(RX 权限)能满足「可执行」要求; - 不需要可写:PLT 指令是编译时固定的(所有动态链接程序的 PLT 结构都一致),运行时不会变化 —— 代码段的「只读」属性刚好匹配,同时避免被恶意修改(但
Pwn中无需修改 PLT,只需劫持它的跳转目标)。
举个 Pwn 实战例子:
- 你在 ret2plt 攻击中,利用的是
plt[puts]这个「可执行指令地址」(代码段权限允许执行),通过call plt[puts]触发函数调用 —— 这正是因为 PLT 在代码段,具备可执行权限。
三、GOT 为什么在数据段?
GOT 的本质是「指针数组」,存储外部函数的真实内存地址,其设计必须满足「可修改」和「可读」:
- 需要可写:动态链接的「延迟绑定」核心是「第一次调用时更新 GOT 指针」—— 动态链接器(
_dl_runtime_resolve)需要将解析出的真实函数地址(如libc+printf_offset)写入 GOT 表,覆盖初始的 PLT 跳转地址(如0x4004f6); - 需要可读:PLT 中的
jmp [GOT地址]指令需要读取 GOT 中存储的指针 —— 数据段的「可读」属性满足此要求。
这正是 Pwn 中「GOT 劫持」的核心前提:GOT 在数据段(RW 权限),存在任意写漏洞时,可直接修改其中的指针,将函数调用目标劫持到 shellcode 或 onegadget。
举个 Pwn 实战例子:
- 若程序存在格式化字符串漏洞,你可以通过
%n直接修改got[printf]的值(将其改为system地址)—— 这正是因为 GOT 在数据段,具备可写权限; - 若 GOT 在代码段(只读),则无法修改,GOT 劫持攻击直接失效。
四、这个设计对 Pwn 攻击的直接影响(核心结论)
- 攻击目标锁定:因为 PLT 不可写、GOT 可写,所以动态链接相关的攻击几乎都围绕「GOT 表」展开(GOT 劫持、GOT 地址泄露),而 PLT 仅作为「触发工具」(如 ret2plt 泄露 GOT 地址);
- 漏洞利用逻辑:任意写漏洞(栈溢出、堆溢出、格式化字符串)的核心利用思路,就是「找到可写的 GOT 地址 → 修改其存储的指针 → 让后续函数调用跳转到恶意地址」;
- 调试与分析技巧:
- 静态分析时,用
readelf -S ./pwn可看到.plt属于PROGBITS类型且权限为AX(可执行),.got.plt属于PROGBITS类型且权限为WA(可写); - 动态调试时,用
vmmap查看内存权限,代码段(含 PLT)的权限是r-x,数据段(含 GOT)的权限是rw-。
- 静态分析时,用
五、实战验证:用工具确认 PLT/GOT 的段归属
1. 静态分析(readelf)
查看 ELF 节的属性,确认 .plt 和 .got.plt 所属的段和权限:
bash
运行
1 | readelf -S ./pwn | grep -E ".plt|.got.plt" |
典型输出(x86-64 动态链接 ELF):
plaintext
1 | [13] .plt PROGBITS 00000000004004e0 000004e0 00000040 00 AX 0 0 16 |
- 关键标识:
.plt的AX表示「可执行(A)+ 可读(X,隐含)」(即 RX 权限),属于代码段; .got.plt的WA表示「可写(W)+ 可读(A,隐含)」(即 RW 权限),属于数据段。
2. 动态调试(pwndbg)
查看内存中 PLT 和 GOT 的权限,验证段属性:
python
运行
1 | # 查看 plt[printf] 的地址和内存权限 |
总结(Pwn 核心记忆点)
- 核心结论:PLT(代码段,RX)= 不可写的跳板指令 → 只能用不能改;GOT(数据段,RW)= 可写的指针数组 → 漏洞利用的核心目标;
- 攻击逻辑:动态链接相关的 Pwn 攻击,本质都是「利用 GOT 的可写性,篡改其指针,让 PLT 的跳转目标指向恶意代码」;
- 实战应用:无论是 GOT 劫持、ret2plt 泄露,还是 ret2dlresolve 伪造解析,都依赖这个分段设计 —— 理解「PLT 不可写、GOT 可写」,就能瞬间想通大部分动态链接相关的攻击原理。
意为:system@got 是一块内存地址(属于全局偏移表 GOT 的一个表项),这块地址中存储的内容就是 system 函数的真实内存地址(即 &system)
&system 的作用是定位 system 函数的指令,
1. CPU 拿到 &system 这个地址(比如 0xf7e4d200);
2. 从这个地址开始,依次读取内存中的二进制指令并执行;
3. 这些指令的集合,就是 system 函数的完整功能实现。
![[Screenshot 2025-11-23 221513.png]]![[Screenshot 2025-11-27 112949.png]]





