JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

旁路攻擊:從時間裡洩漏出去

第二篇用權限等級和記憶體管理單元把門鎖上了。這一篇要讓你看到:一扇鎖上的門仍會洩漏——不是透過硬體「算出」什麼,而是透過它「花了多久」——而你學會愛上的那個快取,正是其中最有名的那扇漏風的門。

鎖沒問題——是鑰匙孔在吹口哨

上一篇你築起了真正的牆。權限等級把使用者程式碼擋在核心外,記憶體管理單元(MMU)強制執行記憶體保護、讓一個行程讀不到另一個行程的分頁,而一次對禁止位址的載入就只是觸發例外。邏輯上,那個祕密是碰不到的。所以這篇要談的,是這個令人不安的念頭:一個「永遠讀不到」你祕密的攻擊者,仍可能「量測」到它。旁路攻擊(side-channel attack)不去破鎖;它去聆聽鎖在運作時發出的聲響——而一台電腦,會發出很多聲響。

日常的比喻是一個有密碼盤的保險箱。你打不開它,但假設每按對一個數字,轉盤就比按錯時慢上一絲。你從沒看見裡面,卻能靠「替每次喀噠聲計時」一個數字一個數字地把密碼還原出來。這就是旁路:資訊透過一個設計者從沒打算讓它載資料的物理性質——時間、耗電、發熱、電磁嗡鳴,甚至風扇噪音——洩漏出去。這裡要誠實的關鍵是:程式的行為完全符合規格。它算出正確答案,也沒有「刻意」透露任何東西。洩漏搭的是 指令集架構(ISA)從未承諾要保密的東西:事情花了多久

時間為何會洩漏:這台機器並不平坦

時間之所以會洩漏,正是因為硬體並不均勻——而你早就知道原因。你在這條學習階梯上學到的每一項最佳化,都是靠讓「某些」路徑比其他路徑更快來運作的。一次快取命中幾個週期就回來,而一次未命中要付上百個週期;一次猜對的分支預測一飛沖天,而一次猜錯卻要把管線清空;甚至一個除法都可能比乘法久。ISA 給你看的那台機器,是一份平坦、簡單的合約,但底下的微架構(microarchitecture)卻是一片快路與慢路交錯的地景。每當一個祕密決定了程式「走哪條路」,它花的時間,就成了那個祕密的影子。

一個與快取無關的經典例子,能讓危險變得鮮活。想像一個密碼檢查程式,把輸入字串逐字元與祕密比對,一找到不符就立刻回傳。第一個字母就錯,幾乎馬上回傳;一個猜對前三個字母的嘗試,會跑完三次比較才失敗——因此可被量測地「慢一點」。一個能替回應計時的攻擊者,只要逐一試每個第一字母、留下最慢的那個,再換到第二個字母,就把一個不可能的 2^n 暴力破解,變成一場逐字母的快速漫步。修法——一個總是檢視完每個字元才回答的固定時間比較(constant-time compare)——正是所有時間防禦的範本:拆掉「時間」對「祕密」的依賴。

快取時間攻擊:填滿、等待、探測

現代電腦裡威力最強的旁路,是穿過快取的——正是你學會珍愛、用來隱藏記憶體牆的那項功能。一次快取時間攻擊(cache-timing attack)利用的是一個樸素的事實:受害者跑完之後,攻擊者能靠量測「自己後來的存取有多快」,看出「受害者碰過哪塊記憶體」。如果攻擊者讀到的某條列很快回來,它本來就在快取裡——意思是受害者(或先前的攻擊者)把它載進來了。當受害者的「位址」取決於某個祕密時,這種洩漏特別狠。許多密碼學的查表,會用一個金鑰位元組去索引一個陣列;被碰到的位址是 `table + 祕密位元組`,所以那個祕密實實在在地選了哪一條快取列被載入——而時間,揭露了那是哪一條列。

教科書上的配方叫 Prime and Probe(填滿與探測),它根本不需要與受害者共享任何資料——只要共享一個快取,而同一核心上(甚至跨核心、在最後一層)的任何兩個程式,本來就共享快取。攻擊者先用自己的資料把快取填滿,放受害者去跑,再重讀自己的資料、替每次存取計時。攻擊者現在哪裡吃到一次慢的未命中,受害者就一定是在那裡逐出了那條列——因為它載入了某個對映到「同一個快取集(cache set)」的東西——於是攻擊者就能憑集合索引,精確指出受害者碰過哪些位址,也就是它拉進了哪一條依祕密而定的列。

Prime and Probe, one cache set:

1. PRIME : attacker reads its own lines A0,A1,A2,A3
           -> they now fill the 4-way set (victim's lines evicted)

2. WAIT  : let the victim run one secret-dependent step
           if victim touches an address mapping to THIS set,
           it evicts one of A0..A3

3. PROBE : attacker re-reads A0,A1,A2,A3 and times each
             fast  (hit)  -> still cached -> victim did NOT use this set
             slow  (miss) -> evicted      -> victim DID  use this set

Threshold example (cycles, illustrative):
   L1 hit   ~   4     |  reads under ~60 cycles -> "hit"
   DRAM miss~ 200     |  reads over  ~60 cycles -> "miss"
Prime and Probe 在從不讀取受害者記憶體的情況下讀出祕密:它純粹從攻擊者自己的命中/未命中時間,推斷受害者用了哪一個快取集。
  1. 填滿(Prime):攻擊者讀自己的資料,把某個選定的快取集填滿,逐出受害者原本擺在那裡的東西。
  2. 觸發並等待:放受害者去跑一步,那一步碰到的位址取決於祕密。
  3. 探測(Probe):重讀剛填進去的資料、替每次存取計時——一次慢的未命中,意味著受害者逐出了那條列。
  4. 解碼:哪些集合變慢的模式,對映回那些依祕密而定的位址,把祕密一位一位地還原出來。

為什麼這如此難以阻止

讓快取時間攻擊令人膽寒的,是攻擊者所需的東西竟如此之少。沒有緩衝區溢位、沒有惡意指標、沒有權限提升——攻擊者只讀「自己的」記憶體,並擁有一個時鐘。受害者做的一切都合法且正確。MMU 依然完美地強制執行行程隔離;沒有任何一個禁止的位元組被讀到。資訊是透過一個兩個程式「按設計就共享」的資源——快取——以及一個架構視為隱形的量——延遲,逃出去的。你沒辦法用一個權限位元去修補它,因為從沒有任何權限被違反。

因此,防禦針對的是那份「依賴」本身,而每一種都帶著代價。固定時間程式碼(constant-time code)是密碼學的黃金標準:把演算法改寫到「它碰的位址」與「它走的分支」都不取決於祕密,於是不論金鑰是什麼,時間都是平的。這很費工,而且常常更慢,因為你刻意放棄了依資料而定的捷徑。其他防禦則切分(partition)共享資源——例如把敏感程式碼釘在它自己的快取區域,或在情境切換時清空快取與緩衝區——拿吞吐量去換隔離。硬體可以加入抗逐出探測的快取設計。這每一種,都是同一筆交易換了件新衣裳:你用花掉微架構安全給你的一部分速度,來買到安全。

這通往何處

你現在握有讀懂這一整階其餘部分的萬能鑰匙。旁路把一個隱形的內部狀態——一條快取列、一個分支結果、一個忙碌的執行單元——變成從外部可量測的東西,所需的不過是一個時鐘。快取時間攻擊是那個典型範例,Prime and Probe 是它最鋒利的工具,但這個模式是通用的:任何「共享的、由效能驅動的、且狀態能被祕密碰到的」資源,都是潛在的洩漏點。這正是為什麼,你在前幾階花時間讚嘆的那些效能點子——快取、預測、亂序執行——會一再出現在安全的頁面上。

下一篇把賭注急遽抬高。到目前為止,受害者是「真的執行了」那段依祕密而定的程式碼——洩漏是一場真實計算被從旁觀察到。SpectreMeltdown 揭露了更詭異、更可怕的事:處理器能透過快取,洩漏它「只是推測、之後就丟掉」的那些工作的資料——那些在架構層面上「從沒發生過」的指令。你剛學會的快取時間通道,是那輛逃逸用的車;推測執行,則是把贓物裝上車的那隻手。把這裡的時間通道學透,Spectre 讀起來就會像下一句話,而不是一門新語言。