跳至主要内容

一年與 Spectre:來自 V8 的觀點

· 閱讀時間約 10 分鐘
Ben L. Titzer 與 Jaroslav Sevcik

2018 年 1 月 3 日,Google Project Zero 和其他團隊 披露 了一種影響使用推測執行的 CPU 的新類型漏洞,名為 SpectreMeltdown。利用 CPU 的 推測執行 機制,攻擊者可以在臨時繞過代碼中防止程序讀取未授權內存數據的隱式和顯式安全檢查的情況下運行。雖然處理器推測的設計是一種微架構細節,應該在架構層面上不可見,但精心設計的程序可以在推測中讀取未授權的信息,並通過諸如程序片段執行時間等側信道披露它。

當證明 JavaScript 可用於發動 Spectre 攻擊時,V8 團隊參與了應對這一問題。我們組建了一支應急響應小組,並與 Google 的其他團隊、其他瀏覽器供應商的合作夥伴以及硬件合作夥伴密切合作。我們與他們協同工作,積極參與了攻擊方面的研究(構建概念驗證工具)和防禦研究(針對潛在攻擊的緩解措施)。

Spectre 攻擊包括以下兩部分:

  1. 將原本無法訪問的數據洩漏到隱藏的 CPU 狀態中。 所有已知的 Spectre 攻擊都使用推測來將無法訪問的數據的位洩漏到 CPU 高速緩存中。
  2. 提取隱藏狀態 以恢復無法訪問的數據。為此,攻擊者需要一個具有足夠精度的時鐘。(令人驚訝的是,即使是分辨率較低的時鐘也可能足夠,特別是使用例如邊緣閾值技術。)

理論上,破壞攻擊的其中一個組成部分就足夠了。由於我們不知道有何方法可以完美破解這兩個部分中的任何一個,因此我們設計並部署了大幅減少洩露到 CPU 高速緩存中的信息量 以及 阻礙恢復隱藏狀態的緩解措施。

高精度定時器

推測執行中可能殘留的微小狀態改變會引起相應的微小、幾乎微乎其微的時間差異,可能在十億分之一秒的範圍內。為了直接檢測這種差異,攻擊程序需要一個高精度定時器。CPU 提供了這樣的定時器,但 Web 平台並未暴露它們。Web 平台中最精確的定時器 performance.now() 的分辨率是單位數微秒,最初認為無法用於此目的。然而,兩年前,一個專注於微架構攻擊的學術研究團隊發表了一篇 論文,探討了網頁平台中定時器的可用性。他們得出結論,通過並行可變共享內存與各種分辨率恢復技術,可以構建更高分辨率的定時器,甚至達到納秒級分辨率。此類定時器精度足以檢測單個 L1 高速緩存命中與未命中的差異,這通常是 Spectre 工具洩露信息的方式。

定時器緩解措施

為了干擾小時間差異的檢測能力,瀏覽器供應商採取了多管齊下的方法。在所有瀏覽器上,performance.now() 的分辨率被降低了(在 Chrome 中,從 5 微秒降低到 100 微秒),並添加了隨機均勻抖動以防止分辨率恢復。在所有供應商的協商之後,我們共同決定採取一個前所未有的步驟,立即並追溯地禁止 SharedArrayBuffer API 在所有瀏覽器中使用,以防止構建可用於進行 Spectre 攻擊的納秒級定時器。

擴大化

在我們的攻擊研究中,我們很早就意識到單靠定時器緩解措施是不夠的。原因之一是,攻擊者可能僅僅是多次執行其工具,從而使累加的時間差異遠遠大於單個高速緩存命中或未命中的差異。我們能夠設計出可靠的工具,這些工具一次使用多條高速緩存行,甚至達到高速緩存容量,導致時間差大至 600 微秒。隨後我們還發現了不受高速緩存容量限制的任意放大技術。這些放大技術依賴於多次嘗試讀取秘密數據。

JIT 緩解措施

為了使用 Spectre 讀取無法訪問的數據,攻擊者誘騙 CPU 投機性執行讀取通常無法訪問的數據的代碼,並將其編碼到緩存中。此類攻擊可通過以下兩種方式阻止:

  1. 防止代碼的投機性執行。
  2. 防止投機性執行讀取無法訪問的數據。

我們通過在每個關鍵條件分支插入建議的投機屏障指令(如 Intel 的 LFENCE),以及使用 retpolines 處理間接分支,來嘗試方式 (1)。不幸的是,這類過於苛刻的緩解措施會大幅降低性能(在 Octane 基準測試中性能下降到 2-3 倍)。我們選擇了方式 (2),插入緩解序列來防止由於錯誤投機而讀取秘密數據。以下代碼片段展示了該技術:

if (condition) {
return a[i];
}

為了簡化,我們假設條件為 01。如果當 i 超出範圍時,CPU 投機性地從 a[i] 讀取數據,就會使上述代碼變得不安全,並訪問原本無法訪問的數據。重要的觀察是,在這種情況下,投機性會嘗試在條件為 0 時讀取 a[i]。我們的緩解方法重新編寫了此程序,使其行為與原始程序完全相同,但不泄露任何投機性加載的數據。

我們保留了一個 CPU 寄存器,稱為毒寄存器,用於跟蹤代碼是否正在錯誤預測的分支中執行。毒寄存器在生成的代碼中的所有分支和調用之間保持一致,因此任何錯誤預測的分支都會使毒寄存器變為 0。接著我們修改所有內存訪問,使它們無條件地使用毒寄存器的當前值屏蔽所有加載的結果。這並不會阻止處理器預測(或錯誤預測)分支,但會破壞由錯誤預測分支引起的(可能超出範圍的)加載值的信息。以下是修改後的代碼(假設 a 是一個數字數組):

let poison = 1;
// …
if (condition) {
poison *= condition;
return a[i] * poison;
}

這些額外的代碼對程序的正常(架構定義的)行為沒有任何影響。它只影響在投機性 CPU 上運行時的微架構狀態。如果程序在源級別進行儀器化,現代編譯器的高級優化可能會移除這些代碼。在 V8 中,我們通過將這些緩解措施插入到編譯的非常後階段來防止編譯器移除它們。

我們還利用毒寄存技術防止在解釋器的字節碼調度循環和 JavaScript 函數調用序列中由錯誤投機引起的數據泄漏。在解釋器中,如果字節碼處理程序(即解釋單個字節碼的機器代碼序列)不匹配當前字節碼,我們將毒寄存設置為 0。對於 JavaScript 調用,我們將目標函數作為參數(在寄存器中)傳遞,如果輸入的目標函數與當前函數不匹配,我們在每個函數的開始處將毒寄存設置為 0。採用毒寄存緩解措施後,在 Octane 基準測試中我們觀察到不到 20% 的性能下降。

對於 WebAssembly 的緩解措施較為簡單,因為主要的安全性檢查是確保內存訪問在範圍之內。對於 32 位平台,除了正常的範圍檢查外,我們還將所有內存填充到下一個 2 的冪次,並對用戶提供的內存索引的高位進行無條件屏蔽。64 位平台不需要此類緩解措施,因為其實現使用虛存保護進行範圍檢查。我們曾嘗試將 switch/case 語句編譯為二叉搜索代碼,而不是使用可能存在漏洞的間接分支,但這在某些工作負載上過於昂貴。間接調用則使用 retpolines 進行保護。

軟體緩解措施是一條不可持續的道路

幸運或不幸的是,我們的進攻性研究進展遠快於防禦性研究,而且我們很快發現,軟體無法有效防止 Spectre 引發的所有可能洩漏。這存在多種原因。首先,為了對抗 Spectre 所投入的工程努力與其威脅水平不成比例。在 V8 中,我們面臨許多其他更加嚴重的安全威脅,例如因普通漏洞導致的直接越界讀取(比 Spectre 更快、更直接)、越界寫入(Spectre 無法造成的,更糟糕),以及潛在的遠端代碼執行(Spectre 無法造成的,且更加糟糕)。其次,我們設計並實施的越來越複雜的緩解措施帶來了顯著的複雜性,這是技術債務,可能實際上增加了攻擊面和性能負擔。第三,測試和維護微架構洩漏的緩解措施比設計惡意小工具本身更加棘手,因為很難確保這些緩解措施能如預期運行。至少有一次,重要的緩解措施被後續的編譯器優化無效化。第四,我們發現要有效緩解某些 Spectre 變體,尤其是變體 4,僅靠軟體是不可能的,即使我們的合作夥伴 Apple 作出了英勇的努力來在他們的 JIT 編譯器中對抗這個問題。

網站隔離

我們的研究得出結論,理論上,未受信任的代碼可以通過 Spectre 和側信道讀取進程整個地址空間。軟體緩解措施降低了許多潛在惡意小工具的有效性,但並不高效或全面。唯一有效的緩解措施是將敏感數據移出進程地址空間。幸運的是,Chrome 已經多年來進行了一項努力,將網站分隔到不同的進程,以減少由常規漏洞引起的攻擊面。這項投資得到了回報,我們在 2018 年 5 月前將網站隔離部署到了盡可能多的平台。因此,Chrome 的安全模型不再假設渲染器進程內的語言強制保密性。

Spectre 是一段漫長的旅程,並展示了行業內外供應商和學術界之間最佳的合作。到目前為止,白帽似乎領先於黑帽。我們仍然未見到野外的攻擊,除了好奇的試驗者和專業研究人員開發概念證明小工具。這些漏洞的新變體仍在逐步出現,未來可能會繼續。我們將繼續追蹤這些威脅,並認真對待它們。

和許多具有程式語言及其實現背景的人一樣,安全語言施行合理的抽象邊界,不允許類型正確的程式讀取任意記憶體的概念一直是我們心智模型的基礎。得出我們的模型是錯誤的結論令人沮喪——在現今的硬體上,這種保證並不成立。當然,我們仍認為安全語言帶來了巨大的工程利益,並將繼續成為未來的基石,但……在現今的硬體上它們會有些洩漏。

有興趣的讀者可以在我們的白皮書中深入瞭解更多細節。