266

为了缓解内核或跨进程内存泄露(Spectre攻击),Linux 内核1将使用一个新选项进行编译-mindirect-branch=thunk-extern引入以gcc通过所谓的retpoline执行间接调用。

这似乎是一个新发明的术语,因为谷歌搜索只出现在最近的使用中(通常都是在 2018 年)。

什么是 retpoline,它如何防止最近的内核信息泄露攻击?


1但是,它不是特定于 Linux 的 - 类似或相同的构造似乎被用作其他操作系统缓解策略的一部分。

4

3 回答 3

171

sgbj 在 Google 的 Paul Turner 撰写的评论中提到的文章更详细地解释了以下内容,但我会试一试:

就我目前有限的信息而言,retpoline 是一个返回蹦床,它使用永远不会执行的无限循环来防止 CPU 推测间接跳转的目标。

基本方法可以在Andi Kleen解决此问题的内核分支中看到:

它引入了新__x86.indirect_thunk调用,该调用加载调用目标,其内存地址(我将调用它ADDR)存储在堆栈顶部,并使用RET指令执行跳转。然后使用NOSPEC_JMP/CALL宏调用 thunk 本身,该宏用于替换许多(如果不是全部)间接调用和跳转。如果需要,宏只是将调用目标放在堆栈上并正确设置返回地址(注意非线性控制流):

.macro NOSPEC_CALL target
    jmp     1221f            /* jumps to the end of the macro */
1222:
    push    \target          /* pushes ADDR to the stack */
    jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
    call    1222b            /* pushes the return address to the stack */
.endm

最后的放置call是必要的,以便在间接调用完成时,控制流在使用NOSPEC_CALL宏之后继续,因此可以使用它来代替常规call

thunk 本身如下所示:

    call retpoline_call_target
2:
    lfence /* stop speculation */
    jmp 2b
retpoline_call_target:
    lea 8(%rsp), %rsp 
    ret

控制流在这里可能会有点混乱,所以让我澄清一下:

  • call将当前指令指针(标签 2)压入堆栈。
  • lea将 8 添加到堆栈指针,有效地丢弃最近推送的四字,这是最后一个返回地址(到标签 2)。此后,栈顶再次指向真正的返回地址 ADDR。
  • ret跳转到*ADDR并将堆栈指针重置为调用堆栈的开头。

最后,这整个行为实际上相当于直接跳转到*ADDR. 我们得到的一个好处是,用于返回语句的分支预测器(Return Stack Buffer,RSB)在执行call指令时,假设相应的ret语句将跳转到标签 2。

标签 2 之后的部分实际上永远不会被执行,它只是一个无限循环,理论上会用指令填充指令管道JMP。通过使用LFENCEPAUSE或更一般地,导致指令流水线停止的指令可以阻止 CPU 在这种推测性执行上浪费任何功率和时间。这是因为如果对 retpoline_call_target 的调用正常返回,LFENCE则将是下一条要执行的指令。这也是分支预测器将根据原始返回地址(标签 2)预测的内容

引用英特尔的架构手册:

LFENCE 之后的指令可能会在 LFENCE 之前从内存中取出,但在 LFENCE 完成之前它们不会执行。

但是请注意,规范从未提到 LFENCE 和 PAUSE 会导致管道停止,所以我在这里读了几句。

现在回到你原来的问题:内核内存信息泄露是可能的,因为结合了两个想法:

  • 即使在推测错误时推测执行应该没有副作用,推测执行仍然会影响缓存层次结构。这意味着当推测性地执行内存加载时,它可能仍然导致缓存行被驱逐。可以通过仔细测量映射到同一缓存集的内存的访问时间来识别缓存层次结构中的这种变化。
    当内存读取的源地址本身是从内核内存中读取时,您甚至可以泄漏一些任意内存。

  • Intel CPU 的间接分支预测器只使用源指令的最低 12 位,因此很容易用用户控制的内存地址毒化所有 2^12 可能的预测历史。然后,当在内核中预测到间接跳转时,可以使用内核权限推测性地执行这些操作。使用缓存定时侧通道,您可以因此泄漏任意内核内存。

更新:在内核邮件列表中,有一个持续的讨论让我相信 retpolines 不能完全缓解分支预测问题,因为当返回堆栈缓冲区 (RSB) 运行为空时,更新的英特尔架构 (Skylake+) 会退回到易受攻击的分支目标缓冲区(BTB):

Retpoline 作为一种缓解策略,将间接分支换成回报,以避免使用来自 BTB 的预测,因为它们可能被攻击者毒害。Skylake+ 的问题在于 RSB 下溢回退到使用 BTB 预测,这允许攻击者控制推测。

于 2018-01-04T16:25:38.510 回答
50

retpoline旨在防止分支目标注入 ( CVE-2017-5715 ) 漏洞利用。这是一种攻击,其中内核中的间接分支指令用于强制推测执行任意代码块。选择的代码是一个对攻击者有用的“小工具”。例如,可以选择代码,以便通过影响缓存的方式泄漏内核数据。retpoline 通过简单地将所有间接分支指令替换为返回指令来防止这种利用。

我认为 retpoline 的关键只是“ret”部分,它用返回指令替换了间接分支,以便 CPU 使用返回堆栈预测器而不是可利用的分支预测器。如果使用简单的推送和返回指令,那么推测性执行的代码将是函数最终返回的代码,而不是对攻击者有用的小工具。蹦床部分的主要好处似乎是维护返回堆栈,因此当函数确实返回到其调用者时,这是正确预测的。

分支目标注入背后的基本思想很简单。它利用了 CPU 不会在其分支目标缓冲区中记录分支源和目标的完整地址这一事实。因此,攻击者可以在自己的地址空间中使用跳转来填充缓冲区,当在内核地址空间中执行特定的间接跳转时,这将导致预测命中。

请注意,retpoline 不会直接防止内核信息泄露,它只会防止使用间接分支指令来推测性地执行会泄露信息的小工具。如果攻击者可以找到一些其他方法来推测性地执行小工具,则 retpoline 不会阻止攻击。

Paul Kocher、Daniel Genkin、Daniel Gruss、Werner Haas、Mike Hamburg、Moritz Lipp、Stefan Mangard、Thomas Prescher、Michael Schwarz 和 Yuval Yarom的论文Spectre Attacks: Exploiting Speculative Execution概述了如何利用间接分支:

利用间接分支。从面向返回的编程 (ROP) 中提取,在这种方法中,攻击者选择了一个小工具从受害者的地址空间,并影响受害者推测性地执行小工具。与 ROP 不同,攻击者不依赖受害者代码中的漏洞。相反,攻击者训练分支目标缓冲区 (BTB) 以错误预测从间接分支指令到小工具地址的分支,从而导致小工具的推测执行。虽然推测性执行的指令被放弃,但它们对高速缓存的影响不会恢复。小工具可以使用这些效果来泄露敏感信息。我们展示了如何通过仔细选择小工具,使用此方法从受害者那里读取任意内存。

为了误导 BTB,攻击者在受害者的地址空间中找到小工具的虚拟地址,然后执行间接分支到该地址。这种训练是从攻击者的地址空间完成的,不管攻击者地址空间中的小工具地址是什么;所需要的只是用于训练分支的分支使用相同的目标虚拟地址。(其实只要攻击者处理异常,即使攻击者的地址空间中gadget的虚拟地址没有映射到任何代码,攻击也可以进行。)同样不需要源地址完全匹配用于训练的分支的地址和目标分支的地址。因此,攻击者在设置训练方面具有很大的灵活性。

Google 零项目团队的一篇题为“ Reading privileged memory with a side-channel ”的博客文章提供了另一个示例,说明如何使用分支目标注入来创建有效的漏洞利用。

于 2018-01-04T21:55:54.733 回答
10

这个问题是不久前提出的,值得一个更新的答案。

执行摘要

“Retpoline”序列是一种软件结构,它允许将间接分支与推测执行隔离开来。这可以用于保护敏感的二进制文件(例如操作系统或管理程序实现)免受针对其间接分支的分支目标注入攻击。

ret poline ”这个词是 return”和“trampoline”这两个词的组合,就像改进“ rel poline ”是从“relative call”和“trampoline”中创造出来的一样。它是一个使用返回操作构建的蹦床结构,它还象征性地确保任何相关的投机执行将无休止地“反弹”。

为了缓解内核或跨进程内存泄露(Spectre 攻击),Linux 内核[1]将使用一个新选项进行编译,-mindirect-branch=thunk-extern引入 gcc 以通过所谓的 retpoline 执行间接调用。

[1] 但是,它不是特定于 Linux 的——类似或相同的构造似乎被用作其他操作系统缓解策略的一部分。

使用此编译器选项只能防止受影响的处理器中的Spectre V2具有 CVE-2017-5715 所需的微码更新。它可以在任何代码(不仅仅是内核)上“工作”,但只有包含“秘密”的代码才值得攻击。

这似乎是一个新发明的术语,因为谷歌搜索只出现在最近的使用中(通常都是在 2018 年)。

LLVM 编译器2018 年 1 月 4 日之前进行了-mretpoline切换。该日期是该漏洞首次公开报告的日期。GCC于2018 年 1 月 7 日发布了他们的补丁。

CVE 日期表明该漏洞是在 2017 年“发现”的,但它影响了过去二十年制造的一些处理器(因此很可能在很久以前就被发现了)。

什么是 retpoline,它如何防止最近的内核信息泄露攻击?

首先,几个定义:

  • Trampoline - 有时称为间接跳转向量 trampoline 是保存指向中断服务例程、I/O 例程等的地址的内存位置。执行跳转到 trampoline 中,然后立即跳出或弹跳,因此称为 trampoline。GCC 传统上通过在运行时在获取嵌套函数的地址时创建可执行蹦床来支持嵌套函数。这是一小段代码,通常驻留在堆栈中,位于包含函数的堆栈框架中。trampoline 加载静态链寄存器,然后跳转到嵌套函数的真实地址。

  • Thunk - thunk 是用于将附加计算注入另一个子程序的子程序。Thunks 主要用于延迟计算直到需要它的结果,或者在其他子程序的开头或结尾插入操作

  • 记忆- 记忆功能“记住”与某些特定输入集相对应的结果。具有记住输入的后续调用返回记住的结果而不是重新计算它,从而消除了使用给定参数调用的主要成本,除了第一次调用具有这些参数的函数之外。

非常粗略地说,retpoline是一个以thunk形式返回蹦床,以“破坏”间接分支预测器中的记忆。

来源: retpoline 包含一条针对 Intel 的 PAUSE 指令,但对于 AMD 而言,一条 LFENCE 指令是必要的,因为在该处理器上,PAUSE 指令不是序列化指令,因此暂停/jmp 循环将使用过多的功率,因为​​它被推测为等待返回错误地预测到正确的目标。

Arstechnica对这个问题有一个简单的解释:

“每个处理器都有一个架构行为(描述指令如何工作以及程序员依赖于编写程序的记录行为)和一个微架构行为(架构的实际实现行为方式)。这些可能会以微妙的方式出现差异。例如,在架构上,从内存中的特定地址加载值的程序将等待直到知道地址后再尝试执行加载。然而,在微架构上,处理器可能会尝试推测性地猜测地址以便它可以启动即使在绝对确定应该使用哪个地址之前,从内存中加载值(这很慢)。

如果处理器猜错了,它将忽略猜测的值并再次执行加载,这次使用正确的地址。因此保留了架构定义的行为。但是这种错误的猜测会干扰处理器的其他部分——尤其是缓存的内容。这些微架构干扰可以通过计时访问应该(或不应该)在缓存中的数据所花费的时间来检测和测量,从而允许恶意程序对存储在内存中的值进行推断。”。

来自英特尔的论文:“ Retpoline:A Branch Target Injection Mitigation ”( . PDF):

“retpoline 序列可防止处理器的推测执行使用“间接分支预测器”(一种预测程序流的方式)推测由漏洞控制的地址(满足分支目标注入的五个元素中的元素 4(Spectre 变体 2) )利用上面列出的组合)。”。

请注意,元素 4 是:“漏洞利用必须成功地影响这个间接分支以推测性地错误预测并执行小工具。漏洞利用选择的这个小工具通过侧通道泄漏秘密数据,通常是通过缓存定时。”。

于 2018-11-22T16:57:03.473 回答