【学习记录】Week1:彻底搞懂动态链接原理——PLT/GOT 懒绑定机制

📅 2026/6/29 15:43:25
【学习记录】Week1:彻底搞懂动态链接原理——PLT/GOT 懒绑定机制
写在前面在之前的checksec学习中我们提到了RELRO保护机制会保护 GOT 表。但很多新手会疑惑GOT 表到底是什么为什么黑客总是想方设法去篡改它要弄懂这个问题我们必须深入理解 Linux 下动态链接的核心机制——PLT 与 GOT 的懒绑定。这是 PWN 学习路上绕不开的一座大山也是后续学习ret2libc的前置基础。本文将用最通俗的语言带你扒开动态链接的底裤。 目录为什么需要动态链接核心概念PLT 与 GOT 是什么懒绑定按需加载的智慧图解第一次调用慢路径图解第二次调用快路径实战观察用 GDB 看 PLT/GOT 的变化PWN 视角为什么 GOT 表是兵家必争之地1. 为什么需要动态链接如果每个 C 语言程序都把printf、system等函数的代码静态编译进自己的可执行文件里会导致文件体积巨大。大量内存浪费每个进程都有一份相同的printf代码。动态链接解决了这个问题系统把公用的函数打包成动态链接库如libc.so。程序运行时才去把 libc 加载到内存中然后去寻找对应的函数地址。但这里有个矛盾libc 的加载地址每次运行都不一样ASLR程序在编译时根本不知道printf的真实内存地址在哪。程序在执行call printf时必须知道确切的跳转地址。为了解决这个矛盾ELF 文件引入了PLT和GOT两张表。2. 核心概念PLT 与 GOT 是什么你可以把它们理解为两个配合默契的“特工”GOT (Global Offset Table全局偏移表)这是一个数据表位于内存的数据段。它的本质是一个指针数组。里面存放着外部函数如printf的真实内存地址。因为地址会变所以 GOT 表是可写的。PLT (Procedure Linkage Table过程链接表)这是一个代码表位于内存的代码段。它是一小段一小段的跳板代码。程序调用外部函数时实际上是先call PLT表中的代码然后由 PLT 去跳转到 GOT 表中记录的真实地址。打个比方你程序要寄快递给张三调用函数但你不知道张三的地址。你先把包裹交给快递驿站PLT表驿站查一下自己的账本GOT表。如果账本上有张三的地址直接寄过去如果没有驿站就去总台查地址查到后写在账本上下次再寄就直接看账本了。3. 懒绑定按需加载的智慧Linux 默认采用懒绑定策略。什么是懒就是不到万不得已不去找地址。如果一个程序引用了 100 个 libc 函数但本次运行只用到了 2 个如果一启动就去找齐 100 个函数的地址会极大拖慢启动速度。懒绑定的逻辑是程序启动时GOT 表里不写真实地址而是写指向 PLT 表中下一条指令的地址。当函数第一次被调用时触发 PLT 的兜底逻辑去调用动态链接器找到真实地址。找到后把真实地址写进 GOT 表里。以后再次调用时直接从 GOT 表里拿真实地址跳转。4. 图解第一次调用慢路径假设程序第一次调用puts函数。1. 程序执行 call putsplt | v 2. 来到 PLT 表的 puts 项 (代码段) putsplt: jmp [GOT表对应puts的地址] -- 此时 GOT 表里存的不是真实地址而是下面这条指令的地址 push 0 -- 告诉动态链接器我要找的是第 0 号函数 jmp PLT[0] -- 跳转到 PLT 表的初始项 | v 3. 来到 PLT[0] (代码段) PLT[0]: push [GOT[1]] -- 压入链接器需要的信息 jmp [GOT[2]] -- 跳转到动态链接器的核心函数 _dl_runtime_resolve | v 4. 动态链接器 执行 根据 push 0 的编号去 libc.so 里找到 puts 的真实内存地址。 将真实地址写入 GOT 表中对应 puts 的位置。 最后跳转到 puts 的真实地址去执行。5. 图解第二次调用快路径当程序第二次调用puts时因为第一次已经填好了 GOT 表1. 程序执行 call putsplt | v 2. 来到 PLT 表的 puts 项 (代码段) putsplt: jmp [GOT表对应puts的地址] -- 此时 GOT 表里已经是 puts 的真实地址了 | v 3. 直接跳转到 libc 中 puts 的真实地址执行。一气呵成无需再找6. 实战观察用 GDB 看 PLT/GOT 的变化纸上得来终觉浅我们在 GDB 中实际观察一下这个过程。gdb ./vuln在gets函数下断点gets也会用到动态链接。运行程序r查看反汇编你会看到程序调用了getsplt。使用 pwndbg 的神奇命令plt和gotpwndbg got你会看到类似这样的输出GOT protection: Partial RELRO ... 0x404000 | getsGLIBC_2.2.5 - [0x401046] -- 注意这里存的不是 libc 地址而是 PLT 里的地址此时gets还没被真正解析。输入字符串让程序继续执行完gets后再次查看gotpwndbg got 0x404000 | getsGLIBC_2.2.5 - 0x7ffff7e5e... -- 变了变成了 libc 中的真实地址这就是懒绑定的全过程7. PWN 视角为什么 GOT 表是兵家必争之地理解了上述原理你就会明白 PWN 攻击中最经典的一种手法GOT 表覆写。攻击思路假设程序开启了 NX不能执行 shellcode也没有后门函数。我们只能利用 libc 里的system函数。程序里有puts函数且puts的 GOT 表项已经被解析了存了 libc 真实地址。我们通过漏洞比如格式化字符串漏洞或任意地址写把 GOT 表中puts的真实地址篡改成system的真实地址。当程序下一次调用puts(hello)时实际上执行的却是system(hello)这就是为什么在checksec中Partial RELROGOT 可写是非常危险的因为它给攻击者留下了篡改执行流的捷径。而Full RELRO的原理就是程序一启动就立即解析所有函数地址填入 GOT然后把 GOT 所在的内存页设为只读。这样黑客就无法篡改 GOT 表了。8. Week1 终极收官感言历经多天的学习Week1 的任务终于圆满完成从最初的环境搭建到二进制工具分析从吃透五大保护机制到摸清汇编级调用约定从手动计算偏移量编写 Python Exploit再到今天深入操作系统底层剖析动态链接。我感觉自己仿佛把一个程序的“前世今生”都看透了。虽然目前还只是停留在静态分析和理论阶段但这座地基已经打得无比坚实。如果本篇硬核原理解析对你有帮助请点赞收藏支持一下感谢阅读我们 Week2 见