Spectre(幽灵)CPU缓存漏洞原理
Spectre(幽灵)CPU缓存漏洞原理
偶然看到了一篇这样的推送,但是感觉作者没有说清楚,所以自己琢磨了好一会儿才弄懂,现在写来说说自己的通俗理解,Meltdown(熔断)原理和这个类似,网上有很多的详解,大家可以去看
相关的Meltdown和Spectre的漏洞代码github上已开源,大家可以去读一下源码
https://github.com/Eugnis/spectre-attack
https://github.com/feruxmax/meltdown
一、前提知识:CPU的缓存
我们知道,CPU的速度提升发展非常快,而且又发展出多核的CPU技术,但是从内存中读取指令的速度远远小于CPU的执行指令速度,为了提高CPU执行指令的速度,在CPU的内部加上了缓存,来存放内存中某一块有很大概率在下次要被执行指令,缓存也分为一级(L1)、二级缓存(L2)(CPU每个核都有)、三级缓存(L3)(多核共享)。
二、CPU的分支预测执行
如果在一段程序中有这样一段代码
int judge = 114514;
void foo(int x) {
if (x < judge) { // judge放在内存中
/*要执行的代码段*/
}
}
int main() {
for (int i = 0; i < 99; i++) {
foo(1); // 执行100次这样的函数,每次都传入1,小于judge
}
foo(11451444); //此时传入的11451444大于了judge
}
judge被放在了内存中,每次foo()函数被调用执行的时候,都要从内存中拿到judge的值和x进行比较,但是从内存中拿到judge的值的过程是很慢的,但是主函数中调取了foo()函数100次,每次都比judge小,所以CPU会对foo()函数的if分支进行预测优化,在下次调用foo()函数时,CPU预测传入的x比judge小,先把要执行的代码做了,并把数据放到缓存中,等到取到judge的时候再把x和judge做判断,如果x确实比judge小,那么CPU就会从缓存中把之前等待取judge时候存好的数据直接拿出来,如果x比judge大,CPU就会丢弃当前的状态,重新恢复到执行if分支前的状态,但是放在缓存中的数据不会删除!!!
二、Spectre(幽灵)原理
参考上面github上Spectre的源代码
// 首先定义了一个secret数据
char *secret = "The secret data";
// array1_size放在内存中
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];
void victim_function(size_t x) {
if (x < array1_size)
temp &= array2[array1[x] * 512];
}
其实array1初始化的数据我们不用管,这里我们用到了一个原理,如果让:
size_t x = (size_t)(secret - (char *)array1);
那么x就是array1地址和secret地址之间的差值,画一个图理解一下:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FcgcHSpA-1589872108577)(C:\Users\asus\Desktop\1.JPG)]
那么x = 0x546789 - 0x123456 = 0x333333,那么这时候通过数组array1去访问secret首地址中的值就可以这样
value_type val = array1[x];
// 那么val的值就是array1的首地址加上偏移量0x333333(也就是0x456789)出的数据
// 当然,这样访问超出了数组array1的大小
// 正常的代码CPU会报异常越界访问并停止,但是通过预测分支的缓存机制可以做到这一点!
所以,如果我们想要访问到地址0x555555中的值(假如他是),那么就可以这样:
size_t visit = 0x555555 - array1;
// 调用victim_function
victim_function((int)visit);
现在,看一下Spectre的过程
-
先训练CPU得到对victim_function的分支预测处理,比如执行10次,每次让index的值小于array1_size
for (int i = 0; i < 9; i++) { victim_function(5); }
10次之后,victim_function()会训练出一个分支预测出来,这时候再传入一个我们想要获取的内核地址中的值,比如说上述的0x555555:
// 那么传入的x就应该是 size_t x = 0x555555 - array1; // x = 4320ff victim_function(x);
-
现在看一下victim_function()函数中发生了什么
void victim_function(size_t x) { // 此时x就是内核地址0x555555相对于array1的偏移量 if (x < array1_size) temp &= array2[array1[x] * 512]; }
由于分支预测的功能,再写入缓存中的时候,虽然发生了array1的越界访问,但是由于数据没有写到内存,有异常但不会停止,这时候
-
CPU先从array1[x]处拿到数据(也就是内核地址0x555555处),假设里面的数据是10
-
与512相乘,得到array2的下标
10 * 512 = 5120
-
然后从内存中把array2[5120]的数据放入到缓存中,执行和变量temp的&(与)操作
-
这时候,array1_size的值送过来了,CPU将它和传入的x做一下比较,发现x比array1_size的值大,丢弃CPU之前等待array1_size传入的时候所做的操作,回到if分支处的状态,但是,array2[5120]的值已经被放在了CPU的缓存中,暂时不会丢弃。
-
-
获取内核地址0x555555处的值
-
紧接着上一步,这时候我们马*问一下array2所在内存的那一片区域,看看array2的哪一个数据我们访问到的时间最短,由于array2[5120]的数据被放在了缓存中,array2[5120]获取数据的时间最短,所以我们马上可以反推出0x555555处的值,即
5120 / 512 = 10;
那么这个就是内存0x555555出的值了。
-
这里只是说了一下Spectre的大致原理,更加详细的内容可以去阅读一下源代码。
上一篇: GD32VF103_DAC
下一篇: 横向越权