前言

下图是bleepingcomputer上总结的一些CPU漏洞,已经有许多博客和论文分析了这些漏洞。值得注意的是,ebpf的Spectre变种1(CVE-2017-5753)的补丁并没有考虑全面,导致了CVE-2019-7308。对于Spectre变种4(CVE-2018-3639)Jann Horn也给出了一个需要patch内核的利用ebpf的POC。这篇文章主要分析这两个漏洞ebpf的利用方式。在一些漏洞分析的文章中对ebpf有所提及[2][4],所以这里也不再赘述,不清楚的读者在阅读接下来的部分之前最好自行查看参考资料。

CVE-2018-3639利用ebpf的攻击方式

为了避免重复,这里只根据微软的文章简述Speculative Store Bypass的原理[7]。考虑下面的代码。

01: 88040F            mov [rdi+rcx],al
02: 4C0FB6040E        movzx r8,byte [rsi+rcx]
03: 49C1E00C          shl r8,byte 0xc
04: 428B0402          mov eax,[rdx+r8]

如果CPU推测加载指令与之前的存储指令无关,那么可以在存储指令执行之前执行加载指令。第1行的MOV指令在特殊情况下可能需要额外的时间来执行(如果计算RDI+RCX的地址表达式正在等待先前的指令执行)。在这种情况下,CPU可能会推测MOVZX指令不依赖于MOV指令,并且可能在MOV指令执行之前执行MOVZX指令。这可能会导致位于RSI+RCX的内存中的旧数据被加载到R8中,从而导致第四行代码使用了错误的数据。
Project Zero给出的利用CVE-2018-3639攻击ebpf的EXP需要patch linux内核源码[8],先来看看改动的内容。在ebpf系统调用中增加了处理0x13370001的情况,处理这个新增的cmd用的是map_time_flush_loc函数。

该函数读取ptr + attr->flags指向的内容,使用rdtscp计算读取的时间。

EXP的array_time_flush_loc函数中用到这部分逻辑计算读取mapfd中偏移idx+off处内容需要的时间。

还有就是在bpf_base_func_proto函数中加了一个helper函数bpf_clflush_mfence_proto,它的主要作用是清空缓存。


EXP中使用BPF_CALL可以调用到这部分逻辑。

EXP中我们先看ebpf指令部分,旁边我已经加上了伪代码注释。首先是一些设置。

然后拿到需要泄露的内存的地址。之前leak_bit函数调用了array_set_dw(input_map, 0, addr)对此进行了设置。

设置r9为leak_map偏移2048处。

设置r1=r7,然后清一下缓存。

设置r3=fp-216。

接下来就是最关键的部分了。因为*(u64 *)(r8)=r3执行起来比较慢,所以提前执行了r1=*(u64 *)(r6)取到了需要泄露的内存的地址,然后r2=*(u8 *)(r1)取到了其中的值。

后面的代码需要结合leak_bit函数理解。对于sockfds[16]来说根据最开始的BPF_ST_MEM(BPF_B, BPF_REG_FP, -216, dummy_ff ? 0x00 : 0xff)指令可以知道sockfds[bit+8]的*(u8 *)(fp-216)=0x00,sockfds[bit+0]的*(u8 *)(fp-216)=0xff。对于sockfds[bit+8]和sockfds[bit+0],它们的selected_bit=bit&7是相同的,如果取到的值是1,r2=0x1000,那么最后r0=*(u8 *)(r9)= r0=*(u8 *)( leak_map偏移2048+0x1000);如果取到的值是0,r2=0,那么最后r0=*(u8 *)(r9)= r0=*(u8 *)( leak_map偏移2048)。而sockfds[bit+8]没有推测执行的话r0=*(u8 *)( leak_map偏移2048);sockfds[bit+0]没有推测执行的话r0=*(u8 *)( leak_map偏移2048+0x1000)。所以如果sockfds[bit+8]读取leak_map偏移2048+0x1000需要的时间短,说明取到的值应该是1;如果sockfds[bit+0]读取leak_map偏移2048需要的时间短,说明取到的值应该是0。

后面再在leak_byte函数中进一步调用leak_bit函数一个一个bit拼起来就得到了泄露的byte。

用该EXP泄露指定地址的数据如下。

$ sudo grep core_pattern /proc/kallsyms
ffffffff9b2954e0 D core_pattern
$ gcc -o bpf_store_skipper_assisted bpf_store_skipper_assisted.c
$ time ./bpf_store_skipper_assisted ffffffff9b2954e0 5
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
BPF PROG LOADED SUCCESSFULLY
4 vs 96
1 vs 99
100 vs 0
100 vs 0
100 vs 0
2 vs 98
0 vs 100
100 vs 0
ffffffff9b2954e0: 0x63 ('c')
2 vs 98
1 vs 99
1 vs 99
1 vs 99
100 vs 0
2 vs 98
0 vs 100
100 vs 0
ffffffff9b2954e1: 0x6f ('o')
100 vs 0
3 vs 97
100 vs 0
100 vs 0
1 vs 99
2 vs 98
0 vs 100
100 vs 0
ffffffff9b2954e2: 0x72 ('r')
2 vs 98
100 vs 0
0 vs 100
100 vs 0
100 vs 0
0 vs 100
0 vs 100
100 vs 0
ffffffff9b2954e3: 0x65 ('e')
100 vs 0
100 vs 0
100 vs 0
100 vs 0
100 vs 0
100 vs 0
100 vs 0
100 vs 0
ffffffff9b2954e4: 0x00 ('')

real    0m31.591s
user    0m2.547s
sys     0m27.429s

ebpf中CVE-2017-5753的补丁不完善导致的CVE-2019-7308

我们仍然只根据Project Zero的文章简述Bounds Check Bypass的原理[1]。考虑下面的代码。

CPU可能会不检查if语句中的条件就推测其为真,先将下一步指令所需要的内存加载到缓存之中,假如发现推测错误再回滚指令。但是这时CPU缓存并没有清空,通过侧信道攻击可以取到arr1->data[untrusted_offset_from_caller]中的值。
在ebpf系统中为了修补这个漏洞,在每个需要取数组元素的操作中都将数组的索引和掩码相与避免越界访问。

如果map不是由具有CAP_SYS_ADMIN权限的用户创建的,还会为JIT代码添加同样的操作。

补丁对数组中的指针计算并没有作用,来看看EXP是如何利用这一点的[5]。EXP中有两个重要的结构体,mem_leaker_progarray_timed_reader_prog


首先设置mem_leaker_prog.data_map的偏移0x1200,0x2000和0x3000处为1。

leak_byte函数中依次调用leak_bit函数得到泄露出指定地址的内存。


下面来看trigger_proc(leakprog->sockfd)调用的ebpf指令。首先,取到之前array_set_2dw(leakprog->control_map, 0, 12-bit_index, byte_offset)设置的12-bit_indexbyte_offset

mem_leaker_prog.data_map的偏移0x1200处的值,前面说了这个值被设置为1,BPF_ANDBPF_OR对值没有影响,只是为了延长时间,使得之后的BPF_JGT指令CPU推测执行为false。

通过r4=*(u8 *)(r4)读到了泄露地址的内存,左移12-bit_index位之后和0x1000相与,如果bit_index位的值是1,结果是0x1000,加上0x2000导致mem_leaker_prog.data_map偏移0x3000处的值被加载到了缓存;如果bit_index位的值是0,结果是0x0,加上加上0x2000导致mem_leaker_prog.data_map偏移0x2000处的值被加载到了缓存。

返回到leak_bit函数中,接下来分别去算读mem_leaker_prog.data_map偏移0x2000处的值需要的时间times[0]和mem_leaker_prog.data_map偏移0x3000处的值需要的时间times[1]。times[0] < times[1]说明bit_index位的值是0,times[0] > times[1]说明bit_index位的值是1。ebpf指令非常简单,结合注释可以很容易看懂。


对于这种情况的补丁,掩码需要考虑的情况有很多,要处理的值可能存在于源寄存器中(如ptr+=val),也可以存在于目的寄存器中(如val+=ptr);limit取决于ALU操作是加或减以及偏移量的正负。在补丁增加的retrieve_ptr_limit函数中如果ALU操作是加但是偏移是负的,或者ALU操作是减但是偏移是正的说明是减法,mask_to_left被设置为1;否则说明是加法,mask_to_left被设置为0。对于map类型的指针减法的limit=ptr_reg->umax_value+ptr_reg->off,加法的limit=ptr_reg->map_ptr->value_size–(ptr_reg->smin_value+ptr_reg->off);对于stack类型的指针减法的limit=MAX_BPF_STACK+off,加法的limit=-off,其中off=ptr_reg->off+ptr_reg->var_off.value

fixup_bpf_calls函数中根据前面计算的limit对寄存器中的值进行掩码运算从而避免越界访问。

PS:如果你还有兴趣了解这方面内容,我整理了一些CPU漏洞相关的资料:https://github.com/houjingyi233/CPU-vulnerability-collections

参考资料

1.Reading privileged memory with a side-channel
2.深入分析Ubuntu本地提权漏洞—【CVE-2017-16995】
3.http://man7.org/linux/man-pages/man2/bpf.2.html
4.Linux CVE-2017-16995整数扩展问题导致提权漏洞分析
5.Issue 1711: Linux: eBPF Spectre v1 mitigation is insufficient
6.https://www.kernel.org/doc/Documentation/networking/filter.txt
7.Analysis and mitigation of speculative store bypass (CVE-2018-3639)
8.Issue 1528: speculative execution, variant 4: speculative store bypass

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖