6

[原创]浅谈侧信道时序攻击【附Demo】

bxc 2018-1-12 22:25 960
最近快被Meltdown与Spectre这两个CPU漏洞刷了屏了,于是有空自己写了个demo玩玩。
虽然没能成功演示上面的两个漏洞,不过大概搞清楚了利用原理。
Meltdown与Spectre 成功获取受保护数据的方式,目前据我所知,好像都是基于侧信道时序攻击的。
不同的是 Meltdown是利用CPU的乱序执行泄漏数据,而Spectre是利用分支预测泄漏数据。
泄露的手段就是读取一个byte,然后以byte为数组索引,访问某个大数组的元素。
然后逐个测试各个元素的访问速度,之前访问过的元素,如果还在缓存中,访问起来就要快不少。
废话不多说,上代码:
#include <intrin.h>
#include <stdio.h>
#include <Windows.h>

#define SHIFT_NUMBER          0x0C
#define PAGE_SIZE             4096
#define BLOCK_SIZE            (1 << SHIFT_NUMBER)


void CacheLineFlush(LPBYTE lpArray, UINT index) {
  _mm_clflush(&(lpArray[index << SHIFT_NUMBER]));
}

void CacheLineFlush_all(LPBYTE lpArray) {
  for (UINT i = 0; i < 256; i++) CacheLineFlush(lpArray, i);
}

/************************************************************************/
/* 统计各个块的访问速度,并返回最快的那个块的索引                       */
/************************************************************************/
BYTE GetAccessByte(LPBYTE lpArray) {
  UINT64 speed[256];
  UINT64 start, min;
  UINT index, junk;
  BYTE result;

  //为min赋初始值
  min = 0;

  //测试访问速度
  for (int i = 0; i < 256; i++) {
    //获取array[index]的地址
    index = i << SHIFT_NUMBER;
    //mfence指令用于序列化内存访问,即让乱序执行无效化。
    //后面的指令必须在前面的内存读写完成后再开始发射执行。
    _mm_mfence();
    //记录开始周期
    start = __rdtscp(&junk);
    junk = *(LPDWORD)(&lpArray[index]);
    _mm_mfence();
    speed[i] = __rdtscp(&junk) - start;

    //如果是初始值,或者比当前值还小
    if ((min == 0) || (speed[i] < min)) {
      min = speed[i];
      result = (BYTE)i;
    }
  }

  return result;
}

/************************************************************************/
/* 用index作为数组索引,访问array的某个元素                             */
/************************************************************************/
BYTE AccessArray(LPBYTE lpArray, UINT index) {
  return lpArray[index << SHIFT_NUMBER];
}


int main()
{
  //假定的kernel内存
  BYTE kernel[4];
  //array是用户可控制的内存
  LPBYTE array;

  kernel[0] = 0x55;
  kernel[1] = 0xAA;
  kernel[2] = 0xF0;
  kernel[3] = 0x0F;

  array = (LPBYTE)_aligned_malloc(256 * BLOCK_SIZE, PAGE_SIZE);

  /*
  实现原理:
    kernel假定是受保护的内存数据 (本demo里可访问)。
    array为用户可控制的一个数组,一共分为256个块,每个块的大小为2的整数倍: (1 << SHIFT_NUMBER)。
    然后从kernel中读取一个byte,以这个byte为索引,去访问array所对应的块。
    之后立刻循环读取一遍array的各个块,如果之前访问成功了,那么对应的块应该还在缓存中,对应的访问时间要少很多。
    统计各个块的访问周期数,最快的块,他的索引就是受保护的那个byte。

    CacheLineFlush_all 函数用于把整个数组从缓存中清除出去,这样不至于污染访问速度。
    AccessArray 函数用于以一个索引去访问一个数组。
    GetAccessByte 函数用于测试数组的各个块的访问速度,返回最快的那个块的索引。
  */


  CacheLineFlush_all(array);
  AccessArray(array, kernel[0]);
  printf("Access fastest: 0x%02X\n", (DWORD)GetAccessByte(array));

  CacheLineFlush_all(array);
  AccessArray(array, kernel[1]);
  printf("Access fastest: 0x%02X\n", (DWORD)GetAccessByte(array));

  CacheLineFlush_all(array);
  AccessArray(array, kernel[2]);
  printf("Access fastest: 0x%02X\n", (DWORD)GetAccessByte(array));

  CacheLineFlush_all(array);
  AccessArray(array, kernel[3]);
  printf("Access fastest: 0x%02X\n", (DWORD)GetAccessByte(array));

  getchar();

  return 0;
}



以上代码,目前只有编译为Debug版本,可以成功演示,Release版本估计把不少关键点给优化掉了。
上传的附件:
最新回复 (4)
2
Keoyo 2018-1-13 12:40
3
收藏,感谢分享!
tsoo 6天前
4

"然后从kernel中读取一个byte,以这个byte为索引,去访问array所对应的块。

之后立刻循环读取一遍array的各个块,如果之前访问成功了,那么对应的块应该还在缓存中,对应的访问时间要少很多。"


你这个例子中的kernel是R3地址, 这个逻辑能执行通.


但如果kernel真的是一个kernel地址 , 比如 BYTE* kernel = 0xfffff80004888f30 ,  在执行kernel[0]  , kernel[1] 的时候就异常了,  后面的"立刻循环读取一遍array..." 也就执行不了.


我试过在执行kernel[0]的时候加_try,   但这样就不能重现了.

6
bxc 5天前
5
tsoo "然后从kernel中读取一个byte,以这个byte为索引,去访问array所对应的块。之后立刻循环读取一遍array的各个块,如果之前访问成功了,那么对应的块应该还在缓存中,对应 ...
所以说,本文只是验证侧信道时序攻击,不验证Meltdown与Spectre漏洞。
只是来打酱油 4天前
6
Access  fastest:  0x04
Access  fastest:  0x5B
Access  fastest:  0xF0
Access  fastest:  0x0F
返回