【问题标题】:What is a retpoline and how does it work?什么是 retpoline,它是如何工作的?
【发布时间】:2018-06-13 20:08:00
【问题描述】:

为了缓解内核或跨进程内存泄露(Spectre 攻击),the Linux kernel1 will be compiled with a new option-mindirect-branch=thunk-externgcc 引入通过所谓的 retpoline 执行间接调用em>。

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

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


1 但是,它不是 Linux 特定的 - 类似或相同的结构似乎被用作其他操作系统上 mitigation strategies 的一部分。

【问题讨论】:

  • 来自 Google 的有趣 support article
  • 哦,读作 /ˌtræmpəˈlin/(美式)或 /ˈtræmpəˌliːn/(英式)
  • 您可能会提到这是 Linux 内核,尽管gcc 指出了这一点!我没有认出 Linux 内核邮件列表站点上的 lkml.org/lkml/2018/1/3/780,甚至在我查看那里时都认不出来(因为它处于脱机状态,所以提供了一个快照)。
  • @PJTraill - 添加了 Linux 内核标签
  • @PJTraill - 好点,我更新了问题文本。请注意,我首先在 Linux 内核中看到了它,因为它是相对开放的开发过程,但毫无疑问,相同或相似的技术正被用作各种开源和闭源操作系统的缓解措施。所以我不认为这是特定于 Linux 的,但链接肯定是。

标签: security assembly x86 cpu-architecture spectre


【解决方案1】:

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

据我目前有限的信息来看,retpoline 是一个 return trampoline,它使用永远不会执行的无限循环来防止 CPU 推测目标间接跳转。

基本方法可见Andi Kleen's kernel branch解决这个问题:

它引入了新的__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 可能的预测历史。然后,当在内核中预测到间接跳转时,可以使用内核权限推测性地执行这些操作。使用缓存定时侧通道,您可以因此泄漏任意内核内存。

更新:在kernel mailing list 上,正在进行的讨论使我相信 retpolines 不能完全缓解分支预测问题,例如当返回堆栈缓冲区 (RSB) 为空时,最近的英特尔架构 (Skylake+) 回退到易受攻击的分支目标缓冲区 (BTB):

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

【讨论】:

  • 我认为 LFENCE 指令并不重要,Google 的实现使用了 PAUSE 指令。 support.google.com/faqs/answer/7625886 请注意,您引用的文档说“不会执行”而不是“不会被推测执行”。
  • 来自该 Google 常见问题解答页面:“上述推测循环中的暂停指令不是正确性所必需的。但这确实意味着非生产性推测执行在处理器上占用的功能单元较少。”所以它不支持你的结论,即 LFENCE 是这里的关键。
  • @RossRidge 我部分同意,对我来说,这看起来像是无限循环的两种可能实现,提示 CPU 在 PAUSE/LFENCE 之后不会推测性地执行代码。但是,如果 LFENCE 是推测性地执行的,并且因为推测是正确的而没有回滚,这将与仅在内存加载完成后才执行它的说法相矛盾。 (否则,已经被推测执行的整套指令必须回滚并再次执行才能满足规范)
  • 这具有push / ret 的优点,它不会不平衡返回地址预测器堆栈。有一个错误预测(在使用实际返回地址之前转到lfence),但使用call + 修改rsp 平衡了ret
  • 哎呀,优势超过 push / ret(在我的最后一条评论中)。回复:您的编辑:RSB 下溢应该是不可能的,因为 retpoline 包含call。如果内核抢占在那里进行了上下文切换,我们将使用从call 启动的 RSB 恢复执行到调度程序中。但也许中断处理程序可以以足够的rets 结束以清空 RSB。
【解决方案2】:

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

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

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

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

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

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

到 错误训练 BTB,攻击者找到 gadget 的虚拟地址 在受害者的地址空间中,然后执行到此的间接分支 地址。这种训练是从攻击者的地址空间完成的,并且 中的小工具地址中的内容无关紧要 攻击者的地址空间;所需要的只是使用的分支 用于训练分支使用相同的目标虚拟地址。 (在 事实上,只要攻击者处理异常,攻击就可以奏效 即使在小工具的虚拟地址上没有映射代码 在攻击者的地址空间中。)也不需要完整的 用于训练的分支的源地址与 目标分支的地址。因此,攻击者具有显着 灵活设置培训。

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

【讨论】:

    【解决方案3】:

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

    Executive Summary:

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

    单词“retpoline”是单词“return”和“trampoline”的portmanteau,就像改进“relpoline”是从“relative call”和“trampoline”中创造出来的。它是一个使用返回操作构建的蹦床结构,它还象征性地确保任何相关的推测执行将无休止地“反弹”。

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

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

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

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

    LLVM compilerbefore Jan 4 2018 以来有一个-mretpoline 开关。该日期是漏洞为first publically reported 的日期。 GCC made their patches available 2018 年 1 月 7 日。

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

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

    首先,几个定义:

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

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

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

    非常粗略地说,retpolinetrampolinereturnthunk strong>,在间接分支预测器中“spoilmemoization

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

    Arstechnica对问题有简单的解释:

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

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

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

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

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

    【讨论】:

      【解决方案4】:

      “retpoline”在许多不同的处理器上已经存在了很长时间,只是以前从未被称为。这是一个滥用硬件堆栈在大多数机器上工作方式的老把戏。要完全理解它,您需要了解以下汇编编程术语:

      • 程序计数器/指令指针:包含要执行的下一条指令的处理器寄存器。每条指令处理完毕后,其值都会更新。
      • 堆栈:硬件可以通过PUSHing 值访问的数据结构。 POP 操作检索压入堆栈的最后一项并将其存储在所选寄存器中。例如,pop %eax 将从堆栈中取出顶部 4 个字节并将它们存储在寄存器 %eax 中。

      当您调用一个函数时,例如用 C 编写的函数:y = foo(x),处理器会执行一条 CALL 指令到函数 foo 的内存位置。 CALL foo 将通过PUSH 将指令指针的值保存到堆栈中,然后对foo 执行JMP(本质上是GOTO)。这个推送的指令指针称为返回地址。

      foo 的主体将以一条名为 RET 的指令结束。这会从堆栈中取出顶部(无论您的机器指针的大小是多少)字节,并将其直接加载到指令指针中,该指针基本上对该地址执行GOTO。请记住,CPU 无法知道堆栈的顶部字节是否实际上是返回地址!通过压入任意字节序列然后执行RET 指令,您可以使CPU 随处跳转。

      到目前为止还和我在一起吗?好的。我提到过这种技术已经存在了很长时间。它主要由直接用汇编语言编写的程序用于机器实现 switch case 结构,因为许多 8 位和 16 位 CPU 没有办法CALL 任何不是直接嵌入的常量内存地址的东西源代码。高级语言会自动管理堆栈,并且不会对 retpolines 过于友好,因为它们倾向于更改堆栈以存储局部变量并期望它始终处于特定状态。

      碰巧这个老把戏再次变得有用了,但原因与以前完全不同。

      【讨论】:

      • 重要的是要指出,retpoline 只是 push/ret。它首先使用call(并丢弃它的返回地址)以避免不平衡CPU调用/ret预测器:这个ret会故意错误预测,但未来的ret指令仍然可以与它们的调用者匹配。除此之外,是的,它是围绕 push/ret 的一些花哨的东西。
      • 这很好,它通常与“返回表”一起使用(这是在 6502 汇编中从列表中选择函数的常用技巧)
      • 除非“retpoline”这个名字实际上被用来描述涉及 RET 在像 6502 这样的旧 8 位微控制器上进行间接分支的技巧,否则将这个特定名称应用于这些技术是没有帮助的。 “retpoline”的现代用法的定义特征之一是它通过间接分支阻止推测执行。直到 2017 年底,当 Spectre 被揭露时,这才成为一个问题。此外,一个 retpoline 涉及两个 call 指令和一个 ret,而不仅仅是一个推送。你的回答没有解释这一点。 (此外,任何获取代码指针的表查找都是另一回事。)
      • 啊,我一定误解了区别。我不认为“retpoline”曾经被用作描述它的术语,在过去,非常相似的东西被用来进行间接分支,但当时并不完全相同。
      猜你喜欢
      • 2019-12-24
      • 2013-02-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-11-08
      • 2020-08-06
      • 2012-11-06
      • 2018-09-05
      相关资源
      最近更新 更多