vatt'ghern jaskier's ballads

同一支 FFmpeg,過去兩年連續吞下 Google Big Sleep 的 13 個、Anthropic Mythos 的一批,現在又被 depthfirst 的自動化代理一口氣挖出 21 個零日——而最後這一輪,只花了大約一千美元。

FFmpeg 一口氣被挖出 21 個零日——媒體解碼的攻擊面有多深

乎每個影音播放器、轉碼服務、瀏覽器內的影片標籤,底下都站著同一支函式庫。FFmpeg 負責把外面餵進來的位元組——一段 RTSP 串流、一個 MP4 容器、一張 AVIF 圖——拆成可以播放的影格。這份工作的本質,就是拿一堆「不受信任、且完全由對方控制」的資料,丟進一套為了效能而高度最佳化的 C 程式去解析。換句話說,它每天都在做整個系統安全裡風險最高的那件事:讓陌生人的位元組,直接決定自己要怎麼配置與讀寫記憶體。報告把這個位置講得很直接,FFmpeg「routinely parses complex, untrusted media」,所以「is inherently security critical」——它的危險不是偶然的瑕疵,而是工作內容本身。

depthfirst 的安全研究員 Zhenpeng(Leo)Lin 把他們的自動化代理指向這個目標,報告的結論是一行字:「discovered 21 zero-day vulnerabilities in FFmpeg」。21 這個數字本身已經夠刺眼,但真正讓這篇報告值得當成趨勢來讀的,是另外兩件事:這些 bug 幾乎全部落在同一種失誤——記憶體破壞——以及挖出它們的總成本,只有大約一千美元。

有意思的不是「又有人在 FFmpeg 找到 bug」。這件事每年都在發生,FFmpeg 是 fuzzing 工具最愛的靶子之一。有意思的是這次的數量、成本,加上一個尷尬的背景事實:這個專案已經「absorbed over two decades of relentless fuzzing and manual audits」,被持續掃了二十年。一個被審查得這麼徹底的目標,為什麼還能一次掉出 21 個洞?而且還是用三位數美元的代價挖出來的?我們把這篇報告當成一樁待解的案子來讀,沿著三條線索走:先把這 21 個洞攤在桌上看它們長什麼樣、集中在哪;再挑其中一個,看攻擊者的一個位元組是怎麼一路走到完全可控的記憶體寫入;最後回答那個核心問題——既然連二十年的 fuzzing 都掃不乾淨,這個攻擊面到底為什麼這麼深,又是什麼方法走進了 fuzzing 走不到的角落。

21 個零日,集中在記憶體破壞這一類

先把案發現場攤開。報告公開了編號 CVE-2026-39210 到 CVE-2026-39218 共 9 個 CVE,另有 12 個以內部編號 DFVULN 記錄,合計 21 個。先看清楚這 9 個公開 CVE 的分佈:它們散落在從 TS demuxer 到 VP9 decoder 的不同元件,但歸類起來幾乎只有一種毛病——緩衝區溢位。把它們依元件列出來,會發現問題遍及整條媒體處理鏈的各個角落:容器層的 TS demuxer(CVE-2026-39210)與 DASH demuxer(39218)、影像編碼層的 yuv4mpegenc rawvideo 輸入(39213)與 img2enc.c(39216)、縮放層的 swscale(39211)、解碼層的 VP9 decoder(39217)與 update_mb_info()(39215),甚至連命令列選項處理的 ffmpeg_opt.c(39212)與 MPEG-TS 的 SDT 實作(39214)都中招。報告自己的描述是「spanning components from the TS demuxer to the VP9 decoder」——這不是某一個解析器寫壞了,而是同一種失誤散佈在整個函式庫。

切換「依元件/依 bug 類別」分組 · 9 個公開 CVE

CVE-2026-39210 CVE-2026-39211 CVE-2026-39212 CVE-2026-39213 CVE-2026-39214 CVE-2026-39215 CVE-2026-39216 CVE-2026-39217 CVE-2026-39218 TS demuxer swscale ffmpeg_opt.c yuv4mpegenc SDT update_mb_info() img2enc.c VP9 decoder DASH demuxer Heap Buffer Overflow ×6 Integer Overflow ×1 Stack Overflow ×1 Stack Buffer Overflow ×1
9 個公開 CVE(CVE-2026-39210…39218)的元件與類別。另有 12 個以 DFVULN 內部編號記錄,合計 21 個。資料:depthfirst research

切到「依類別」分組會看得更清楚:九個公開 CVE 裡,五個是同一種 heap buffer overflow,再加上一個 stack buffer overflow、一個 stack overflow、一個 integer overflow——除了 swscale 那個 integer overflow 之外,本質上全是「寫超出該寫的範圍」這同一回事。這不是巧合,而是這類程式碼的天性。一個 demuxer 或 decoder 的工作,拆到最底層就是一個重複的動作:讀一個長度欄位或偏移量,照那個數字去配置一塊記憶體、或在已配置的記憶體上讀寫。問題在於,那個長度欄位往往直接來自正在被解析的檔案或封包——也就是來自攻擊者。只要程式在用這個數字之前沒有重新對照緩衝區的實際邊界,溢位就成立。

integer overflow 與 buffer overflow 在記憶體安全裡常常是同一條因果鏈的兩端:合理的推測是,一個來自輸入的長度先在某次乘法或加法裡溢位、繞回成一個很小或為負的值,於是配置出來的緩衝區比實際需要的小,接著照原本那個大長度去填,就溢位了——報告把 CVE-2026-39211 標為 swscale 的 integer overflow,但沒有展開它的完整因果,這條鏈是依類別做的一般性推斷。不論細節如何,與其說 FFmpeg 有 21 個彼此無關的 bug,不如說它有一種反覆出現的失誤模式——「拿外部數字當記憶體尺寸,卻沒在最後一刻核對」——而這個模式在一套解析數百種格式的程式碼裡,被複製了成百上千次。問題從來不在某個工程師寫錯了一行,而在於這個模式天生難以一次掃乾淨。

一個 183 位元組的封包,怎麼變成完全可控的寫入

抽象地說「緩衝區溢位」沒什麼說服力,報告挑了其中一個 RTP 相關的洞做了完整的資料流追蹤,值得跟著走一遍——這也是整篇報告最有教學價值的一段。攻擊者送的是一個 RTP 串流,落在 AV1 的 depacketizer(libavformat/rtpdec_av1.c)。AV1 的位元流由一連串 OBU(Open Bitstream Unit)組成,depacketizer 的工作是從 RTP 封包裡把這些 OBU 一個個切出來;切的依據,就是每個單元前面那個由輸入提供的長度。麻煩出在解析迴圈裡的一個指標。報告寫道「Because buf_ptr never advanced, the next loop iteration re-parses the TD's own bytes」——指標 buf_ptr 在某條路徑上沒有往前移動。在一個解析迴圈裡,指標不前進是個典型的災難開關:迴圈以為自己在讀下一個單元,實際上原地踏步,把剛讀過的同一段位元組重新當成新的東西來讀。

接下來的連鎖反應就順理成章了。那段位元組原本是一個 TD(temporal delimiter),現在被當成新單元重新解析:「its header byte is re-read as a fresh OBU length, and its payload becomes that fabricated OBU's contents」——TD 的 header byte 被重讀成一個新 OBU 的長度欄位,它的 payload 則變成這個憑空捏造出來的 OBU 的內容。一個本來只是分隔用途的小單元,搖身一變成了「長度+內容」俱全的偽造資料塊,而這兩樣全都來自攻擊者那 183 位元組的封包。

關鍵在於這條路徑上的每一個數字都來自封包本身。報告的結論是「The data that will eventually land at the poisoned offset is fully attacker-supplied」——最終落到那個被汙染偏移量的資料,完全由攻擊者提供。為什麼「偏移量和內容都可控」這麼要緊?因為記憶體破壞原語的威力,取決於攻擊者能控制多少自由度。如果只能控制「寫到哪」,攻擊者頂多讓程式崩潰;如果只能控制「寫什麼」、但位置固定,利用難度也高。當偏移量和內容兩者都落在對方手上,攻擊者就能把任意值寫到任意位置——這正是報告所說的,「a heap buffer overflow with a fully controlled offset _and_ fully controlled contents, which is about as strong a primitive as a memory-corruption bug can offer」,幾乎是記憶體破壞 bug 能提供的最強原語。難怪一個 183 位元組的封包,就足以「redirect execution」、改寫程式的執行流。下面這張圖把這條路走一遍:點任一階段,看那一步發生了什麼、哪些位元組是攻擊者控制的。

點任一階段,看那一步攻擊者控制了什麼 · 4 個階段

183-byte 封包 buf_ptr 沒前進 header 被重讀成 OBU 長度 完全可控的寫入 1 / 4 183-byte 封包 攻擊者送出單一 183 位元組的 RTP 封包,落入 AV1 depacketizer。報告 :a single 183-byte packet is enough to redirec t execution。
AV1 RTP depacketizer(libavformat/rtpdec_av1.c)的資料流。階段內容引自 depthfirst 報告對該洞的逐步描述。

觸發這個洞需要什麼前提?這才是真正讓人不安的部分,報告的回答幾乎是「什麼都不需要」。對任何把 FFmpeg 指向受攻擊者影響的 RTSP URL 的部署來說,它都暴露在外,而這類部署比想像中普遍:「media ingest pipelines, surveillance and CCTV systems, and transcoding services」——媒體匯入管線、監視與 CCTV 系統、轉碼服務。這些系統有個共通點:它們的日常工作就是去開一條別處來的串流。而觸發不需要任何特殊條件:「No authentication, no user interaction beyond opening the stream, and no unusual command-line flags are required — the vulnerability triggers during the normal RTSP PLAY phase that every one of these clients performs by design」。沒有認證、沒有使用者額外操作、沒有奇怪的命令列參數;漏洞就在每個用戶端依設計都會執行的那個正常 RTSP PLAY 階段裡觸發。

把這句話翻成工程師的白話:你不需要被釣魚、不需要點開什麼可疑檔案,你的服務只要照常去播放一條對方控制的串流,就中了。對一個 24 小時都在拉攝影機畫面、或者接受使用者上傳網址去轉碼的後端來說,「開串流」不是一個可以拒絕的危險動作,而是它存在的全部理由。報告最後附上了可驗證的證據:「You may find the PoC code here」——這不是理論上的可能,而是有可重現 PoC 的真洞。

為什麼二十年 fuzzing 還掃不乾淨:攻擊面的尺度

到這裡,案子的真正問題浮出來了。FFmpeg 不是疏於審查的小專案,恰恰相反,它「absorbed over two decades of relentless fuzzing and manual audits」——吸收了二十年不間斷的 fuzzing 與人工審查。按常理,被掃了二十年的程式碼應該所剩無幾才對,那為什麼還能一次掉 21 個?報告給的根因是尺度。它「is massive, comprising roughly 1.5 million lines of heavily optimized C code dedicated to parsing hundreds of complex media formats」。一百五十萬行為效能而高度最佳化的 C,專門用來解析數百種複雜的媒體格式。把這兩個數字疊在一起看:每一種容器、每一種 codec、每一種 RTP depacketizer,都是一個多少獨立的解析器,都在處理對方控制的位元組;而它們又是為了速度而手工最佳化的 C——最佳化往往意味著更多手寫的指標運算、更多被省略的邊界檢查,正是記憶體破壞最容易藏身的地方。

1.5M 行高度最佳化的 C heavily optimized, all parsing untrusted bytes 數百 種複雜媒體格式 每種容器/codec =一個獨立解析器 20+ 年 fuzzing + 人工審查 已吸收了二十年的 持續掃描——仍然有 21 個
三個尺度數字解釋了「為什麼掃不乾淨」。引自報告對 FFmpeg 規模的描述。

把這三個數字放在一起,矛盾就化解了:fuzzing 二十年確實有效,它把「淺層、容易碰到」的 bug 大多清掉了。fuzzing 的工作方式是不斷餵進變異過的隨機輸入,看程式有沒有崩潰;它擅長碰到那些只要輸入夠隨機就會走到的路徑。但一百五十萬行、數百種格式構成的攻擊面太深,二十年下來,淺水區早被掃過無數遍,剩下的 bug 往往藏在需要特定狀態、特定先決條件、特定路徑序列才能觸發的角落。前面那個 AV1 depacketizer 的洞就是典型一例:它要求解析迴圈先走到某條 buf_ptr 不前進的特殊路徑,再讓被重讀的位元組剛好構成可控的偏移量——這種多步、有狀態的條件,隨機輸入要碰巧湊齊的機率極低。

depthfirst 的做法不是繼續加大 fuzzing 的算力,而是換一條路:讓代理去讀程式碼、推理可達性。報告描述它的代理「deeply analyze the code, branching out in parallel to test various hypotheses」——深入分析程式碼,平行地分支出去測試各種假設。更關鍵的是接下來這句方法論:它們「trace execution paths, validate whether an attacker controls the right inputs, and determine if the data flow actually reaches a vulnerable sink」。追蹤執行路徑、驗證攻擊者是否控制了對的輸入、判斷資料流是否真的到達一個危險的 sink。這正是前一節那張資料流圖在做的事:不是亂試輸入碰運氣,而是順著程式邏輯反推——這個危險的寫入要被觸發,需要哪些前提?這些前提攻擊者控制得到嗎?從輸入到那個 sink 的路徑真的走得通嗎?fuzzing 是從輸入端往前撞,代理是從危險 sink 往回推可達性,兩者剛好在 fuzzing 撞不到的深路徑上分出了高下。這也解釋了為什麼一個被掃了二十年的目標,仍能在一輪分析裡掉出 21 個洞——不是 fuzzing 沒用,而是這兩種方法本來就覆蓋著攻擊面的不同區域,過去缺的那一塊,現在被補上了。

同一個目標,三種代理,成本差一個量級

這篇報告之所以值得當成趨勢來讀,還有一個原因:FFmpeg 最近不只被 depthfirst 一家盯上,而是成了好幾組自動化安全研究的共同靶場。把它們的產出與成本擺在一起,比任何單一數字都更能說明方向往哪走。報告自己點名了兩組先行者。一組是 Google:「Google's Big Sleep team disclosed 13 vulnerabilities in FFmpeg」——Big Sleep 團隊揭露了 13 個 FFmpeg 漏洞。另一組是 Anthropic:「Anthropic used their Mythos model to scan FFmpeg and successfully discovered some security issues」,用 Mythos 模型掃 FFmpeg、找到了一些安全問題(報告沒給具體數量)。depthfirst 這一輪挖出 21 個,而成本是「a total cost of roughly $1k (10% of what Anthropic spent using Mythos)」——總共約一千美元,是 Anthropic 用 Mythos 花費的十分之一。

研究方/工具在 FFmpeg 的產出成本
Google Big Sleep揭露 13 個漏洞報告未列
Anthropic Mythos「discovered some security issues」約 $10k(depthfirst 的十倍)
depthfirst 自動化代理21 個零日(9 個 CVE + 12 個 DFVULN)約 $1k
三組最近指向 FFmpeg 的自動化安全研究。成本欄的 $10k 為報告「10% of what Anthropic spent」的反推;Big Sleep 成本報告未提供。資料:depthfirst research

這個成本數字,才是報告真正想讓人記住的訊息。一千美元能在一個被二十年 fuzzing 反覆掃過的目標上挖出 21 個洞——而且報告再三強調這些洞不是紙上談兵:「our agent produces concrete, reproducible PoC inputs to confirm its findings」,代理會產出具體、可重現的 PoC 來確認每個發現;「Every finding delivered is real, reachable, and actionable」,每個交付的發現都真實、可達、可行動。可達與可重現是這裡的重點——它把「理論上可能有 bug」和「這是一個可以拿來打的真洞」之間的距離抹平了。

當這件事的成本被壓到三位數美元,攻防的經濟學就變了。過去,「找到一個可利用的零日」需要稀缺的專家人力與大量時間,這個成本本身就是攻擊者天然的門檻——值不值得為一個目標投入幾週的人工逆向,會先把大量潛在攻擊者擋在外面。當同樣的產出可以用一千美元、由自動化代理批次生產,門檻就塌了。對任何長期暴露在「解析不受信任媒體」這個位置上的服務來說,這意味著它面對的不再是偶爾光顧的高階研究者,而是隨時可能被低成本掃過一遍的攻擊面。

那麼,一個會去解析外部媒體的後端,現在該怎麼想這件事?報告自己沒有開出緩解處方,但它把曝險面講得夠清楚,足以反推出第一步該做什麼。它點名的暴露對象——media ingest pipeline、CCTV、轉碼服務——共通點都是「會主動去開一條別人給的串流」。所以最直接的問句不是「我有沒有跟上 FFmpeg 的版本」,而是「我的服務在什麼地方,會把一段不受我控制的位元組餵進解碼器」。RTSP URL、使用者上傳的檔案、第三方拉進來的串流,每一個入口都是這 21 個洞可能落地的地方。報告強調這些洞「reachable」、可達——可達的另一面,就是這些入口必須被當成真正的信任邊界來對待,而不是預設安全的內部管線。

同樣需要誠實標註的,是這篇報告沒有給的東西,而且不少。它沒有談 FFmpeg 的維護者是否人力不足、是否缺乏資金;沒有提 sandbox 隔離、程序隔離、或用記憶體安全語言重寫這類常被討論的緩解方向;也沒有細談這 21 個洞向上游通報與協調揭露的時程——它只提到其餘問題都已修補、只是還沒拿到 CVE 編號。這些後續細節文章正文大多沒攤開,硬要替它補上就是捏造。合理的推測是,既然已經公開了九個正式的 CVE 編號,背後通常意味著走過了協調揭露的流程——但報告正文並未明說這一點,所以這只能當推測、不能當事實。我們能從這篇文章確定的,僅限於它白紙黑字寫下的那幾件事:21 個洞、9 個公開 CVE、幾乎全集中在緩衝區溢位、一個 183 位元組的封包就能在正常 RTSP PLAY 流程裡改寫執行流、總成本約一千美元、是同期 Anthropic Mythos 的十分之一。其餘的,等下一篇報告或上游的修補紀錄。

Take-away:下次評估一個「成熟、被反覆 fuzzing 過」的解析器有多安全時,別只問它被掃過多少年——先量它的攻擊面有多深(多少種格式、多少行為效能犧牲安全的 C、多少條只有特定狀態才走得到的路徑),因為自動化代理現在能用三位數美元的成本,順著程式邏輯走進那些 fuzzing 二十年都沒碰到的角落。