CPU的熔斷與幽靈漏洞

本文從CPU的視角來講一下這個大名鼎鼎的漏洞。

自我介紹
我叫阿Q,是CPU一號車間裏的員工,我所在的這個CPU足足有8個核,就有8個車間,幹起活來槓槓滴。
我所在的一號車間裏,除了負責執行指令的我,還有負責取指令的小A,負責分析指令的小胖和負責結果回寫的老K。
CPU的每個車間都有一堆箱子,人們把這些箱子叫做寄存器,我所在的一號車間也不例外,我們每天的工作就是不斷執行指令,然後折騰這些箱子,往裏面存東西取東西。
由於我們四個人的出色工作,一號車間業績突出,在年會上還多次獲得了最佳CPU核心獎呢。

緩存
我們每天都需要跟內存打交道,不過由於內存這傢伙實在太慢了,我們浪費了很多時間等待他給我們數據傳輸。
終於有一天,上面給我們下了命令,說競爭對手CPU的速度快趕上我們了,讓我們想辦法提升工作效率。這一下可難倒了我們,我們平時幹活絕沒有偷懶,要怪只能怪內存那傢伙,是他拖了我們後腿。
一天晚上,我們哥四個在一起聚餐,討論起上面的這道命令來,大家都紛紛嘆氣。
就在一籌莫展之際,老K提出了一個想法:“兄弟們,我發現了一個現象,咱們和內存打交道的時候,如果訪問了某個地址的數據,它周圍的數據隨後也大概率會被訪問到”,說到這裏,老K停頓了一下。
我一邊聽一邊想着,小A倒是先開口:“然後呢?你想表達什麼意思?”
老K繼續說道:“咱每次數據都找內存要,太慢了,我尋思在咱們車間劃一塊區域,結合我發現的那個現象,以後讓內存一次性把目標區域附近的數據一起給我們,我們存在這塊區域,後面在需要用到的時候就先去這裏找,找不到再去找內存要,豈不省事?”
聽老K這麼一描述,感覺靠譜,我也趕緊附和:“好辦法!你們看啊,這內存老是拖咱後退,但是這傢伙一時半會也快不起來,要不咱先用這招試試,看看能不能加快一點工作效率,給上面也有個交代。”
說幹就幹,我們很快就付諸實踐了,我們還給這技術取了個名字叫緩存,效果居然出奇的好,後來爲了進一步優化,我們還把緩存分爲了兩塊,一塊離寄存器很近叫一級緩存,剩下的叫二級緩存。一級緩存中進一步分了指令緩存和數據緩存兩塊。

我們車間的工作效率那是飛速提升,但不知道是誰走漏了風聲,其他幾個車間也知道了這項技術,紛紛效仿。
這天,爲了業績,我們決定再加第三級緩存,這次把空間弄大點,不過咱們車間地盤有點侷促,放不下,我們偷偷給上面領導反饋了這事兒,想讓領導幫我們協調一下。
領導倒是同意了,不過告訴我們他得一碗水端平,平衡各車間的利益。但是咱廠裏空間也有限,不可能給每個車間都分配那麼大的空間,於是決定由廠裏統一安排一塊大的區域,讓各個車間來共享。沒有辦法,我們也只好同意了。現在,我們用上了三級緩存技術,內存那傢伙拖後腿的現象緩解了不少,相當部分時間我們都能從這三級緩存裏面找到我們需要的數據。

亂序執行
隨着技術的發展,咱們CPU工廠的工作性能也是不斷攀升,慢慢的,我們幾個又開始閒下來了,因爲我們實在太快了,儘管有了緩存,但我們還是有了不少閒暇時間。
這天我還是像往常一樣,小A取指令去了,我們知道這得要點時間,於是我和小胖還有老K我們仨鬥起了地主。

打了好幾把,小A才氣喘吁吁的回來,“小胖,該你去指令分析了,你起來讓我來打幾把”。小胖趕緊起身幹活,換上了小A上桌。
就這樣我們幾個輪流工作,一直保持着三個人的鬥地主牌桌。
沒想到的是,沒過多久,廠裏領導過來視察了,正好撞見我們幾個打牌,狠狠的訓斥了我們一頓。
“你們幾個上班時間玩得挺嗨啊”,領導的臉拉的老長。
“領導,我們沒有偷懶,這取指令、譯碼、執行、回寫幾個步驟都得分步執行,但是我們工作太快,存儲器跟不上我們,我們等得無聊打發時間嘛”,我上前解釋到。
“乾等着你們也可以提前做一些後面的準備工作嘛,不要浪費時間,讓生產效率更上一層樓”,領導說完就離開了,留下我們幾個面面相覷。
不過領導的一番話倒是如一記重錘敲在我的頭上,對啊,我們有這打牌的時間不如提前把後續指令的準備工作先做了,肯定能提升不少效率呢!
我開始組織兄弟幾個商討方案,“兄弟們,我們最主要的時間都浪費在等待內存數據上了,如果我們能在等待的時間裏把後續指令需要的數據提前準備到緩存中來,那可就節約不少時間了,不用每次都等那麼久。”

老K聽後很讚賞我的思路,並補充到:“不僅是準備工作,像有些指令,比如加法,如果參與加法的數據不依賴前面指令的結果,咱們完全可以提前把這加法指令執行了嘛,把結果保存在緩存中,等真正輪到這條指令執行的時候,再把緩存中的結果寫到內存中,這不也是節約了時間嗎?”
大家開始頭腦風暴起來,原來可以做的事情還這麼多,之前光想着等靠要,現在要主動出擊了,因爲打亂了順序提前會執行後面的指令,我們把這個技術叫做亂序執行.
“這次大家要保密哦,不能讓隔壁車間知道咱們的這次討論內容”,會議結束前,我提醒大家。

分支預測
按照這次會議討論的結果,咱們第二天準備實行,不過剛一開始,就遇到了麻煩。
按照計劃,我們在空閒時間裏,會提前把後續要執行的指令能做的工作先做了,但麻煩的是我們遇到了一條判斷指令,因爲不知道最終結果是true還是false,我們沒法知道後續是應該執行分支A的指令還是分支B的指令。不敢輕舉妄動,怕一會做了無用功。大家只好放棄了提前做準備工作的想法,還是一步步來。
不過很快我們發現,我們經常執行到這個判斷指令,而且每次結果都是去執行A分支,從沒有去過B分支。

於是我們幾個又商量,發明了一種叫分支預測的技術,遇到分支跳轉時,按照之前的經驗,如果某個分支經常被執行,那後續再去這個分支的概率一定很大,那這樣咱們預測後面會去到這個分支,就提前把這個分支後面指令能做的工作先做了。
果然,用上了分支預測和亂序執行後,我們車間的效率又狠狠的提升了一把,在工廠的集體大會上又一次表揚了我們,並且把我們的先進技術向全廠推廣,在我們8個CPU核心車間都鋪開了,性能甩開競爭對手CPU幾條街。

然而幸福的日子沒過太長,我們就因爲這兩項技術闖下了彌天大禍。
那天,我們還是如往常一般工作,可不久發現我們的分支預測頻頻出錯,提前做的準備工作也屢屢白費,很快,我們發現出事兒了······

事情還得從不久前的一個晚上說起。

神祕代碼

這天晚上,我們一號車間遇到了這樣一段代碼:

  •  
uint8_t array1[160] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};uint8_t array2[256 * 512];uint8_t temp = 0;void bad_guy(int x) {if (x < 16) {temp &= array2[array1[x] * 512];}}

不到一會兒功夫,我們就執行了這個bad_guy()函數很多次,這不,又來了。
負責取指令的小A向內存那傢伙打了一通電話,讓內存把參數x的內容傳輸過來,我們知道,以內存那蝸牛的速度,估計得讓我們好等。
這時,負責指令譯碼的小胖忍不住說了:“你們看,我們這都執行這個函數好多次了,每次的參數x都是小於16的,這一次估計也差不多,要不咱們啓動分支預測功能,先把小於16分支裏的指令先提前做一些?大家看怎麼樣”我和負責數據回寫的老K互相看了一眼,都點頭表示同意。
於是,就在等待的間隙,我們又給內存那傢伙打了電話,讓他把array1[x]的內容也傳過來。
等了一會兒,數據總算傳了過來:

  •  
x: 2array1[x]: 3

拿到結果之後,我們開始一邊執行x<16的比較指令,一邊繼續打電話給內存索要array2[3]的內容。
比較指令執行的結果不出所料,果然是true,接下來就要走入我們預測的分支,而我們提前已經將需要的數據準備到緩存中,省去了不少時間。
就這樣,我們成功的預測了後續的路線,我們真是一羣機智的小夥伴。

遭遇滑鐵盧
天有不測風雲,不久,事情發生了變化。
“呀!比較結果是false,這一次的x比16大了”,我執行完結果後發現和我們預期的有了出入。
小A聞訊而來,“額,咱們提前執行了不該執行的指令不會有問題吧?”老K安慰道:“沒事兒,咱們只是提前把數據讀到了我們的緩存中,沒問題的,放心好啦”
我想了想也對,大不了我們提前做的準備工作白費了,沒有多想就繼續去執行>16的分支指令了。
隨後,同樣的事情也時有發生,漸漸的我們就習慣了。

災難降臨
夜越來越深,我們都有點犯困了,突然,領導來了一通電話,讓我們放下手裏的工作火速去他辦公室。
我們幾個不敢耽誤,趕緊出發。
來到領導的辦公室,裏面多了兩個陌生人,其中一個還被綁着,領導眉頭緊鎖,氣氛很是緊張。
“阿Q啊,你知不知道你們新發明的亂序執行和分支預測技術闖了大禍了?”
我們幾個一聽傻眼了,“領導,這是從何說起啊?”
領導從椅子上站了起來,指着旁邊的陌生人說到:“給你們介紹一下,這是操作系統那邊過來的安全員,讓他告訴你們從何說起吧!”
這位安全員向大家點了點頭,指着被捆綁那人說道:“大家好,我們抓到這個線程在讀取系統內核空間的數據,經過我們的初審,他交代了是通過你們CPU的亂序執行和分支預測功能實現的這一目的。”
我和小A幾個一聽都是滿臉問號,我們這兩個提升工作效率的技術怎麼就能泄漏系統內核數據呢?

真相大白
安全員顯然看出了我們的疑惑,指着被捆綁的那個線程說道:“你把之前交代的再說一遍”
“幾位大爺,你們之前是不是遇到了分支預測失敗的情況?”,那人擡頭看着我們。
“有啊,跟這有什麼關係?失敗了很正常嘛,既然是預測那就不能100%打包票能預測正確啊”,我回答道。
“您說的沒錯,不過如果這個失敗是我故意策劃的呢?”
聽他這麼一說,我的心一下懸了起來,“納尼,你乾的?”
“是的,就是我,我先故意給你連續多次小於16的參數,誤導你們,誤以爲後面的參數還是小於16的,然後突然來一個特意構造的大於16的參數,你們果然上鉤了,預測失敗,提前執行了一些本不該執行的指令。”
“那又如何呢?我們只是把後面需要的數據提前準備到了緩存中,並沒有進一步做什麼啊”,我還是不太明白。
“這就夠了!”
“你小子都被捆上了,就別吊胃口了,一次把話說清楚”,一旁急性子的老K忍不住了。
“好好好,我這就交代。你們把數據提前準備到了緩存中,我後面去訪問這部分數據的時候,發現比訪問其他內存快了很多”
“那可不,我們的緩存技術可不是吹牛的!哎等等,怎麼又扯到緩存上去了?”,老K繼續問道。
那人繼續說道:“如果我想知道某個地址單元內的值,我就以它作爲數組的偏移,去訪問一片內存區域。利用你們會提前預測執行而且會把數據緩存的機制。你們雖然預測失敗了,但對應的那一塊數據已經在緩存中了,接着,我依次去訪問那一片內存,看看誰的訪問時間明顯比其他部分短,那就知道哪一塊被緩存了,再接着反推就能知道作爲偏移的數值是多少了,按照這個思路我可以知道每一個地址單元的內容。”

我們幾個一邊聽着一邊想着,琢磨了好一會兒總算弄清楚了這傢伙的套路,老K氣得火冒三丈,差點就想動手修理那人。
“好你個傢伙,倒是挺聰明的,可惜都不用在正途上!好好的加速優化機制竟然成爲了你們的幫兇”,我心中也有一團火氣。

亡羊補牢
事情的真相總算弄清楚了,我們幾個此刻已經汗流浹背。
經過和安全員的協商,操作系統那邊推出了全新的KPTI技術來解決這個問題,也就是內核頁表隔離。以前的時候,線程執行在用戶態和內核態時用的是同一本地址翻譯手冊,也就是人們說的頁表,通過這本手冊,我們CPU就能通過虛擬地址找到真實的內存頁面。
現在好了,讓線程運行在用戶態和內核態時使用不同的手冊,用戶態線程的手冊中,內核地址空間部分是一片空白,來一招釜底抽薪!
本以爲我們可以回去了,沒想到領導卻給我們出了難題,“這禍是你們闖下的,人家操作系統那邊雖然做了保護,你們是不是也該拿出點辦法來呢,要不然以後我們CPU還怎麼擡得起頭來?”
你有什麼好辦法嗎,幫幫我們吧!

幕後
本文描述的是兩年前爆發的大名鼎鼎的CPU的熔斷與幽靈漏洞。
亂序執行與分支預測是現代處理器普遍採用的優化機制。和傳統軟件漏洞不同,硬件級別的漏洞影響更大更深也更難以修復。
通過判斷內存的訪問速度來獲知是否有被緩存,這類技術有一個專門的術語叫側信道,即通過一些場外信息來分析得出重要結論,進而達成正常途徑無法達成的目的。
後面的文章中此類手法的故事還將繼續上演,敬請期待!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章