什么是 retpoline,它是如何工作的? [英] What is a retpoline and how does it work?

查看:37
本文介绍了什么是 retpoline,它是如何工作的?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

In order to mitigate against kernel or cross-process memory disclosure (the Spectre attack), the Linux kernel1 will be compiled with a new option, -mindirect-branch=thunk-extern introduced to gcc to perform indirect calls through a so-called retpoline.

这似乎是一个新发明的术语,因为 Google 搜索只是最近才出现的(通常都在 2018 年).

This appears to be a newly invented term as a Google search turns up only very recent use (generally all in 2018).

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

What is a retpoline and how does it prevent the recent kernel information disclosure attacks?

1 它不是 Linux 特定的,但是 - 类似或相同的结构似乎被用作 缓解策略在其他操作系统上.

1 It's not Linux specific, however - similar or identical construct seems to be used as part of the mitigation strategies on other OSes.

推荐答案

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

The article mentioned by sgbj in the comments written by Google's Paul Turner explains the following in much more detail, but I'll give it a shot:

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

As far as I can piece this together from the limited information at the moment, a retpoline is a return trampoline that uses an infinite loop that is never executed to prevent the CPU from speculating on the target of an indirect jump.

基本方法见Andi Kleen 的内核分支 解决了这个问题:

The basic approach can be seen in Andi Kleen's kernel branch addressing this issue:

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

It introduces the new __x86.indirect_thunk call that loads the call target whose memory address (which I'll call ADDR) is stored on top of the stack and executes the jump using a the RET instruction. The thunk itself is then called using the NOSPEC_JMP/CALL macro, which was used to replace many (if not all) indirect calls and jumps. The macro simply places the call target on the stack and sets the return address correctly, if necessary (note the non-linear control flow):

.macro NOSPEC_CALL target
    jmp     1221f            /* jumps to the end of the macro */
1222:
    push    	arget          /* 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

The placement of call in the end is necessary so that when the indirect call is finished, the control flow continues behind the use of the NOSPEC_CALL macro, so it can be used in place of a regular call

thunk 本身如下所示:

The thunk itself looks as follows:

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

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

The control flow can get a bit confusing here, so let me clarify:

  • call 将当前指令指针(标签 2)压入堆栈.
  • lea 将 8 添加到 堆栈指针,有效地丢弃了最近推送的四字,它是最后一个返回地址(到标签 2).之后,栈顶再次指向真正的返回地址ADDR.
  • ret 跳转到 *ADDR 并将堆栈指针重置到调用堆栈的开头.
  • call pushes the current instruction pointer (label 2) to the stack.
  • lea adds 8 to the stack pointer, effectively discarding the most recently pushed quadword, which is the last return address (to label 2). After this, the top of the stack points at the real return address ADDR again.
  • ret jumps to *ADDR and resets the stack pointer to the beginning of the call stack.

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

In the end, this whole behaviour is practically equivalent to jumping directly to *ADDR. The one benefit we get is that the branch predictor used for return statements (Return Stack Buffer, RSB), when executing the call instruction, assumes that the corresponding ret statement will jump to the label 2.

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

The part after the label 2 actually never gets executed, it's simply an infinite loop that would in theory fill the instruction pipeline with JMP instructions. By using LFENCE,PAUSE or more generally an instruction causing the instruction pipeline to be stall stops the CPU from wasting any power and time on this speculative execution. This is because in case the call to retpoline_call_target would return normally, the LFENCE would be the next instruction to be executed. This is also what the branch predictor will predict based on the original return address (the label 2)

引用英特尔的架构手册:

To quote from Intel's architecture manual:

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

Instructions following an LFENCE may be fetched from memory before the LFENCE, but they will not execute until the LFENCE completes.

但是请注意,规范从未提到 LFENCE 和 PAUSE 会导致管道停顿,所以我在这里阅读了一些内容.

Note however that the specification never mentions that LFENCE and PAUSE cause the pipeline to stall, so I'm reading a bit between the lines here.

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

Now back to your original question: The kernel memory information disclosure is possible because of the combination of two ideas:

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

  • Even though speculative execution should be side-effect free when the speculation was wrong, speculative execution still affects the cache hierarchy. This means that when a memory load is executed speculatively, it may still have caused a cache line to be evicted. This change in the cache hierarchy can be identified by carefully measuring the access time to memory that is mapped onto the same cache set.
    You can even leak some bits of arbitrary memory when the source address of the memory read was itself read from kernel memory.

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

The indirect branch predictor of Intel CPUs only uses the lowermost 12 bits of the source instruction, thus it is easy to poison all 2^12 possible prediction histories with user-controlled memory addresses. These can then, when the indirect jump is predicted within the kernel, be speculatively executed with kernel privileges. Using the cache-timing side-channel, you can thus leak arbitrary kernel memory.

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

UPDATE: On the kernel mailing list, there is an ongoing discussion that leads me to believe retpolines don't fully mitigate the branch prediction issues, as when the Return Stack Buffer (RSB) runs empty, more recent Intel architectures (Skylake+) fall back to the vulnerable Branch Target Buffer (BTB):

作为缓解策略的 Retpoline 交换间接分支以获得回报,避免使用来自 BTB 的预测,因为它们可以被攻击者毒死.Skylake+ 的问题是 RSB 下溢回落到使用BTB 预测,允许攻击者控制推测.

Retpoline as a mitigation strategy swaps indirect branches for returns, to avoid using predictions which come from the BTB, as they can be poisoned by an attacker. The problem with Skylake+ is that an RSB underflow falls back to using a BTB prediction, which allows the attacker to take control of speculation.

这篇关于什么是 retpoline,它是如何工作的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆