尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

FFmpeg模糊测试实战:用vibecode工具发现除零漏洞

FFmpeg模糊测试实战:用vibecode工具发现除零漏洞 FFmpeg 对很多开发者来说只是一个命令行工具。遇到ffmpeg -i 1.m4s -c copy 1.mp4这种需求时它负责把 m4s 转成 mp4遇到视频没声音、封面抽不出来、直播流拉不动这些问题时大家也习惯先去搜“ffmpeg 安装”“ffmpeg 命令”“ffmpeg 的 -y 是什么意思”之类的关键词。但 FFmpeg 的另一面是它是一套用 C 语言写成的大型多媒体解析与转码框架每天都要接收来自网络、用户上传、视频文件、直播流等“不可信输入”。在这些场景里FFmpeg 不是命令行工具而是库是播放器、转码服务、点播系统、短视频 App 的底层解析器。一旦某个畸形文件能让它崩溃攻击者就可能用一条链接或一个文件让服务端转码任务不断失败、worker 反复重启甚至拖垮整个处理链路。最近我们做了一次内部安全测试思路比较特殊我们用了一个基于 vibecode 工作流搭建的模糊测试工具对 FFmpeg 的 demuxer 做了一段时间的自动化测试最终稳定地撞出了一个“除以零”漏洞。按 CWE 分类这类问题通常是 CWE-369 Divide By Zero表现形式是进程收到 SIGFPE 后直接崩溃。这篇文章不打算只讲“我们发现了漏洞”这个结果而想把整条链路拆开FFmpeg 为什么总在除零这类问题上翻车除以零漏洞在 C 语言里到底是什么vibecode 工具在这个过程中到底做了多少事又没做多少事如何自己搭一个最小可复现的 FFmpeg fuzz 环境拿到 crash 文件之后如何一步一步定位到具体代码。1. FFmpeg 为什么还需要继续挖漏洞很多人第一反应是FFmpeg 已经发展了这么多年全世界都在用难道不是早就被挖干净了真实情况恰恰相反FFmpeg 的暴露面非常大而且极具“历史遗留感”。首先FFmpeg 不是一个简单的解码器而是一整套格式生态。它支持几十种容器格式每一种容器又有大量可选字段。比如 MOV/MP4 里的mvhd、tkhd、stts、elstFLV 里的 AudioTagHeaderMKV 里的SegmentInfoTS 流里的各类 table。每一段元数据都要经过手工写的 C 代码去读取、校验、计算任何一个字段算错都可能导致除零、越界、空指针、整数溢出。其次FFmpeg 的定位是“能读就尽量读”。为了兼容市面上各种不规范的文件很多解析器的容错逻辑写得非常宽松。这种宽容从用户角度是优点但从安全角度意味着它会把大量看似离谱的字段当成“正常输入”继续往下走。如果开发者只在某个地方检查了分子没检查分母或者检查顺序有问题除零就会发生。再看实际调用场景。在个人电脑上用户用ffmpeg -i input.flv output.mp4时输入文件多数是来自自己设备的“可信文件”。但在服务端FFmpeg 处理的常常是用户上传的视频、第三方拉取的 URL、CDN 回源文件、实时直播流。这些输入没有一个是可信的。攻击者只要把一个精心构造的短视频 POST 到上传接口服务端转码进程就可能崩溃。所以结论很清楚FFmpeg 这种体量、这种输入面、这种长期兼容性包袱它天然是模糊测试最好的靶子之一。找到一个新的除零崩溃不值得大惊小怪真正值得关注的是为什么我们用一套“AI 辅助生成的 fuzz 工具”也能快速撞出来。这说明同类问题在代码库中的存量仍然不少。2. 除以零漏洞先看现象再看原理先给不熟悉 C 底层行为的读者解释一下在数学里除以零没有意义在 C 语言里整数除以零更加特殊。C 标准规定整数除法除以零属于“未定义行为”。未定义行为的意思是编译器、CPU、运行时环境可以表现出任何结果。在 x86 平台上最常见的表现是 CPU 抛出#DE异常操作系统把这个异常转成信号SIGFPE进程默认行为是终止。也就是说除零并不像常规的“逻辑错误”一样返回一个错误码。它往往直接杀死进程属于典型的拒绝服务漏洞。对桌面播放器来说一个恶意视频能让播放器闪退对云端转码服务来说一个恶意文件能让 worker 崩溃严重时导致任务队列积压。看一个最小的 C 语言示例#include stdio.h int main(void) { int den 0; int result 42 / den; printf(%d\n, result); return 0; }如果用普通方式编译运行在 x86 Linux 上很容易直接看到Floating point exception或者进程被信号杀死。注意明明执行的是整数除法报错却叫“Floating point exception”这是历史命名问题并不代表真的发生了浮点异常。如果使用 Clang 并开启 UndefinedBehaviorSanitizer就能看到更明确的信息clang -fsanitizeundefined -g test.c -o test ./test输出大致是runtime error: division by zero这就比直接看SIGFPE友好得多。在 FFmpeg fuzz 的过程中UBSan 会在真正触发异常前打印出除零发生的源码位置这对定位问题帮助极大。另外要区分浮点除零。C 语言里float/double除以零并不会触发未定义行为结果通常是inf或nan。所以本文讨论的“除以零漏洞”默认是指整数除零。FFmpeg 元数据计算中大量使用整数型的num / den这正是除零漏洞的高发位置。这类漏洞的直接危害是拒绝服务通常不直接导致内存破坏也不等于任意代码执行。但在真实的转码服务里进程崩溃同样是一个严重问题。攻击者如果把崩溃样本持续打给线上服务就能稳定地消耗 CPU、重启资源、干扰正常转码任务最终形成一条非常廉价的攻击路径。3. vibecode 不等于“AI 找漏洞”工具链拆解先解释一下标题里的“vibecode”。vibecode 并不是某个官方工具名也不是 FFmpeg 官方项目而是一种工作方式开发者用自然语言描述目标让大模型生成大部分代码人负责审查和验收。在我们的流程里团队把它封装成了一个内部叫 VibeFuzz 的辅助工具所谓的“基于 vibecode”本质是让 AI 参与编写 fuzz 相关的胶水代码而不是让 AI 去挖洞。这一点非常重要。很多读者一听“AI 发现漏洞”要么觉得是玄学要么觉得是营销。实际拆开看VibeFuzz 做的事情非常简单用自然语言描述目标解析场景比如“只需要解析 MP4/MOV 容器不关心转码输出”由大模型生成 fuzz harness、种子文件处理脚本、编译参数、跑批脚本人类工程师 review 生成的代码修正头文件、内存管理、链接参数这类细节工具把生成好的 harness 接入 libFuzzer / Clang Sanitizer 运行对 fuzzer 产出的 crash 文件做分类、去重、最小化再进入人类漏洞分析流程。所以真正发现“除以零”的不是自然语言也不是大模型而是 fuzzer 不断产生的畸形输入加上 ASan/UBSan 的运行时检查。vibecode 在这里降低的是“写 fuzz 工程代码”的门槛它没有替代漏洞分析更没有替代对 FFmpeg 内部机制的理解。这个判断对团队协作非常关键。如果团队以为“AI 工具能自动挖洞”很容易把大量畸形样本丢给大模型去解释结果既看不出来漏洞价值也没法给出修复建议。如果大家理解“AI 只是减少重复劳动”就会把更多时间花在 crash 分类、代码审计和回归验证上。我们这次能比较快地定位到除以零正是后半部分工作做得比较扎实。当然vibecode 也有坑。大模型生成的 harness 如果对 FFmpeg 的内存管理理解不到位很容易把avformat_open_input的失败路径写错导致二次释放或泄漏。所以 AI 辅助生成的每一行关键内存管理代码都必须有人类 review。安全测试工具本身不能成为新的漏洞来源。4. 环境准备给 FFmpeg 装上“安全探针”要 fuzz FFmpeg不能直接下载官方编译好的二进制包。普通 release 包默认不做 sanitizer 插桩就算崩溃了也只能看到SIGFPE拿不到源码级的错误信息。我们需要自己从源码编译一个“带探针”的 FFmpeg。以 Ubuntu 22.04 / Debian 12 为例先安装基础依赖sudo apt update sudo apt install -y build-essential clang llvm pkg-config yasm nasm \ zlib1g-dev libssl-dev如果还要验证解码、编码链路可以额外装一些编解码器开发库比如libx264-dev、libx265-dev、libvpx-dev。但如果只做容器解析和 demuxer 层面的 fuzz基础依赖其实就够了。然后拉取 FFmpeg 源码git clone https://github.com/FFmpeg/FFmpeg.git ffmpeg-src cd ffmpeg-src mkdir -p build cd build接下来是最关键的一步配置编译参数同时打开 AddressSanitizer 和 UndefinedBehaviorSanitizer。../configure \ --disable-stripping \ --enable-debug \ --ccclang \ --cxxclang \ --extra-cflags-fsanitizeaddress,undefined -fno-sanitize-recoverundefined -g -O1 \ --extra-ldflags-fsanitizeaddress,undefined这里几个参数的含义--enable-debug保留调试符号便于后续用 gdb / llvm-symbolizer 定位--ccclang强制使用 ClanglibFuzzer 和 Sanitizer 与 Clang 配合最顺-fsanitizeaddress,undefined开启 ASan 和 UBSan-fno-sanitize-recoverundefined遇到未定义行为立即终止而不是继续运行-O1保留一部分优化能力同时让栈信息比-O0更接近真实崩溃现场。然后编译make -j$(nproc)如果你的机器核数很多第一次编译 FFmpeg 可能要几分钟到十几分钟这很正常。编译完成后确认一下产物./ffmpeg -version如果输出正常说明 FFmpeg 本身构建成功。这里额外提醒一句Windows 用户如果想复现不建议直接下载ffmpeg.exe更推荐用 WSL2 或 Docker 跑 Linux 环境。因为我们需要从源码编译、接入 sanitizer、再配合 libFuzzer 运行Windows 原生路径的坑会非常多。5. 编写 Fuzz Harness让解析器吃随机数据fuzzer 不是一个“能自动跑的工具”它需要一个入口函数也就是 harness。这个函数把 fuzzer 生成的字节流交给被测试代码去处理。我们这里的目标是 FFmpeg 的 demuxer也就是容器解析层。所以只需要把字节流当成一个文件调用avformat_open_input打开再调用avformat_find_stream_info获取流信息然后关闭即可。下面是一个最小可用的 harness文件名存为fuzz_target.c#include stdio.h #include stdint.h #include stdlib.h #include libavformat/avformat.h #include libavutil/log.h static const char *tmp_path /tmp/ffmpeg_fuzz_input.bin; int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { static int inited 0; AVFormatContext *fmt_ctx NULL; FILE *fp NULL; if (!inited) { av_log_set_level(AV_LOG_QUIET); inited 1; } if (size 8 || size 8 * 1024 * 1024) { return 0; } fp fopen(tmp_path, wb); if (!fp) { return 0; } fwrite(data, 1, size, fp); fclose(fp); int ret avformat_open_input(fmt_ctx, tmp_path, NULL, NULL); if (ret 0) { return 0; } avformat_find_stream_info(fmt_ctx, NULL); avformat_close_input(fmt_ctx); return 0; }这段代码里有几个细节值得解释。if (size 8 || size 8 * 1024 * 1024)用来限制输入尺寸。FFmpeg 的 demuxer 需要读取文件头完全随机且极短的输入没有价值超大输入又会让单次执行时间变长。对容器解析来说1MB 以内通常足够覆盖大部分逻辑。av_log_set_level(AV_LOG_QUIET)是为了关掉 FFmpeg 在解析畸形文件时输出的海量日志。不关的话fuzzer 控制台会被刷屏很难看清真正有用的 Sanitizer 输出。写成临时文件再调avformat_open_input是性能折中方案。真实的 fuzz 工程中更专业的做法是用自定义AVIOContext做纯内存读取避免磁盘 IO。但对第一次接触 FFmpeg fuzz 的读者来说临时文件方案更直观也更容易跑通。接下来编译这个 harness。假设 FFmpeg 源码在ffmpeg-src目录编译产物在ffmpeg-src/build目录export PKG_CONFIG_PATH/path/to/ffmpeg-src/build/lib/pkgconfig clang -g -O1 -fsanitizefuzzer,address,undefined \ -I/path/to/ffmpeg-src \ fuzz_target.c \ $(pkg-config --cflags --libs libavformat libavcodec libavutil) \ -o fuzz_ffmpeg如果pkg-config路径不对也可以直接手动指定clang -g -O1 -fsanitizefuzzer,address,undefined \ -I/path/to/ffmpeg-src/include \ fuzz_target.c \ -L/path/to/ffmpeg-src/build/lib \ -lavformat -lavcodec -lavutil \ -lm -lz -lpthread \ -o fuzz_ffmpeg链接时如果报undefined reference不要慌。这是因为 FFmpeg 配置时可能启用了额外模块需要把对应库补到链接命令末尾。常见的补充项是-lswresample -lswscale -lavfilter。链接顺序保持从左到右依赖一般就能解决。6. 种子语料与运行从正常文件到崩溃样本libFuzzer 不是完全从头随机生成输入。给它一个正常的种子语料它会在已有文件基础上做变异效率会高很多。先建 corpus 目录并放几个正常的媒体文件mkdir -p corpus cp ~/Videos/sample.mp4 corpus/base.mp4 cp ~/Videos/sample.mkv corpus/base.mkv cp ~/Videos/sample.flv corpus/base.flv种子文件不需要多但类型尽量覆盖目标解析器的常见格式。对 MP4/MOV 解析来说一个正常 mp4、一个带旋转信息的 mov、一个带多音轨的 mkv就比放一百个模糊不清的样本有用得多。然后启动 fuzzmkdir -p artifacts ASAN_OPTIONSdetect_leaks0 \ UBSAN_OPTIONShalt_on_error1:print_stacktrace1 \ ./fuzz_ffmpeg \ -max_len1048576 \ -timeout5 \ -rss_limit_mb2048 \ -artifact_prefix./artifacts/ \ corpus这里几个 libFuzzer 参数需要重点说明-max_len1048576限制单条输入最大 1MB避免执行时间过长-timeout5单条输入超过 5 秒就认为超时-rss_limit_mb2048限制单进程内存防止畸形文件把内存吃满把宿主机拖垮-artifact_prefix./artifacts/把崩溃样本统一保存到 artifacts 目录。运行后控制台会不断打印覆盖率、执行次数等信息。正常现象是#1048576 NEW cov: 372 ft: 9180 corp: 240/110Kb #2097152 NEW cov: 380 ft: 9420 corp: 252/118Kb如果某条输入触发了除零日志会出现类似runtime error: division by zero #0 0x... in function_name /path/to/ffmpeg-src/libavformat/xxx.c:123:45 #1 0x... in avformat_open_input ... SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ... Test unit written to ./artifacts/crash-...看到Test unit written to就说明 fuzzer 已经把一个可稳定触发崩溃的样本保存下来了。到这一步我们已经完成了“从正常文件到崩溃样本”的闭环。7. 复现与定位一次除零崩溃的排查流程拿到crash-*文件后第一步是复现确认它不是随机假阳性。用我们编译出的 fuzz 工具单独跑一次./fuzz_ffmpeg -runs1 ./artifacts/crash-xxxxxx如果输出稳定出现division by zero说明样本具备可复现性。接下来需要判断崩溃发生在哪个模块。一个很实用的方法是直接用 FFmpeg 命令行去打开这个 crash 文件./ffmpeg -v debug -i ./artifacts/crash-xxxxxx -f null -这里的-f null -表示只解析、不输出文件。-v debug会打印出解析器选择了哪个容器格式、读取了哪些流、在哪一步报错。对除零问题来说命令行输出里通常能看到崩溃前的最后一个模块信息。然后再回到 Sanitizer 给出的栈信息按下面步骤定位看崩溃栈最上层的函数名在 FFmpeg 源码里搜索对应函数检查该函数里的除法表达式特别是num / den、duration / time_base.den、bit_rate / frame_rate.den这类模式确认哪个字段来自文件输入哪个字段没有被校验使用 gdb 或 lldb 在崩溃点打断点查看实际运行时den的值对照文件结构规范确认这个字段在正常文件里应该是什么值畸形文件又是怎么让它变成 0 的。在 FFmpeg 的媒体元数据计算里最常见的除零点包括time_base.num / time_base.den时间基换算avg_frame_rate.num / avg_frame_rate.den帧率计算sample_aspect_ratio.num / sample_aspect_ratio.den像素宽高比码率、时长、基准时间戳等二次换算。这些字段很多都直接来自文件头。正常文件由封装器写入时通常保证分母非零但攻击者可以手动修改文件把分母改成 0。只要解析代码没有提前判断除零就会发生。修复模式也不复杂。最常见的做法是在除法前加一个保护判断if (den 0) { den 1; }或者直接复用 FFmpeg 的 safe math 相关函数避免手工除法的风险。真正复杂的是要判断“分母为 0 时业务上应该回退到哪个默认值”。这需要开发者理解该字段的含义而不是简单地把 0 改成 1 了事。所以fuzzer 负责发现问题人负责理解问题和给出正确修复。8. 常见问题与排查思路在实际操作中很多人第一次跑 FFmpeg fuzz 并不会顺利。下面是几个我们遇到过的高频问题。问题现象可能原因排查方式解决方案编译 FFmpeg 时undefined reference启用了额外模块但链接时没有补库看 ld 报错里的符号名搜索属于哪个库在链接命令末尾追加-lswresample -lswscale -lavfilter等库fuzzer 启动后全部超时起始语料太大或-max_len设置过高看日志里单条执行耗时把种子文件裁剪成 1MB 以内设置-max_len1048576崩溃信息只有SIGFPE没有 UBSan 输出FFmpeg 编译时没有开启-fsanitizeundefined检查 configure 日志和./ffmpeg -version编译参数重新 configure加上 UBSan 相关 flags系统自带 ffmpeg 无法复现 crash官方二进制通常不做 sanitizer 插桩且版本不同对比崩溃文件和编译参数统一使用带 ASan/UBSan 的自编译 FFmpeg运行ffmpeg -i 1.m4s -c copy 1.mp4提示输出文件已存在目标 mp4 文件已存在FFmpeg 默认不会覆盖看命令行提示是File already exists按需加-y覆盖或-n拒绝覆盖avformat_open_input失败后内存异常harness 对失败路径处理不当用 ASan 看是否有 leak/use-after-freereview 失败时的 AVFormatContext 释放逻辑这里要特别强调最后一种情况。很多 AI 生成的 C 代码在“成功路径”看起来没问题但失败路径往往被忽略。比如avformat_open_input失败后fmt_ctx应该由 FFmpeg 内部释放还是由调用方释放不同版本和不同失败原因可能有差异。写 fuzz harness 的人如果不懂这一点很容易写出一版“跑一次没问题跑一万次就崩”的 harness。这也是我们前面反复强调“AI 生成 人工 review”的原因。9. 工程建议与安全红线如果你准备在自己项目里也引入这套方法建议遵守下面几个原则。第一fuzz 环境一定要隔离。FFmpeg 的 ASan/UBSan 构建、fuzz 进程、crash 样本都应该放在独立容器或专用机器上。不要在主开发机上直接开多核 fuzz否则一个隐藏的内存越界可能影响同机其他服务。第二资源控制必须放在第一位。libFuzzer 的-timeout、-rss_limit_mb、-max_len不是可选项而是必选项。对多媒体解析来说限制单条输入大小、限制单次执行时间、限制进程内存可以避免 fuzzer 变成“拒绝服务攻击自己的工具”。第三crash 样本要天天回收、分类和回归。顺手写一个脚本把artifacts/下的样本按签名哈希去重保留最小复现样本并把它们加入自动化回归集。否则今天发现的崩溃明天可能在新代码里再次出现没人知道。第四生产环境不要直接解析不可信文件更不要用 root 权限跑 FFmpeg 转码服务。正确的做法是输入文件先做格式识别、大小限制、时长限制再交给转码 workerworker 运行在低权限用户、独立容器、受内存限制的环境中。即使某个畸形文件再次触发除零影响也只局限在单个 worker 内。第五安全测试要遵守边界。用 fuzzer 掘漏洞只能针对自己拥有或已获授权的系统。不要拿公开网站、公网转码服务、别人家的播放器去测试否则可能触犯法律或平台规则。本文介绍的方法只用于内部安全测试、开源项目漏洞挖掘和合法授权范围。另外还要提一句如果你的目的是给开源项目提交漏洞不要一发现崩溃就立刻公开 PoC。正确做法是先确认影响、准备最小复现样本、联系项目安全团队或维护者给出可行的修复建议。在修复发布前公开畸形样本反而会让大量用户处于危险中。10. 总结与后续学习方向这篇文章从一次“基于 vibecode 的模糊测试工具发现 FFmpeg 除以零漏洞”的经历出发把整条链路拆成了几个独立环节FFmpeg 为什么需要 fuzz、除以零漏洞的底层原理、vibecode 工具在流程中的真实作用、如何编译带 sanitizer 的 FFmpeg、如何编写最小 harness、如何运行 fuzzer、如何定位和修复崩溃。如果你也想亲手试一次建议按下面顺序走先用一个最简单的 C 程序验证 UBSan 能检测整数除零再编译一个最小化的 FFmpeg只保留movdemuxer写一版最朴素的临时文件型 harness用几个正常 mp4 当种子跑半小时 fuzzer拿到第一个 crash 后用ffmpeg -v debug -i crash_file -f null -复现并分析。这套流程跑通之后你就能理解为什么说“FFmpeg 的漏洞大概率不是被天才发现而是被正确工具和正确流程发现的”。vibecode 降低了写 fuzz 工具的门槛但它没有降低漏洞分析的门槛。真正值钱的还是对 C 语言内存模型的理解、对 FFmpeg 解析流程的熟悉以及对安全边界的尊重。建议先收藏这篇文章等到你下次面对division by zero时就知道该从哪里下手了。
返回列表