大佬 您能演示一下 CVE-2019-7308 的poc 使用方法吗 我不是很理解 Invocation: "./bpf_stuff <main-cpu> <bounce-cpu> <hex-offset> <hex-length>", main-cpu bounce-cpu 怎么填写</hex-length></hex-offset></bounce-cpu></main-cpu>
前言
下图是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_prog
和array_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_index
和byte_offset
。
取mem_leaker_prog.data_map
的偏移0x1200处的值,前面说了这个值被设置为1,BPF_AND
和BPF_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