从fuzz到PoC:一次二进制漏洞挖掘的完整流程"

admin 2026-10-16 11:12:13 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

从 fuzz 到 PoC:一次二进制漏洞挖掘的完整流程

原创

Sink
Sink

船山信安

2026年9月22日 00:00
湖南

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

底层手艺 · BINARY

从 fuzz 到 PoC:一次二进制漏洞挖掘的完整流程

实战流程 · 写给学完基础、想挖真漏洞的人

你以为挖漏洞是从一个聪明的输入开始的。其实它从挑一个不折磨人的目标开始——选错了,你会在前两周就耗光耐心,然后告诉自己\"这活不适合我\"。这篇把从选目标到交出 PoC 的整条链路摊开讲,不省略任何一步,包括那些让人怀疑人生的步骤。

01目标怎么选

新手最喜欢一口吞下浏览器内核。那是最坏的起点:攻击面有几百万行,状态机深不见底,光把程序跑起来就要配一周环境。你还没见到第一个 crash,就已经在编译 V8 的报错里迷路了。

真正适合练手的是输入面清楚、反馈快的东西:文件解析器、编解码库、协议实现。给一段字节,它吐一个结果,崩不崩当场知道。这种目标的循环特别短,你一天能跑几千个变异,而不是一周才调通一个构建。

挑之前拿五个维度过一遍,别凭\"听说这个项目有名\"就上手:

维度 看什么 新手该挑
是否开源 能不能读到源码、对得上崩溃栈 开源优先,闭源留给后面
有无历史 CVE 说明这段代码有人踩过坑,patch 里藏着思路 有 CVE 更友好,不是更危险
是否已有 harness 别人写好入口,你省掉最枯燥的胶水 有 harness 直接上手,没有就选简单的
攻击面是否清晰 入口是不是\"读一段字节就干活\" 越窄越好,别挑状态机迷宫
维护活跃度 没人管的代码,你报了也白报 活跃项目,能真的走到披露

常见的第一印象是\"越大的项目漏洞越多\"。错的。大项目的维护者也被 fuzz 了很久,浅坑早填平了。挑一个被低估的、输入面清楚的小目标,比在巨无霸里刨食强十倍。

02攻击面梳理:别一上来就 fuzz 整个程序

很多人把\"fuzz 一个程序\"和\"fuzz 一个入口\"混为一谈。fuzz 整个程序,等于让模糊器自己去猜\"先点哪个按钮、再发哪段报文\"——它根本不知道。结果覆盖率永远卡在第一层解析,后面几百个函数它一辈子摸不到。

先把这个程序所有的输入口列出来,再挑一个最薄的往里钻:

入口点 典型位置 常见漏洞类型
文件解析 解码器、文档/图片/视频库 越界读写、整数溢出
网络报文 协议栈、RPC、服务端监听 堆溢出、UAF、解析状态错乱
命令行参数 CLI 工具、守护进程启动 栈溢出、格式化字符串
IPC 共享内存、管道、socket 本地 信任边界绕过、越界
环境变量 启动脚本、setuid 程序 缓冲区溢出、注入
反序列化接口 对象编解码、配置加载 类型混淆、UAF

你会遇到的典型情况是:文档库有二十个解析函数,你只挑其中一个入口写 harness。把那一个入口打透,比把整个库模糊一遍有用。覆盖率反馈会告诉你——它只走过你喂进去的那条路,这才是正常的。

一句话记牢:fuzz 一个入口,是\"我指定程序走哪条路\";fuzz 一个程序,是\"让机器自己猜路\"。前者你控得住,后者基本是在浪费 CPU。

03写 harness

harness 就是一段把\"输入字节\"喂给\"你想测的那个函数\"的胶水。libFuzzer 的用法里,核心就是一个函数:

include

include

include \"codec.h\"

extern \"C\" int LLVMFuzzerTestOneInput(const uint8\_t \*data, size\_t size) {

Parser p;

p.reset();                          // 清掉上次的状态

Result r = p.parse(data, size);    // 只调这一个入口

(void)r;

return 0;                           // 0=没意见,非0=有情况

}

三件事写 harness 时最容易翻车。一是太大:顺手把整个 main 的逻辑抄进来,初始化、日志、配置文件全带上,结果崩溃栈里一半是你自己的胶水代码,真正出问题的那行反而被淹了。二是跑不独立:依赖某个全局单例,第一次能跑,第二次状态就脏了。三是留了全局状态:忘了 reset(),上一次解析的残渣混进下一次,崩溃时你分不清是谁的锅。

我踩过最蠢的一个坑:harness 里忘了关掉日志输出,变异一来就打一行,一秒两万行,磁盘先满了,fuzzer 自己先挂。那天的产出是 Zero,外加一个需要清的几十 G 日志目录。

覆盖率反馈的意义在这儿:libFuzzer / AFL++ 会在编译时插桩,记录\"这次输入走到了哪些分支\"。每发现一个没走过的新分支,就把触发它的输入存进语料库。你的 harness 越干净,覆盖率曲线越能反映真实代码的边界,而不是你胶水代码的边界。

04跑起来

编译插桩这一步决定你后面能不能拿到精准的崩溃栈。libFuzzer 把 fuzzer 引擎和 sanitizer 一起编进二进制;AFL++ 则靠环境变量控制插桩模式。常用命令长这样:

libFuzzer + ASan:覆盖率与内存错误一起抓

clang++ -g -O1 -fsanitize=fuzzer,address harness.cc -o fuzz\_target

跑:语料库 + 字典 + 时间上限

./fuzz\_target corpus/ -dict=proto.dict -max\_total\_time=86400

AFL++ 插桩编译(CLASSIC 模式 + 地址消毒 + 硬化)

AFL\_USE\_ASAN=1 AFL\_LLVM\_INSTRUMENT=CLASSIC make

主从并行:-M 主节点做确定性变异,-S 从节点随机

afl-fuzz -i seeds -o sync -M main -- ./target @@

afl-fuzz -i seeds -o sync -S s1  -- ./target @@

语料库(seed corpus)别空着。给几个真实有效的输入当起点,fuzzer 第一秒就过了\"这是不是合法格式\"那道门,变异直接往深处走。字典(dict)对文本协议、带魔数的二进制格式尤其值钱——把关键字、标签值写进去,它一步就能拼出多字节的魔数,而不是一个字节一个字节地蒙。

并行用 -M 加若干 -S,主节点做确定性变异、从节点做随机变异,二者共享语料目录。跑多久停?看两条线:覆盖率曲线变平了、且长时间没有新路径,说明这一轮到头了;或者你只是想挂一晚上,设个 -max_total_time 就行。别指望\"再跑五分钟就有突破\"——那种期待几乎每次都落空。

05Crash 不是漏洞

fuzzer 报 crash,你先别激动。AFL++ 一个晚上能攒几百个 crash 文件,里面九成是同一个 bug 的不同触发。三件事按顺序做:

去重。按栈回溯哈希分桶。同一个根因,无论输入长什么样,回溯栈是一样的。AFL++ 自己会做崩溃分类,你别把\"id:000001\"和\"id:000237\"当成两个洞——它们多半是同一个。

最小化。原始触发输入可能几 MB,大部分字节跟崩溃无关。用 afl-tmin 缩,用 llvm-symbolizer 看符号,再手动删字节——直到少一个字节就不崩。这一步不是洁癖,是给后面写报告的人(包括未来的你)留一条能读懂的路。

可复现。换台机器、换个编译选项、重跑十次,它还崩吗?很多\"崩溃\"只在某次内存布局下出现,是噪音。复现不了的,先丢一边,等它再来。

大部分 crash 是重复的。你真正要处理的那一个,往往藏在几百个文件里、看起来最不起眼的那条栈。去重做不好,你会把时间花在给同一个 bug 写第十遍报告上。

06根因分析

最小化之后,用 ASan / UBSan / Valgrind 重新编译复跑。sanitizer 会在坏内存访问发生的那一刻 abort,并吐一份报告。一份典型的 heap-buffer-overflow 报告长这样:

==4121==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6020000000b1

READ of size 1 at 0x6020000000b1 thread T0

#0 in read\_varint /src/codec/varint.c:41

#1 in decode\_field /src/codec/decode.c:118

#2 in codec\_decode /src/codec/module.c:72

0x6020000000b1 is located 0 bytes after 1-byte region

[0x6020000000b0,0x6020000000b1)

allocated by thread T0 here:

#0 in malloc

#1 in PyBytes\_FromStringAndSize

SUMMARY: AddressSanitizer: heap-buffer-overflow /src/codec/varint.c:41

逐行读:

第一行,漏洞类型。heap-buffer-overflow 直接告诉你这是堆上的越界,不是栈也不是释放后重用。地址是出错那块内存的位置。

第二行,越界偏移。READ of size 1 说读了 1 字节;0 bytes after 1-byte region 说缓冲区只有 1 字节长,这次读正好踩在它右边一格。一个变长整数解码器,没检查输入是不是在半路就结束了。

第三到五行,访问栈。从 read_varint 一路到 codec_decode,告诉你\"坏读发生在哪、是谁调进来的\"。根因通常不在最底层那行,而在它的调用者——为什么给了它一个会越界的输入。

第七到九行,分配栈。这块内存是哪来的。配合访问栈,你能拼出\"谁分配、谁越界、偏移多大\"的完整故事。UBSan 则补另一类:有符号整数溢出、空指针、对齐错误——这些 ASan 看不见。

同一份报告里,漏洞类型 + 越界大小 + 偏移 三样齐了,你才有资格判断它值不值得往下做。少一样,都是在赌。

07可利用性判断

能崩不代表能利用。MITRE 和 CISA 的 2024 年 CWE Top 25(基于 2023–2024 年 31,770 条 CVE 记录)里,越界写(CWE-787)排第 2、越界读(CWE-125)排第 6、释放后重用(CWE-416)排第 8——内存破坏类占了前八里三个席位,是最常见的那一类。但排位高不等于都好利用,得看你能写多少、控什么:

漏洞类型 能写几个字节 能控什么 典型利用难度
栈溢出 可覆盖返回地址 控制执行流 低(有保护则中)
堆溢出 取决于相邻块 改写堆元数据/指针 中–高
UAF 可复用已释放块 类型混淆/任意读写 高
越界读 只读,不写 泄露内存内容 低(组合则高)
整数溢出 间接导致分配偏小 触发上述任一 中(常作跳板)

越界读最容易被低估。它不能直接写,但能读。读出来的若是堆地址、模块基址、canary,ASLR 就废了——信息泄露本身就是高危。你会遇到\"只是读了一下\"就被定成严重的案例,因为它把别的漏洞的利用门槛削平了。

判断可利用性,问自己一句:攻击者拿了这块越界,下一步能改变什么?能改执行流,就是高危;只能让程序崩,那它只是 DoS。中间那一大片,靠的就是你对这块内存布局的理解。

08写报告与披露

报告的读者是修代码的人,不是看你文笔的人。四样东西缺一不可:影响范围(哪些版本、什么配置中招)、触发条件(怎么跑到那行)、PoC(最小可复现输入)、修复建议(在哪加哪道检查)。别写\"建议加强输入校验\"这种废话,写\"在 varint.c:41 的循环里加上输入边界判断\"。

披露时限有个绕不开的惯例:Google Project Zero 的90+30 政策——给厂商 90 天修,补丁出来后 30 天才公开细节;若 90 天内修不完,可申请 14 天宽限;若发现已在野外被利用,则压缩到 7 天。据其 2025 年披露政策 FAQ,自宽限机制启用以来约 96.9% 的问题都在期限内修掉了。这不是铁律,但你得有个自己的时间锚点,别让厂商无限期拖。

申请 CVE 走 CNA(CVE Numbering Authority)体系。全球已有 300 多家 CNA,大厂和开源基金会都在其中——能直接给自己产品发号。如果你测的项目属于某家 CNA 的 scope,就通过它走;否则去 MITRE 的 cveform.mitre.org 选 \"Report Vulnerability / Request CVE ID\",填邮箱、受影响组件、攻击向量、建议描述,提交后等编号,编号先处于保留状态,公开时再走一次提交流程。流程不复杂,难的是前面把证据做扎实。

一个常犯的错:跳过厂商直接公开。多数项目的安全策略会写明联系窗口和禁发期,你绕过去,不仅拿不到配合,还可能把自己放进\"不守规矩的研究者\"名单。先协调,再公开。

09现实的一面

说点不中听的。绝大多数 crash 是重复的,绝大多数去重后的 bug 是不可利用的,跑三天零收获是常态。我见过有人挂了一周 fuzzer,产出一个 crash,去重完发现是别人两年前报过的。

这不是劝退。是想让你把预期放对:挖掘不是\"努力就有回报\"的活,是\"大部分时间在空转、偶尔撞上一次\"的活。你能不能留下来,不取决于你多会写 harness,取决于空转那三天你还在不在。


ONE LINE
       机器负责穷举,你负责判断哪里值得穷举。


参考来源

· Google Project Zero,Vulnerability Disclosure Policy / FAQ(projectzero.google,2025):90+30 披露时限、14 天宽限、7 天在野利用、约 96.9% 期限内修复

· MITRE / CISA,CWE Top 25 Most Dangerous Software Weaknesses(2024):基于 2023–2024 年 31,770 条 CVE 记录,CWE-787 第 2、CWE-125 第 6、CWE-416 第 8

· AFL++ Documentation(aflplus.plus),Parallel Fuzzing / Instrumentation:AFL\_LLVM\_INSTRUMENT=CLASSIC、AFL\_USE\_ASAN=1、-M/-S 主从并行、afl-tmin

· Google,AddressSanitizer 文档(Serebryany & Vyukov):heap/stack/global-buffer-overflow 与 use-after-free 报告结构、READ of size 与 region 偏移解读

· CVE Program / MITRE,cveform.mitre.org 与 CNA Partner 信息(cve.org):全球 300+ CNA、\"Report Vulnerability / Request CVE ID\" 流程、协调披露

· Python Testing & Debugging,Fuzzing a C Extension with Atheris and ASan:ASan 报告的四个部分与\"四问\"解读框架


从fuzz到PoC:一次二进制漏洞挖掘的完整流程" 网络安全文章

从fuzz到PoC:一次二进制漏洞挖掘的完整流程"

本文系统讲解从fuzz到POC的完整二进制漏洞挖掘流程,涵盖目标选择、攻击面梳理、harness编写、fuzzing运行、crash去重与根因分析、可利用性判断及报告披露,强调选择输入面清晰的小目标、控制入口而非整个程序,并给出GoogleProjectZero的90+30披露政策等实操建议。
Linux内核漏洞连续暴露:从本地提权到实际攻击,内核安全进入高风险窗口期" 网络安全文章

Linux内核漏洞连续暴露:从本地提权到实际攻击,内核安全进入高风险窗口期"

linux内核近期披露多个本地提权漏洞,攻击者可利用内核内存破坏将权限提升至root,部分漏洞已被CISA列入KEV并遭现实利用。漏洞涉及网络代码、用户命名空间及SCTP协议栈,AI辅助研究正提升漏洞发现效率。企业需建立从资产识别到补丁验证的完整治理闭环,持续监测攻击行为。
从20亿美元暗网遗产到勒索风暴:当公开账本遇上“门罗币”与数字监控帝国" 网络安全文章

从20亿美元暗网遗产到勒索风暴:当公开账本遇上“门罗币”与数字监控帝国"

本文详述美国IRS-CI特工利用区块链分析打击暗网犯罪:追回丝绸之路黑吃黑20亿美元比特币、破获Helix与BitcoinFog混币器、应对ColonialPipeline勒索攻击,并揭露Chainalysis的Rumker节点监控与WalletExplorer蜜罐工具。核心结论是区块链公开账本与网络层监控结合可有效追踪犯罪,但门罗币等隐私币与跨国执法边界构成新挑战。
评论:0   参与:  0