信任總得從某個地方開始
這一級稍早的導覽,是一趟看信任如何逃逸的旅程:一條快取計時通道靠一次載入花多久而把祕密漏出去,而 Spectre 誘使機器去推測性地碰它本不該讀的記憶體。這篇收尾的導覽把問題反過來。我們不問「我的祕密從哪裡漏出去?」,而是問那個更難、卻更有盼望的問題:從一台冰冷、斷電、可能已被動過手腳的機器開始,你究竟要怎麼抵達一個你能信任的狀態?答案不是單一的小工具,而是一條鏈;而就像每一條鏈,它只跟它的第一環一樣牢。
那第一環就是信任根:一小塊你決定信任的硬體——因為你別無選擇,總得有個起點,你無法無止盡地往回驗證。實務上它是製造晶片時燒進唯讀記憶體或熔絲裡的一小段程式碼,裡頭存著一把公鑰,握有軟體存取權的攻擊者改不動它。整座安全開機的大廈都靠這一個假設撐著:這第一塊石頭是被誠實地放下的、而且搬不動。如果信任根是健全的,建在它之上的一切都能被檢查;如果它被偽造,它之上再聰明的檢查也毫無意義。
安全開機:一連串被量測的交棒
安全開機是一門紀律:把那單一可信的根,一環接一環地往上延伸,一路抵達一個正在執行的作業系統。這個想法說起來很簡單:任何一個階段在把控制權交給下一個之前,都必須先檢查下一個階段是貨真價實的。回想指令週期:一顆從重置中醒來的處理器,就只是從一個固定位址開始擷取。安全開機讓那最起頭的程式碼成為可信的根,從此每一個階段都先驗證下一塊軟體上的密碼簽章,再放它執行;而且只有在簽章對上一把它早已信任的金鑰時,才放它跑。
- 上電:CPU 開始執行燒進晶片、不可變更的信任根程式碼。這段程式碼是被假定信任的、從不被驗證,因為它之前沒有任何東西能來做驗證。
- 信任根量測並驗證下一個階段——開機載入程式——拿它的簽章去對照可信的公鑰。檢查失敗就停機;通過了,才把控制權交出去。
- 如今自身也已可信的開機載入程式,對作業系統核心重複這套儀式:先驗證它的簽章,唯有它是真品時才執行它。
- 核心可以把這條鏈再延伸到驅動程式與使用者程式碼。每一環都驗證下一環,於是信任像接力一樣,從那唯一的根向外流動,從不憑空冒出。
把兩個看起來很像的概念分開會有幫助。驗證式開機(上面那條安全開機路徑)拒絕執行任何簽章對不上的東西——它是一道擋住被竄改軟體的閘門。量測式開機採取不同的姿態:它不是拒絕,而是在每個階段載入時算出一個滾動的雜湊值,把這些量測記在一個防竄改的日誌裡,好讓驗證者事後能證實究竟開機了什麼。前者阻止壞程式碼執行;後者讓遠端的一方能證明這台機器處於一個已知良好的狀態。真實系統往往兩者都做,而兩者最終都回溯到同一個根。
飛地:一間連核心都進不去的私密房間
安全開機把你帶到一個可信的作業系統。但如果你不信任那個作業系統呢——比方說,你跑在別人的雲端機器上,而核心、超管理器、甚至一個惡意的管理員都可能在讀你的記憶體?這一級稍早那個經典的特權層級模型在這裡幫不上忙,因為它假定最高特權的程式碼(核心)就是最受信任的。可信執行環境把那個假設翻轉過來:它劃出一塊區域,讓你的程式碼和資料受到保護——即使是對機器上最高特權的軟體也是如此。
安全飛地就是那塊受保護區域的具體化身。把它想成屋子裡的一間私密房間:虛擬記憶體早已讓每個程式相信它擁有整棟建築,同時有個翻譯者把它的房間映射到真實那棟——但飛地多加了一道連房東都打不開的門。硬體強制:只有飛地內部的程式碼才能讀飛地記憶體;核心可以排程它、換頁它、甚至停掉它,但若它試圖讀取內容,看到的只是密文。為了讓這成真,飛地的分頁住在物理上加密的記憶體裡,於是即使是一個把探針焊到 DRAM 晶片上、直接讀匯流排的攻擊者,也只能擷取到一堆攪亂的位元組。
飛地通常還配有遠端證實:在你把祕密送進別人的機器之前,飛地硬體會簽出一份報告——同樣以一把製造商金鑰為根——證明「就是這段程式碼,正跑在一台真品硬體上的真品飛地裡」。唯有那份簽署的報告通過檢查,你才交出祕密。但誠實要求把限制大聲說出來。飛地保護的是記憶體的內容;它並不堵住這一級其餘部分講的那些微架構側通道。快取計時仍會透過共享快取漏出去,而 Spectre 式的推測洩漏已被示範可以針對飛地施展。牆是真的,但那些窗——計時、推測、功率——正是稍早那幾篇導覽警告過的那些。
Rowhammer:當資料本身發動攻擊
到目前為止的一切都假定記憶體是一個忠實的檔案櫃:寫一個位元,讀回同一個位元。Rowhammer 在物理層面打破了那個假設。回想 DRAM 把每個位元當作電荷存在一顆微小的電容裡,密密地排成一列列,密到那些單元幾乎相觸。如果一個程式一遍又一遍地讀同一列——在週期性的更新把電荷補回來之前,捶打它上千次——這份電氣擾動就可能讓電荷從相鄰的列裡漏出去,悄悄翻掉它們的位元。攻擊者從不直接碰受害者的資料;他只是猛捶隔壁的列,直到受害者的位元自己變了。
這之所以驚人,是因為它把一個可靠性缺陷變成了一個安全漏洞——這一級的兩半在同一個臭蟲裡相遇了。一次擺對位置的翻轉就可能造成毀滅:翻掉一個分頁表項裡的某個位元,一個行程就可能被誘騙去映射一塊它本不獲准抵達的記憶體,擊敗了 MMU 存在所要強制的那個記憶體保護。防禦在物理與簿記兩個層面反擊:更頻繁地更新列,讓記憶體控制器追蹤激進的存取樣式並預先更新受威脅的鄰居(目標列更新),以及倚靠 ECC 記憶體去逮住那偶發的翻轉。這些沒有一個是完美的解藥,這正是為什麼 Rowhammer 的變種一再回來。
誠實的張力:安全要付出效能
用一個承諾來收尾會讓人寬慰:好的工程能讓系統既快又安全。誠實的真相是,兩者往往直接張力相抗,而假裝並非如此已經燒掉了真實的系統。在每個開機階段驗證簽章要花啟動時間。為每一次飛地記憶體存取加解密,要在通往主記憶體的路上付出延遲與頻寬。為了對抗 Rowhammer 而更頻繁地更新 DRAM 列,偷走了記憶體本可花在服務真實讀取上的週期。而上一篇講的 Spectre 與 Meltdown 緩解,是其中最鋒利的例子:清空預測器、加上屏障,丟掉的正是稍早幾級費了極大力氣才弄快的那個推測執行。
所以安全不是一個你栓上去的功能;它是一筆你花掉的預算,拿去跟這整道階梯教你去追逐的效能對換。對的工程問題從來不是一個是非題式的「它安全嗎?」,而是「對哪一種攻擊者安全、要付多少代價、而那筆交換在這裡值不值得?」一支裝著你指紋的手機,和一個處理公開氣象資料的批次作業,落在那個判斷的兩個極端。成熟的設計把自己的威脅模型大聲說出來,接受它所需防禦的價碼,並拒絕為它不需要的那些付錢。
這收束了這一級,也隨之收束了這整道階梯的安全弧線。我們從只需算出正確答案的晶片出發,而以也必須把那答案對一個跟你一樣熟悉微架構的對手保持私密又可信的晶片作結。最深的教訓,正是這道階梯從最初的地基就一直低聲說著的:契約是一個美麗又有用的謊言,而真正的理解——以及真正的安全——來自誠實地認識住在它正下方那層凌亂的硬體。