SPECTRE.VARIANT1
预测执行的可能利用
Spectre 为 Google 于 2018 年初发现和公布的硬件漏洞 (https://googleprojectzero.blogspot.ca/2018/01/reading-privileged-memory-with-side.htm)。为了在使用不同内存级别的同时提高 CPU 性能,谷歌进行了硬件优化,于是发现了该漏洞。尽管 Spectre 是一个硬件漏洞,但是为了利用它,在该硬件中运行的软件中仍应显示特定的代码模式。Google 公布的原始文件位于 https://spectreattack.com/spectre.pdf 中,说明了利用该漏洞的大量模式,以及一个允许非法访问经其泄露的机密数据的示例程序。该漏洞存在不同的变型;变型 1 在原始文件中有大篇幅的探讨,主要是关于静态检测,因为和其他变型不一样,它似乎不能通过常规方法轻松迁移,并且在源中要求有特定的代码模式检测。变型 1 漏洞允许通过数组边界外读取从受害者机器 RAM 中泄露机密数据。以下简化的代码片段在较高层面概述了此漏洞:
struct array {
unsigned long length;
unsigned char data[];
};
struct array *createArrayOfSize(unsigned int size);
void vulnerable_pattern(unsigned long offset)
{
struct array *arr1 = createArrayOfSize(5); /* small array */
struct array *arr2 = createArrayOfSize(0x500); /* a large array */
unsigned char secret_value;
if (offset < arr1->length) { /* untrusted data controls the branch */
secret_value = arr1->data[offset]; /* out of boundary read in a mispredicted speculative execution */
unsigned char value2 = arr2->data[secret_value * 256]; /* arr2->data[secret_value * 256] will be loaded to the CPU cache */
/* resulting in a measurable side effect */
/* some more code */
} else {
/* code for the else branch */
}
}
- 机密数据驻留于以下数组的边界外内存区域: arr1->data
- 假设这些机密数据(攻击者没有合法访问权限)最近由 CPU 进行了访问,从而使其实际缓存在一个寄存器中
- 也假设“offset”变量位于 CPU 高速缓存中
- 相反地,应该从高速缓存中清除 arr1->length 和 arr2->data 。
- 在第 14 行,控制需要比较 'offset' 和 'arr1->length' 。
- 'offset' 是一个高速缓存命中,而 'arr1->length' 则是一个高速缓存缺失。CPU 请求从下一级高速缓存(或 DRAM)中加载它。
- 通常要花多达几百个 CPU 周期,才能使 'arr1->length' 的值出现在其中一个 CPU 寄存器中。CPU 并不只是等待和浪费这些周期,而是开始预测执行分支。待执行的特定分支由 CPU 分支预测器基于此代码过去的执行情况进行选择。由于此函数大多数常见的合法调用涉及有效的 'offset' 边界值,因此大多数可能的分支将为 true 分支。分支预测器还可以由攻击者进行“训练”,以选择他们想要的分支。为了实现预测执行,如果截至 'arr1->length' 加载到 CPU 寄存器中时,证明分支预测器错误预测分支,则 CPU 将停止执行该分支中的剩余指令,并且仅执行其他的分支。
- 预测执行期间,当值 'arr1->length' 加载到 CPU L1 高速缓存中时,控制会达到第 15 行。
- 恶意选中的值 'offset' 指向机密数据。该数据读取为高速缓存命中,并且立即变得可存储于 'secret_value' 。
- 在第 17 行读取的 'arr2->data[secret_value * 256]' 为高速缓存缺失。它会被请求从下一级高速缓存(或 DRAM)中进行加载。
- 值 'arr1->length' 最终进入 L1 高速缓冲中。CPU 意识到分支预测错误,遂中断其执行,控制改为开始执行 'else' 分支。
- 'arr2->data[secret_value * 256]' 的值进入 L1 高速缓存中。
尽管仍然无数据可供攻击者直接访问,但是作为本次执行的结果,他们会在 CPU 中留下副作用,也即“arr2->data[secret_value * 256]”值已复制到 CPU L1 高速缓存中,而“arr2->data”数组的其他元素仍然仅可从下一级高速缓存或 DRAM 中获取(如果忽略硬件内存操作的额外详细信息)。要度量这种副作用和结束数据泄露,攻击者仅需要逐一读取“arr2->data”的元素(每次读取 256 个,假设他们有一些便利的方法进行该操作),并且测量每次读取要花多长时间(这是可以轻松实现的;请参见定时攻击)。“arr2->data[secret_value * 256]”除外的所有读取将比较缓慢,而“arr2->data[secret_value * 256]”的读取则很快,并且这会透露“secret_data”的值。
通过在访问“arr2->data[secret_value * 256]”前在分支的任意位置中调用特殊的“内部”函数,强制 CPU 等待从下一级高速缓存请求的所有值先实际进入其寄存器中,可以消除该漏洞。英特尔编译器提供了函数“_mm_lfence()”以在英特尔架构中实现此目的。
代码示例
code.c:17:9: [SPECTRE.VARIANT1] 潜在不受信任的数据 offset 可用于在预测分支执行中于第 15 行处读取超出数组 arr1->data 边界的任意数据。
struct array {
unsigned long length;
unsigned char data[];
};
struct array *createArrayOfSize(unsigned int size);
void vulnerable_pattern(unsigned long offset)
{
struct array *arr1 = createArrayOfSize(5); /* small array */
struct array *arr2 = createArrayOfSize(0x500); /* a large array */
unsigned char secret_value;
if (offset < arr1->length) { /* untrusted data controls the branch */
_mm_lfence(); /* serialization intrinsic that stops speculative execution */
secret_value = arr1->data[offset]; /* no vulnerability exists so no defect is reported */
unsigned char value2 = arr2->data[secret_value * 256];
/* some more code */
} else {
/* code for the else branch */
}
}
- _mm_lfence
function_name - FENCE
上述记录将由检查器进行解释,函数“function_name”将使 CPU 等待高速缓存先与 RAM 状态进行同步或者以任何其他方式使预测执行中的数据泄露变得不重要。将停止沿该路径进行分析。