首先,我们来介绍一下Spectre攻击,也就是幽灵攻击。本文主要用的Spectre v1是Spectre 漏洞的第一个变种:Bounds Check Bypass(边界检查绕过),它揭示了推测性执行的内在不安全性,展示了如何利用分支预测来任意泄漏受害者地址空间的值。
例如,现在有以下受害者代码,条件语句中的嵌套访问,其中,攻击者可以控制变量 X。
大家可能会认为所有的访问都必须是在范围内的,因为这个条件语句充当边界检查。
但 Spectre 表明,我们可以发送 X 值来训练分支预测器,让它预测 X 的下一个值将在边界内,并预测应该执行分支。
然后,我们发送越界值 X,此时我们进入一种推测执行状态,允许我们接触一些越界的秘密数据,在这里秘密值secret是array1[x]。
然后,秘密值又用来获取了array2[y]。
特别的,要注意,预测时访问过的数据会被提取到缓存中,这里即我们的array2[secret]会保存在缓存中。
我们可能会认为这显然是一个错误的预测,因此一切访问都将被撤销,并且不会提交任何内容。
但 Spectre 表明,即使预测撤销了,仍会存在某些残留。特别是,预测时访问过的数据仍然保留在缓存中。因此,现在我们的缓存中已经有了依赖于秘密的数据即array2[secret]。
此时,攻击者可以通过一种叫 “缓存侧信道攻击” 的方法来确定哪些内存位置最近被访问过。
比如,在这里,攻击者尝试访问内存中 array2的不同元素,假设是 array2[0] 到 array2[15],并测量每个访问的时间。
如果某个元素的访问时间较长,则说明该数据需要从主存中加载(没有被缓存)。
如果某个元素的访问时间很短,则说明该地址的数据在缓存中(已经被加载过)。
从而,假设如果攻击者发现访问array2[5]的速度明显更快,则说明 array2[5] 在缓存中,推测出 secret = 5。
刚刚我们展示了分支预测如何允许我们任意读取受害者地址空间中的内存值。
然而,虽然 Spectre 攻击很强大,但他存在一个关键限制,就是攻击者必须能控制关键变量 X 才能进行攻击。
而事实上,如果关键变量 X 是来自不可信的来源(例如攻击者的输入),防御措施(如污点追踪或代码序列化)会识别并阻止这种不安全的投机执行。
而如果 x 是可信的,即它是由内核生成或控制的,攻击者无法操纵它,攻击自然无法实施。
既然由内核生成或控制的 X 是可信的,那如果我们有一种方法可以间接写入可信的内核变量,即使我们不是特权用户,那我们就可以执行前面所说的攻击了。
事实证明,有另一个漏洞可以让我们做到这一点,称为Row Hammer,中文叫做行锤攻击。
本文就是探讨了当结合Spectre和Row Hammer这两种攻击时会发生什么,以及我们如何利用它来创建更灵活的投机性攻击。
因此,我们先来介绍一下Row Hammer攻击。
此漏洞利用了 DRAM 的性质。DRAM 是一种计算机内存,用于存储数据。DRAM 阵列由许多单元组成,每个单元都使用电容器来存储位值。充满电的电容器存储1,而没电的电容器存储0。
当我们想要访问 DRAM ,以读取或写入数据时,它必须先激活整行数据。激活的过程就是把这行的数据从电容器里“读出来”(放电),然后再立刻“恢复电量”(充电)。
而Rowhammer 会滥用此效果,从而翻转相邻的行中的数据。
具体来说,攻击者疯狂交替访问目标区域相邻的行,由于DRAM 中的每一行数据紧挨在一起,这种“快速充放电”会使得目标区域的电容器受到电磁干扰,从而出现 “位翻转”,即 1 变成 0 或 0 变成 1。
总的来说,Row Hammer攻击就是“敲击”黄色行(攻击行),通过频繁访问这些行,制造干扰,让中间的红色行(受害行)受到影响,最终导致红色行中的数据发生错误(位翻转)。
例如PPT上用红色框出来的两个电容器就是受到干扰使得电荷降至阈值以下,值从1翻转成了0。
因此,现在我们在不直接控制 X 的情况下,也能利用Row Hammer来执行Spectre攻击,即我们的SpecHammer。
假定和之前一样的受害者代码。这次攻击者不控制 X。
小标题里的双重gadget的意思是双重代码逻辑单元,也就是指这里的双重嵌套访问。至于为什么要特别说是双重,后面会有所呼应。
同样的我们假设,如果我们调用这段代码足够多的次数,每次 X 都在条件范围内,以至于分支预测器自然会受到训练,以预测 X 的下一个值也将在边界内,并且应该采用分支。
而假设在某个时刻 X 等于 2。
从这里开始,我们要敲击 x,让它的位翻转一下,变成10,然后指向秘密值。
由于分支预测器已经过训练,我们将进入一种推测执行状态,在这种状态下,一个秘密值将被用来索引另一个数组。进一步得到的值g会被调入缓存。
就像之前一样,我们会在缓存中保存依赖于秘密的数据即array2[secret]。
即使分支预测撤销后,这些数据也会保留在缓存中。接下来,用之前说过的缓存侧信道攻击就能得到secret值。
作者进行了一次概念验证攻击实验,利用SpecHammer来泄露堆栈金丝雀。
虽然攻击者线程无法直接修改 X 的值,但只需要用Row Hammer翻转 X 的一个比特,就能指出这些数组所在堆栈的边界,从而指向金丝雀。
实验证明,目前的方法可以以每秒 8 比特的速度 100% 准确地泄露金丝雀。
然而,由于Row Hammer攻击的物理机制和 DRAM 结构特性,攻击每次只能翻转一位,因此很难任意指向受害者内存中的数据。
比如,假设我们翻转0000001的这个位变成0000101以指向这个值 A。
现在我们想在内存中泄漏一个新值B。由于攻击只能翻转一位,我们首先要做的就是找到一个新的位翻转,因此我们将0000001翻转成0001001,这样指向的B与A相隔了2的幂次的距离(这里是2的2次等于4)。
而接下来想进一步泄露数据时,由于翻转的位权值会更大,因此我们会被推得更远。比如把0000001翻转成0010001指向新值C,中间间隔了更大的距离。
我们每翻转一个新的比特,这些间隔距离就会继续增加。因此,我们无法像 Spectre 那样灵活地任意瞄准内存中我们想要泄漏的任何值。
为了解决这个问题,作者设计了三重gadget。
具体来说,这与我们之前看到的行为类似,只是现在我们有了一个由三个数组而不是两个数组组成的三重嵌套访问。和之前一样,在这个条件语句中,我们无法直接控制 X。
这里的目标是展示我们如何只使用一个Row Hammer位翻转,就能任意地瞄准受害者内存中的任何值。
开始时我们像以前一样,调用这个受害者代码足够多的次数来自然地训练分支预测器来预测 X 将在边界内。
再假设 X 等于 2。现在,我们使用Row Hammer来翻转位并使得x指向越界。
但是,此时不是直接指向秘密,而是指向我们控制的数据。
我们可以将这个值设置为任何我们需要的值,这样当它被用来索引下一个数组时,它就会指向我们想要的目标秘密。
现在,就像之前一样,秘密被用来索引另一个数组,获得array2中的g。
访问的g也就是array2[secret]将被拉入缓存。缓存中的秘密相关数据允许我们通过缓存侧通道攻击进行泄漏。
这样做的好处是,我们不需要翻转位来直接将 X 指向我们的秘密。我们只需要翻转x的一个位,使其指向这个包含我们所控制数据的内存区域。在这个内存区域中,我们可以修改任意我们想要的值,从而允许我们精确地定位我们想要泄漏的地方。
然而,即使这样还是有一个限制,那就是我们需要一个特定的位翻转来使其工作。
例如,现在有一个指向内核空间的 X ,我们想要引起一个位翻转,使得 X 指向我们在用户空间中控制的数据。
假设我们需要翻转第 45 位才能到达我们的数据。
之所以说这是一种限制,是因为 Rowhammer 攻击翻转的位具有随机性。为了找到能够影响第 45 位的内存地址,我们需要快速访问(敲击)大量内存地址,通过测试和筛选,定位那些能够触发第 45 位翻转的目标地址,从而实现翻转。
这叫做内存模板化,是正式开始攻击前的准备操作。
然而,在这个过程中,作者花了很多时间尝试,结果发现只有很少地址能引发位翻转,这使得攻击变得不切实际。
此时,作者注意到现有代码中的一个关键疏忽,它掩盖了内存中实际发生的许多翻转。
具体来说,先前工作的代码首先对受害者内存进行初始化,初始化后的内存为全0。
这将导致数据被拉入缓存。
然后,锤击内存以在 DRAM 中诱导位翻转。
再然后,我们要读取内存来查找翻转。因为初始化时内存为全0,所以如果我们读取到了任何 1,我们就知道有一个位翻转了。
然而,当我们读取内存以检查是否有翻转发生时,实际上读取的并不是内存本身,而是缓存中的初始化数据。缓存中的数据没有被 Row hammer 干扰,因此不会显示翻转,即使实际的内存中已经发生了翻转。这样,我们就错过了很多位翻转的发现。
因此,作者做了一个简单的修改,首先,还是初始化受害者内存为全0,这些数据同时进入缓存。
使用 cache flush 指令将缓存中的数据清除,并将这些初始化数据写回 DRAM。
然后再锤击内存以引发位翻转。
现在,当我们读取内存以检查翻转时,会发生缓存未命中,使得我们读取的是 DRAM 中的数据而不是缓存中的数据,确保我们能发现发生的任何翻转。
最后,作者重新进行测试,比较先前的工作成果和他们修改过的代码。在之前的工作中,作者在两个小时内只发现了 38 次位翻转。而加上缓存刷新后,在两小时内发现了 11,000 多次位翻转。因此,这使得攻击变得可以执行了。
接下来,作者执行了三重小工具概念验证攻击实验。首先,再次用在范围内的X执行此调用足够的次数,以训练分支预测器。
然后,锤击X使其位翻转,指向攻击者控制的数据。
这个攻击者控制的数据会指向我们想要的秘密,然后秘密作为索引访问array2数组,这将导致依赖于秘密的数据被拉入缓存。再用之前所说的缓存侧信道攻击即可间接泄露秘密。
实验的目标是泄漏一个数组边界之外的全局变量字符串。
实验表明,目前的方法能够以 100% 的准确率从这个字符串中泄漏值,在 DDR3 上以每秒 24 位的速率,在 DDR4 上以每分钟 6 位的速率泄漏值。DDR3 和 DDR4 都是 DRAM 的一种类型,分别是第三代和第四代双倍数据速率内存。
现在,有最后一个问题,如何找到我们上面所需的代码?
作者使用了一款早期的gadget搜索工具,叫做 Smatch,它是一款专门用于查找 Linux 内核安全漏洞的工具。此工具通过查找存在嵌套区域访问的代码片段来查找我们需要的gadget,其中非特权用户可以控制值 X。
作者对这一工具进行了扩展,以查找非特权用户可控制 X 的三重gadget,以及非特权用户无法控制 a 的双重gadget和三重gadget。
通过查找,该工具报告了100 个非特权用户可控制 X 的双重gadget和两个三重gadget,当取消攻击者必须控制变量 的限制时,这个数字增加到 20,000 个双重gadget和 170 个三重gadget。
最后总结一下,这篇文章放宽了 Spectre V1 的一个关键要求,即攻击者必须控制数组偏移变量X。
通过结合Row hammer 攻击并使用在内核中找到的三重gadget,作者实现了秘密泄漏速率为DDR3上每秒 24 位和DDR4上每分钟 6 位的概念验证攻击。
此外文章还发现,现有的Row hammer工作中存在一个疏忽,它掩盖了 DRAM 中发生的许多位翻转,于是加入缓存刷新改善了这个问题,大大提高了可观察到的Rowhammer翻转率。