Retpoline 一种分支目标注入的缓解措施

前言

继续Meltdown && Spectre安全漏洞的相关分析,本文关注V2 patch中引入的retpoline技术。

一、Retpoline

Retpoline是针对Spectre V2漏洞(CVE-2017-5715)提出的规避方案。

Spectre V2漏洞利用CPU的间接分支预测(indirect branch predictor)功能,通过事先训练分支,让分支预测器去影响受害者进程,然后通过侧信道的方式来获取受害者进程的信息。

其实这个变种2的漏洞利用是非常困难的,Jann Horn的利用其实也是在一个老版本的kvm上,按照 Linus的说法是利用Spectre是”fairly hard”。

目前有两种方案来缓解Spectre漏洞,即硬件方案和软件方案。

  • 硬件方案就是IBRS + IBPB, 直接在硬件层面阻止投机执行 (speculative execution),当然,这会导致性能很低,所以IBRS没有进入内核。
  • 软件方案主要就是retpoline了, 因为 性能影响较低,最终得以进入内核主线。

Retpoline的基本原理

retpoline是 “return” 和 “trampoline”,也就是在间接跳转的时候用return指令添加了一个垫子。

在间接指令跳转时,在原有代码中加入不会被执行到的死循环代码,于时在CPU执行间接跳转指令(JMP/CALL)时,进行预测执行时,只能预取到被加入的死循环代码,不会预取到内核中的其他数据,从而防止了数据泄露,规避掉了Spectre V2。

由于几乎没有添加额外的执行流程(添加的死循环代码永远不会被执行),所以对性能影响比较小,官方给出的数据是:0.5%-2%。

原理比较简单,一些关键点:

  1. 只针对间接跳转指令[2]。即JMP和CALL指令。
  2. 对于C代码,包括内核代码和应用程序代码。需要借助新的编译器选项-mindirect-branch=thunk-extern,通过新的编译器(gcc)来实现死循环代码的插入和跳转。
  3. 对于内核中的汇编代码。由于不能借助编译器完成,则只能自己实现,其原理跟编译器中的类似,如前面所述。本文主要关注这部分内容。

ret指令的预测跟jmp和call不太一样,ret依赖于Return Stack Buffer(RSB)。跟indirect branch predictor不一样的是,RSB是一个 先进后出的stack。当执行call指令时,会push一项,执行ret时,会pop一项,这很容易由软件控制,比如下面的指令系列:

__asm__ __volatile__("       call 1f; pause;"
		     "1:     call 2f; pause;"
		     "2:     call 3f; pause;"
		     "3:     call 4f; pause;"
		     "4:     call 5f; pause;"
		     "5:     call 6f; pause;"

在这里插入图片描述上图显示了retpoline的基本原理,即用一段指令代替之前的简介跳转指令,然后CPU如果投机执行会陷入一个死循环。

下面分析一下jmp间接跳转指令如何被替换成retpoline的指令。
在这里插入图片描述
在这个例子中,jmp通过rax的值进行间接跳转,如果没有retpoline,处理器会去询问indirect branch predictor,如果之前有攻击者去训练过这个分支,会导致CPU执行特定的一个gadget代码。下面看看retpoline是如何阻止CPU投机执行的。

“1: call load_label”这句话把”2: pause ; lfence”的地址压栈,当然也填充了RSB的一项,然后跳到load_label;

“4: mov %rax, (%rsp)”,这里把间接跳转的地址(*%rax)直接放到了栈顶,注意,这个时候内存中的栈顶地址和RSB里面地址不一样了;

如果这个时候ret CPU投机执行了,会使用第一步填充在RSB的地址进行,也就是”2: pause ; lfence”,这里是一个死循环;

最后,CPU发现了内存栈上的返回地址跟RSB自己投机的地址不一样,所以,投机执行会终止,然后跳到*%rax。

下面看看call指令被替换成retpoline的指令之后如何工作。
在这里插入图片描述

  1. 首先从”1: jmp label2”跳到”7: call label0”;
  2. “7: call label0”将”8: … continue execution”的地址压入了内存栈以及RSB中,然后跳到label0;
  3. “2: call label1”将”3: pause ; lfence”的地址压入了内存栈以及RSB中,然后跳到lable1;

这个时候内存栈和RSB如下:
在这里插入图片描述

  1. “5: mov %rax, (%rsp)” 这里把间接跳转的地址(*%rax)直接放到了栈顶,注意,这个时候内存中的栈顶地址和RSB里面地址不一样了;
  2. “6: ret”.如果这个时候ret CPU投机执行了,会使用第3步填充在RSB的地址,”3: pause ; lfence”. 这是一个死循环;
  3. 最后,CPU发现了内存栈上的返回地址跟RSB自己投机的地址不一样,所以,投机执行会终止,然后跳到*%rax;

这个时候内存栈和RSB如下
在这里插入图片描述
当间接地址调用(*%rax)返回的时候,通过RSB和内存中地址继续执行步骤2的压的地址,也就是8那里。

二、部署

由于大部分的间接跳转都是由编译器产生的,所以需要编译器的支持,目前最新的gcc已经支持了-mindirect-branch=thunk选项用于替换间接指令为retpoline系列。下面看看一个简单的例子:

#include <stdio.h>
#include <stdlib.h>

typedef void (*fp)();

void test()
{
	printf("indirect test\n");
}
int main()
{
	fp f = test;
	f();
}

上面是一个典型的间接跳转。

# gcc  -mindirect-branch=thunk  test.c  -o test
# objdump -d test

... 
00000000004004d8 <main>:
4004d8:	55                   	push   %rbp
4004d9:	48 89 e5             	mov    %rsp,%rbp
4004dc:	48 83 ec 10          	sub    $0x10,%rsp
4004e0:	48 c7 45 f8 c7 04 40 	movq   $0x4004c7,-0x8(%rbp)
4004e7:	00 
4004e8:	48 8b 55 f8          	mov    -0x8(%rbp),%rdx
4004ec:	b8 00 00 00 00       	mov    $0x0,%eax
4004f1:	e8 07 00 00 00       	callq  4004fd <__x86_indirect_thunk_rdx>
4004f6:	b8 00 00 00 00       	mov    $0x0,%eax
4004fb:	c9                   	leaveq 
4004fc:	c3                   	retq   

00000000004004fd <__x86_indirect_thunk_rdx>:
4004fd:	e8 07 00 00 00       	callq  400509 <__x86_indirect_thunk_rdx+0xc>
400502:	f3 90                	pause  
400504:	0f ae e8             	lfence 
400507:	eb f9                	jmp    400502 <__x86_indirect_thunk_rdx+0x5>
400509:	48 89 14 24          	mov    %rdx,(%rsp)
40050d:	c3                   	retq   
40050e:	66 90                	xchg   %ax,%ax
...

我们可以看到间接跳转已经被retpoline的指令系列所替换。 当然,如果是一些内嵌汇编的间接跳转,则需要自己手动去增加retpoline序列。

在Linux内核中,是通过一个内核命令行参数来决定是否开启retpoline的,如果开启则内核在启动时动态替换指令。这样最大限度的减小了内核的性能损耗。

参考资料

  1. retpoline: 原理与部署
  2. 追寻计算世界的指引:探索Indirect jump的奥秘
  3. Retpoline: A Branch Target Injection Mitigation.
  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值