Intel CPU漏洞技術解讀:都是緩存惹的禍!
背景
2017年6月1日,Google的安全團隊向Intel、AMD、ARM報了一個硬體級的漏洞,造成的危害是內核數據泄露,修復該漏洞的代價是至少30%的性能損失。2017年末Linux內核社區推出了KPTI「Kernel Page Table Isolation」補丁,Linus Torvalds在內核郵件列表上毫不留情地抨擊了Intel。
安全人員將這兩個漏洞命名為Meltdown和Spectre;Meltdown目前只存在於Intel的處理器和部分ARM處理器,Spectre存在於一切有亂序執行的現代處理器架構裡面,包括AMD。從原理上來說漏洞無法徹底修復。
本次的漏洞會對所有雲廠商造成較大影響,已經有跡象表明有黑客在利用漏洞攻擊雲系統。Microsoft Azure中國區已發布公告稱,將於北京時間2018 年 1 月 4 日上午 11:30 開始自動重啟受影響的虛擬機,並全部關閉向部分客戶開放的自助維護窗口;AWS也發送了通知郵件聲稱本周五將進行重大安全更新。
原因
一切還是要從CPU指令執行的框架——流水線說起。Intel當然不至於明知你要用一個用戶態的進程讀取Kernel內存還會給你許可。但現代CPU流水線的設計,尤其是和性能優化相關的流水線的特性,讓這一切充滿了變數。
給所有還沒有看過雲杉網路連載的系列文章《x86高性能編程箋注系列》的讀者一點背景知識的介紹:
x86 CPU為了優化性能,在處理器架構方面做了很多努力。諸如「多級緩存」這一類的特性,是大家都比較熟悉的概念。還有一些特性,比如分支預測和亂序執行,也都是一些可以從並行性等方面有效提升程序性能的特性,並且它們也都是組成流水線的幾個關鍵環節。即便你暫時還不能準確理解其含義,但望文生義,也能看出來這肯定是兩個熵增的過程。熵增帶來無序,無序就會帶來更多漏洞。
緩存的困境
講緩存,必然先掛一張memory hierarchy鎮樓:
不過我要說的和這個沒太大關係。現在需要考慮的是,如果能讀取到內核地址的內容,那這部分內容最終肯定是跑到緩存中去了,因為真正直接和CPU核心交互的存儲器,就是緩存。這對一級緩存(L1 Cache,業內也常用縮寫L1$,取cash之音)提出的要求就是,必須要非常快,唯有如此才能跟上CPU處理核心的速度。
Side Notes: 為什麼在不考慮成本的情況下緩存不是越大越好,也是因為當緩存規模越大,查找某一特定數據就會越慢。而緩存首先要滿足的要求就是快,其他的都是次要的。
根據內核的基本知識我們知道,進程運行時都有一個虛擬地址「Virtual address」和其所對應的物理地址「physical address」。
從虛擬地址到物理地址的翻譯轉換也由CPU通過page table完成。Page table並不儲存在CPU里,但近期查找到的Page table entry「PTE」都像數據一樣,緩存在了CPU中的translation lookaside buffer「TLB」里。為了不再過多堆砌術語和名詞,畫張圖說明一下:
當CPU根據程序要求需要讀取某個地址上的數據時,首先會在L1 Cache中查找。為了適應CPU的速度,L1緩存實現為Virtually indexed physically tagged「VIPT」的形式,即用虛擬地址即可直接讀取該虛擬地址對應的物理地址的內容,而不再需要多加一道轉換的工序。
如果L1 Cache miss,則會在下級緩存中查找。但越過L1 Cache之後,對L2$和L3$的速度要求就不再這麼嚴苛。此時CPU core給出的虛擬地址請求會先通過TLB轉換為物理地址,再送入下級緩存中查找。而檢查進程有沒有許可權讀取某一地址這一過程,僅在地址轉換的時候發生,而這種轉換和檢查是需要時間的,所以有意地安排在了L1 Cache之後。
L1緩存這種必須求「快」的特性,成了整個事件的楔子。
分支預測
分支預測是一種提高流水線執行效率的手段。在遇到if..else..這種程序執行的分支時,可以通過以往的歷史記錄判斷哪一分支是最可能被執行的分支,並在分支判斷條件真正返回判斷結果之前提前執行分支的代碼。詳情可以在上面提到的連載文章中閱讀。
需要強調的是,提前執行的分支代碼,即便事後證明不是正確的分支,其執行過程中所讀取的數據也可以進入L1緩存。在Intel的官網文檔《Intel? 64 and IA-32 Architectures Optimization Reference Manual》第2.3.5.2節中指:
L1 DCache Loads:- Be carried out speculatively, before preceding branches are resolved.- Take cache misses out of order and in an overlapped manner.
Show you the [偽] code:
if (likely(A < B)) { value = *(kernel_address_pointer);}
當分支判斷條件A < B被預測為真時,CPU會去提前執行對內核地址的讀取。當實際條件為A > B時,雖然內核的值不會真正寫入寄存器(沒有retire),但會存入L1 Cache,再加之上一節介紹的,獲取L1 Cache的值毋須地址轉換,毋須許可權檢查,這就為內核信息的泄漏創造了可能。
從理論上來講,如果可以控制程序的分支判斷,並且可以獲取L1緩存中的數據(這個沒有直接方法,但可以通過其他間接手法)的話,就完全可以獲取內核信息。而分支預測這種特性是不能隨隨便便就關閉的,這也就是這次問題會如此棘手的原因。
亂序執行
還有一個原因是亂序執行,但原理大致類似。亂序執行是Intel在1995年首次引入Pentium Pro處理器的機制。其過程首先是將我們在彙編代碼中看到的指令「打散」,成為更細粒度的微指令「micro-operations」,更小的指令粒度將會帶來更多的亂序排列的組合,CPU真正執行的是這些微指令。
沒有數據依賴的微指令在有相應執行資源的情況下亂序並行執行,進而提升程序的並行程度,提高程序性能。但引入的問題是,讀取內核數據的微指令可能會在流水線發出exception之前將內核數據寫入L1 Cache。與分支選擇一樣,為通過用戶態進程獲取內核代碼提供了可能。
限於篇幅,更詳細的內容讀者可以在國外安全團隊發布的消息中獲取。
後續
剛剛查閱之前連載中的一些細節的時候,看到在「流水線」那一章里寫過這樣一段話:
在面對問題的時候,人總是會傾向於引入一個更複雜的機制來解決問題,多級流水線就是一個例子。複雜可以反映出技術的改良,但「複雜」本身就是一個新的問題。這也許就是矛盾永遠不會消失,技術也不會停止進步的原因。但「為學日益,為道日損」,愈發複雜的機制總會在某個時機之下發生大破大立,但可能現在時機還沒有到來:D
很難講現在是不是就是所謂的那個「時機」。雖然對整個行業都產生了負面影響,但我對此仍保持樂觀。因為這就是事物自然發展的一個正常過程。性能損失並不是一件壞事,尤其是對牙膏廠的用戶來說。
◆◆◆
作者簡介
張攀
一個不耽誤碼字的網工
張攀,雲杉網路工程師,專註於x86網路軟體的開發與性能優化,深度參與ONF/OPNFV/ONOS等組織及社區,曾任ONF測試工作組副主席。
推薦閱讀:
※再談CVE-2017-7047 Triple_Fetch和iOS 10.3.2沙盒逃逸
※【S2-046】Struts2遠程命令執行漏洞(CVE-2017-5638)
※ADV170014 NTLM SSO:利用指南
※Windows遠程桌面漏洞Esteemaudit(CVE-2017-9073)補丁簡要分析
※如何看待大疆漏洞獎勵計劃 ,信息安全研究員疑似遭威脅,拒絕獎勵協議並準備公開獲得的信息?