之前在做音视频模块安全评估时,我尝试用“vibecode”这种 AI 辅助编码的方式快速搭建了一个模糊测试工具,对 FFmpeg 的输入解析逻辑做了几轮随机测试。结果比预期来得更快,测试刚开始几十秒,就触发了一个“除以零”崩溃。把崩溃栈拉出来之后,发现所有解析器都会遇到的老问题又出现了:某个从输入流里读出来的“长度/索引/时间戳”字段被直接当成除数使用,而字段值没有做非零校验。
这篇文章就围绕这次实战展开,讲清楚 vibecode 辅助开发的方式、模糊测试的基本原理,以及在 FFmpeg 这类 C/C++ 多媒体解析库中,除以零漏洞是如何产生、如何被 fuzzing 工具发现、又如何修复和避免的。
如果你对音视频方向感兴趣,或者在做 C/C++ 项目的安全测试,又或者只是听说过“模糊测试”“libFuzzer”但还没完整跑通过一个例子,这篇文章都比较适合往下读。
1. 背景与核心概念
1.1 vibecode 是什么意思
vibecode 是最近两年流行起来的一个开发模式,指的是“借助 AI 编程助手,用对话式的方式快速产出代码”的工作流。很多同学会把 vibecode 理解成“让 AI 把整个项目写完”,但在实际工程里,它更接近“AI 辅助编码 + 人工审查 + 快速迭代”的组合。
在我们的模糊测试项目中,vibecode 主要被用在三个环节:
- 生成 Fuzz Harness,也就是连接被测函数和模糊测试引擎之间的桥接代码。
- 生成构建脚本和 Dockerfile,快速搭出编译环境。
- 分析 sanitizer 输出的崩溃日志,让 AI 帮忙归纳可能的根因。
需要注意的是,AI 生成的代码虽然效率高,但边界处理不一定可靠。后面我会专门讲一下,AI 辅助生成的 Fuzzer 代码反而更需要做代码审查。
1.2 模糊测试是什么
模糊测试(Fuzzing)是一种自动化的软件测试方法,核心思路是向被测程序不断输入随机、变异或结构化的数据,然后监控程序是否发生崩溃、超时、内存溢出、未定义行为等异常。
模糊测试并不是一个新概念,但最近几年因为 libFuzzer、AFL++、honggfuzz 这些工具越来越成熟,加上 Sanitizer 系列工具的普及,它已经成为安全测试和质量保障里的标准手段。
常见的模糊测试流程如下:
- 准备一批种子输入(Seed Corpus),比如小的音视频文件、图片文件、配置片段。
- 模糊测试引擎会对这些种子做变异,生成大量新输入。
- 每次变异后的输入都会被喂给被测目标执行。
- 如果被测目标发生崩溃,fuzzer 会保存触发崩溃的输入样本,供后续分析。
模糊测试能发现很多人工测试难以覆盖的问题,尤其是除零、整数溢出、数组越界、空指针解引用、内存泄漏等。
1.3 除以零漏洞为什么值得关注
在 C/C++ 语言里,整数除以零是未定义行为(Undefined Behavior)。程序一旦执行到整数除零,在多数平台上会触发 SIGFPE 信号,导致进程直接崩溃。如果是服务端程序,一次崩溃可能导致整个服务不可用;如果是解析器,攻击者可能只需要构造一个很小的恶意文件,就能让播放器、转码服务或者流媒体网关崩溃。
需要注意的是,浮点数除以零通常不会崩溃,而是得到inf或nan。所以本文讨论的“除以零漏洞”,通常指整数运算中的除数为零。
在很多解析器场景里,除数往往来自输入文件中的某个字段,例如:
- 视频流里的帧率、采样率。
- 容器格式里的 duration、timescale。
- 索引表里的 entries 数量。
- 某个 chunk 的长度值。
这些字段是外部可控的,如果代码没有校验就直接作为除数,就很容易产生除零崩溃。在 FFmpeg 这种需要解析大量音视频格式的代码库里,输入字段结构复杂、格式众多,这个问题会被进一步放大。
2. 为什么 FFmpeg 容易出现除零问题
2.1 复杂输入解析带来的风险
FFmpeg 是一个庞大的多媒体处理框架,支持的封装格式、编码格式、滤镜、设备非常多。不同的格式有各自不同的头结构、索引结构、描述字段,很多字段的值在逻辑上应当是“正数”,但在实际文件中可能是 0,甚至可能是负数。
一个典型的解析流程是这样的:
- 从文件中读取若干字节。
- 按照格式规范把字节解析成字段。
- 用字段去计算偏移量、时长、采样率、帧率。
- 根据结果分配内存或执行后续逻辑。
如果第 2 步读到的字段是 0,而第 3 步直接拿它做除数或做除数的一部分,就会触发除零。对于模糊测试来说,这类路径通常很容易被覆盖到,因为只需要把某个字段变异为 0 就可以了。
2.2 C/C++ 整数除零语义
在 C++ 标准中,整数除以零属于未定义行为。不同平台的处理方式不完全一样:
- 在 Linux x86 平台上,整数除以零会触发
SIGFPE信号,默认动作是终止进程。 - 在 Windows 平台上,整数除零会触发结构化异常
EXCEPTION_INT_DIVIDE_BY_ZERO。 - 如果使用未定义行为检测工具(UBSan),代码执行到整数除零时会被插入检查逻辑,运行时会输出类似
division by zero的报错。
由于未定义行为本身是一个“约定之外”的代码缺陷,编译器可能对代码做各种优化,导致实际表现非常隐蔽。所以复现这类问题最理想的方式,是在编译时开启 UBSan,让未定义行为暴露出来。
2.3 典型触发场景
在 FFmpeg 相关代码中,有几类常见的除零触发场景:
时间基换算 从容器中解析出
time_base后,用另一个流的时间戳除以它做归一化,但没有判断time_base是否为 0。采样率换算 音频重采样时,用输入采样率与输出采样率计算比例,输入采样率来自文件头字段,可能是 0。
帧数/索引计算 以“总字节数 / 每帧字节数”来估算帧数时,每帧字节数可能为 0。
视频宽高比计算 用高度字段作为除数计算宽高比时,高度为 0 会导致除零。
下面我们会用一个最小化的示例代码来模拟这种场景,然后通过模糊测试把崩溃揪出来。
3. 环境准备与工具链
在实际开始模糊测试前,需要准备好 FFmpeg 环境、C/C++ 编译工具链和模糊测试引擎。
3.1 FFmpeg 安装与验证
FFmpeg 的安装方式很多,不同系统略有差异。
在 Windows 上,最常见的方式是下载官方编译好的静态构建包,解压后将bin目录加入PATH。也可以使用包管理器安装,例如:
winget install ffmpeg在 CentOS 7 或类似系统上,如果直接使用系统自带的源安装,版本通常比较旧:
yum install ffmpeg如果系统源里没有 FFmpeg,或者需要较新版本,推荐使用静态构建包或者源码编译。源码编译的好处是后续如果要给 FFmpeg 自身做 fuzzing,可以直接基于源码构建。
安装完成后验证版本:
ffmpeg -version能看到版本信息,说明 FFmpeg 可执行文件已经可以正常使用。本文的模糊测试示例并不直接运行ffmpeg命令行,而是针对一个模拟的解析函数,不过后续扩展时可能需要用 FFmpeg 的源码库做真正的 fuzz target。
3.2 编译工具链
由于我们使用 libFuzzer 和 Sanitizer,建议安装 Clang。在 Ubuntu 或 Debian 系统上:
apt install clang llvm在 CentOS 7 等系统上,如果包管理器里的 clang 版本太旧,建议使用较新版本的 LLVM 或从源码编译。libFuzzer 是 Clang 自带的一个库,不需要额外下载。
需要说明的是,编译时实际使用的 Clang 版本需要根据你的系统环境调整。不同版本的 Sanitizer 在输出格式和参数上可能有细微差别,但整体思路是一致的。
3.3 模糊测试引擎选择
目前主流的模糊测试引擎有:
| 引擎 | 特点 | 适用场景 |
|---|---|---|
| libFuzzer | 集成在 Clang 中,进程内执行,速度快 | 适合对单个函数做单元级 fuzz |
| AFL++ | 独立工具,支持多种插桩方式 | 适合对可执行程序做黑盒/灰盒 fuzz |
| honggfuzz | 支持硬件反馈和软件插桩 | 适合复杂目标 |
本文选择 libFuzzer,因为它配置简单、开箱即用,和 Sanitizer 配合得也最好。你如果更熟悉 AFL++,也可以用类似的思路,只是启动方式和参数会有区别。
4. 使用 vibecode 快速搭建模糊测试工具
这一节是本文的核心实操部分。我会展示如何用 AI 辅助编码的方式快速生成一个 Fuzz Target,以及如何配合 libFuzzer 跑出一个除零崩溃。
为了演示清晰,我构造了一个“模拟解析函数”,这个函数模仿了 FFmpeg 中从输入字节流解析参数并计算结果的写法。它不是为了还原 FFmpeg 的真实代码,而是为了用最小案例说明除零漏洞如何被 fuzzer 发现。
4.1 用 AI 辅助生成 Fuzz Target
在使用 vibecode 模式时,你可以直接把目标描述给 AI,让 AI 生成一份初始 Fuzz Target 代码。
例如,给 AI 的提示词可以是:
请帮我写一个 C++ 的 libFuzzer target。 目标函数是一个音频解析函数,接收 uint8_t* data 和 size_t size。 需要从 data 中解析出 sample_rate 和 frame_size, 然后返回 sample_rate / frame_size 的结果。 请确保函数可以直接被 LLVMFuzzerTestOneInput 调用。AI 生成的代码很可能类似下面这样:
#include <stddef.h> #include <stdint.h> int parse_audio_info(const uint8_t *data, size_t size) { if (size < 8) { return 0; } int sample_rate = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; int frame_size = (data[4] << 8) | data[5]; // 返回帧率相关的结果 return sample_rate / frame_size; } extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { parse_audio_info(data, size); return 0; }这里可以看到,AI 生成的代码有一个明显问题:frame_size没有做非零校验。当data[4]和data[5]都为 0 时,frame_size就是 0,于是sample_rate / frame_size触发整数除零。
在实际项目中,你不应该直接信任 AI 生成的代码,而应该先把这种边界问题修掉或记录下来。但为了演示 fuzzing 发现漏洞的过程,我们先保留这个有缺陷的版本,让它被 fuzzer 运行。
4.2 编写最小示例
把上面的代码保存为fuzz_audio_parser.cpp。
为了避免真实项目干扰,这里单独创建一个目录:
ffmpeg-fuzz-demo/ ├── corpus/ ├── crashes/ ├── fuzz_audio_parser.cpp └── build.sh4.3 构建种子语料库
在corpus目录里放一些种子文件。libFuzzer 会从这些种子开始变异,生成更多输入。
我们可以用脚本生成几个简单的种子文件:
#!/bin/bash # 文件路径:corpus/gen_seeds.sh mkdir -p corpus # 生成 4 个字节的种子文件 printf '\x00\x00\x00\x10\x00\x00\x00\x20' > corpus/seed1.bin printf '\x00\x00\x00\x20\x00\x10\x00\x20' > corpus/seed2.bin printf '\x00\x00\x00\x30\x10\x00\x00\x20' > corpus/seed3.bin printf '\x00\x00\x00\x40\x00\x00\x10\x20' > corpus/seed4.bin种子文件的核心目的是让 fuzzer 有初始的覆盖范围。即使种子很简单,libFuzzer 也会快速变异出大量新的字节组合。
4.4 构建脚本
在build.sh中写入编译命令:
#!/bin/bash # 文件路径:build.sh clang++ -g -O1 \ -fsanitize=fuzzer,address,undefined \ -o fuzz_audio_parser \ fuzz_audio_parser.cpp参数解释:
-g:生成调试信息,崩溃时能看到带源码行号的堆栈。-O1:优化级别,适合 fuzzing,既能提升速度,也保留一定的可调试性。-fsanitize=fuzzer,address,undefined:依次启动 libFuzzer、AddressSanitizer 和 UndefinedBehaviorSanitizer。
这里特别说明一下,UBSan 是发现除零的关键。当frame_size为 0 时,UBSan 会捕获到未定义行为,即使没有真正触发SIGFPE,也会输出类似下面的信息:
runtime error: division by zero给脚本加上执行权限并编译:
chmod +x build.sh corpus/gen_seeds.sh ./build.sh如果编译成功,会生成fuzz_audio_parser可执行文件。
5. 运行模糊测试并复现除零崩溃
5.1 编译并启动 fuzzer
运行下面的命令启动模糊测试:
./fuzz_audio_parser corpus/ -max_total_time=60这里corpus/是种子语料库目录,-max_total_time=60表示最多运行 60 秒。
libFuzzer 启动后会打印覆盖率信息,例如:
INFO: Seed: 1234567890 INFO: Loaded 4 corpus files INFO: -max_len is not provided, using 4096 #4 NEW cov: 12 ft: 13 corp: 4/16b exec/s: 0 rss: 12Mb #123 NEW cov: 15 ft: 18 corp: 5/24b exec/s: 123 rss: 12Mbcov是被覆盖到的代码块数量,corp是当前累计的测试语料数量,exec/s是每秒执行次数。
5.2 观察 sanitizer 输出
很快,fuzzer 就会发现除零问题并输出崩溃信息。类似下面的内容:
ERROR: libFuzzer: deadly signal UndefinedBehaviorSanitizer: undefined-behavior fuzz_audio_parser.cpp:14:20 Runtime error: division by zero SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior fuzz_audio_parser.cpp:14:20如果frame_size为 0 且优化后直接执行到整数除法,也可能看到SIGFPE信号:
ERROR: libFuzzer: deadly signal ==123456== ERROR: libFuzzer: deadly signal #0 0x4f0f1a in parse_audio_info fuzz_audio_parser.cpp:14:20 #1 0x4f10a3 in LLVMFuzzerTestOneInput fuzz_audio_parser.cpp:19:3 ...同时,libFuzzer 会把触发崩溃的输入样本保存到当前目录下的crash-*文件或crashes/目录中。如果是 undefined behavior,生成的样本可能保存在timeout-*或leak-*,具体取决于 fuzzer 的类型。
5.3 崩溃样本分析
拿到崩溃样本后,可以通过如下命令复现:
./fuzz_audio_parser crash-xxxxxxxx或者用十六进制查看样本内容:
xxd crash-xxxxxxxx对于上述代码,触发崩溃的输入通常是后两位字节为 0。也就是data[4]和data[5]都为 0,导致:
frame_size = (0 << 8) | 0 = 0然后执行:
sample_rate / 0这样就触发除零。
从 fuzzing 的角度来看,整个发现过程只需要几个步骤:
- 把输入字节变异成某个字段为 0。
- 程序执行了解析逻辑。
- sanitizer 捕获到了未定义行为。
- fuzzer 保存触发样本并终止。
6. 漏洞根因分析与修复思路
6.1 根因分析
这个漏洞的根因非常典型:外部输入字段作为除数,却没有做非零校验。
在真实 FFmpeg 代码中,情况会更复杂。因为真实解析函数往往会经过多层结构体、多个条件分支,可能只有当多个条件同时满足时,除零路径才会执行。但本质是一样的:
- 输入字段来自不可信数据。
- 字段的合法取值范围没有被约束。
- 计算逻辑把字段当成了可信任的数学常量。
另外还有一个容易被忽略的点:很多开发者会用“位运算拼出来的整数”直接参与除法,但没有意识到拼出来的整数可能是 0。比如:
int frames = (data[2] << 8) | data[3]; int offset = total_bytes / frames;当data[2]和data[3]都为 0 时,frames为 0,除零随之发生。
6.2 代码层面修复
修复思路很简单,做除数前必须检查除数是否为 0。修改后的代码可以这样写:
int parse_audio_info(const uint8_t *data, size_t size) { if (size < 8) { return 0; } int sample_rate = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3]; int frame_size = (data[4] << 8) | data[5]; // 除数不能为 0 if (frame_size == 0) { return -1; } // 也可以根据业务语义限制最小值,例如 frame_size < 16 则视为非法 if (frame_size < 16) { return -1; } return sample_rate / frame_size; }在实际项目中,还需要考虑负数的情况。很多格式规范里长度字段是无符号整数,但拼完后如果赋给有符号类型,可能变成负数。C/C++ 中负数做除数不会崩溃,但会导致业务逻辑错误,甚至可能引发逻辑漏洞。所以更严谨的做法是:
if (frame_size <= 0) { return -1; }对于真实 FFmpeg 源码,修复类似问题时应该充分利用 FFmpeg 自身提供的工具函数。FFmpeg 中有很多类似的辅助函数:
av_clip:把值裁剪到指定范围。av_sat_add32、av_sat_sub32:饱和加减法。av_clip_int16、av_clip_uint8:带类型范围裁剪。int64_t转换:很多长度运算先转成 64 位再计算,减少溢出可能。
比如:
int safe_divide(int a, int b) { if (b == 0) { return 0; } return a / b; }把这类函数统一封装,比在每个调用点手工写 if 更利于维护。
6.3 系统层面防护
有些场景下,代码是第三方库提供的,没法直接修改。这时可以通过系统层面做兜底:
- 在调用 FFmpeg 解析输入文件前,先对文件大小、格式标识、元数据做预检。
- 使用独立进程或子进程解析不可信文件,防止一次崩溃拖垮主服务。
- 在容器中运行 FFmpeg,并设置 CPU 和内存限制。
- 对输入文件做大小限制,例如超过 100MB 直接拒绝解析。
- 开启操作系统的 core dump 和监控告警,崩溃后能及时感知。
不过这些措施的优先级要低于代码修复。系统层面只能降低影响,不能消除漏洞。
7. 常见问题与排查思路
在实际运行模糊测试时,会遇到不少问题。下面用表格整理常见的几类:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
编译时报错找不到-fsanitize=fuzzer | Clang 版本过旧,或者没有安装完整 LLVM | 升级 Clang,或改用-fsanitize=fuzzer-no-link配合单独链接 |
| 运行 fuzzer 时没有新 coverage 产生 | 种子语料太小,或目标函数没有覆盖到关键路径 | 增大种子集,或编写更贴近真实调用方式的 harness |
| 运行一段时间后 CPU 占用接近 100% | libFuzzer 本身是 CPU 密集型的,属正常现象 | 限制并发数,或在 CI 中设置运行时长 |
| sanitizer 没有输出除零信息 | 编译器优化把未定义行为优化掉了 | 降低优化等级,或显式加上-fno-sanitize-recover=undefined |
| 崩溃样本无法稳定复现 | 目标函数依赖全局状态或随机性 | 在 harness 中增加状态初始化,保证可重复 |
| fuzzer 内存增长很快 | 被测代码存在内存泄漏或无限分配 | 加 AddressSanitizer,并限制-rss_limit_mb |
运行时提示unable to load shared library | 缺少 FFmpeg 动态库依赖 | 设置LD_LIBRARY_PATH或重新编译时静态链接 |
另外,还有一个容易踩的坑:如果你在真实 FFmpeg 源码上做 fuzzing,编译时记得要开启--enable-fuzzing之类的配置。FFmpeg 官方提供了 fuzzing 相关的补丁和构建方式,不同版本的配置方式差异较大,需要结合源码目录中的文档进行配置。
8. 最佳实践与工程建议
8.1 把模糊测试接入 CI
在日常项目中,不建议只在手工测试时跑一次 fuzz。更推荐把模糊测试接入持续集成流程,作为常规回归手段。
CI 中可以设置一个短时间的 fuzz 任务,例如:
./fuzz_audio_parser corpus/ -max_total_time=300 -runs=100000这样每次代码变更时,都能自动跑 5 分钟左右的 fuzz,及时发现问题。对于 FFmpeg 这种大型项目,还应该维护一个固定的种子语料库,把真实音视频文件、异常文件、历史崩溃样本都放进去。
8.2 使用 FFmpeg 内置安全函数
在 FFmpeg 相关代码中,尽量使用项目提供的安全计算函数。例如:
av_sat_add32:安全的 32 位饱和加法。av_sat_sub32:安全的 32 位饱和减法。av_sat_dadd32:饱和双精度加法。av_clip:带边界裁剪。av_q2d:有理数转浮点,内部会处理分母为 0 的情况。
使用这些函数有两个好处:
- 避免重复造轮子,减少手写边界判断的遗漏。
- 这些函数已经经过大量 fuzz 和评审,行为一致性更好。
8.3 AI 辅助编码的安全审查
vibecode 模式能大幅提高编码效率,但也带来了新的风险。AI 模型生成的代码往往是“平均优先级”的代码,它不会天然知道你某个输入字段是否来自不可信源,也不会主动在所有除零场景中加校验。
因此,在使用 AI 生成代码时,建议人工重点检查以下几类位置:
- 除法、取余运算的除数是否来自外部输入。
- 数组索引是否来自外部输入。
- 循环次数是否可能为负数。
- 文件读写长度是否受限。
- 内存拷贝长度是否与实际 buffer 大小匹配。
可以建立一个简单的自查清单,把 AI 生成的代码过一遍。这个步骤不能省。
8.4 生产环境的量化控制
对于 FFmpeg 这类处理不可信输入的服务,生产环境建议做到:
- 限制输入文件大小,避免超大文件耗尽内存。
- 限制解析时长,超过阈值直接终止任务。
- 使用独立进程执行解析,配合 supervisor 自动拉起。
- 记录崩溃转储和输入样本,方便后续分析。
- 新版本上线前,先跑一轮基于公开样本和私有样本的 fuzz。
其实这已经不只是除零漏洞的处理思路,而是所有解析类服务的通用安全基线。
9. 总结与学习路线
通过这篇文章,我们完整走了一遍“用 vibecode 辅助搭建模糊测试工具,在 FFmpeg 相关解析逻辑中发现除以零漏洞”的流程。你至少应该掌握以下内容:
- vibecode 的本质是 AI 辅助编码,效率高但需要人工审查边界条件。
- 模糊测试的核心是构造大量输入,用 sanitizer 捕获未定义行为。
- 整数除以零在 C/C++ 中是未定义行为,通常会导致进程崩溃。
- FFmpeg 这类解析器因为大量处理不可信输入,所以容易出现除零漏洞。
- libFuzzer 配合 AddressSanitizer、UndefinedBehaviorSanitizer 是发现这类问题的高效组合。
- 修复除零漏洞的基本思路是除数非零校验,结合 FFmpeg 自带安全函数会更稳健。
下一步你可以继续做的事情:
- 把同样的思路应用到 FFmpeg 真实源码上,编写一个针对某个具体解码器的 fuzz target。
- 学习 AFL++ 的使用方法,丰富模糊测试的手段。
- 阅读 FFmpeg 官方文档中关于 fuzzing 的说明,了解官方推荐的构建方式。
- 整理自己的种子语料库,把日常遇到的各种异常媒体文件放进 corpus。
模糊测试不是一次性工作,而是一个需要长期沉淀的测试基础设施。你积累的每一个崩溃样本、每一条修复记录,都是后续开发里最宝贵的经验。
如果这篇文章对你有帮助,可以收藏备用。后面如果再遇到类似的除零、整数溢出问题,希望你能快速定位并修复。