章节大纲

  • 本任务的目标是利用侧信道从一个函数中提取其使用的一个秘密值。假设存在一个函数(我们称其为受害者函数),它使用秘密值作为索引来获取数组中的某些值。并且假设该秘密值不能从外部访问。我们的目标是通过侧信道获取这个秘密值。我们将使用的技术称为 FLUSH+RELOAD(下图说明了此技术,包括三个步骤):
    • FLUSH:将整个数组从缓存中清除,以确保数组没有被缓存。
    • 调用受害者函数,该函数根据秘密值访问数组中的一个元素。这将导致对应数组元素被缓存。
    • RELOAD:重新加载整个数组并测量重新加载每个元素所需的时间。如果某个特定元素的加载时间比较快,则很可能这个元素已经存在于缓存中。这个元素必定是受害者函数所访问的那个元素,因此我们就可以确定秘密值是什么。
    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)。