尧图精选

渗透测试Fuzz实战:从底层逻辑到环境搭建与进阶玩法

🕒 发布时间:2026/9/26 7:22:09 📁 来源:尧图网络
1. 渗透测试与Fuzz的底层逻辑拆解1.1 从一次真实项目说起为什么要用Fuzz去年接手一个物联网设备的固件审计项目设备通过MQTT协议与云端通信固件里跑着一个用C写的协议解析模块。手工构造了几十个畸形报文测了两天只发现了两个边界溢出效率低得让人抓狂。后来换了个思路写了个简单的变异脚本把协议报文里的长度字段、类型字段、载荷内容做随机翻转和替换一晚上跑了十几万条用例第二天早上看日志直接定位到三个可复现的崩溃点。这就是Fuzz的威力——它不靠人的直觉去猜漏洞在哪而是用自动化的方式去“撞”撞到程序扛不住的地方漏洞就浮出来了。很多人第一次听到“渗透测试fuzz”这个词会把它和“漏洞扫描”混为一谈。其实两者差别很大。漏洞扫描器像是拿着一份已知问题的清单去逐条核对比如“你有没有装这个版本的组件”“这个端口有没有开弱口令”它依赖的是已知特征库。而Fuzz是主动向目标程序输入大量非预期的数据观察程序是否出现异常行为——崩溃、挂起、内存泄漏、断言失败等等。它不预设漏洞长什么样而是通过制造异常来暴露问题。在渗透测试的语境下Fuzz通常被归类为“动态测试”的一种属于主动探测手段和静态代码审计形成互补。1.2 Fuzz在渗透测试流程中的位置一个完整的渗透测试流程大致会经过信息收集、威胁建模、漏洞发现、漏洞利用、后渗透、报告输出这几个阶段。Fuzz主要活跃在“漏洞发现”这个环节但它并不是孤立存在的。信息收集阶段拿到的协议格式、接口定义、文件格式样本都是Fuzz的输入素材威胁建模阶段确定的重点目标决定了Fuzz的资源往哪里倾斜而Fuzz跑出来的崩溃样本又需要漏洞利用阶段去进一步验证是否可利用、危害等级多高。我习惯把Fuzz看作渗透测试里的“放大器”。手工测试能覆盖的输入空间非常有限一个人的精力再旺盛一天能构造并验证的用例也就几百条。但Fuzz可以把用例数量拉到百万甚至千万级别把那些藏在深层逻辑里的边界条件、类型混淆、状态机异常统统翻出来。尤其是在面对闭源目标、没有源码可审计的时候Fuzz几乎是发现内存破坏类漏洞最有效的手段之一。1.3 常见Fuzz类型与适用场景对照Fuzz的分类方式很多按输入生成策略分有基于变异的、基于生成的、基于语法的按目标类型分有文件格式Fuzz、网络协议Fuzz、API接口Fuzz、内核系统调用Fuzz等等。下面这张表是我在实际项目中总结的对照关系方便你快速判断什么场景该用什么路子。Fuzz类型核心思路典型目标工具举例上手难度基于变异对已有样本做随机翻转、截断、拼接文件解析器、图片解码器AFL、honggfuzz低基于生成按语法规则从零构造输入协议实现、编译器Peach、boofuzz中基于覆盖率根据代码覆盖反馈引导变异方向二进制程序、内核AFL、libFuzzer中高基于协议状态按状态机流转构造多阶段交互网络服务、车载中控boofuzz、自定义脚本高基于API调用序列按接口依赖关系编排调用链Web API、移动应用RESTler、自定义框架中选择哪种类型取决于你手头有什么素材、目标是什么形态、你能投入多少时间。如果目标是一个处理图片的库你手上有大量正常图片样本那基于变异的AFL就是最快出结果的路子。如果目标是车载中控的CAN总线通信协议规范你拿到了但样本很少那就得走基于生成的路线用boofuzz之类的框架把协议状态机描述出来。2. Fuzz核心机制与实操要点解析2.1 变异引擎Fuzz的心脏怎么跳变异引擎决定了Fuzz能不能在有限时间内触达更深的代码路径。最朴素的变异就是随机翻转比特位但这种方式效率极低因为大部分翻转都会让输入在解析早期就被拒绝根本走不到深层逻辑。真正有效的变异引擎会结合多种策略位翻转、字节替换、算术增减、块复制、块删除、字典插入等等。AFL在这方面做得非常成熟它维护了一个变异调度队列会根据每个变异操作的历史命中率动态调整权重。我在实际使用中有一个体会字典的质量直接决定Fuzz的命中率。比如目标是一个JSON解析器你在字典里放上{、}、、:、null、true、false这些token变异出来的输入就更容易通过语法检查进入语义处理阶段。如果目标是一个二进制协议字典里放上常见的魔数、长度字段的边界值0、1、255、256、65535、65536效果会立竿见影。AFL支持通过-x参数加载字典文件格式就是每行一个token支持\x转义。# AFL字典文件示例 dict.txt GET POST HTTP/1.1 \x00\x00\x00\x00 \xff\xff\xff\xff Content-Length:2.2 覆盖率反馈怎么知道Fuzz有没有“跑偏”覆盖率反馈是现代化Fuzz工具的核心竞争力。简单说工具在编译目标程序时插桩记录每次执行走了哪些代码分支然后优先保留那些触发了新分支的输入样本用它们作为下一轮变异的种子。这样Fuzz就会像爬山一样一步步往代码更深处走而不是在浅层逻辑里原地打转。AFL的插桩方式有两种编译时插桩afl-gcc、afl-clang-fast和二进制插桩QEMU模式。编译时插桩效率高但需要源码二进制插桩不需要源码但性能损耗大通常是编译时插桩的5到10倍。如果目标程序有源码强烈建议用编译时插桩。如果只有二进制QEMU模式也能用但要有心理准备跑得慢。注意覆盖率反馈不是万能的。如果目标程序有大量并行分支或者状态依赖单纯的边覆盖率可能引导Fuzz走进死胡同。这时候可以考虑用上下文敏感的插桩或者结合符号执行来突破。2.3 种子样本垃圾进垃圾出种子样本的质量对Fuzz效率的影响怎么强调都不为过。一个精简、多样、覆盖了主要功能路径的种子集能让Fuzz在几小时内就触达深层逻辑而一堆冗余、格式错误的种子只会让Fuzz在入口处反复碰壁。我通常这样准备种子集先从目标程序的文档、测试用例、公开样本库里收集一批正常输入然后用afl-cmin做去重把那些触发相同覆盖率的样本剔除掉再用afl-tmin对每个样本做最小化在保持覆盖率不变的前提下把文件体积压到最小。经过这两步处理种子集通常能从几百个缩减到几十个但Fuzz效率反而更高。# 种子去重 afl-cmin -i seeds_raw -o seeds_min -- ./target # 单样本最小化 afl-tmin -i seeds_min/input1 -o seeds_tmin/input1 -- ./target 2.4 崩溃去重与分类别被假阳性淹没Fuzz跑起来之后最让人头疼的不是没崩溃而是崩溃太多。同一个漏洞可能触发几十上百次崩溃如果不去重你会在日志里看到一片红根本分不清哪些是真正独立的漏洞。AFL自带了一个afl-collect脚本可以根据崩溃时的执行路径做去重。更精细的做法是用exploitable这个GDB插件它会自动分析崩溃的可利用性给出“EXPLOITABLE”“PROBABLY_EXPLOITABLE”“NOT_EXPLOITABLE”等评级。我在项目里通常会搭一个简单的流水线Fuzz跑出崩溃样本后先用afl-collect做初步去重然后用exploitable做可利用性分级最后人工复核那些评级高的样本。这样能把精力集中在真正有价值的崩溃上而不是被一堆空指针解引用浪费时间。3. 从零搭建一个Fuzz实战环境3.1 环境准备与工具链安装这里以AFL为例走一遍完整的搭建流程。操作系统用Ubuntu 22.04这是目前兼容性最好的选择。先装基础依赖sudo apt update sudo apt install -y build-essential clang llvm gdb python3-pip然后从源码编译安装AFLwget https://github.com/google/AFL/archive/refs/tags/v2.57b.tar.gz tar -xzf v2.57b.tar.gz cd AFL-2.57b make sudo make install安装完成后afl-fuzz、afl-gcc、afl-clang-fast这些命令应该都在PATH里了。可以用afl-fuzz -h验证一下。提示如果目标程序是64位且需要大量内存记得调整系统的核心转储模式。echo core /proc/sys/kernel/core_pattern可以避免AFL因为核心转储问题报错。3.2 目标程序编译与插桩假设我们要Fuzz一个简单的C程序它从文件读取数据并解析。先用afl-clang-fast编译afl-clang-fast -o target target.c编译完成后可以用afl-showmap验证插桩是否生效afl-showmap -o /dev/null -- ./target input_sample如果输出里有-- Program output begins --之类的信息说明插桩成功了。3.3 种子准备与Fuzz启动准备一个种子目录放几个正常输入样本mkdir -p seeds cp sample1.bin seeds/ cp sample2.bin seeds/然后启动Fuzzafl-fuzz -i seeds -o findings -- ./target -i指定种子目录-o指定输出目录是AFL替换输入文件路径的占位符。启动后你会看到一个彩色的终端界面显示执行速度、覆盖率、崩溃数量等信息。3.4 结果分析与崩溃复现Fuzz跑了一段时间后findings目录下会有crashes、hangs、queue等子目录。crashes里就是触发崩溃的样本。用GDB复现gdb --args ./target findings/crashes/id:000000,sig:11,src:000000,op:havoc,rep:4在GDB里跑起来看崩溃时的调用栈定位到具体的代码行。如果崩溃原因是内存破坏还需要进一步分析是否可被利用比如能否控制EIP、能否写入任意地址等。3.5 持续化与并行化让Fuzz跑得更久更广单实例Fuzz的效率有限AFL支持多实例并行。主实例用-M从实例用-S# 主实例 afl-fuzz -i seeds -o findings -M master -- ./target # 从实例 afl-fuzz -i seeds -o findings -S slave1 -- ./target 多个实例会共享findings目录互相同步新发现的种子。如果机器核多可以开十几个从实例把CPU吃满。另外AFL支持断点续跑如果中途停了用-i-参数可以恢复afl-fuzz -i- -o findings -- ./target 4. 常见问题与排查技巧实录4.1 Fuzz跑不起来先查这几个地方新手最常遇到的问题就是AFL启动后报错退出。根据我的经验九成以上的问题出在三个地方核心转储模式没设对、CPU频率调节器没关、目标程序没有插桩。核心转储的问题前面提过了echo core /proc/sys/kernel/core_pattern能解决。CPU频率调节器可以用cpupower frequency-set -g performance关掉。插桩问题可以用afl-showmap验证如果输出为空说明编译时没插桩成功检查一下是不是用了普通的gcc而不是afl-gcc。还有一个坑是目标程序需要从标准输入读取数据但AFL默认是用文件参数。这时候可以用占位符或者用-f参数指定一个固定文件路径。如果目标程序需要网络交互那就得用afl-fuzz的-n模式非插桩模式或者换用支持网络协议的Fuzz框架。4.2 覆盖率上不去试试这几招Fuzz跑了一段时间覆盖率曲线变平了说明遇到了瓶颈。这时候可以尝试几个方向一是丰富字典把目标程序里出现的魔法字符串、关键字都加进去二是调整变异策略AFL的-p参数可以切换调度策略explore偏向探索新路径fast偏向快速变异coe偏向稀有路径三是换用更强大的插桩方式比如afl-clang-lto它比afl-clang-fast的插桩粒度更细能发现更多边。如果目标程序有复杂的校验逻辑比如CRC校验、加密签名那Fuzz很难绕过。这时候要么用符号执行辅助求解要么直接patch掉校验逻辑让Fuzz能进入深层代码。patch的时候要注意别把漏洞也patch没了。4.3 崩溃太多怎么筛分级处理前面提过用exploitable做分级这里补充一下具体用法。先装GDB插件pip3 install exploitable然后在GDB里加载gdb -ex source /usr/share/gdb/auto-load/usr/lib/exploitable/exploitable.py ./target跑崩溃样本exploitable会自动输出评级。评级为EXPLOITABLE的优先看PROBABLY_EXPLOITABLE的次之NOT_EXPLOITABLE的基本可以忽略。但要注意exploitable的判断基于启发式规则不是百分之百准确最终还是要人工确认。4.4 常见问题速查表问题现象可能原因排查方法解决方案AFL启动即退出核心转储模式不对查看报错信息echo core /proc/sys/kernel/core_pattern覆盖率长期不变种子质量差或字典缺失检查queue目录补充字典、优化种子执行速度极慢用了QEMU模式或目标程序太重看exec speed改用编译时插桩、精简目标崩溃全是同一类去重没做好看崩溃地址用afl-collect去重目标程序需要网络AFL不支持网络输入看目标行为用boofuzz或自定义harnessFuzz跑一段时间后卡死内存泄漏或死循环看hangs目录设置超时参数-t4.5 几个让我印象深刻的踩坑经历有一次Fuzz一个协议解析器跑了一整天一个崩溃都没有。后来发现是种子样本里的长度字段和实际载荷长度不匹配程序在解析早期就返回错误了根本没走到深层逻辑。把种子样本修正后两小时就出了三个崩溃。这件事让我明白种子样本的“合法性”比“数量”重要得多。还有一次Fuzz一个车载中控的蓝牙协议栈用AFL跑了很久没结果。后来换了个思路用boofuzz把蓝牙的配对、连接、数据传输这几个状态描述出来按状态机流转构造输入很快就发现了配对过程中的一个缓冲区溢出。这让我意识到对于有状态协议基于生成的Fuzz比基于变异的Fuzz更有效。另外Fuzz环境的稳定性很重要。我有一次跑Fuzz跑了三天结果机器重启了所有进度都没了。后来学乖了用screen或tmux把Fuzz会话挂起来再配合-i-断点续跑再也不怕意外中断了。5. Fuzz在渗透测试中的进阶玩法5.1 结合AI的Fuzz让变异更聪明传统的Fuzz变异是盲目的虽然覆盖率反馈能引导方向但效率仍然有限。最近几年用机器学习模型来指导Fuzz变异成为一个热门方向。核心思路是训练一个模型让它学习什么样的输入更可能触发新路径或崩溃然后用模型来生成变异。比如用RNN或Transformer对种子样本建模生成符合语法的新样本或者用强化学习来优化变异策略的选择。我在一个Web API的Fuzz项目里试过用简单的序列模型来生成JSON请求体效果比纯随机变异好不少尤其是对于嵌套结构比较深的接口。但这类方法也有局限模型训练需要大量样本而且泛化能力不一定好。目前来看AI辅助Fuzz还处于探索阶段适合作为传统方法的补充而不是替代。5.2 内核Fuzz从系统调用入手内核Fuzz是Fuzz领域的一个硬骨头也是高价值目标。内核的输入面主要是系统调用但系统调用数量多、参数复杂、状态依赖强直接Fuzz效率很低。目前比较成熟的方案是syzkaller它用一套描述语言把系统调用的参数类型、依赖关系描述出来然后自动生成调用序列。syzkaller还支持覆盖率反馈能引导Fuzz往未覆盖的内核代码走。跑syzkaller需要一台独立的机器或者虚拟机因为它会把内核跑崩。配置过程比较繁琐需要编译内核、配置SSH、设置镜像等等。但一旦跑起来它能发现很多深层次的内核漏洞。我在一个项目里用syzkaller跑了两周发现了四个内核崩溃其中两个是可提权的。5.3 Web Fuzz接口层面的自动化测试Web应用的Fuzz和二进制程序的Fuzz思路不太一样。Web接口的输入是HTTP请求参数是结构化的JSON、表单、XML所以基于生成的Fuzz更合适。可以用OpenAPI/Swagger文档自动生成请求然后对参数值做变异。RESTler是微软开源的一个工具它能根据OpenAPI文档自动推断接口依赖关系生成有意义的调用序列然后对参数做Fuzz。我在一个微服务架构的项目里用RESTler跑过一轮发现了好几个参数校验缺失的问题比如某个接口对数组长度没有限制传一个超大数组直接导致服务OOM。这类问题手工测试很容易漏掉但Fuzz能稳定复现。5.4 车载渗透测试中的Fuzz实践车载系统的Fuzz和传统IT系统有很大不同。车载网络以CAN总线为主报文格式固定但状态机复杂。而且车载ECU对实时性要求高Fuzz的时候如果发太多报文可能导致总线拥塞影响车辆正常功能。所以车载Fuzz通常要在台架上做不能直接在实车上跑。我参与过一个车载中控的渗透测试项目目标是中控与仪表之间的通信。我们用CANoe模拟仪表发送报文然后用自定义脚本对中控的响应做变异。重点测了诊断服务UDS的几个关键服务比如0x27安全访问、0x2E写数据、0x31例程控制。在0x2E服务里发现了一个长度校验漏洞传超长数据会导致中控重启。这个漏洞如果被利用可以在车辆行驶中让中控黑屏影响驾驶安全。5.5 Fuzz与渗透测试智能体的结合最近“渗透测试智能体”这个概念很火核心思路是用大模型来编排渗透测试流程自动决定下一步做什么。Fuzz可以作为智能体的一个工具智能体根据当前掌握的信息决定要不要启动Fuzz、Fuzz哪个目标、用什么策略。比如智能体发现了一个文件上传接口它可以自动下载一些样本文件启动Fuzz分析崩溃结果然后决定是否进一步利用。目前这类智能体还处于早期阶段实际项目里更多是辅助角色比如帮渗透测试工程师生成Fuzz脚本、分析崩溃日志、推荐变异策略。但方向是明确的未来Fuzz的自动化程度会越来越高人的角色会更多转向策略制定和结果研判。6. 学习路线与资源推荐6.1 从零到一的学习路径如果你刚接触Fuzz我建议按这个顺序来先理解Fuzz的基本概念和分类知道什么场景用什么工具然后动手跑通一个最简单的例子比如用AFL Fuzz一个开源的图片解析库接着学习如何编写harness也就是把目标程序包装成Fuzz能调用的形式再深入学覆盖率反馈的原理和插桩方式最后根据你的工作方向选择专攻二进制Fuzz、协议Fuzz还是Web Fuzz。整个过程大概需要两到三个月前提是每天能投入一两个小时。不要一上来就啃syzkaller或者AFL的源码那样容易劝退。先从用起来开始遇到问题再往深里挖。6.2 值得反复看的资料AFL的官方文档和源码是必看的尤其是afl-fuzz.c里的变异调度逻辑看懂了会对Fuzz有更深的理解。libFuzzer的文档也值得看它和AFL的思路不同是基于进程内的Fuzz适合Fuzz库函数。boofuzz的文档里有大量协议Fuzz的例子适合做网络协议方向的人。syzkaller的文档比较硬核但如果你想做内核Fuzz这是绕不过去的。另外Google的fuzzing仓库里有很多教程和示例质量很高。Fuzzing 101这个系列视频也值得看它用AFL和libFuzzer演示了十几个真实目标的Fuzz过程跟着做一遍收获很大。6.3 面试中常被问到的Fuzz问题渗透测试岗位的面试里Fuzz相关的问题通常不会问得太深但有几个点是高频的Fuzz和漏洞扫描的区别是什么AFL的覆盖率反馈是怎么实现的种子样本怎么准备崩溃怎么去重和分级有没有实际用Fuzz发现过漏洞。如果你能结合一个具体项目讲清楚从种子准备到崩溃复现的完整流程基本就能过关。如果面试的是更偏研究性质的岗位可能会问得更细比如AFL的变异调度算法、QEMU模式的工作原理、如何绕过Fuzz中的校验逻辑等等。这些问题就需要你对Fuzz的内部机制有比较深的理解了。6.4 我个人的一些经验之谈Fuzz这件事工具只是手段核心还是对目标的理解。你越了解目标程序的输入格式、处理逻辑、状态流转就越能设计出高效的Fuzz方案。我见过很多人一上来就afl-fuzz跑起来然后就不管了跑了一周没结果就放弃了。其实Fuzz是一个需要不断调优的过程种子要调、字典要调、变异策略要调、超时参数要调。每一次调整都可能带来效率的成倍提升。另外Fuzz发现的崩溃不等于漏洞。很多崩溃是不可利用的比如空指针解引用、断言失败。真正有价值的崩溃是那些能控制程序执行流的比如栈溢出、堆溢出、类型混淆。所以Fuzz之后的崩溃分析同样重要甚至比Fuzz本身更重要。学会用GDB、学会看汇编、学会判断可利用性这些技能和Fuzz本身一样值得投入时间。最后Fuzz是一个需要耐心的活。有时候跑几天没结果有时候一晚上出好几个崩溃。保持耐心持续优化结果总会来的。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →