☰
vibecode辅助模糊测试:从FFmpeg除以零漏洞看C/C++解析安全
2026/10/28 10:54:49 网站建设 项目流程

之前在做音视频模块安全评估时,我尝试用“vibecode”这种 AI 辅助编码的方式快速搭建了一个模糊测试工具,对 FFmpeg 的输入解析逻辑做了几轮随机测试。结果比预期来得更快,测试刚开始几十秒,就触发了一个“除以零”崩溃。把崩溃栈拉出来之后,发现所有解析器都会遇到的老问题又出现了:某个从输入流里读出来的“长度/索引/时间戳”字段被直接当成除数使用,而字段值没有做非零校验。

这篇文章就围绕这次实战展开,讲清楚 vibecode 辅助开发的方式、模糊测试的基本原理,以及在 FFmpeg 这类 C/C++ 多媒体解析库中,除以零漏洞是如何产生、如何被 fuzzing 工具发现、又如何修复和避免的。

如果你对音视频方向感兴趣,或者在做 C/C++ 项目的安全测试,又或者只是听说过“模糊测试”“libFuzzer”但还没完整跑通过一个例子,这篇文章都比较适合往下读。

1. 背景与核心概念

1.1 vibecode 是什么意思

vibecode 是最近两年流行起来的一个开发模式,指的是“借助 AI 编程助手,用对话式的方式快速产出代码”的工作流。很多同学会把 vibecode 理解成“让 AI 把整个项目写完”,但在实际工程里,它更接近“AI 辅助编码 + 人工审查 + 快速迭代”的组合。

在我们的模糊测试项目中,vibecode 主要被用在三个环节:

  1. 生成 Fuzz Harness,也就是连接被测函数和模糊测试引擎之间的桥接代码。
  2. 生成构建脚本和 Dockerfile,快速搭出编译环境。
  3. 分析 sanitizer 输出的崩溃日志,让 AI 帮忙归纳可能的根因。

需要注意的是,AI 生成的代码虽然效率高,但边界处理不一定可靠。后面我会专门讲一下,AI 辅助生成的 Fuzzer 代码反而更需要做代码审查。

1.2 模糊测试是什么

模糊测试(Fuzzing)是一种自动化的软件测试方法,核心思路是向被测程序不断输入随机、变异或结构化的数据,然后监控程序是否发生崩溃、超时、内存溢出、未定义行为等异常。

模糊测试并不是一个新概念,但最近几年因为 libFuzzer、AFL++、honggfuzz 这些工具越来越成熟,加上 Sanitizer 系列工具的普及,它已经成为安全测试和质量保障里的标准手段。

常见的模糊测试流程如下:

  1. 准备一批种子输入(Seed Corpus),比如小的音视频文件、图片文件、配置片段。
  2. 模糊测试引擎会对这些种子做变异,生成大量新输入。
  3. 每次变异后的输入都会被喂给被测目标执行。
  4. 如果被测目标发生崩溃,fuzzer 会保存触发崩溃的输入样本,供后续分析。

模糊测试能发现很多人工测试难以覆盖的问题,尤其是除零、整数溢出、数组越界、空指针解引用、内存泄漏等。

1.3 除以零漏洞为什么值得关注

在 C/C++ 语言里,整数除以零是未定义行为(Undefined Behavior)。程序一旦执行到整数除零,在多数平台上会触发 SIGFPE 信号,导致进程直接崩溃。如果是服务端程序,一次崩溃可能导致整个服务不可用;如果是解析器,攻击者可能只需要构造一个很小的恶意文件,就能让播放器、转码服务或者流媒体网关崩溃。

需要注意的是,浮点数除以零通常不会崩溃,而是得到inf或nan。所以本文讨论的“除以零漏洞”,通常指整数运算中的除数为零。

在很多解析器场景里,除数往往来自输入文件中的某个字段,例如:

  • 视频流里的帧率、采样率。
  • 容器格式里的 duration、timescale。
  • 索引表里的 entries 数量。
  • 某个 chunk 的长度值。

这些字段是外部可控的,如果代码没有校验就直接作为除数,就很容易产生除零崩溃。在 FFmpeg 这种需要解析大量音视频格式的代码库里,输入字段结构复杂、格式众多,这个问题会被进一步放大。

2. 为什么 FFmpeg 容易出现除零问题

2.1 复杂输入解析带来的风险

FFmpeg 是一个庞大的多媒体处理框架,支持的封装格式、编码格式、滤镜、设备非常多。不同的格式有各自不同的头结构、索引结构、描述字段,很多字段的值在逻辑上应当是“正数”,但在实际文件中可能是 0,甚至可能是负数。

一个典型的解析流程是这样的:

  1. 从文件中读取若干字节。
  2. 按照格式规范把字节解析成字段。
  3. 用字段去计算偏移量、时长、采样率、帧率。
  4. 根据结果分配内存或执行后续逻辑。

如果第 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 相关代码中,有几类常见的除零触发场景:

  1. 时间基换算 从容器中解析出time_base后,用另一个流的时间戳除以它做归一化,但没有判断time_base是否为 0。

  2. 采样率换算 音频重采样时,用输入采样率与输出采样率计算比例,输入采样率来自文件头字段,可能是 0。

  3. 帧数/索引计算 以“总字节数 / 每帧字节数”来估算帧数时,每帧字节数可能为 0。

  4. 视频宽高比计算 用高度字段作为除数计算宽高比时,高度为 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.sh

4.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: 12Mb

cov是被覆盖到的代码块数量,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 的角度来看,整个发现过程只需要几个步骤:

  1. 把输入字节变异成某个字段为 0。
  2. 程序执行了解析逻辑。
  3. sanitizer 捕获到了未定义行为。
  4. 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 系统层面防护

有些场景下,代码是第三方库提供的,没法直接修改。这时可以通过系统层面做兜底:

  1. 在调用 FFmpeg 解析输入文件前,先对文件大小、格式标识、元数据做预检。
  2. 使用独立进程或子进程解析不可信文件,防止一次崩溃拖垮主服务。
  3. 在容器中运行 FFmpeg,并设置 CPU 和内存限制。
  4. 对输入文件做大小限制,例如超过 100MB 直接拒绝解析。
  5. 开启操作系统的 core dump 和监控告警,崩溃后能及时感知。

不过这些措施的优先级要低于代码修复。系统层面只能降低影响,不能消除漏洞。

7. 常见问题与排查思路

在实际运行模糊测试时,会遇到不少问题。下面用表格整理常见的几类:

问题现象常见原因解决思路
编译时报错找不到-fsanitize=fuzzerClang 版本过旧,或者没有安装完整 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 的情况。

使用这些函数有两个好处:

  1. 避免重复造轮子,减少手写边界判断的遗漏。
  2. 这些函数已经经过大量 fuzz 和评审,行为一致性更好。

8.3 AI 辅助编码的安全审查

vibecode 模式能大幅提高编码效率,但也带来了新的风险。AI 模型生成的代码往往是“平均优先级”的代码,它不会天然知道你某个输入字段是否来自不可信源,也不会主动在所有除零场景中加校验。

因此,在使用 AI 生成代码时,建议人工重点检查以下几类位置:

  • 除法、取余运算的除数是否来自外部输入。
  • 数组索引是否来自外部输入。
  • 循环次数是否可能为负数。
  • 文件读写长度是否受限。
  • 内存拷贝长度是否与实际 buffer 大小匹配。

可以建立一个简单的自查清单,把 AI 生成的代码过一遍。这个步骤不能省。

8.4 生产环境的量化控制

对于 FFmpeg 这类处理不可信输入的服务,生产环境建议做到:

  1. 限制输入文件大小,避免超大文件耗尽内存。
  2. 限制解析时长,超过阈值直接终止任务。
  3. 使用独立进程执行解析,配合 supervisor 自动拉起。
  4. 记录崩溃转储和输入样本,方便后续分析。
  5. 新版本上线前,先跑一轮基于公开样本和私有样本的 fuzz。

其实这已经不只是除零漏洞的处理思路,而是所有解析类服务的通用安全基线。

9. 总结与学习路线

通过这篇文章,我们完整走了一遍“用 vibecode 辅助搭建模糊测试工具,在 FFmpeg 相关解析逻辑中发现除以零漏洞”的流程。你至少应该掌握以下内容:

  • vibecode 的本质是 AI 辅助编码,效率高但需要人工审查边界条件。
  • 模糊测试的核心是构造大量输入,用 sanitizer 捕获未定义行为。
  • 整数除以零在 C/C++ 中是未定义行为,通常会导致进程崩溃。
  • FFmpeg 这类解析器因为大量处理不可信输入,所以容易出现除零漏洞。
  • libFuzzer 配合 AddressSanitizer、UndefinedBehaviorSanitizer 是发现这类问题的高效组合。
  • 修复除零漏洞的基本思路是除数非零校验,结合 FFmpeg 自带安全函数会更稳健。

下一步你可以继续做的事情:

  1. 把同样的思路应用到 FFmpeg 真实源码上,编写一个针对某个具体解码器的 fuzz target。
  2. 学习 AFL++ 的使用方法,丰富模糊测试的手段。
  3. 阅读 FFmpeg 官方文档中关于 fuzzing 的说明,了解官方推荐的构建方式。
  4. 整理自己的种子语料库,把日常遇到的各种异常媒体文件放进 corpus。

模糊测试不是一次性工作,而是一个需要长期沉淀的测试基础设施。你积累的每一个崩溃样本、每一条修复记录,都是后续开发里最宝贵的经验。

如果这篇文章对你有帮助,可以收藏备用。后面如果再遇到类似的除零、整数溢出问题,希望你能快速定位并修复。

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

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

立即咨询