CTFshow-PWN-前置基础(pwn24-pwn25):从RWX到NX保护的栈溢出实战

发布时间:2026/6/30 11:01:30

CTFshow-PWN-前置基础(pwn24-pwn25):从RWX到NX保护的栈溢出实战 1. 从RWX到NX栈溢出攻击的防护演变第一次接触PWN题时很多人都会被各种内存保护机制搞得晕头转向。我刚开始做CTFshow的pwn24和pwn25这两道题时就深刻体会到了RWX可读可写可执行和NX不可执行这两个保护机制带来的巨大差异。简单来说RWX就像给你的代码开了个万能通行证而NX则是给敏感区域加装了防盗门。在pwn24这道题里我们用checksec工具查看保护机制时会看到一行醒目的提示RWX: Has RWX segments。这意味着程序里存在可以同时读写执行的内存段就像给你留了个可以随意涂改的黑板。这种情况下我们只需要把恶意代码shellcode直接写在栈上然后让程序跳转执行这段代码就能拿到shell。用pwntools的shellcraft模块三行代码就能生成攻击载荷payload asm(shellcraft.sh()) p.sendline(payload) p.interactive()但现实中的程序不会这么慷慨。pwn25就开启了NX保护这个保护机制会阻止程序执行栈上的代码相当于给栈内存贴上了禁止运行的标签。这时候我们就需要更高级的技巧——ret2libc。这种攻击方式不是直接执行自己的代码而是复用程序本身或库函数中的代码片段就像用乐高积木拼出想要的功能。2. pwn24实战RWX段的直球攻击2.1 环境分析与漏洞定位拿到pwn24的二进制文件后我习惯先用checksec做个快速体检。看到RWX标志时我就知道这道题的关键在于找到可以注入代码的位置。用IDA反编译后会发现一个典型的栈溢出漏洞——ctfshow函数里用了不安全的read函数而且读取长度超过了缓冲区大小。这里有个小技巧即使反编译的伪代码看起来不太直观直接看汇编往往能更快发现问题。我在汇编视图中看到sub esp, 0x88这样的指令时立即意识到这是为局部变量分配栈空间结合后面的read调用基本可以确定存在栈溢出。2.2 shellcode构造与调试技巧构造payload时我推荐先用cyclic工具生成测试字符串。当程序崩溃时通过查看崩溃点的值就能快速计算出偏移量。对于这道题因为可以直接执行栈上的代码所以payload构造特别简单from pwn import * context(log_leveldebug, archi386) p remote(pwn.challenge.ctf.show, 28251) payload asm(shellcraft.sh()) p.sendline(payload) p.interactive()实际调试时我遇到过一个坑不同版本的pwntools生成的shellcode可能略有差异。有次本地测试成功但远程就是打不通后来发现是忘了统一设置context.arch。所以现在我都习惯在脚本开头明确指定架构。3. pwn25攻坚NX保护下的迂回战术3.1 ret2libc攻击原理剖析当NX保护开启时栈上的代码就像被冻住了一样——可以读可以写就是不能执行。这时候就需要ret2libc这种借力打力的技巧。它的核心思想是虽然不能执行自己的代码但可以复用程序加载的libc库中的函数。具体到pwn25这道题我们需要完成以下几个关键步骤泄露libc函数的实际地址比如puts根据偏移计算出libc基地址找到system函数和/bin/sh字符串的地址构造ROP链调用system(/bin/sh)3.2 实战中的地址泄露技巧地址泄露是ret2libc最关键的环节。我常用的方法是利用程序的puts或printf函数输出GOT表中的函数地址。这里有个细节需要注意32位程序传参是通过栈进行的所以构造payload时要确保参数放在正确的位置。offset 0x88 4 puts_plt elf.plt[puts] puts_got elf.got[puts] main_addr elf.symbols[main] payload flat([ bA * offset, puts_plt, # 覆盖返回地址 main_addr, # puts返回后继续执行main puts_got # puts的参数 ]) p.sendline(payload)接收泄露地址时要注意处理数据格式。我习惯用u32(p.recv(4))来解包32位地址有时候远程环境网络延迟可能导致接收不全这时候需要调整recv的大小或次数。4. 工具链的深度使用技巧4.1 pwntools高效调试方法pwntools绝对是PWN选手的瑞士军刀但很多新手只用了它10%的功能。我特别推荐gdb.attach()这个功能可以在脚本中直接附加gdb调试if args.LOCAL: p process(./pwn) gdb.attach(p, b *0x0804856A\nc) else: p remote(pwn.challenge.ctf.show, 28116)另一个实用技巧是使用fmtstr_payload自动生成格式化字符串攻击的payload省去了手动计算的麻烦。4.2 Libc数据库的灵活运用当不知道远程使用的libc版本时LibcSearcher本是个好帮手但正如题目中提到的这个库现在维护状态不太理想。我的替代方案是使用libc-database项目本地搭建查询服务直接下载多个常见libc版本进行尝试利用在线工具如libc.blukat.me进行查询对于重要的比赛环境我通常会准备一个libc集合包含glibc 2.23-2.35的主流版本。这样即使没有网络也能快速测试。5. 从入门到精通的进阶路径通过这两道题的对比练习我总结出了一个循序渐进的学习路线先掌握基础的栈溢出如pwn24理解各种保护机制的原理NX/ASLR/PIE等学习绕过技巧ret2libc/ROP等掌握动态调试技巧gdbpwntools积累不同环境下的攻击方法如不同libc版本在实际做题过程中我建议建立自己的代码片段库。比如把常见的payload构造、地址泄露、偏移计算等写成可复用的函数。这样遇到新题目时可以快速组合已有代码进行测试大大提高解题效率。最后分享一个调试小技巧当攻击不成功时不要急着修改payload先用cyclic确认偏移是否正确然后用gdb单步跟踪程序执行流程。很多时候问题不是出在攻击逻辑上而是某些细节处理不当比如栈对齐问题或者参数传递方式理解错误。

相关新闻