欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spectre(幽灵)CPU缓存漏洞原理

程序员文章站 2024-03-19 14:18:10
...

Spectre(幽灵)CPU缓存漏洞原理

偶然看到了一篇这样的推送,但是感觉作者没有说清楚,所以自己琢磨了好一会儿才弄懂,现在写来说说自己的通俗理解,Meltdown(熔断)原理和这个类似,网上有很多的详解,大家可以去看

相关的Meltdown和Spectre的漏洞代码github上已开源,大家可以去读一下源码

https://github.com/Eugnis/spectre-attack

https://github.com/feruxmax/meltdown

https://github.com/gkaindl/meltdown-poc

https://github.com/turbo/KPTI-PoC-Collection

一、前提知识: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

下一篇: 横向越权