返回所有文章
發佈於 · 作者 Renaud Deraison

那個不肯死的 service worker,以及那台毫不在乎的虛擬機

上週 Google 不小心重新公開了一個四年前的 Chromium 臭蟲——一個在瀏覽器關閉之後仍會繼續執行 JavaScript 的 service worker,在每一款主流 Chromium 瀏覽器上都還沒被修補。概念驗證程式碼已經流到外面。有意思的問題不是它怎麼運作。有意思的問題是:當一個瀏覽器底下那整台機器,會在你關掉分頁時整個消失,所謂的「持久化」到底還算什麼。

持久化不是程式碼的屬性。它是程式碼被允許賴以為生的那個地方 的屬性。當一個「永遠不死」的 service worker,所安裝的那台機 器在你每次關掉視窗時就被摧毀,它就有麻煩了。

在 2026 年 5 月 20 日,Chromium 的議題追蹤系統悄悄地撤除了一個 自 2022 年起就處於受限狀態的臭蟲存取限制。幾個小時之內,這個臭蟲 ——連同一份可運作的概念驗證——就在網路上被各處鏡像、封存與整 理。Google 隨即動手再把這個議題關回去。已經太遲了;那個頁面已經 進到了 Wayback Machine 裡,而 BleepingComputer 的整理文章 也在隔天上線。

這個臭蟲是真的,概念驗證可以動,而在它最初被回報的四年之後,它 在 Chrome Dev 150 和 Edge 148 上仍然沒被修補。它影響每一款 Chromium 衍生的瀏覽器:Chrome、Edge、Brave、Opera、Vivaldi、Arc。

它讓一個頁面可以做的事,用一句話講起來其實很平淡:註冊一個 service worker——一小段瀏覽器同意讓它在背景持續執行的 JavaScript——而它在頁面關閉、分頁關閉、瀏覽器關閉、甚至機器重 開機之後都不會停下來。最初在 2022 年 12 月回報這個臭蟲的研究員 Lyra Rebane,向 BleepingComputer 概括了最糟的情況:在 Microsoft Edge 上, 下載選單甚至不會出現, 所以結果就是「完全靜默的 JS RCE,在你關掉瀏覽器之後還繼續跑」。

這個臭蟲為什麼存在的技術細節,比起它使能的那一類攻擊要次要得 多。它是一塊持久化原語。一個你曾經造訪過一次的頁面,可能是好幾 個月前,可能是一則你早不記得看過的廣告,被允許無限期地在你的電 腦上繼續執行程式碼。殭屍網路徵召、廣告詐欺、加密貨幣挖礦、DDoS 參與、慢速資料外洩、每當你再次登入同一個網站時的順手撈取憑證 ——這一切,一旦你拿到一個能熬過瀏覽器關閉的據點,就全部變得不 費吹灰之力。

這是一個好的臭蟲,從具有啟發性這個意義上來說。這是一個壞的臭 蟲,從它會被用上這個意義上來說。

service worker 究竟是什麼

過去大約十年裡,網頁平台允許頁面安裝一種叫做 service worker 的 小型 JavaScript 程式。它們在背景執行,獨立於任何分頁之外,存在 的目的是讓網頁應用程式能做一些合理的事——把資源快取起來好讓網 站在你離線時還能載入、遞送推播通知、在網路恢復時同步資料。

一個 service worker 是針對某個 origin 註冊的(譬如說,https、 主機名 example.com、以及預設連接埠這個組合)。一旦註冊,瀏覽 器就會把它存到磁碟上,並在你下一次造訪該 origin 上任何東西時把 它帶回來——或者,在像是推播訊息這類背景功能的情形下,自行週期 性地把它喚醒。從使用者的角度看,這個 worker 是看不見的。從作業 系統的角度看,這個 worker 是瀏覽器的一部分,以瀏覽器行程所擁有 的任何權限執行。

1. 頁面註冊 workernavigator.serviceWorker.register('/sw.js')2. 瀏覽器把它持久化寫進磁碟上的設定檔,以 origin 為索引鍵,範圍綁在某個 URL 路徑下,熬得過重新啟動。3. 瀏覽器把它喚醒在重新造訪時、推播時、背景同步時磁碟上的瀏覽器設定檔cookies · localStorage · IndexedDB · service-worker 註冊資訊——「持久化」實際上住的那個地方——
一個 service worker 的正常生命。某個頁面把它註冊起來,瀏覽器把它寫到磁碟上、按頁面的 origin 編索引,從那之後瀏覽器就負責把這個 worker 重新喚醒——當使用者重新造訪這個 origin、當推播到達、當背景同步觸發。worker 的住址是你本機的磁碟;worker 的壽命就是承載它的那個瀏覽器設定檔的壽命。

Rebane 回報的這個臭蟲,大致上是說:一個惡意頁面可以濫用一個背景 下載 API,去註冊一個瀏覽器後來不肯終止的 worker。同一系列的其 他 Chromium service-worker 臭蟲過去也出過貨;這至少是過去十二 個月裡的第二件。這個樣式——頁面種下某個東西,瀏覽器把它存起 來,瀏覽器之後再自行把它拿回來——才是值得記住的部分。它也是 那種一旦瀏覽器不再擁有任何長期留存的磁碟時,就會撞上一面牆的 樣式。

持久化所需要的

持久化——在資安意義上,而不是網頁平台意義上——是攻擊者在拿到 初始存取之後立刻會做的那個動作。一個頁面或一份二進位執行檔落到 你的機器上之後,在做任何其他有意思的事情之前,會先安排自己的某 個部分去熬過顯而易見的清理步驟:關掉分頁、退出瀏覽器、重新開 機、回家的列車上把筆電闔起來。在桌面作業系統上,持久化有它自 己的一套詞彙——macOS 上的 launch agent、Windows 上的排程工作、 Linux 上的 systemd 使用者 unit、瀏覽器擴充功能、開機自啟動項。

一個 service worker 實際上,就是瀏覽器內的一個 launch agent。它 被註冊、被儲存、再依照瀏覽器掌控的時程被帶回來活過來。它是網頁 平台所提供的、比較優雅的那批持久化原語之一,這也是它一直在吸引 臭蟲上門的原因之一。

關鍵的是,上面那份清單裡每一種持久化形式,都有同一個要求,可以 用三個字寫出來:得有東西在那裡留下來。一個 launch agent 之 所以能持久化,是因為有一個檔案系統可以讓那個 agent 的 .plist 檔被寫上去,並且在每次開機時從那個檔案系統把它讀回來。一個 service worker 之所以能持久化,是因為有一個瀏覽器設定檔可以讓 這個 worker 的註冊資訊被寫上去,並在瀏覽器每次啟動時把這份註冊 資訊讀回來。把儲存層拿掉,「持久化」也就跟著沒了。

這句話——把儲存層拿掉,持久化也就跟著沒了——就是 Bromure 整 個架構繞著去設計的那一部分。

Bromure 是怎麼跑一個瀏覽器的

一個 Bromure 工作階段不是一個行程。它是一台虛擬機。當你打開一 個瀏覽器視窗,主機會在 Apple Silicon 的 hypervisor 上啟動一台 小小的可拋棄式 Linux guest。那台 guest 裡頭的瀏覽器行程,才是 你的分頁實際上跑的地方。這台 guest 有它自己的檔案系統、它自己 的記憶體、它自己的核心。這些東西沒有一個是主機的。

這個架構裡有兩塊,對這個 service-worker 臭蟲來說特別有關係。

第一塊,service worker 註冊資訊一般會住的磁碟上設定檔,是放在 guest 的檔案系統裡,而不是你 Mac 的檔案系統裡。當 guest 消失, 那個檔案系統也跟著消失。第二塊,guest 本身的壽命是由使用者決定 的——預設情況下,一台 guest 會綁在一個設定檔上(工作、個人、銀 行、一個給 LLM 寫程式用的沙箱),而它在使用者選擇的時候才會被 抹除,但對於那些短暫的分頁而言,guest 在視窗關閉時就會被摧毀。 即使在設定檔模式下,guest 也只是一個磁碟卷快照,使用者隨時可以 回滾。

傳統瀏覽器你的使用者 · 你的磁碟分頁造訪了一個惡意頁面註冊了一個 service workerworker 被寫進設定檔~/Library/Application Support/...在你真實的磁碟上你關掉了分頁……worker 留在磁碟上……瀏覽器稍後把它復活在下一次啟動、推播、同步時——JS 持續跑著。跑好幾個月。Bromure可拋棄式 GUEST VMGUEST 檔案系統分頁造訪了一個惡意頁面註冊了一個 service workerworker 被寫到 guest 磁碟上在 VM 裡頭——不在主機上完全不會碰到你 Mac 的檔案系統你關掉了分頁VM 被摧毀guest 的磁碟跟著一起走。沒東西可以復活。
同一個惡意 service worker 在兩種瀏覽器上註冊的情況。在傳統瀏覽器裡,註冊資訊被寫到使用者主機磁碟上的設定檔內;關掉瀏覽器並不會把它移除,下一次啟動就會把這個 worker 帶回來。在 Bromure 裡,註冊資訊被寫進一台 guest VM 的檔案系統裡;關掉分頁就摧毀了 VM,註冊資訊也跟著一起走了。

在可拋棄式的場景裡,那個「永遠不死」的 worker 沒有可以「不死」 的地方。下一次瀏覽器啟動時本來會把它讀回來的那一列磁碟資料, 是放在一台已經不存在的虛擬機裡。「在瀏覽器關閉之後還繼續跑」 的 JavaScript,之所以還能繼續跑,是因為有某個東西可以讓它繼續 跑。當瀏覽器的關閉同時也會關掉底下的機器,這個 worker 就會和 瀏覽器在同一時刻停下來——這正好是原本規格所預設的行為。

在設定檔的場景裡——譬如說,你保留了一個長期存在的「社群媒體」 設定檔,它本來就_應該_在不同工作階段之間記得你——這個 worker 會像它一直以來那樣存活,但只在那一個設定檔的 VM 範圍內。它仍然 看不到你的「文件」、你的鑰匙圈、你的其他設定檔,或你電腦上剩下 的任何東西。而如果你哪一刻懷疑那個設定檔沾上了什麼不該沾的東西, 你可以把整個 VM 回滾到它最後一個乾淨的快照,所花的時間大概就和 重新開一個瀏覽器差不多。

一般性的論點

把上面這些讀成「Bromure 剛好可以擋下這一個特定的 service-worker 臭蟲」,會是很容易的事。這是真的,但那不是有意思的主張。有意思 的主張更為一般,而這次事件只是它的一小塊證據。

瀏覽器零時差會繼續落下來。這至少是過去十二個月以來的第二件 Chromium service-worker 臭蟲。 2024 和 2025 年那些大件的 是 V8 型別混淆和 Mojo 沙箱逃逸,付出六位數價錢買來,由商業間諜 軟體廠商和國家對齊的團體所使用。在 service worker 出現之前就有 過 renderer 妥協;在這個特定臭蟲被補上之後,還會有 renderer 妥 協。要找到新的,那筆經濟學——尤其在Mythos 等級的 AI 稽核者 如今已經站到揭露窗口的兩邊——正在朝對防守方不利的方向移動, 而不是朝對他們有利的方向。

當一個零時差開火的時候,相關的問題不是_瀏覽器本身有沒有瑕疵_。 它就是有。相關的問題是_這個漏洞利用實際碰到了什麼_。

那道牆 = 行程內沙箱Renderer(V8、Blink、codec 等)沙箱(Mojo IPC、seccomp)和那個臭蟲共用一套位址空間譜系瀏覽器行程(broker)你的主機文件 · 鑰匙圈 · SSH 金鑰 · ~/.awsiCloud · 攝影機 · 麥克風 · 本機網路——沙箱逃逸所能碰到的東西——那道牆 = 獨立 VMRenderer(V8、Blink、codec 等)瀏覽器行程(broker)——在 guest 裡頭HYPERVISOR 邊界獨立的核心、獨立的位址空間,和那個臭蟲沒有共用任何位元組你的主機文件、鑰匙圈、SSH 金鑰、~/.aws——這些都不對 guest 可見。——沙箱逃逸所能碰到的:更多的 guest——
一個攻擊者在瀏覽器臭蟲開火之後,可以用兩種方式延伸他的觸及範圍。在傳統瀏覽器上,行程內的沙箱就是 renderer 程式碼與主機之間的那道牆;當有人在 V8 或某個剖析器裡找到一個記憶體臭蟲時,破掉的就是那道牆。在 Bromure 上,那道牆是一個獨立的 VM,它和瀏覽器沒有共用位址空間、不去剖析網頁的位元組、也不會在 renderer 出事時跟著破掉。

一個 Chromium 沙箱逃逸——也就是過去十年每隔幾個月你就會讀到一 次的那種典型「瀏覽器零時差」——是一條鏈,它拿到 renderer 某個 部分(V8、Skia、WebP、Dawn、libxml2)裡的一個記憶體臭蟲,並用它 去說服瀏覽器的 broker 行程做某件 renderer 本來不該能做到的事。 這些臭蟲之所以付得起六位數的價錢,是因為它們跨過了 Chromium 工 程師大部分時間都花在維護的那道牆:行程內的沙箱。

那道牆是一件了不起的工程。它在血緣上,也是由和跨過它的那個臭蟲 同一類東西構成的——C++、共用位址空間、像 Mojo 這樣的 IPC 原語。 當 V8 或者 Chromium 裡頭那一百個剖析器其中之一冒出一個新的記憶 體臭蟲時,它就是那種只要花夠多力氣,就有可能把沙箱撬開的東西。

一台 VM 是不同種類的牆。在它裡頭跑的瀏覽器並沒有一個會映射到主 機核心的虛擬記憶體位址;主機核心並不剖析瀏覽器正在剖析的那個頁 面;和瀏覽器比起來,整個虛擬化堆疊是一片非常小的面積,而它所剖 析的東西(vCPU 暫存器狀態、paravirt hypercall、virtio buffer 描述子)並不是某個網頁安排出來的對抗性位元組。在這道邊界上的臭 蟲確實存在。但它們屬於不同等級的難度,數量級上稀有得多,並且當 它們出現時,通常是由 hypervisor 廠商配著一個 CVE 號宣布出來的。

這對使用者改變了什麼

在上週揭露的這個 service-worker 臭蟲的特定情況下,Bromure 使用 者並不需要等修補。這個臭蟲在他們的瀏覽器裡也存在——Bromure 出 貨的就是上游的 Chromium——但這個臭蟲所使能的行為,正是 Bromure 的架構本來就會關掉的行為。一個「永遠活著」的 worker 只能活到它 所註冊在的那台 guest VM 還在的時候,最長也就是你保留的那個設定 檔還在的時候,最短至少也是那個分頁開著的時候。

在「下個月某個時候會落下一個瀏覽器零時差」這個更一般的情境下, Bromure 使用者拿到的是另一種形狀的結果。最壞的情況下,這個漏 洞利用攻擊的對象是 guest 的內容:瀏覽器工作階段、那個設定檔裡 目前已登入網站的 cookies、使用者在那個瀏覽器裡輸入過的任何東西。 那是一筆真實的損失,我們也不想把它藏起來。但沒有失去的是主機: 沒有 SSH 金鑰、沒有 ~/.aws、沒有鑰匙圈、沒有「文件」、沒有原 始碼樹、沒有 LLM 代理人的工作空間。

那個交換條件——瀏覽器工作階段可能被攻陷、主機不會、而且這個 工作階段本身是短的——就是在新瀏覽器臭蟲速率往上跑時,依然不 斷站得住腳的那個交換條件。

我們不會聲稱什麼

兩個直白的但書:

可拋棄式 VM 並不修補 Chromium

那個 service-worker 臭蟲是一個 Chromium 臭蟲。應該去把它修 好的人是 Chromium 工程師,他們也應該去修。Bromure 的架構改 變的是「這個臭蟲還沒被修」的代價;它不能取代修這件事本身。 當我們發現東西的時候會往上游回報,我們也讀和大家一樣的安全 公告。

VM 逃逸是一個真實存在的類別

hypervisor 本身的一個臭蟲——Apple 的 Virtualization 框架、或 它周圍那些較小面積裡的某一個——原則上會讓攻擊者得以抵達主 機。這些臭蟲存在。它們也比瀏覽器記憶體臭蟲在數量級上要稀有 得多,而且攻擊面要小得多。Bromure 讓主機暴露於 renderer 妥 協這件事,取決於_那_一類臭蟲,而不是每幾週就出貨一次的那一 類臭蟲。

下個月你會讀到的那則標題

會再有一個 Chromium service-worker 臭蟲。會再有一個 V8 型別混 淆。會再有一個 libwebp 堆積溢位。會再有一個零時差,被某個資 源極為充沛的對手,長時間用在極為特定的人身上極為長的時間,在某 個禮拜二的早上隨著一份緊急修補公告而公開。

在那當中任何一天的早上,有用的問題並不是你是不是已經重新啟動了 瀏覽器。有用的問題是:那個臭蟲落進去的那個盒子裡頭,實際上裝著 什麼。對今天大多數使用者而言,那個盒子就是裝著他們檔案、鑰匙圈 和已儲存登入資訊的那同一個盒子。對一個 Bromure 使用者而言,那個 盒子是一台 guest 虛擬機,它最壞的損失就是他們本來反正就要關掉的 那扇視窗。

持久化需要某個東西可以留下來。把那個東西做成可拋棄的,整整一類 攻擊就不再是一類攻擊。