要講 Meltdown 之前,我們必須講一些原理,因為相比之下 Meltdown 會複雜一些。
上一篇就說,Meltdown 可以從 ring 3 突破到 ring 0。這聽起來很不可思議,因為在虛擬記憶體的狀況下,這樣做會觸發 page fault,不過,請先看以下的解釋。
分頁表是一種資料結構,也是一種管理記憶體的方式。在使用虛擬記憶體的狀況下,程式會以為記憶體是一大塊連續的,不過實際上,因為分頁表做了轉譯地址的動作,所以這些記憶體有可能在實際上並不是連在一起的。
處理器的記憶體控制器會有一份快取表,紀錄最近用到的實體位置-虛擬位置組合。這個叫做 TLB,Translation lookaside buffer。
當要將虛擬記憶體位置轉成實體位置的時候,記憶體控制器會先去看這張表,如果有這組紀錄,就是中了,tlb hit,返回要查的實體位置。如果沒中,就得去分頁表裡面做掃表的動作。
掃分頁表也可能會發生失敗的問題,例如說地址寫錯(這狀況有可能會引起存取錯誤,segfault),或是那東西被趕去硬碟上了(這時候就得去讀硬碟)。
之前有提過,處理器有預先執行的能力。這裡要更詳細的講解一下:
處理器有一個處理指令的流水線。大致上是:
註:如果執行過程中出錯了, micro-op 在緩衝區裡面會被貼上標籤。在執行完成時,就會發出中斷信號。發中斷信號,會將流水線重置,所以在這緩衝區裡面的東西都會清空。(CPU表示:_____)
想了解更多的話,請參考 Tomasulo 算法。
假設在 user mode,執行以下指令:
mov rax, <某個 kernel mode 的位置>
就算這指令執行了,但是在執行完成時,這指令會導致處理器發出一個中斷信號(因為從 user mode 硬讀 kernel mode 會出錯),然後這指令也同時不會把結果寫到目標去。
順帶一提,FDIV 那篇提到的 #UD
即是中斷信號之一。
那假設我們同時下:
mov rax, <某 kernel mode 位置>
mov rbx, <某 user mode 位置>
因為剛好有兩個執行單元可以跑這個指令,所以他們會做並行運算。第一條會馬上出錯,所以第二條雖會執行,但不會執行完成。但根據前一篇提到,預先執行的機制,<某 user mode 位置>
的內容會被收進快取當中。
在中斷信號後的錯誤處理結束之後,我們會發現讀 <某 user mode 位置>
這東西的速度變快了。如果我們要更確定一點,可以在下這些指令前,先下 clflush
,強制把這東西從快取中趕出去。
再考慮這樣下指令的狀況:
mov rax, <某 kernel mode 位置>
and rax, 1
mov rbx, [rax+某 user mode 位置]
若先故意把重排序緩衝區塞爆,塞一堆垃圾,故意找使用不同運算單元的指令,然後故意寫一些會跟我們指令需求互相卡死的東西,就會讓第二和第三行在第一行執行完成之前被預先執行。
然後,讀 RBX
就行了。Intel 處理器中,「發中斷然後做錯誤處理」這件事是異步進行的,就算執行該指令,出錯了,也不會馬上爆炸。這攻擊手法和 Spectre 類似,而且可以繞過權限檢查 :-)
因為 AMD 處理器架構不同 (發音: AMD 窮,不能搞太複雜的架構),所以 Spectre 的第二種攻擊模式,和這篇介紹的 Meltdown,都不會對他們的處理器起作用。