资讯动态

vibecode辅助模糊测试:从FFmpeg除以零漏洞看C/C++解析安全

发布时间:2026/8/31 2:06:08 来源:尧图企业网站定制
之前在做音视频模块安全评估时我尝试用“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 中进程内执行速度快适合对单个函数做单元级 fuzzAFL独立工具支持多种插桩方式适合对可执行程序做黑盒/灰盒 fuzzhonggfuzz支持硬件反馈和软件插桩适合复杂目标本文选择 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 \ -fsanitizefuzzer,address,undefined \ -o fuzz_audio_parser \ fuzz_audio_parser.cpp参数解释-g生成调试信息崩溃时能看到带源码行号的堆栈。-O1优化级别适合 fuzzing既能提升速度也保留一定的可调试性。-fsanitizefuzzer,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_time60这里corpus/是种子语料库目录-max_total_time60表示最多运行 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. 常见问题与排查思路在实际运行模糊测试时会遇到不少问题。下面用表格整理常见的几类问题现象常见原因解决思路编译时报错找不到-fsanitizefuzzerClang 版本过旧或者没有安装完整 LLVM升级 Clang或改用-fsanitizefuzzer-no-link配合单独链接运行 fuzzer 时没有新 coverage 产生种子语料太小或目标函数没有覆盖到关键路径增大种子集或编写更贴近真实调用方式的 harness运行一段时间后 CPU 占用接近 100%libFuzzer 本身是 CPU 密集型的属正常现象限制并发数或在 CI 中设置运行时长sanitizer 没有输出除零信息编译器优化把未定义行为优化掉了降低优化等级或显式加上-fno-sanitize-recoverundefined崩溃样本无法稳定复现目标函数依赖全局状态或随机性在 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_time300 -runs100000这样每次代码变更时都能自动跑 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。模糊测试不是一次性工作而是一个需要长期沉淀的测试基础设施。你积累的每一个崩溃样本、每一条修复记录都是后续开发里最宝贵的经验。如果这篇文章对你有帮助可以收藏备用。后面如果再遇到类似的除零、整数溢出问题希望你能快速定位并修复。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价