引言:
eBPF-verifier模块原理分析与漏洞复现
1 eBPF 漏洞相关知识
eBPF (Extended Berkeley Packet Filter) 由 cBPF (Classic Berkeley Packet Filter) 衍生而来,是一项可在内核虚拟机中运行程序的技术。使用eBPF无需修改内核源码,或者插入驱动,对系统的入侵性相对没那么强,可以安全并有效地扩展内核的功能。
1.1 eBPF指令
eBPF 使用类似 x86 的虚拟机指令,基础指令为 8 字节,其编码格式为:
32 bits (MSB) | 16 bits | 4 bits | 4 bits | 8 bits (LSB) |
---|---|---|---|---|
immediate | offset | source register | destination register | opcode |
扩展指令在基础指令基础上增加 8 个字节的立即数,总长度为 16 字节。
伪指令是内核代码中定义的方便理解记忆的助记符,通常是对真实指令的包装。
下文中出现的指令/伪指令及其功能如下:
指令/伪指令 | 功能 |
---|---|
BPF_MOV64_REG(DST, SRC) |
dst = src |
BPF_MOV64_IMM(DST, IMM) |
dst_reg = imm32 |
BPF_ST_MEM(SIZE, DST, OFF, IMM) |
*(uint *) (dst_reg + off16) = imm32 |
BPF_STX_MEM(SIZE, DST, SRC, OFF) |
*(uint *) (dst_reg + off16) = src_reg |
BPF_LDX_MEM(SIZE, DST, SRC, OFF) |
dst_reg = *(uint *) (src_reg + off16) |
BPF_ALU64_IMM(OP, DST, IMM) |
dst_reg = dst_reg 'op' imm32 |
BPF_JMP_IMM(OP, DST, IMM, OFF) |
if (dst_reg 'op' imm32) goto pc + off16 |
BPF_LD_MAP_FD(DST, MAP_FD) |
dst = map_fd |
BPF_EXIT_INSN() |
exit |
1.2 eBPF寄存器
eBPF 共有 11 个寄存器,其中 R10 是只读的帧指针,剩余 10 个是通用寄存器。
- R0: 保存函数返回值,及 eBPF 程序退出值
- R1 - R5: 传递函数参数,调用函数保存
- R6 - R9: 被调用函数保存
- R10: 只读的帧指针

1.3 eBPF程序类型
所有 eBPF 程序类型定义在以下枚举类型:
1 | enum bpf_prog_type { |
下文涉及到的类型只有 BPF_PROG_TYPE_SOCKET_FILTER
。该类型 eBPF 程序通过 setsockopt
附加到指定 socket 上面,对 socket 的流量进行追踪、过滤,可附加的 socket 类型包括 UNIX socket。
该类型程序的传入参数为结构体 __sk_buff
指针,可通过调用 bpf_skb_load_bytes_relative
辅助函数经由该结构体获取 socket 流量。
1.4 eBPF map
eBPF map 是 eBPF 程序和用户态进行数据交换的媒介。其类型包括:
1 | enum bpf_map_type { |
下文使用到的类型包括 BPF_MAP_TYPE_ARRAY
和 BPF_MAP_TYPE_RINGBUF
。
- BPF_MAP_TYPE_ARRAY: 顾名思义,类似数组,索引为整形,值可为任意长度的内存对象。
- BPF_MAP_TYPE_RINGBUF: 环形缓冲区,如果写入的数据来不及读取,导致积累的数据超过缓冲区长度,新数据则会覆盖掉旧数据。
bpf_map是用户态空间与内核空间进行数据交换的桥梁,下文的很多操作都是围绕bpf_map
进行展开的。
1.5 eBPF辅助函数
eBPF 辅助函数(eBPF helper)是可在 eBPF 程序中使用的辅助函数。
内核规定了不同类型的eBPF程序可使用哪些辅助函数,比如,bpf_skb_load_bytes_relative
只有 socket 相关的 eBPF 程序可使用。
各 eBPF 辅助函数的函数原型由内核定义,下文使用到的一些辅助函数的原型如下:
1 | const struct bpf_func_proto bpf_map_lookup_elem_proto = { |
可见 bpf_map_lookup_elem
的返回值类型是 RET_PTR_TO_MAP_VALUE_OR_NULL
,bpf_ringbuf_reserve
的返回值类型是 RET_PTR_TO_ALLOC_MEM_OR_NULL
。
各 eBPF 辅助函数的功能可通过 man bpf-helpers
命令查看。
1.6 eBPF verifier
eBPF 程序在加载进内核之前,必须通过 eBPF verifier 的检查。只有符合要求的 eBPF 程序才允许被加载进内核,这是为了防止 eBPF 程序对内核进行破坏。
eBPF verifier 对 eBPF 程序的限制包括:
- 函数调用限制: 不能调用任意的内核函数,只限于内核模块中列出的 eBPF helper 函数
- 代码有效性: 不允许包含无法到达的指令,防止加载无效代码,延迟程序的终止
- 循环限制: 限制循环次数,必须在有限次内结束
- 栈大小限制: 栈大小被限制为
MAX_BPF_STACK
,截止到内核 5.10.83 版本,被设置为 512 - 复杂度限制: 限制 eBPF 程序的复杂度,verifier 处理的指令数不得超过
BPF_COMPLEXITY_LIMIT_INSNS
,截止到内核 5.10.83 版本,被设置为100万 - 内存访问限制: 限制 eBPF 程序对内存的访问,比如不得访问未初始化的栈,不得越界访问 eBPF map
在verifier
进行bpf
程序验证过程中,会对每个寄存器维持一个结构体,用于描述每个寄存器现在的状态:
1 | ptype struct bpf_reg_state |
smin_value
和 smax_value
保存当寄存器被当做是有符号数的时候可能的取值范围,同样umin_value
和umax_value
表示的是无符号的时候。 var_of
是struct tnum
类型
1 | ptype struct tnum |
对于每个寄存器,在程序最开始有这四种状态:
NOT_INIT
: 寄存器的默认状态,此时不能被读取。SCALAR_VALUE
: 寄存器包含一个 scalar 值,这个值要么是一个已知的常量,要么是一个范围,例如 1 - 5 。PTR_TO_MAP_VALUE_OR_NULL
:寄存器可以包含指向 map 的指针或是 NULL 。如果要使用这个指针,必须先对其进行检查,检查其指针是否为 NULL 。PTR_TO_MAP_VALUE
:一个寄存器包含已经被检查过的 map 指针。可以被用来对 map 进行读或写。
我们主要关注第二点SCALAR_VALUE
, 也就是说,verifier
在执行指令检测的过程中,有时候是不能够准确得知寄存器的具体数值的,只能根据已知指令去猜测寄存器的可能数值范围
如果一个寄存器从bpf_map中加载,则被声明为scalar value, 其具体数值完全取决于用户态是如何设置bpf_map的,在verifier阶段不可能知道。这时候verifier只能根据指令的上下文去猜测该寄存器可能的范围。
例如下面这段bpf程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53 // 假设已有一个array类型的map: config_map
0: BPF_MOV64_REG(R6, R1) // 保存ctx参数到R6
1: BPF_MOV64_IMM(R1, 0) // R1 = 0 (map key)
2: BPF_STX_MEM(BPF_W, R10, R1, -4) // 将key存储到栈上: *(u32*)(fp-4) = 0
3: BPF_MOV64_REG(R2, R10) // R2 = fp (栈指针)
4: BPF_ALU64_IMM(BPF_ADD, R2, -4) // R2 = fp - 4 (key的地址)
5: BPF_LD_MAP_FD(R1, 1) // R1 = config_map_fd
6: BPF_CALL BPF_FUNC_map_lookup_elem // 调用bpf_map_lookup_elem(map, &key)
// 此时R0包含map value的指针或NULL
7: BPF_JMP_IMM(BPF_JEQ, R0, 0, 15) // if (R0 == NULL) goto exit
// verifier记录:R0 != NULL (指向map value)
8: BPF_LDX_MEM(BPF_DW, R1, R0, 0) // R1 = *(u64*)R0 (从map加载数据)
// ** 关键点:R1现在是scalar value **
// verifier标记:R1 = scalar_value(id=1, umin=0, umax=U64_MAX)
// 数值完全取决于用户态如何设置map,verifier无法知道具体值
9: BPF_JMP_IMM(BPF_JGT, R1, 1000, 13) // if (R1 > 1000) goto error
// verifier更新:R1的范围缩小为 [0, 1000]
// umin=0, umax=1000
10: BPF_MOV64_REG(R2, R1) // R2 = R1 (复制scalar value)
// verifier记录:R2也是scalar_value,范围[0, 1000]
11: BPF_ALU64_IMM(BPF_MUL, R2, 8) // R2 = R2 * 8
// verifier推算:R2范围变为[0, 8000]
12: BPF_JMP_IMM(BPF_JGE, R2, 4096, 13) // if (R2 >= 4096) goto error
// 进一步限制:如果继续执行,R2范围为[0, 4095]
// 使用scalar value作为数组索引(需要边界检查)
13: BPF_MOV64_REG(R3, R10) // R3 = fp
14: BPF_ALU64_IMM(BPF_ADD, R3, -512) // R3 = fp - 512 (栈数组基址)
15: BPF_ALU64_REG(BPF_ADD, R3, R2) // R3 = 栈数组基址 + R2
// verifier检查:R2必须在有效范围内,防止栈溢出
// 由于R2范围[0, 4095],但栈空间只有512字节,这里会被拒绝
16: BPF_ST_MEM(BPF_B, R3, 0, 1) // *(u8*)R3 = 1
// 如果上面的边界检查通过,这里才会执行
error:
17: BPF_MOV64_IMM(R0, -1) // R0 = -1 (错误返回值)
18: BPF_EXIT_INSN() // exit
success:
19: BPF_MOV64_IMM(R0, 0) // R0 = 0 (成功返回值)
20: BPF_EXIT_INSN() // exit
exit:
21: BPF_MOV64_IMM(R0, 1) // R0 = 1 (NULL指针)
22: BPF_EXIT_INSN() // exitVerifier分析过程:
- 指令8:从map加载数据到R1 - R1被标记为scalar_value
- 初始范围:umin=0, umax=U64_MAX
- verifier无法知道具体数值(取决于用户态设置)
- 指令9:条件跳转 R1 > 1000
- 如果不跳转,verifier推断:R1 ∈ [0, 1000]
- 更新寄存器状态 3.
- 指令11:算术运算 R2 = R1 * 8 - verifier
- 计算新范围:R2 ∈ [0, 8000]
- 指令12:另一个条件检查
- 进一步缩小范围
- 指令15:用作内存偏移
- verifier必须确保不会造成越界访问
- 由于栈空间限制,此处会失败
但在verifier
的验证过程中由于种种原因会存在对reg_state
的误判,导致verifier
不能够准确地判断寄存器的数值边界,从而导致一些严重的越界读写等漏洞。
2 CVE漏洞复现
CVE-2020-8835
漏洞复现环境:
1 | Linux kernel x86 boot executable bzImage, version 5.6.0 (winegee@ubuntu) #5 SMP Sat Apr 18 23:42:35 CST 2020, RO-rootFS, swap_dev 0X8, Normal VGA |
该漏洞在commit 581738a中被引入,在这次更新中在verifier.c
中引入了一个新的函数:
1 | static void __reg_bound_offset32(struct bpf_reg_state *reg) |
这个函数的意思是对寄存器进行32位的截取.
现在又要回到上面的问题了
对于跳转指令, 假如当前遇到了下面这样一条指令,
BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)
会有下面这样两行代码来更新状态,false_reg
和true_reg
分别代表两个分支的状态, 这是我们前面__reg_bound_offset32
的64位版本
1 | __reg_bound_offset(false_reg); |
也就是说,在verifier
静态分析过程中,如果遇到了这样一条关于r5
的分支判断语句,那么会在不同分支上分裂出关于r5
的不同的reg_state
,我们这里分别叫做false_reg
和true_reg
。
__reg_bound_offset32
会在使用BPF_JMP32
的时候调用,ebpf 的BPF_JMP
寄存器之间是64bit比较的,换成BPF_JMP32
的时候就只会比较低32位
我们先来看看源码中的tnum_range
是怎么做的:
1 | struct tnum tnum_range(u64 min, u64 max) |
注意这里TNUM(min & -delta, delta)
直接取了min
的低32位。
1 | struct tnum tnum_intersect(struct tnum a, struct tnum b) |
那这就有问题了,在tnum_intersect
之后,会设置reg->var_off
,但现在的range
是错的,也就会导致verifier
对寄存器的值产生幻觉。
举个例子:
假如我们在之前把r6设定为一个从
map
中load
进来的数值,这时reg6->tnum->value = 0
因为verifier
并不知道map
中的数值。
1
2
3
4
5
6
7
8
9 BPF_JMP_IMM(BPF_JGE,6,1,1),
BPF_EXIT_INSN(), // false_reg => ...
BPF_MOV64_IMM(9,0x1), // reg6->true_reg -> umin_value = 0x1
BPF_ALU64_IMM(BPF_LSH,9,32),
BPF_ALU64_IMM(BPF_ADD,9,1),
/*BPF_JLE tnum umax 0x100000001*/
BPF_JMP_REG(BPF_JLE,6,9,1),
BPF_EXIT_INSN(), //false_reg => ...
// reg6->true_reg -> umax_value = 0x100000001这时我们使用
jmp32
来触发漏洞:
1
2 BPF_JMP32_IMM(BPF_JNE,6,5,1),
BPF_EXIT_INSN(), // false_reg => ...这时
false_reg
是什么我们不关心,因为他会走到exit
中去。
true_reg
在执行前:
1
2
3
4
5
6
7
8 var_off = {
value = 0x0,
mask = 0x1ffffffff
},
smin_value = 0x1,
smax_value = 0x100000001,
umin_value = 0x1,
umax_value = 0x100000001,执行后:
1
2
3
4
5
6
7
8 var_off = {
value = 0x1,
mask = 0x100000000
},
smin_value = 0x1,
smax_value = 0x100000001,
umin_value = 0x1,
umax_value = 0x100000001,这是因为
__reg_bound_offset32
对umax_vlaue
进行了低32位的截断,导致原本verifier
认为1 <= r6 <= 0x100000001
, 现在verifier
认为1 <= r6 <= 0x1
那r6
只能是1,所以verifier
将这条分支上的r6->value->var_off
设置成1 (这里这么说只是便于理解,其实这些逻辑在tnum_intersect
中实现)
我们现在就可以利用上述verifier
对r6
的错觉在内核空间内进行越界读写了。
比如我们可以通过如下操作,让reg6
在verifier
的眼中彻底人畜无害:
1 | r6 = (r6 & 2) >> 1 |
假如我们r6
真实的数值是2,那么经过这一系列操作之后r6
就会变成1
, 但在verifier
看来他是0。这时我们就可以对r6
做任何的乘法操作来构造任意溢出需要的偏移量,但这在verifier
看来0乘任何数都是0,所以无论一个寄存器加0减0,verifier
都会认为是安全的。
漏洞利用的bpf
程序如下:
1 | struct bpf_insn core_bpf_exploit_instructions[] = { |
漏洞利用思路如下:(完整exp见附件,具体原理见Appendix)
在用户态创建
bpf_map
并在其中设置一些元数据,在内核的bpf
程序中将上述bpf_map
指针load
给寄存器r1
,初始加载两个特制的 BPF 映射作为攻击载体。主映射(文件描述符3)用作控制缓冲区,存储攻击参数和泄露的数据;辅助映射(文件描述符4)作为越界访问的跳板。在主映射中预设特定的控制值,将
control_buffer_qwords[0]
设置为5,。同时,程序在栈上准备临时存储空间,为后续的映射查找操作做准备利用上述
verifier
的逻辑漏洞制造一个被错误判断的reg6
, 通过上述操作构造偏移量0x110
越界访问bpf_map
结构体中的元信息,泄漏内核地址,通过越界读写,覆盖元字段与map_ops
将
map_push_elem
覆盖为map_get_next_key
触发任意地址写,更改modprobe_path
,恢复到bash
下在用户权限下执行错误的文件,让内核以root
越权执行恶意文件,进而实现提权
其实我们可以用另一种攻击手法,就是直接搜索
init_pid_ns
, 找到当前的task_struct
, 然后写 cred 来获取一个 root shell。
漏洞复现过程:
- 运行
exp
更改modprobe_path
并生成两个恶意文件


- 现在我们尝试执行
custom_executor
,里面是更改flag
权限的命令,发现权限不够。

- 现在执行
probe_trigger
, 里面是0xff 0xff 0xff 0xff
是错误的指令,这时内核会触发custom_executor
, 我们就可以获取到flag
的内容了。

CVE-2020-27194
漏洞复现环境:
1 | bzImage: Linux kernel x86 boot executable bzImage, version 5.8.14 (winegee@ubuntu) #1 SMP Sun May 13 23:50:27 PST 2025, RO-rootFS, swap_dev 0X8, Normal VGA |
这个漏洞的成因和CVE-2020-8835
很像,都是由于对32位的操作处理不当导致的。这个漏洞的成因是32位or运算的取值范围分析有错误:
1 | static void scalar32_min_max_or(struct bpf_reg_state *dst_reg, |
在进行两个有符号正数or运算时, 最后2行代码将寄存器的64位无符号数的取值范围赋值给了32位有符号数的取值范围。
那就很错了,举出上面的例子:
1 | BPF_LD_MAP_FD(BPF_REG_9, bpf_array_map_fd), |
1 <= r5 <= 0x600000001
, 在执行BPF_ALU64_IMM(BPF_OR, BPF_REG_5, 0)
后 就变成了1 <= r5 <= 0x1
后面的利用原理和CVE-2020-8835
就一样了,只不过不同的内核版本各个指针之间的偏移可能有变化。
漏洞利用思路:(完整exp见附件,具体原理见Appendix)
在用户态创建
bpf_map
并在其中设置一些元数据,在内核的bpf
程序中将上述bpf_map
指针load
给寄存器r1
,初始加载两个特制的 BPF 映射作为攻击载体。主映射(文件描述符3)用作控制缓冲区,存储攻击参数和泄露的数据;辅助映射(文件描述符4)作为越界访问的跳板。在主映射中预设特定的控制值,将
control_buffer_qwords[0]
设置为5,。同时,程序在栈上准备临时存储空间,为后续的映射查找操作做准备利用上述
verifier
的逻辑漏洞制造一个被错误判断的reg6
, 通过上述操作构造偏移量0x110
越界访问bpf_map
结构体中的元信息,泄漏内核地址,通过越界读写,覆盖元字段与map_ops
搜索
init_pid_ns
, 找到当前的task_struct
, 覆盖cred
结构体进而实现提权。

CVE-2022-23222
复现环境
1 | bzImage: Linux kernel x86 boot executable bzImage, version 5.10.91 (root@iZbp1hx3qce738wmtfyf13Z) #1 SMP Sun Jun 1 19:08:06 CST 2025, RO-rootFS, swap_dev 0X9, Normal VGA |
在verifier
中,会对寄存器进行指针类型检查,以禁用一些违法的指针类型进行加减操作:
1 | static int adjust_ptr_min_max_vals(struct bpf_verifier_env *env, |
但没有列举完所有的 OR_NULL 类型指针,导致部分 OR_NULL 类型指针可以进行非法运算。
通过比对,发现下面这四者没有被禁用:
1 | PTR_TO_BTF_ID_OR_NULL |
同时,在verifier
分析条件跳转指令时,有如下调用链子:
1 | check_cond_jmp_op -> mark_ptr_or_null_regs -> __mark_ptr_or_null_regs |
1 | static void __mark_ptr_or_null_regs(struct bpf_func_state *state, u32 id, |
也就是说,当verifier
在分析*_OR_NULL类型指针与0的比较时,在其中一条分支中,reg_state->type
总会变成SCALAR_VALUE
, 其值也会被认为是0。同时,在该条分支中,会同时对所有具有相同id
的寄存器做一样的处理。
id
是reg_state
中对寄存器类型进行跟踪的字段,它表示着该寄存器类型的来源,在用户程序对寄存器进行一些操作时,便于verifier
更好地对寄存器进行"连坐"式的检查。举个例子:
1
2
3
4
5
6
7
8
9
10 BPF_MOV64_REG(BPF_REG_1, BPF_REG_0), // r0 是 PTR_TO_MEM_OR_NULL类型指针 值为0(NULL)
// 此时 r1->type = r0->type / r1->id = r0->id
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1), // r1 = 1
// if (r0 != NULL) {}
BPF_JMP_IMM(BPF_JEQ, BPF_REG_0, 0, 5),
...... // 5 insns
// 这时verfier认为r0->type = SCALAR_VALUE r0->var_off = {0, 0xffffffff}
// 同时verifier发现r1->id = r0->id, 那么在该条分支下,verifier认为
// r1->var_off = r0->var_off, r1->type = r0->type 但在运行时r1的值是1
但由于上述对*_OR_NULL类型限制不全的漏洞,导致在该条分支中,如果一个寄存器的值不是0,那么也会被认为是0,我们可以通过创造这几类指针类型的寄存器,来触发越界读写的漏洞。
首先我们需要准备一个*_OR_NULL类型的寄存器,观察到有bpf_ringbuf_reverse()
函数,我们可以通过这个函数去获取PTR_TO_MEM_OR_NULL
类型的指针
1 | ctx->ringbuf_fd = bpf_create_map(BPF_MAP_TYPE_RINGBUF, 0, 0, PAGE_SIZE) |
官方文档对bpf_ringbuf_reverse()
函数的介绍:
bpf_ringbuf_reserve()
avoids the extra copy of memory by providing a memory pointer directly to ring buffer memory. In a lot of cases records are larger than BPF stack space allows, so many programs have use extra per-CPU array as a temporary heap for preparing sample. bpf_ringbuf_reserve() avoid this needs completely. But in exchange, it only allows a known constant size of memory to be reserved, such that verifier can verify that BPF program can’t access memory outside its reserved record space. bpf_ringbuf_output(), while slightly slower due to extra memory copy, covers some use cases that are not suitable forbpf_ringbuf_reserve()
.
接下来通过一系列操作创建一个越狱的寄存器:
1 | // r0 = bpf_ringbuf_reserve(ctx->ringbuf_fd, PAGE_SIZE, 0) |
我们需要在运行时保证r0
一定是NULL
——我们在调用bpf_ringbuf_reverse
中直接要求申请一个PAGE_SIZE
大小的块,因为一个PAGE
里面一定是存有一些原始数据的,所以一定是不够分的,所以就一定是返回NULL
的。
之后我们就可以去构建任意读写的原语,通过partial overlap
操纵一个内核指针来完成一系列提权操作。
漏洞复现流程:(具体细节见Appendix)
- 创建 BPF Maps 并泄露内核地址,创建 BPF_MAP_TYPE_ARRAY (通信) 和 BPF_MAP_TYPE_RINGBUF (触发漏洞)。加载 eBPF 程序,利用
verifier
和运行时runtime
的不一致性verifier bypass
,通过ringbuf
和skb_load_bytes_relative
等操作,从栈上部分覆写数据,最终泄露内核中某个array_map
的实际地址。 - 基于泄露的内核 map 地址和类似的 verifier bypass 技巧,加载新的 eBPF 程序, 将 BPF map 操作(如读/写 map 元素)重定向到任意指定的内核内存地址,从而实现对内核内存的任意读和任意写。
- 创建多个子进程,并让它们暂停,使用已获得的任意读能力,在内核中扫描并定位到当前利用程序某个子进程的 cred 结构体(包含 UID, GID 等权限信息)。使用任意写能力,将该 cred 结构体中的 UID, GID, EUID, EGID 等字段修改为 0 (root 权限)。
- 唤醒之前暂停的、其 cred 已被修改为 root 的子进程。其中一个子进程执行 system("/bin/sh"),从而以 root 权限启动一个新的 shell。
CVE-2023-2163
漏洞复现环境:
1 | bzImage: Linux kernel x86 boot executable bzImage, version 6.3.0-rc6 (root@iZbp1hx3qce738wmtfyf13Z) #3 SMP PREEMPT_DYNAMIC Sun Jun 1 15:30:12 CST 2025, ROA |
改漏洞发生于verifier
对路径剪枝的逻辑中:(关于寄存器精确标记详见Appendix.E)
我们输入的
bpf
程序会被verifier
进行静态分析,将程序解析成代码块与抽象语法树的形式。假设我们有如下程序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 0: (b7) r6 = 1 // r6 = 1
1: (b7) r7 = 2 // r7 = 2
2: (b7) r8 = 0 // r8 = 0
3: (b5) if r6 == 1 goto pc+2 // if r6 == 1, 跳转到 6
4: (b7) r6 = 0 // r6 = 0
5: (05) goto pc+2 // 跳转到 8
6: (b7) r7 = 0 // r7 = 0
7: (b7) r6 = 0 // r6 = 0
8: (18) r1 = 0xffff888123456789 // r1 = 映射指针 (ks=4, vs=8)
10: (bf) r2 = r10 // r2 = 栈指针
11: (07) r2 += -4 // r2 = r10 - 4
12: (63) *(u32 *)(r10 - 4) = r8 // 栈写入 r8 (0)
13: (85) call bpf_map_lookup_elem#1 // 调用映射查找,r0 = 映射值
14: (55) if r0 != 0x0 goto pc+1 // if r0 非空,跳转到 16
15: (95) exit // 退出
16: (7b) *(u64 *)(r0 + 0) = r6 // 写入 r6 到 r0
17: (95) exit // 退出
verifier
将上述程序解析为抽象语法树,并对每个分支做迭代探索与验证:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 Root (指令 0-2)
R6=1, R7=2 (R6: P, R7: 非精确)
↓
指令 3 (if r6 == 1)
├── 跳转到 6 (r6 == 1)
│ 指令 6-7: R6=0, R7=0 (R6: P, R7: 非精确)
│ 指令 8: R6=0, R7=0
│ ↓
│ 指令 14 (if r0 != 0)
│ ├── 跳转到 16: R6=0 → 写入 r0 + 0, 安全
│ └── 退出: 安全
└── 继续到 4 (r6 != 1)
指令 4-5: R6=0, R7=2
指令 8: R6=0, R7=2 (R6: P, R7: 非精确)
[PRUNED: 等价于指令 8 (R6=0, R7=0)]
※ 继续路径:
指令 14-16: R6=0 → 写入 r0 + 0, 安全但循环遍历所有分支会造成时间复杂度以指数级增长,所以
verifier
引入了对路径的剪枝pruning
即上述
[PRUNED: 等价于指令 8 (R6=0, R7=0)]
即
verifier
检测到5->6->7->8
之后的状态与5->8
之后的状态是相同的,在检查过5->6->7->8
之后就不会再检查5->8
的分支,即完成剪枝。剪枝点(指令 8)
- 状态比较
- 旧状态(指令 6-7 路径):R6=0, R7=0(R6: P, R7: 非精确)。
- 新状态(指令 4-5 路径):R6=0, R7=2(R6: P, R7: 非精确)。
- 比较逻辑
- 精确寄存器:R6=0(旧)等于 R6=0(新),匹配。
- 非精确寄存器:R7=0(旧)与 R7=2(新)不同,但 r7 非精确,验证器忽略差异。
在该漏洞被修复之前,verifier
是不会对比较跳转指令做寄存器的精确标记的,但综合前几个CVE
我们可以得知,比较指令是可以改变reg_state -> var_off
的,比如r9 = 0, if r6 <= r9 ...
,那在verifier
眼中r6
就只能是0。
下面我们考虑这样一个程序:
1 | 0: (b7) r6 = 1024 |
我们可以画出如下语法树与状态跟踪图:
1 | Root (指令 0-4) |

图中,白色框表示解析出的正常代码块,旁边是我们定义的该代码块的序号,绿色框是我们特殊标注的分支语句的代码块,粉色框中代码着
verfier
解析到该代码块时,对该代码块判定的状态。
verifier
在解析一个语法树时会先按照所有分支False
的逻辑进行中序遍历。
由图所示,当程序按照绿色线路遍历1->2->3->4->5->6->7->exit->7->8
时,在程序下行遍历时,verifier
记录每个代码块在进入时的寄存器状态组,声明为该代码块的状态,如果该代码块已经存在一个状态,那么就需要通过比较新旧状态的差异来裁决是否需要剪枝(后续讨论),在verifier
运行时会有如下输出,流程图中为节省空间,故只标出需要关注的部分。
现在程序按照路径运行到8
代码块,检测到了r6
被用于和map
指针的算数操作,故r6
需要被标注为精确,这时backtrack_insn()
被调用,上行回溯将每个父节点(代码块)的状态中的r6
标记为精确
在
verifier
在寄存器状态组中,r6 = 0 / r6 = scalar()
表示r6
是非精确的,r6 = P0 / r6 = Pscalar()
表示r6
是精确的,对寄存器的精确标注通常在前面加一个P
来表示。

上图中蓝线表示已经遍历过的路径,红线表示我们讨论的,正在执行的路径
现在程序已经遍历了1->2->3->4->5->6->7->exit->(->7)8->(->7->6->5->4)->6->...
, 回溯到2
, 开始遍历True
路径,此时进入到4
中会生成关于4
的新状态r6 = scalar(), r9 = -214...
, verifier
判断旧状态是新状态的超集,将当前路径剪枝。
4->new: R1=ctx(off=0,imm=0) R6_w=scalar(umax=18446744071562067968) R7_w=0 R8_w=0 R9_w=-2147483648 R10=fp0.
4->old: R6_rwD=Pscalar() R9_rwD=0 R10=fp0
在新旧状态的比较中,
verifier
认为Pscalar
要比scalar
更加精确,并且不关心所有非精确寄存器数值是不是一致的,所以既然在更加严格条件下已经判断了该条代码块下的所有分支是安全的,那在稍微放宽条件的情况下必然安全,即旧状态是新状态的超集。verifier
会对当前路径进行剪枝。
但是我们站在第三方的角度可以明显看出,如果该条路径被剪枝,那么在运行时,就会在
r0+=r6
时引发越界读写的漏洞。
所以该漏洞实际上发生在寄存器的精确判定的逻辑上,即我们什么时候应该将一个寄存器判定为精确(见Appendix)
我们重新观察3->4->6->7->8
这条路径,为什么这条路径可以通过检查——实际上是因为在条件跳转的比较时,r9
的值间接影响了verifier
对r6
寄存器的数值判断,即r9=0
, if r6 <= r9 then jmp
的逻辑,让verifier
在True
分支下将r6
的值判定为0。
所以该漏洞发生的原因是非精确的寄存器影响了verifier
对精确寄存器的数值判断,linux
在之后的版本中在寄存器的精确标记逻辑中新加了一条,即在条件跳转时,如果一个非精确寄存器和精确寄存器进行比较,那么也会将非精确寄存器标记为精确
在修改后的版本中我们可以看到4->old
和4->new
是不同的,此时在4->new
中r9
被标记为精确:

漏洞利用流程:
初始化 eBPF 环境,创建
BPF_MAP_TYPE_ARRAY
型映射用于用户态/内核态通信,以及另一辅助 BPF 映射,通过精确构造的 eBPF 指令序列,在运行时触发漏洞(例如,在skb_load_bytes_relative
等辅助函数调用时,传递被污染或非预期的参数,如被篡改的寄存器R6
或栈指针),导致对栈或其他内存区域的越界/非预期读操作。基于已泄露的内核基址,加载第二阶段的 eBPF 程序。这些程序进一步利用
verifier bypass
技术。通过在 eBPF 程序执行流中篡改指向 BPF map 或其他内核对象的指针,使得 BPF 内建的 map 操作函数(如map_lookup_elem
,map_update_elem
)或内存访问指令实际作用于指定的任意内核内存地址,而非其原始目标。最终效果是构建起两个核心原语:从任意内核地址读取数据,以及向任意内核地址写入数据。运用已构建的任意内核读原语,在内核地址空间中进行扫描,以定位目标进程的
cred
(凭证)结构体。此利用任意内核写原语,将其内部的uid
(用户ID)、gid
(组ID)、euid
(有效用户ID)、egid
(有效组ID) 及相关权能(capabilities)字段覆写为0
(root 用户的标识符)。调用
system("/bin/sh")
继承此 root 权限,从而赋予对系统的完全控制。

这是漏洞发现者给出的容器逃逸的运行结果,但我们并未在本地成功复现,我们只实现了系统级别的提权
好像是qemu里是不允许对init进程动手动脚的?暂时不清楚是为什么,但是我们真的找不到可以复现的ubuntu/debian发行版本...

4 eBPF 自动化漏洞挖掘
这些项目都聚焦于利用 eBPF技术进行自动化漏洞挖掘。ebpf-fuzzer 和 Buzzer 主要测试 eBPF 验证器,BRF 关注运行时,而 bpf-fuzzer 在用户空间操作,各自解决不同的安全挑战。

- ebpf-fuzzer
- 架构与原理:ebpf-fuzzer 是一个开源工具,专门用于模糊测试 Linux 内核中的 eBPF 验证器。它通过模拟大量的 eBPF 程序来检测验证器中的漏洞。它的核心原理是使用 QEMU 作为测试环境,生成自定义的 eBPF 样本程序,并观察这些程序在验证器中的行为。如果验证器出现错误(如无限循环),ebpf-fuzzer 就能捕获这些问题。它依赖于 QEMU 提供的虚拟化环境来模拟复杂的内核行为,确保测试的全面性。
- 不同点:与其他工具相比,ebpf-fuzzer 特别注重于 eBPF 验证器的测试,而非运行时。它使用 QEMU 提供的虚拟化环境来模拟测试,这使得它在测试复杂的内核行为时更加灵活,适合研究 eBPF 验证器的安全性。
- Buzzer
- 架构与原理:Buzzer 是 Google 开发的开源 eBPF 模糊测试框架,专注于测试 eBPF 验证器的安全性。它通过生成大量的 eBPF 程序(每分钟约 35,000 个)来检测验证器中的逻辑错误。Buzzer 的核心原理是使用一个易用的 eBPF 生成库,支持分布式虚拟机测试(计划中),从而高效地覆盖更多测试场景。它通过快速生成测试用例,确保验证器的鲁棒性。
- 不同点:Buzzer 的生成能力非常强大,能够快速产生大量测试用例,这使得它在覆盖率和效率上优于 ebpf-fuzzer。另外,它支持分布式测试的设计,使其更适合大规模的安全研究,区别于 ebpf-fuzzer 的单机模拟测试。
- BRF(BPF Runtime Fuzzer)
- 架构与原理:BRF 是一个学术研究项目,专注于模糊测试 eBPF 运行时,而不是验证器。它通过生成符合 eBPF 验证器语义的程序来测试运行时的行为。BRF 的核心原理是使用高效的模糊测试策略,确保生成的 eBPF 程序不仅能通过验证器,还能在运行时执行,从而发现更深层次的漏洞。它通过优化执行效率和代码覆盖率,显著提升了测试能力。
- 不同点:BRF 与其他工具的最大区别在于它关注 eBPF 运行时,而不是验证器。这使得它能发现验证器无法捕获的运行时错误。同时,BRF 的执行效率和代码覆盖率显著高于 Syzkaller 和 Buzzer,达到了 8 倍和 32 倍的性能提升,适合学术研究和复杂场景测试。
- bpf-fuzzer
- 架构与原理:bpf-fuzzer 是一个在用户空间运行的工具,用于模糊测试 eBPF 验证器。它通过 libfuzzer 和 clang sanitizer 来生成测试用例,并在用户空间中执行测试。它的核心原理是利用用户空间的灵活性,避免了直接操作内核的复杂性,同时使用 sanitizer 检测内存泄漏等错误。它通过生成初始测试用例,覆盖验证器的部分行为,确保测试的全面性。
- 不同点:bpf-fuzzer 在用户空间操作,这使得它比其他工具更容易部署和使用,不需要直接访问内核。然而,它的测试范围主要限于验证器,且发现的漏洞数量较少(至少 1 个错误),不如其他工具那样高效,适合用户空间的 eBPF 测试。
针对以上四个项目,我们对bpf-fuzzer
项目进行详细介绍:
bpf-fuzzer
bpf-fuzzer 是 ioVisor 组织的一个开源项目,旨在通过模糊测试来检测 Linux 内核中的 BPF 验证器的漏洞。BPF 是 Linux 内核的一个子系统,用于在内核空间执行用户提供的程序(如网络过滤器、跟踪器等)。由于 BPF 程序运行在内核中,其安全性至关重要,因此需要通过模糊测试来确保其稳定性和安全性。bpf-fuzzer 的开发背景是应对 eBPF 验证器可能存在的逻辑错误或内存问题,特别是在用户空间进行测试以降低部署复杂性。

bpf-fuzzer 的架构设计使其能够在用户空间运行,减少了对内核直接访问的依赖。其工作流程包括以下几个关键步骤:
- 使用 libfuzzer(LLVM 的模糊测试库)生成随机的 BPF 程序作为测试用例。这些程序旨在模拟各种可能的输入,以触发验证器的潜在漏洞。
- 生成的测试用例被传递给 BPF 验证器,验证器会检查这些程序是否符合安全规则(如无无限循环、无非法内存访问等)。如果验证器处理这些程序时出现问题,bpf-fuzzer 会记录这些错误。
- bpf-fuzzer 集成了 clang sanitizer(内存错误检测工具),用于捕获验证器在处理测试用例时的内存泄漏、缓冲区溢出或其他运行时错误。
- 项目提供了覆盖率报告,显示其测试覆盖了 BPF 验证器的 886/1106 条边,约 80%。
bpf-fuzzer 提供两个主要二进制文件:test_verifier 用于直接测试 BPF 验证器,test_fuzzer 用于生成初始测试用例,支持开发者和研究者在本地环境中进行测试。
bpf-fuzzer
用于模糊测试bpf的verifier
模块,主要关注生成的恶意程序可不可以通过verifier
我们关注的点在 verifier
的代码覆盖率与执行的行为等因素,同时我们知道在内核空间进行fuzz的速度是很慢的,考虑到内核中上下文切换的开销很大,但这些开销和都是我们不关心的事情。
所以该项目把内核函数又在一个库文件里重新hook
了一下,现在bpf
模块就可以使用我们在用户空间定义的基本函数运行,也就是说我们可以在用户态下进行内核模块的fuzz。
首先生成包含eBPF verifier
及其主要函数bpf_check()
的源代码(处理宏并包含头文件),写入.i
文件,这一步是为了获得 verifier
引用的所有内核符号。生成.i
文件的示例:
1 | KERNEL_SRC=/path/to/kernel/to/fuzz-test |
接着编译每个.i
文件并链接到一起,这个过程很复。上一步虽然获得了 verifier
引用的所有的符号声明,但是并未获得所有的定义。例如,已获得kmalloc()
函数的定义,但是没有获得该函数的定义。bpf-fuzzer
是怎么解决的呢?采用user-space hooks
,例如,用用户标准函数malloc()
来定义kmalloc()
,这两个函数的行为是一样的,BPF verifier
不会察觉。
1 | void kfree(const void *addr) { |
5 Appendix
A. 关于bpf_map(Queue and Stack)结构与相关函数
BPF_MAP_TYPE_QUEUE
和 BPF_MAP_TYPE_STACK
在内核版本 4.20 中引入, bpf_map
用于在用户空间和内核空间传递信息。BPF_MAP_TYPE_QUEUE
为 BPF 程序提供 FIFO 存储,BPF_MAP_TYPE_STACK
为 BPF 程序提供 LIFO 存储。这些映射支持 peek、pop 和 push 操作,这些操作通过各自的助手暴露给 BPF 程序。这些操作通过以下方式使用现有的 bpf
系统调用暴露给用户空间应用程序
BPF_MAP_LOOKUP_ELEM
-> peekBPF_MAP_LOOKUP_AND_DELETE_ELEM
-> popBPF_MAP_UPDATE_ELEM
-> push
BPF_MAP_TYPE_QUEUE
和 BPF_MAP_TYPE_STACK
不支持 BPF_F_NO_PREALLOC
。
其中的一些结构和定义如下:
bpf_map_push_elem()
(内核空间下)
1 | long bpf_map_push_elem(struct bpf_map *map, const void *value, u64 flags) |
可以使用 bpf_map_push_elem
助手将元素 value
添加到队列或堆栈中。flags
参数必须设置为 BPF_ANY
或 BPF_EXIST
。如果 flags
设置为 BPF_EXIST
,则当队列或堆栈已满时,将删除最旧的元素,以便为要添加的 value
腾出空间。成功时返回 0
,失败时返回负错误。
bpf_map_update_elem()
(用户空间下)
1 | int bpf_map_update_elem (int fd, const void *key, const void *value, __u64 flags) |
用户空间程序可以使用 libbpf 的 bpf_map_update_elem
函数将 value
推入队列或堆栈。 key
参数必须设置为 NULL
,flags
必须设置为 BPF_ANY
或 BPF_EXIST
,其语义与 bpf_map_push_elem
内核助手相同。成功时返回 0
,失败时返回负错误。
bpf_map_lookup_elem()
(用户空间下)
1 | int bpf_map_lookup_elem (int fd, const void *key, void *value) |
用户空间程序可以使用 libbpf bpf_map_lookup_elem
函数查看队列或堆栈头部的 value
。 key
参数必须设置为 NULL
。成功时返回 0
,失败时返回负错误。
在向内核传递的bpf
程序中,我们可以通过如下方式来定义一个map
此代码段显示如何在 BPF 程序中声明队列
1 | struct { |
在用户态空间下,我们也可以用更简单的方法声明一个可用于数据传递和共享的map
1 | int create_queue() |
B. 关于modprobe_path覆写的内核攻击手法
modprobe_path
是用于在Linux
内核中添加可加载的内核模块,当我们在Linux
内核中安装或卸载新模块时,就会执行这个程序。他的路径是一个内核全局变量,默认为 /sbin/modprobe
,可以通过如下命令来查看该值:

modprobe_path
存储在内核本身的modprobe_path
符号中,且具有可写权限。也即普通权限即可修改该值。
而当内核运行一个错误格式的文件(或未知文件类型的文件)的时候,也会调用这个 modprobe_path
所指向的程序。如果我们将这个字符串指向我们自己的sh
文件 ,并使用 system
或 execve
去执行一个未知文件类型的错误文件,那么在发生错误的时候就可以执行我们自己的二进制文件了。其调用流程如下:
1 | do_execve() |
在__request_module
的源码中我们也可以看到,最终是执行了modprobe_path
指向的程序:
1 | int __request_module(bool wait, const char *fmt, ...) |
在函数call_usermodehelper
中,实际上就会去执行指向的程序, 在内核空间以root权限执行用户指定的程序。
1 | int call_usermodehelper(char *path, char **argv, char **envp, int wait) |
所以我们在普通用户空间去执行chmod 777 flag
的时候肯定是不被允许的,因为我们没有对flag
文件的修改权限,但是我们可以覆盖modprobe_path
再触发内核modehelper
去执行chmod 777 flag
, 这样就可以达到攻击的目的。
但在很多时候
modprobe_path
的符号地址并不会呈现在/proc/kallsyms
中,但是request_mudule中有对该符号的引用,我们可以通过在request_module
打断点来查看modprobe_path
的地址~
C. 关于task_struct覆写的内核攻击手法
fork
进程/线程时,copy_process()
会给此线程alloc
一个struct pid
结构体。当是fork
进程/线程时,copy_process()
的pid
参数将是null
,所以会call alloc_pid()
1 | static __latent_entropy struct task_struct *copy_process( |
在内核中,ksymtab保存着init_pid_ns结构的偏移,而kstrtab则保存着init_pid_ns的字符串。通过GDB调试可以观察到这一过程:

查找的核心思路是首先通过搜索"init_pid_ns"字符串来获得kstrtab_init_pid_ns的地址。检查某个地址加上该地址处的四个字节偏移值是否等于kstrtab_init_pid_ns的地址,这样可以判断是否为ksymtab_init_pid_ns。找到的地址实际上是ksymtab_init_pid_ns+4,因此需要减去4才能得到真正的ksymtab_init_pid_ns地址。最终通过ksymtab_init_pid_ns地址加上其中保存的init_pid_ns结构偏移,就能获得init_pid_ns结构的实际地址。
一旦获得了pid和init_pid_ns,就需要通过它们查找对应进程的task_struct。这个过程实际上是在模拟内核的查找机制。内核使用find_task_by_pid_ns函数来实现这一功能:
1 | struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns) |
这个函数的参数nr是当前进程的pid,ns是init_pid_ns结构地址。函数内部首先调用find_pid_ns来查找对应的pid结构,然后通过pid_task获取最终的task_struct。
find_pid_ns函数进一步调用了idr_find函数,这里需要获取idr字段的内容。idr_find的实现相对简单,它调用radix_tree_lookup函数在基数树中进行查找:
1 | void *idr_find(const struct idr *idr, unsigned long id) |
这里需要注意两个关键参数:idr_rt和idr_base。
基数树查找是整个过程中最复杂的部分。__radix_tree_lookup函数实现了核心的查找逻辑。函数首先将root->xa_head的值赋给node变量,然后进入循环查找过程。在循环中,通过radix_tree_descend函数不断向下搜索,直到找到目标进程的node。
1 | static unsigned int radix_tree_descend(const struct radix_tree_node *parent, |
radix_tree_descend函数通过读取parent->shift的值并与0x3f进行与运算来计算偏移,然后获取parent->slots[offset]作为下一个node。当parent->shift为0时,说明已经找到了当前进程的node,此时退出循环。
获得当前进程的node后,需要通过pid_task函数获取相应的task_struct。这个函数使用PIDTYPE_PID作为类型参数,其值为0。
1 | struct task_struct *pid_task(struct pid *pid, enum pid_type type) |
函数首先获取pid->tasks[0]的内容,这里的first实际上是pid_links[0]的地址。通过hlist_entry宏,可以从这个地址反推出task_struct的起始地址。相关的结构字段偏移可以通过GDB查看,task_struct中pid_links[0]的偏移为0x500,而pid结构中tasks[0]的偏移为0x8。
整个查找过程完成后,就能够定位到task_struct中cred字段的位置,从而实现对进程凭证的访问和操作。这一机制在内核利用开发中具有重要意义,因为它揭示了内核如何组织和管理进程信息的内部结构。

D. 关于map_get_next_key触发任意地址写
首先我们知道map_push_elem
的函数声明:
1 | long bpf_map_push_elem(struct bpf_map *map, const void *value, u64 flags) |
可以使用 bpf_map_push_elem
助手将元素 value
添加到队列或堆栈中。flags
参数必须设置为 BPF_ANY
或 BPF_EXIST
。如果 flags
设置为 BPF_EXIST
,则当队列或堆栈已满时,将删除最旧的元素,以便为要添加的 value
腾出空间。成功时返回 0
,失败时返回负错误。
我们再来看一下map_get_next_key
的函数实现:
1 | static int array_map_get_next_key(struct bpf_map *map, void *key, void *next_key) |
如果我们将map_push_elem
的指针覆盖为map_get_next_key
就会有:
map
=map
value
=key
flags
=next_key
这样在执行*next = next_key
时,实际上就是将next = flags
, 同时注意到index = *key
, *key
即是bpf_map
中的value[0]
, key
为bpf_map
中首个键值对中值的地址。
当执行到*next = index + 1
时,实际上就是*flags = value[0] + 1
这样这要我们在输入的时候控制flags
的值为我们想要写的地址,value[0]
是bpf_map
中可控的值,我们就可以触发内核空间任意地址写。
需要注意,
bpf_map
调用map_push_elem
的前提是map
的类型是BPF_MAP_TYPE_QUEUE
或者BPF_MAP_TYPE_STACK
, 在触发该漏洞之前必须通过其他手段更改map
的类型字段。
E. 关于verifier
对寄存器的精确标记
精确标记存储在 bpf_reg_state
结构中,定义在include/linux/bpf_verifier.h
:
1 | struct bpf_reg_state { |
精确追踪实际上是为了辅助verifier
进行剪枝,在下面几种情况下需要对寄存器进行精确标记:
1. 指针运算
当一个寄存器用于指针偏移(如 r0 += r6
),r6
被标记为精确,因为其值直接影响内存访问的安全性。
1 | 0: (b7) r6 = 4 // r6 = 4 |
第 7 行:
r0 += r6
,r6
用于指针偏移,影响r0
的地址。第 8 行:读取
r0
,需确保r0
在映射值边界内(0 到 7 字节)。r6
被标记为精确(reg->precise = true
),因为其值(4)决定指针偏移。验证器回溯确保
r6
在所有路径中是精确的常数(R6=4
)。验证器状态:
1
2
30: R6_w=4
7: R0=map_value(off=0,ks=4,vs=8) R6=4 (R6: P)
8: 访问 r0 + 4: 安全
2. 辅助函数调用
某些函数(如 bpf_map_lookup_elem
)要求参数是精确的常数。
1 | 0: (b7) r6 = 0 // r6 = 0 |
第 5 行:
*(u32 *)(r10 - 4) = r6
,r6
写入栈,作为bpf_map_lookup_elem
的键。第 6 行:调用
bpf_map_lookup_elem
,要求键(r10 - 4
的值)是精确常数。r6
被标记为精确,因为其值(0)被写入栈,栈槽r10 - 4
用作函数参数。验证器确保
r6
在所有路径中是精确的(R6=0
)。验证器状态:
1
2
30: R6_w=0
5: fp-4=0 (R6: P)
6: R0=map_value_or_null
3. 条件跳转 (在cve-2023-2163修复中新增)
如果一个条件跳转(如 if r6 <= r9
)影响一个精确寄存器的值,则相关寄存器可能需要标记为精确。
1 | 0: (b7) r6 = 2 // r6 = 2 |
第 2 行:
if r6 <= r9
,r6
和r9
比较,影响r6
的值。第 8 行:
r6
写入栈,需精确(用于bpf_map_lookup_elem
)。r6
被标记为精确(R6: P
),因为其值写入栈。r9
也被标记为精确(R9: P
),因为第 2 行的条件跳转(r6 <= r9
)约束了r6
的值。
1 | 0: R6_w=2 |
- 跳转到 4:
r6 <= 1
不成立(2 > 1
),不触发。 - 继续到 3:
r6 = 0
,写入栈,安全。
4. 依赖传播
如果一个精确寄存器的值依赖于其他寄存器或栈槽,这些依赖项也会被标记为精确。
1 | 0: (b7) r7 = 8 // r7 = 8 |
第 1 行:
r6 = r7
,r6
的值依赖于r7
。第 6 行:
r6
写入栈,需精确(用于bpf_map_lookup_elem
)。第 10 行:
r0 += r6
,r6
用于指针运算,需精确。r6
被标记为精确(R6: P
),因为其用于栈写入和指针运算。r7
被标记为精确(R7: P
),因为r6
的值依赖于r7
(第 1 行)。验证器状态:
1
2
3
4
50: R7_w=8 (R7: P)
1: R6_w=8 (R6: P)
6: fp-4=8 (R6: P)
10: R0=map_value(off=8) (R6: P)
11: 访问 r0 + 8: 安全r7
的精确标记传播到r6
,确保r6 = 8
在所有路径一致。- 偏移 8 等于映射值大小(8 字节),验证器确认安全(边界情况)。
在verifier
发现某个寄存器需要被标记为精确,backtrack_insn()
会被调用,回溯指令以传播精确标记:
1 | static int backtrack_insn(struct bpf_verifier_env *env, int idx, |
运行时示例输出如下:
1 | frame 0: propagating r6 |
参考文献
- Hung, H. W., & Sani, A. A. (2023). BRF: eBPF Runtime Fuzzer. arXiv preprint arXiv:2305.08782. https://arxiv.org/abs/2305.08782
- López Jaimez, J. J., & Inge, M. (2023). Buzzer: An eBPF Fuzzing Framework. Google Security Blog. https://github.com/google/buzzer
- Zhou, S., Jiang, M., Chen, W., Zhou, H., Wang, H., & Luo, X. (2024). Toss a Fault to BpfChecker: Revealing Implementation Flaws for eBPF runtimes with Differential Fuzzing. In Proceedings of the 2024 ACM SIGSAC Conference on Computer and Communications Security (pp. 939-950). ACM.
- Nelson, L., Van Geffen, J., Torlak, E., & Wang, X. (2020). Specification and verification in the field: Applying formal methods to BPF just-in-time compilers in the Linux kernel. In Proceedings of the 14th USENIX Conference on Operating Systems Design and Implementation (OSDI '20). USENIX Association.
- Vishwanathan, H., Shachnai, M., Narayana, S., & Nagarakatte, S. (2023). Automatically and formally proves the ranges analysis of the Linux verifier. In Proceedings of the Computer Aided Verification Conference (CAV '23).
- Sun, H., & Su, Z. (2024). Devises a test oracle to fuzz the eBPF verifier. In Proceedings of the 18th USENIX Symposium on Operating Systems Design and Implementation (OSDI '24). USENIX Association.
- Krishna, P. (2024). Grammar-based Fuzzing of the eBPF Verifier for Security Testing. Master's Thesis, CISPA Helmholtz Center for Information Security.
- Zhang, P., Wu, C., Meng, X., Zhang, Y., Peng, M., Zhang, S., Hu, B., Xie, M., Lai, Y., Kang, Y., & Wang, Z. (2024). SafeBPF: Hardware-assisted Defense-in-depth for eBPF. In Proceedings of the IEEE Security and Privacy Conference (SP '24).
- Paul, M. (2020). CVE-2020-8835: Linux Kernel Privilege Escalation via Improper eBPF Program Verification. Zero Day Initiative Technical Report. https://www.thezdi.com/blog/2020/4/8/cve-2020-8835-linux-kernel-privilege-escalation-via-improper-ebpf-program-verification
- Google Bug Hunters. (2023). A deep dive into CVE-2023-2163: How we found and fixed an eBPF Linux Kernel Vulnerability. Google Security Blog. https://bughunters.google.com/blog/6303226026131456/a-deep-dive-into-cve-2023-2163-how-we-found-and-fixed-an-ebpf-linux-kernel-vulnerability
- Bangladesh e-Government CIRT. (2022). Linux Kernel eBPF local privilege escalation (CVE-2022-23222) vulnerability. Technical Advisory. https://www.cirt.gov.bd/linux-kernel-ebpf-local-privilege-escalation-cve-2022-23222-vulnerability/
- Deep Kondah Security Research. (2024). Profiling Libraries With eBPF: Detecting Zero-Day Exploits and Backdoors - CVE-2021-31440 Analysis. Technical Report. https://www.deep-kondah.com/ebpf-library-profiling/
- IOVisor Community. (2019). bpf-fuzzer: Fuzzing framework based on libfuzzer and clang sanitizer. GitHub Repository. https://github.com/iovisor/bpf-fuzzer
- Snorez. (2020). ebpf-fuzzer: Fuzz the linux kernel bpf verifier. GitHub Repository. https://github.com/snorez/ebpf-fuzzer
- Google Security Research. (2023). Introducing a new way to buzz for eBPF vulnerabilities. Google Security Blog. https://security.googleblog.com/2023/05/introducing-new-way-to-buzz-for-ebpf.html
- Linux Kernel Community. (2024). eBPF Instruction Set Specification, v1.0. The Linux Kernel Documentation. https://docs.kernel.org/next/bpf/instruction-set.html
- Linux Kernel Community. (2024). eBPF verifier. The Linux Kernel Documentation. https://www.kernel.org/doc/html/v6.7/bpf/verifier.html
- Linux Kernel Community. (2024). BPF Documentation. The Linux Kernel Documentation. https://www.kernel.org/doc/html/latest/bpf/index.html
- eBPF Foundation. (2024). What is eBPF? An Introduction and Deep Dive into the eBPF Technology. eBPF.io. https://ebpf.io/what-is-ebpf/
- ControlPlane & NCC Group. (2024). eBPF Security Threat Model and Verifier Code Audit. eBPF Foundation Security Report. https://www.linuxfoundation.org/press/threat-model-and-independent-verifier-audit-examine-the-security-of-ebpf
- Red Canary Threat Research Team. (2025). eBPF: A new frontier for malware. Red Canary Security Research. https://redcanary.com/blog/threat-detection/ebpf-malware/
- Liber, M. (2024). Understanding the Security Aspects of Linux eBPF. Pentera Security Research. https://pentera.io/blog/the-good-bad-and-compromisable-aspects-of-linux-ebpf/
- eBPF Foundation. (2024). Research Update: Isolated Execution Environment for eBPF. eBPF Foundation Research Report. https://ebpf.foundationebpf-verifierearch-update-isolated-execution-environment-for-ebpf/
- Tigera. (2024). eBPF Explained: Use Cases, Concepts, and Architecture. Tigera Technical Guide. https://www.tigera.io/learn/guides/ebpf/
- Red Hat. (2024). Analyzing system performance with eBPF. Red Hat Enterprise Linux Documentation. https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/10/html/managing_monitoring_and_updating_the_kernel/analyzing-system-performance-with-ebpf
- Eunomia-eBPF Community. (2024). The Evolution and Impact of eBPF: A list of Key Research Papers from Recent Years. Technical Survey. https://eunomia.dev/blogs/ebpf-papers/
- Alibaba Cloud. (2022). What You Need to Know About eBPF Security Observability. Alibaba Cloud Technical Blog. https://www.alibabacloud.com/blog/what-you-need-to-know-about-ebpf-security-observability_599614
- ricksanchez. (2024). Academic papers related to fuzzing, binary analysis, and exploit dev. Paper Collection Repository. https://github.com/0xricksanchez/paper_collection
- Chaigno, P. (2025). eBPF Research Papers. Interactive Research Paper Database. https://pchaigno.github.io/bpf/2025/01/07ebpf-verifierearch-papers-bpf.html