2018年1月3日,Google Project Zero和其他项目
揭示了影响推测执行处理器的一类新漏洞
的前三个。 它们分别称为
Spectre (1和2)和
Meltdown 。 使用
推测性 CPU
执行机制,攻击者可以暂时绕过显式和隐式软件安全检查,以防止程序读取内存中不可访问的数据。 尽管推测性执行被设计为微体系结构的一部分,但在体系结构级别上是不可见的,但精心设计的程序可以读取推测性块中不可访问的信息,并通过辅助渠道(例如程序片段的执行时间)将其显示出来。
当显示使用JavaScript可以进行Spectre攻击时,V8团队参加了该问题的解决。 我们成立了应急小组,并与其他Google团队,其他浏览器合作伙伴和硬件合作伙伴紧密合作。 我们与他们一起积极地进行了进攻性研究(构造攻击模块以证明这一概念)和防御性研究(减轻潜在的攻击)。
幽灵攻击包括两部分:
- 否则将无法访问的数据泄漏到CPU的潜在状态中 。 所有已知的Spectre攻击都使用推测将无法访问的数据传输到CPU缓存。
- 检索隐藏状态以还原无法访问的数据。 为此,攻击者需要一块足够准确的手表。 (出人意料的低准确性,尤其是使用诸如边缘阈值化的方法-与沿选定轮廓线的阈值进行比较)。
从理论上讲,足以阻止攻击的两个组成部分。 由于我们不知道如何完全阻止它们中的任何一个,因此我们开发并部署了缓解措施,可以显着减少泄漏到CPU缓存中的信息量,以及缓解措施,这些措施使恢复隐藏状态变得困难。
高精度计时器
在投机执行之后仍然存在的微小状态变化会产生相应的微小的,几乎不可能的微小的时间差异-大约十亿分之一秒。 为了直接检测个体的这种差异,攻击者需要一个高精度计时器。 处理器提供此类计时器,但Web平台未设置它们。
performance.now()
Web平台上最准确的计时器的分辨率为几微秒,最初被认为不适合此目的。 但是,两年前,专门研究微体系结构攻击的研究小组在Web平台上发表
了有关计时器
的文章 。 他们得出结论,同时可变的共享内存和各种分辨率恢复方法可以创建分辨率更高的计时器(低至纳秒级)。 这样的计时器足够准确以检测L1高速缓存的个别命中和未命中。 通常是在Spectre攻击中捕获信息的人是他。
计时器保护
为了破坏及时发现细微差别的能力,浏览器开发人员选择了一种多边方法。 在所有浏览器中,
performance.now()
的分辨率都降低了(在Chrome中从5微秒降低到100微秒),并且引入了随机抖动来防止分辨率恢复。 在所有浏览器的开发人员进行协商之后,我们共同决定采取前所未有的措施:立即并追溯禁用所有浏览器中的
SharedArrayBuffer
API,以防止创建纳秒计时器。
增益
在我们的进攻研究开始时,很明显仅靠计时器缓解是不够的。 原因之一是攻击者可以简单地重复运行其代码,从而使累积时间差远远大于一次命中或缓存未命中。 我们能够构建可靠的“小工具”,一次使用许多缓存行,直到整个缓存容量为止,这提供了高达600微秒的时间差。 后来,我们发现了不受缓存容量限制的任意放大方法。 这样的放大方法基于重复尝试读取秘密数据。
准时保护
为了使用Spectre读取无法访问的数据,攻击者迫使CPU推测性地执行读取通常无法访问的数据并将其放入缓存的代码。 保护可以从两个方面考虑:
- 防止推测性代码执行。
- 防止从推测管道中读取无法访问的数据。
我们通过插入推荐的指令以防止推测进行了第一个选项的试验,例如每个关键条件分支的Intel
LFENCE
并将
retpolins用于间接分支。 不幸的是,如此严重的缓解措施显着降低了生产率(Octane基准速度降低了2-3倍)。 相反,我们采用了第二种方法,即插入缓解序列,以防止由于不正确的推测而导致读取敏感数据。 让我用以下代码片段说明该技术:
if (condition) { return a[i]; }
为简单起见,我们假设条件
0
或
1
。 如果在
i
超出范围时CPU推测性地从
a[i]
读取数据,则上述代码容易受到攻击,从而可以访问通常无法访问的数据。 一个重要的观察结果是,在这种情况下,当条件为
0
时,推测会尝试读取
a[i]
。 我们的缓解措施会重写此程序,以使其行为与原始程序完全相同,但不允许任何推测加载的数据泄漏。
我们保留一个CPU寄存器(称为“毒药”),以跟踪代码是否在错误解释的分支中执行。 在所有分支和生成的代码调用中都支持带毒寄存器,因此,任何错误解释的分支都会导致带毒寄存器变为
0
。 然后,我们测量所有内存访问,以使它们无条件地使用有毒寄存器的当前值屏蔽所有下载的结果。 这不会阻止处理器预测(或错误解释)分支,但是由于分支解释不正确,它会破坏已加载值的信息(可能超出限制)。 工具代码如下所示(
a
是数字数组)。
let poison = 1;
附加代码不会影响程序的正常行为(由体系结构定义)。 仅当在具有推测执行的CPU上工作时,它才会影响微体系结构状态。 如果您在源代码级别对程序进行检测,则现代编译器中的高级优化可以删除此类检测。 在V8中,我们通过在编译的最后阶段插入缓解措施来防止编译器删除缓解措施。
我们还使用这种中毒技术来防止解释程序的字节码循环和JavaScript函数调用序列中的间接分支泄漏。 在解释器中,如果字节码处理程序(即解释一个字节码的机器代码序列)与当前字节码不匹配,则将毒药设置为
0
。 对于JavaScript调用,我们将目标函数作为参数传递给寄存器(在寄存器中),如果传入的目标函数与当前函数不匹配,则在每个函数的开头将中毒设置为
0
。 随着这种疲软,我们发现辛烷值基准指标的降幅不到20%。
WebAssembly的缓解措施更为简单,因为主要的安全检查是确保内存访问在边界之内。 对于32位平台,除了通常的边界检查之外,我们还将所有内存填充为2的下一个幂,并无条件地屏蔽用户内存索引的任何高位。 64位平台不需要这种缓解措施,因为该实现将虚拟内存保护用于边界检查。 我们尝试将switch / case语句编译为二进制搜索代码,而不是使用可能会受到攻击的间接分支,但是对于某些工作负载而言,这样做太昂贵了。 间接调用受retpolins保护。
软件保护-不可靠
幸运的是,我们的进攻性研究进展快于防守性,并且我们很快发现不可能以编程方式减轻幽灵攻击期间所有可能的泄漏。 这有几个原因。 首先,与幽灵作斗争的工程努力与威胁程度不成比例。 在V8中,我们面临许多其他更严重的安全风险,包括由于常见错误(比Spectre更快和更容易)而在边界外直接读取,在边界之外写入(对于Spectre或更糟,这是不可能的)以及潜在的远程威胁。代码执行(Spectre无法实现,甚至更糟)。 其次,我们开发和实施的日益复杂的缓解措施带来了极大的复杂性,这是一项技术义务,实际上会增加攻击面和性能开销。 第三,测试和保持缓解微体系结构泄漏的风险甚至比为攻击设计小工具本身还要困难,因为很难确定缓解措施是否能够按照设计的方式继续发挥作用。 至少一次,以后的编译器优化有效地撤销了重要的缓解措施。 第四,我们发现即使在我们的Apple合作伙伴为他们的JIT编译器解决该问题做出了巨大努力之后,也无法在软件中有效缓解某些Spectre选项,尤其是选项4。
现场隔离
我们的研究得出结论:原则上,不受信任的代码可以使用Spectre和辅助通道读取进程的整个地址空间。 软件缓解措施会降低许多潜在小工具的有效性,但并不有效或不全面。 唯一有效的措施是将敏感数据移到进程地址空间之外。 幸运的是,Chrome多年来一直在尝试将站点划分为不同的进程,以减少由于常见漏洞而造成的攻击面。 这些投资获得了回报,到2018年5月,我们进入准备阶段,并在最大数量的平台上扩展了
站点的
隔离 。 因此,Chrome安全模型在渲染过程中不再承担语言隐私权。
Spectre已经走了很长一段路,并强调了行业和学术界中开发人员协作的优点。 到目前为止,白帽子领先于黑帽子。 除了好奇的实验者和专业研究人员开发小工具来证明这一概念外,我们仍然不知道真正的一次袭击。 这些漏洞的新变种继续出现,并且将持续一段时间。 我们继续监视这些威胁,并予以认真对待。
像许多程序员一样,我们也认为安全的语言为抽象提供了正确的边界,从而防止了类型良好的程序读取任意内存。 遗憾的是,事实证明这是一个错误-该保证与当今的设备不符。 当然,我们仍然相信安全的语言具有更多的工程优势,并且未来取决于他们,但是...在当今的设备上,它们泄漏得很少。
有兴趣的读者可以在我们的
科学文章中更深入地研究该主题并获得更详细的信息。