用AI辅助模糊测试在FFmpeg中发现除以零崩溃
2026/8/31 17:38:10 网站建设 项目流程

FFmpeg 几乎每天都有新的提交,但基于它做模糊测试,仍然能挖出问题。这次我们用一个基于 vibe coding 搭建的轻量模糊测试工具,在 FFmpeg 中发现了一个除以零崩溃,触发条件并不复杂,本质是解析输入时某个分母字段没有做非零校验。整个过程下来,最有价值的不是“挖到一个洞”,而是把“AI 辅助写工具、模糊测试、崩溃分析、修复回归”这条链路完整跑通了一次。

如果你正在学习漏洞挖掘,或者已经在看模糊测试,这篇内容会比较合适。它只讲最普通的工程流程:怎么搭一个最小可运行的 fuzzer,怎么让 FFmpeg 在 sanitizer 下暴露异常,怎么确认崩溃样本,以及后续怎么修复和回归。接下来按我实际执行的顺序拆开讲。

1. 为什么把 FFmpeg 当作模糊测试目标

新手选目标时容易犯一个错误:哪里有漏洞就往哪里冲,忽略了目标本身的稳定性和可复现性。FFmpeg 是个很合适的目标,原因有三点。第一,它确实复杂,输入格式多,解析逻辑多,边界条件也多。第二,它影响面大,大量服务端转码、播放器、流媒体系统都会调用它。第三,它非常适合用模糊测试验证,因为命令行入口明确,输入是一个文件,输出可以丢弃,方便做子进程级测试。

1.1 FFmpeg 为什么是高频漏洞目标

FFmpeg 不是单个程序,而是一套多媒体处理框架,包含大量 demuxer、decoder、encoder、filter。每种格式都有自己的解析逻辑,很多逻辑直接用 C 写指针位移、位域、除法和移位,边界条件非常多。一个解析函数出问题,影响可能不局限于某个软件,而是所有依赖 FFmpeg 的产品。

还有一点很关键:FFmpeg 处理的是不可信输入。一个音视频文件从网络上下载下来,可能来自任意来源,文件头里的宽度、高度、采样率、时间基等字段完全由文件内容决定。如果解析端没有做好校验,文件头里一个普通字段就能让进程崩溃。

现在可能有人会说,FFmpeg 不是一直在 OSS-Fuzz 上跑吗,还能挖到什么?实际上,别人的覆盖并不等于你的 mutation 路径也能覆盖到。换一套种子、换一个工具,甚至换一组 configure 选项,都可能走到不同分支。这次发现的除零问题严格来说不算严重,但它说明自定义 fuzzer 仍然有价值。

1.2 除以零崩溃为什么值得认真对待

每次看到崩溃日志里出现 division by zero,第一反应可能是“就这?”。放在普通程序里,除零可能只是一个 bug,但放在解析不可信输入的程序里,问题就变了。攻击者可以把一个正常文件改几个字节,让 FFmpeg 解析到某个非法字段,触发 SIGFPE。进程退出,服务端如果没做防护,就变成拒绝服务。

更关键的是,除以零往往是输入校验不全的信号。同一个字段如果被后续代码当作数组下标、内存大小、循环次数使用,可能就不是崩溃,而是越界读写。所以发现除零后,最好顺着这个字段再往下查一遍,不要急着跳过。

对初学者来说,除以零也是一个很好的入门案例:复现稳定、报错信息清楚、修复成本低。跑通一次后,你会理解 sanitizer 怎么工作、崩溃样本怎么分析、补丁怎么验证,这套经验可以迁移到其他漏洞类型。

2. 用 vibe coding 搭一个轻量模糊测试工具

这次工具不是从头手搓的,而是用 vibe coding 方式做的。解释一下:所谓 vibe coding,就是让 AI 根据你的需求描述直接生成初版代码,你再做检查、修改和运行。它适合快速搭脚本,但不代表生成后就能直接扔到生产环境。

2.1 先想清楚工具要做什么,再让 AI 写代码

给 AI 的需求越具体,代码越能用。我最初的需求是:读取 seeds 目录里的种子文件,随机改动若干字节,写入临时文件,然后执行 FFmpeg 解复用并输出到 null,捕获返回码和 stderr,如果发现异常退出或者 stderr 里出现 division by zero,就把样本保存到 crashes 目录。

这一步看起来简单,但它决定了整个工具的结构。需求里已经隐含了四个模块:输入生成、进程执行、结果判断、样本收集。AI 根据这种需求很容易生成一个初版脚本,难点在后面的审查和迭代。

2.2 让 AI 生成初版,但必须逐行审查

AI 生成的 Python 脚本通常能跑,但细节往往需要改。我拿到初版后重点检查三处。

第一是超时。FFmpeg 对某些畸形输入可能会卡住甚至死循环,如果没有 timeout,fuzzer 会挂在一个样本上。第二是临时文件处理。很多初版脚本会固定写同一个路径,这没问题,但多进程跑的时候都会往同一个路径写,互相覆盖,结果判断就乱了。第三是错误码判断。FFmpeg 正常解析失败也可能返回非零码,不能把所有非零码都当成崩溃,要看 stderr 里有没有 sanitizer 的报错。

下面是 AI 生成后我修改过一轮的简化版本。它已经能完成基本闭环,但只能算最小演示,不是生产级。

#!/usr/bin/env python3 import subprocess import hashlib import os import random import time FFMPEG = "./ffmpeg_fuzz" TMP = "/tmp/fuzz_input.bin" OUT = "crashes" def run_case(data, timeout=3): with open(TMP, "wb") as f: f.write(data) try: r = subprocess.run( [FFMPEG, "-v", "error", "-i", TMP, "-f", "null", "-"], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=timeout, ) return r.returncode, r.stderr.decode("utf-8", "ignore") except subprocess.TimeoutExpired: return None, "timeout" def mutate(data): data = bytearray(data) if len(data) == 0: return bytes(data) for _ in range(random.randint(1, 8)): pos = random.randrange(len(data)) data[pos] = random.randrange(256) return bytes(data) def main(): seed_dir = "seeds" os.makedirs(OUT, exist_ok=True) seeds = [] for name in os.listdir(seed_dir): with open(os.path.join(seed_dir, name), "rb") as f: seeds.append(f.read()) for i in range(10000): seed = random.choice(seeds) data = mutate(seed) code, err = run_case(data) if code is None: continue if code not in (0, 1) or "division by zero" in err: digest = hashlib.md5(data).hexdigest()[:12] name = f"crash-{int(time.time())}-{digest}.bin" with open(os.path.join(OUT, name), "wb") as f: f.write(data) print("saved", name, "return", code) if "division by zero" in err: print(err[:500]) if __name__ == "__main__": main()

这段代码能覆盖最简单的场景:随机替换若干字节,跑 FFmpeg,看返回码和 stderr。它没有做覆盖率反馈、没有最小化样本、没有并发调度,而且每次 mutation 都基于原始 seed,效率不高。但这些可以后面再迭代,第一步是让它稳定收到崩溃。

审查时还要注意 AI 有没有用shell=Trueos.system拼接命令。fuzz 场景下输入文件名大多是自己生成的,风险不大,但在其他场景就会变成命令注入问题。这类隐患在 AI 生成的代码里很常见。

2.3 为什么初版工具不能直接拿去批量跑

初版工具看起来能跑,但离“批量跑”还有距离。如果你把循环次数改成 10 万,并且开 8 个进程,会遇到几个问题:临时文件冲突、日志输出混乱、一个 worker 的崩溃样本被另一个 worker 读走、CPU 调度不稳定。我一般先跑 100 条,观察单进程速度,再决定是否加多进程。

还有一个容易忽略的点:工具的职责是帮你发现问题和保存证据,不是帮你判断漏洞。它输出一个 crash 样本,只代表进程异常退出。这个异常是除零、是超时、是 OOM 还是 assert,都要人工去看。如果代码里把所有非零退出都当成漏洞,几小时后你会得到一大堆垃圾样本。

3. 环境准备和最小复现流程

模糊测试的稳定性取决于环境和验证方式。这里的核心准备是三点:编译一个带 sanitizer 的 FFmpeg、准备一个复现用种子集、写一条最小验证命令。

3.1 编译带 sanitizer 的 FFmpeg

要发现除零问题,最直接的办法是给 FFmpeg 开 UndefinedBehaviorSanitizer,也就是常说的 UBSan。它能检测整数除零、移位越界、整数溢出等未定义行为,并打印出文件和行号。如果想同时排查内存问题,可以再加上 AddressSanitizer。

我用的编译命令大致如下,这里给的是示例,具体组件按你自己的目标调整。

cd FFmpeg ./configure --disable-shared --enable-static --cc=clang \ --extra-cflags="-fsanitize=address,undefined -g -O1" \ --extra-ldflags="-fsanitize=address,undefined" \ --disable-everything \ --enable-demuxer=mov,flv,mpegts \ --enable-decoder=h264,aac make -j"$(nproc)"

有几个点要解释。

configure 选项作用说明
--cc=clang指定编译器为 clangASan/UBSan 在 clang 上支持更好
-fsanitize=address,undefined同时开启内存错误和未定义行为检测本次除零主要靠 undefined 部分
-O1保留基本优化-O0快,栈回溯仍然可用
-g生成调试信息分析栈回溯必须有
--disable-everything关闭大部分组件减少编译时间,按需开启 demuxer/decoder

需要提醒的是,FFmpeg 代码量大,全量编译需要一段时间。如果机器资源有限,建议先做最小化 configure。只要你测试的目标是某一类文件,比如 mp4、flv、ts,就只开启对应的 demuxer 和 decoder。这样编译时间会明显缩短,跑起来也更干净。

编译完成后,默认生成的是ffmpeg这个二进制。建议手动复制一份叫ffmpeg_fuzz,方便和生产环境的 FFmpeg 区分。后续所有 fuzz 命令都用这个专用二进制,避免干扰。

3.2 准备种子文件时要注意什么

种子文件是模糊测试的起点。不要去网上下一个几 GB 的大视频,mutation 一次要复制整个文件,效率很低。准备几个小体积但格式合法的样例,几百 KB 以内就行,最好覆盖你想测的容器类型。

我一般会准备三到五个种子:一个正常的 mp4、一个正常的 flv、一个正常的 mpegts。如果下一步想测试某个特殊解码器,再准备对应格式的小文件。种子文件本身要能被 FFmpeg 正常读取,否则后续所有崩溃都建立在无效输入上,分析起来会很乱。

3.3 用最小用例验证工具是否正常

工具接好环境后,先不要直接跑 fuzz。找三个样本手动验证:一个正常文件、一个空文件、一个截断文件。

正常文件应该正常退出,返回码为 0 或者符合预期;空文件可能在解析时报错,但不会触发 sanitizer;截断文件可能让 FFmpeg 报 invalid data,但不应该崩溃。如果这三个基础输入都能得到预期结果,说明子进程捕获、超时机制和输出判断基本可用。

这时候再跑 fuzzer,得到的崩溃样本才可信。否则可能工具本身有问题,把正常退出当成崩溃,或者把超时当成漏洞。

注意:如果你在 Windows 上跑这套流程,建议优先使用 WSL 或容器环境。FFmpeg 的 configure 脚本在纯 Windows 环境下的编译链差异会消耗不少调试时间

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询