资讯动态

SEED Labs实战:从缓冲区溢出到格式化字符串漏洞利用

发布时间:2026/10/21 8:10:48 来源:尧图企业网站定制
1. 项目到底在讲什么SEED Labs 软件安全实验的核心价值很多刚接触安全方向的同学第一次听到“软件安全”这四个字脑子里浮现的要么是《黑客与画家》里那种神秘莫测的攻防对抗要么是铺天盖地的漏洞预警新闻。但真到了自己要动手做点什么的时候往往一脸茫然缓冲区溢出到底怎么溢格式化字符串漏洞为什么能把内存打穿栈上的返回地址是怎么被劫持的这些概念背得滚瓜烂熟但真扔给你一个漏洞程序你却不知道从哪开始下刀。SEED Labs 软件安全这套实验解决的就是这个“知道但不会做”的问题。它把软件安全领域最经典的几个漏洞类型——缓冲区溢出、格式化字符串、整数溢出、命令注入、环境变量攻击等——每一个都做成了独立且可以直接上手实操的实验。实验环境是一个预装好的虚拟机镜像里面塞满了精心构造的脆弱程序、配套的说明文档和工具链。你不需要自己从零搭建复杂环境也不需要担心把系统搞崩只需要按着实验手册一步步走就能在真实的Linux环境里亲手完成一次完整的攻击利用过程同时看到防御方是怎么加补丁的。这套实验适合谁我觉得有三类人特别合适。第一类是安全方向的学生课堂上听过理论但没动手验证过这套实验能帮你把课本上的“栈帧”“返回地址”“格式化占位符”这些抽象概念变成你肉眼可见的内存变化。第二类是转行做安全研发的工程师你想理解漏洞的本质而不是只会用扫描器那这套实验是最好的入门教材。第三类是普通的后端开发者就算你不做专职安全搞明白缓冲区溢出是怎么发生的你在写C、C代码时自然会对数组越界、字符串拼接多一分警惕。从我个人的使用体验来说SEED Labs 最有价值的一点是它没有把攻击利用过程包装成“黑魔法”。每个实验都鼓励你先读代码、理解程序在内存里长什么样再动手修改数据或构造输入。换句话说它不是教你背几个exp而是在训练你建立“程序在内存中如何运行”的心智模型。有了这个模型你再去看真实世界的漏洞报告会发现那些复杂的利用链本质上都是基础原语的组合。下面我结合自己做过的软件安全系列实验把这个项目拆开讲讲它到底让实验者做了什么、关键技术点在哪里、实操中有哪些坑、做完之后能留下什么。2. 核心实验拆解缓冲区溢出与格式化字符串漏洞2.1 缓冲区溢出实验从栈帧布局到返回地址劫持缓冲区溢出Buffer Overflow是软件安全里最经典、也最“根正苗红”的议题SEED Labs 软件安全部分的第一个重头戏就是它。实验的目标程序是一个存在栈缓冲区溢出的普通C程序你需要通过输入过长的字符串覆盖栈上相邻的变量最终改写函数的返回地址让程序跳转到你自己构造的shellcode中去执行。这个实验的第一个关键点是理解栈帧布局。很多同学一开始会觉得“溢出”就是“把数组写爆”但真正困难的是搞清楚你写出去的字节到底落在了哪个内存位置。在一个典型的32位x86架构下函数调用时栈上会依次压入参数、返回地址、旧帧指针然后为局部变量分配空间。如果局部变量是一个定长字符数组而你往里面写入的内容超出了数组边界就会向上覆盖到旧帧指针和返回地址。举个例子假设函数里声明了char buf[16]正常情况下你往buf里写16个字节以内都没事。但如果你用strcpy(buf, user_input)且不检查输入长度而user_input是24字节那么多出来的8个字节就会一路“顶”上去先覆盖掉保存的帧指针再覆盖返回地址。当你覆盖了返回地址之后函数在ret指令执行时就会从栈上弹出一个已经被你控制的地址CPU就跳到你指定的位置去执行了。SEED Labs 实验里会专门让你做两步准备工作。第一步是关闭现代系统的内存保护机制比如用-fno-stack-protector编译去掉栈金丝雀用-z execstack让栈可执行再用sudo sysctl -w kernel.randomize_va_space0关闭地址随机化ASLR。为什么需要这么干因为真实系统里这些缓解措施会让入门难度陡增教学实验先把这些限制去掉是为了让学生先掌握利用原理之后再逐步开启防御机制理解攻防对抗的升级过程。第二步就是精确计算偏移量。你需要知道输入字符串从哪个字节开始会覆盖到返回地址。这一步我用过的技巧有两种。一种是用调试器逐字节观察在return之前设断点查看栈顶附近的值手动数出偏移。另一种是生成一个特征明显的pattern字符串比如用工具生成“AAAABBBBCCCC...”这样每4字节一个模式崩溃后看调试器报出的地址就能反推出精确偏移。实际做下来第二种方法效率更高也更有“实战”感。实验最后一步是构造shellcode并把它和填充物一起塞进输入。shellcode通常是一段机器码功能是执行execve(/bin/sh)相当于给你弹出一个rootshell。因为你已经关闭了栈不可执行保护所以这段shellcode放在栈上也能被CPU当作指令来执行。偏移量加上跳转地址填好之后运行程序就能看到#提示符说明你成功拿到了一个shell。整个实验做完你会对“返回地址”“栈帧”“shellcode”这三个词有一个非常立体的理解。以后再看到任何关于栈溢出的漏洞描述你都能脑补出攻击者是怎么一步步从输入点走到控制流的。2.2 格式化字符串漏洞从崩溃到任意内存读写缓冲区溢出实验之后SEED Labs 紧接着安排了格式化字符串漏洞实验。这个漏洞的入口更加“不起眼”但威力一点都不弱。它的根源在于程序员把用户输入直接当作格式化字符串参数传给了printf函数比如printf(user_input);而不是安全的写法printf(%s, user_input)。就这么一个简单的疏忽攻击者就能用%x、%n这些格式占位符把栈上的数据读出来甚至对任意内存地址进行写入。我第一次做这个实验时最震撼的是%x的能力。构造一个输入字符串AAAA.%x.%x.%x.%x运行时你会发现打印出来一堆十六进制数字其中某些数字和内存地址有关。原理是printf是按格式字符串中占位符的个数从寄存器或栈上依次取参数。当你用%x时它会把调用者栈中下一批数据当作整数打印出来。这就等于你拥有了一个“栈望远镜”能一层层往下看把函数调用链上遗留的数据全部拉出来。更厉害的是%n格式化符它会把当前已经打印的字符总数写入到一个指针参数指向的内存地址中。攻击者可以用这个功能来覆写任意内存最经典的利用就是覆写GOT表全局偏移表中的某个函数指针让程序调用system(/bin/sh)。GOT表是程序在运行时解析动态库函数地址用的每次调用exit()或strcpy()时都要去GOT表里取真实地址。如果你能通过格式化字符串漏洞修改表格中某个函数的地址把它改成system的地址那么程序下次调用那个函数时相当于直接调用了system然后你再控制参数部分的值比如让它用/bin/sh当参数就完成了一次提权或代码执行。为了完成这种利用你需要解决一个难题如何把目标内存地址作为参数传入printf并且精确控制写入值SEED Labs 的实验指导会教你使用%hn写入两个字节甚至%hhn写入一个字节实现精细的内存写入。因为一次写满32位地址数值通常太大%n打印的字符数是位数不可能真的打印几亿个字符所以主流做法就是拆分高低位依次写入。这个实验做完你会彻底理解“格式化函数不是类型安全的”。很多业务代码里为了省事直接用用户输入当格式化字符串平时看起来没毛病但一旦遇到有心人就是任意代码执行。防御方法其实很简单printf(%s, user_input)永远不要把未受信任的内容直接放到格式串的位置。但就是这么简单的事在真实项目里依然屡见不鲜SEED Labs 把这个实验摆在软件安全系列里我觉得就是在反复提醒开发者规范的代价永远小于漏洞的代价。3. 实操过程从零完成一个软件安全实验的全流程3.1 准备实验环境虚拟机、镜像与工具链SEED Labs 通常提供预构建好的虚拟镜像你只需要装一个虚拟机软件把镜像导入进去就能开始。但我强烈建议你不要直接拿默认环境就用而是花十分钟自己配置一遍因为配置的过程本身就是对工具链和安全机制的再熟悉。虚拟机镜像建议使用VirtualBox因为它免费且跨平台。导入镜像后第一件事是检查内核版本和编译器版本。很多实验需要32位编译而现代64位系统默认编译出来是64位程序栈布局和寄存器参数传递规则都不一样。你需要在目标机上确认有gcc-multilib支持也就是能gcc -m32编译32位程序。如果没有先执行sudo apt update sudo apt install gcc-multilib gdbGDB是实验里的灵魂工具强烈建议你花点时间学习它的基础命令break下断点、x/20wx $esp查看内存、disas反汇编、si单步执行等。很多同学卡在实验中间不是原理不懂而是GDB不会用看不到内存变化就觉得“玄学”。实际上你只要在关键位置断点用x/20wx $esp打印栈顶20个字就能直观看到自己的输入是怎么在栈上排布的。另一个值得提前装的工具是python3因为构造输入字符串最方便的就是Python。你可以写一个简单脚本生成payload比如payload bA * 100 b\x00\x11\x22\x33然后把payload输出到文件用管道喂给漏洞程序。对于复杂payload建议用Python的struct模块来处理地址的字节序避免手写错位。环境准备好之后我通常的习惯是先跑一遍实验提供的“setup”脚本或编译命令。SEED Labs 每个实验都带详细的Makefile里面有经过精心设计的编译选项比如gcc -m32 -fno-stack-protector -z execstack。你要注意理解这些选项的意思而不是简单照抄。-fno-stack-protector表示关闭栈保护canary-z execstack表示允许栈上代码执行-no-pie表示关闭位置无关可执行文件防止程序基址随机化。这些都是为了教学方便实际系统不会这么裸奔但你要记住“当时是怎么开的真实环境是怎么默认的”这样才不会被实验环境带偏。3.2 编写漏洞利用代码的关键步骤与参数计算拿缓冲区溢出实验来说通用的payload构造公式大概是[填充物 JUNK] [返回地址 RET] [shellcode]但具体到每个实验变体你都要重新计算三个参数第一偏移量。也就是填充物到底要填多长才能刚好把返回地址顶到栈上的目标位置。我之前遇到一个程序局部变量除了buf外还有一个target变量。实验要求是先覆盖target让它等于一个特定值然后再触发下一层漏洞。这种情况下你得先计算buf到target的偏移这可以通过查看反汇编代码或调试器确认。不要上来就蒙一个100先看sub $0x78, %esp这样的指令0x78就是整个栈帧为局部变量分配的大小再减去buf的大小基本上就是偏移了。第二shellcode的存放位置。因为跳转地址是绝对地址在关闭ASLR的前提下你先要知道shellcode在栈上的实际地址。常见做法是在shellcode前刻意填充多个NOP指令\x90然后把返回地址指到NOP雪橇的任意位置这样无论偏移几个字节最终都会滑进shellcode。计算栈地址的方法是在GDB里设断点查看$esp的值然后根据输入位置到栈顶的距离推算。你可以先在payload末尾放一个标志字符串比如“ABCD”运行后在GDB里查找这个标志在栈上的地址然后反推shellcode位置。第三返回地址的字节序。x86采用小端字节序比如地址0xffffd120在内存里要写成\x20\xd1\xff\xff。新手最容易栽在这里写反导致跳转地址完全错误。我自己的做法是写一段Python脚本用p32(0xffffd120)来自动转换避免手误。格式化字符串实验的参数计算则完全不同。你需要确定要覆写的GOT表项地址还要确定每次写入的数值。比如你想把某个地址的低16位写入0xcdcd高16位写入0xaaaa那payload里就会包含两个地址分别用%hn写入。中间要用精确数量的填充字符来调整“已打印字符数”这需要一点点心算或者直接用工具辅助。我提供一个格式化字符串里常用的计算逻辑假设当前输出长度为len目标值是target那这一次要打印的额外字符数是target - len然后紧跟%hn。如果差为负数就借位处理把target加上65536再减。这部分属于纯手工装配多做几次就会熟。3.3 常见坑点与排查技巧速查表做软件安全实验绕不开各种“看起来像是坏了”的问题。我把自己踩过的坑整理成了一张表希望你能少走点弯路。现象可能原因排查方向程序崩溃时提示“段错误”但GDB看不到返回地址被覆盖栈帧偏移算错了覆盖的不是返回地址而是其他数据用pattern工具生成字符串反查崩溃位置确认偏移量跳转地址指向shellcode区域但程序仍然崩溃栈不可执行虽然你编译时加了-z execstack但系统或硬件可能有NX默认打开用execstack工具检查文件属性确认真实状态shellcode能起但立刻退出没有进入shell进程的输入输出缓冲区问题或者shellcode本身针对不同系统版本失效使用实验提供的标准shellcode不要随便从网上复制确认是32位还是64位环境格式化字符串打印不出栈上的数据占位符不够多或者程序是64位参数从寄存器传递多试几十个%x或者用%p64位下可能需要用%rdi等寄存器规则用%n写入后程序崩溃写入地址非法或地址没有对齐用GDB查看目标地址是否可写注意%hn只写2字节地址要按2字节对齐虚拟机里GDB调试权限不够无法attach内核的ptrace_scope限制sudo sysctl -w kernel.yama.ptrace_scope0还有一个特别容易忽略的坑环境变量和地址偏移。同一个shellcode你在GDB里跑能成功但退出GDB直接在终端跑就失败这种“GDB灵异事件”多半是因为GDB在运行时额外设置了一些环境变量导致程序的环境变量数量变化间接影响了栈布局shellcode地址偏移了几个字节。解决方式有几种一是直接在GDB里运行程序来验证最终利用二是使用setarch禁用ASLR三是尽量用相对跳转或NOP雪橇扩大命中范围。我个人最推荐的方法是在最终验证利用时用gdb -q ./vul直接加载程序然后在GDB里用run payload来执行。GDB下的栈布局虽然和纯终端不同但只要你关闭了ASLR而且程序本身没有动态变化你就可以通过调试中的实际地址来修正payload。没有必要强行追求在所有环境下通用。4. 从实验到防御如何把攻击知识转化为安全能力4.1 现代系统缓解机制栈金丝雀、ASLR、NX、RELRO只学会了攻击那只是完成了一半。SEED Labs 好就好在每个实验后面都会引导你重新开启防御机制让你看看攻击还会不会成功。这部分内容放到真实安全工程里就是现代系统安全机制的基本盘我强烈建议你把这些机制逐一看懂。栈金丝雀Stack Canary是最直观的防御机制。编译时加上-fstack-protector编译器会在缓冲区之后、返回地址之前插入一个随机值。函数返回前会检查这个值有没有被改写如果被改了就直接触发异常终止。这个机制专门对付经典栈溢出。SEED Labs 实验会让你对比开启/关闭canary后攻击成功率的差异你能亲眼看到开启后原本的payload一进去就导致程序“爆破”退出。NXNo-eXecute也就是数据执行保护它让栈和堆的页面不可执行。这意味着即使你成功覆盖了返回地址指向栈上shellcode的跳转也会因为页面权限不足而崩溃。攻击者为了绕过NX通常转向ROPReturn-Oriented Programming也就是拼接已有代码片段。SEED Labs 在实验后面也提供了一些扩展练习让你尝试构造ROP链但那个难度会明显上升。ASLR地址空间布局随机化让每次程序运行时的栈地址、共享库地址都随机变化。这意味着你在渗透测试里常用的“硬编码跳转地址”直接失效。绕过ASLR的方法通常需要先泄露出一个地址再计算基址这就是信息泄漏漏洞的由来。SEED Labs 软件安全实验默认关闭ASLR是为了让核心利用逻辑不被随机性干扰但你心里要清楚真实环境里ASLR就是第一道大门。RELRORelocation Read-Only和GOT表保护直接关系格式化字符串漏洞的利用。如果GOT表被设为只读那你想通过格式化字符串改写GOT条目就会触发保护。现代系统会编译生成带RELRO的程序攻击者往往需要转向覆写其他可写结构比如栈上的变量或堆上的对象。理解这些机制你才算真正理解了为什么同一个漏洞在不同系统上利用难度差别巨大。把这些机制全部开回去你会发现原本轻松拿下的shell变得极其艰难。这时候你就站在了攻防对抗的十字路口而不是只会照着实验手册做的小白。我见过不少同学做完实验后很兴奋地说“我拿到root了”但对“为什么换一台机器就不行了”完全没有概念。这正是防御机制实验存在的意义。4.2 实验之后的思考从“会利用”到“会防御”参与完整个 SEED Labs 软件安全系列我最深刻的体会是漏洞利用能力只是表象真正的收获是建立了一套“以攻击者视角审查代码”的思维方式。当你回头看自己写的C、C代码时看到gets()、strcpy()、sprintf()这类函数会立刻在脑子里拉响警报当你看到printf(input)这种写法时会条件反射地想到格式化字符串漏洞当你看到数组下标来自用户输入且没有边界检查时会不自觉地开始推演溢出路径。这种能力很难靠刷题获得只有亲手把一次漏洞从发现到利用再到消灭的完整闭环走下来才会内化成为你的本能。如果你是想把软件安全当作职业方向我建议你在做完SEED Labs的基础实验后继续挑两个方向深入下去。第一个方向是回归到防御学习怎么编写安全的C代码、怎么启用编译器的安全选项、怎么做代码审计。第二个方向是研究现代利用技术比如绕过ASLR的地址泄漏、绕过NX的ROP链构造、内核里的漏洞利用。SEED Labs 本身就提供了一批更高阶的实验模块你可以顺着那个路径往下走。还有一个很值得做的事把你做实验的步骤整理成一篇有思路的笔记。不是把实验手册简单抄一遍而是记录你计算偏移量的过程、踩过的坑、为什么某个地址在调试器里和运行时不一样。这种笔记才是你自己的经验资产回头找工作面试或者写博客都比生硬地背理论有价值得多。5. 一些实操中的额外心得与建议最后再分享几个我做了多遍 SEED Labs 软件安全实验后觉得特别值得划重点的内容。第一不要跳过“前置阅读”就直接开虚拟机。SEED Labs 每个实验前面的README都写得非常仔细通常会介绍漏洞类型的历史背景、核心原理、实验任务的拓扑图以及相关的系统命令。花20分钟把README读完比你在虚拟机里瞎折腾两个小时更有效。我做缓冲区溢出实验时第一次就是没仔细看栈布局图结果一直搞不清为什么覆盖了12个字节后返回地址才变后来回头读文档才发现目标程序里除了buf还有一个其他变量。第二强烈建议在虚拟机里保留一份“干净系统快照”。实验过程中你会频繁修改系统设置、关闭保护、甚至写一些可能导致系统不稳定的内核参数。一个快照能让你随时回到初始状态重新开始特别是在你做格式化字符串实验时一个错误的payload可能让shell崩溃但没有关系重启虚拟机恢复快照一切从头来。第三认真对待实验报告里的“问题与思考”部分。SEED Labs 实验手册每一章后面都有思考题比如“为什么禁用ASLR后攻击成功启用后失败”“如何用一个小技巧绕过栈金丝雀”这类问题看起来像是作业实际上是帮助你打通知识脉络的关键点。我建议你每道题都自己动手验证一遍不要只写答案。这种主动探索比单纯完成规定步骤学到的东西多得多。第四善用调试器但不能依赖调试器。GDB是透视内存的眼睛但真实攻击场景中没有调试器你只能靠代码分析和计算。所以在做实验时我建议你先尝试阅读汇编代码和源码推断出payload结构再用GDB验证。如果一开始就依赖GDB去试错那你的分析能力锻炼就会打折扣。第五注意环境的一致性。SEED Labs 提供的镜像是基于某个特定版本的操作系统但操作系统更新后默认的编译选项、shellcode行为、内核接口可能会有差异。如果你在自己的Ubuntu环境上复现实验遇到莫名其妙的问题优先考虑是不是环境不一致导致。最简单的办法还是用官方镜像而你只需要在镜像里做修改。我自己的经验是做完整个软件安全系列大概需要一到两周的业余时间每天投入两三个小时。节奏上不宜贪快一个实验做完、理解透了再进入下一个。缓冲区溢出和格式化字符串两个实验是黄金搭档先把这两个啃下来后面的整数溢出、命令注入会顺理成章很多。SEED Labs 最大的价值不是给你一套现成的答案而是把那些只能在“实验室条件”下表现的漏洞机理变成你能亲手触碰和修改的东西。如果你还在犹豫要不要在这个项目上花时间我可以说这可能是你自学安全路上回报率最高的几周时间之一。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑