eBPF 与 eBPF Pwn 入门教学文档
1. 什么是 eBPF
eBPF 是一项革命性的 Linux 内核技术,允许用户编写的程序在内核空间中安全、高效地运行。其核心思想是“可编程内核”。
1.1 核心概念
- 附着点:eBPF 程序可以被附加到内核中的几乎任何地方,如网络数据包处理路径、系统调用、内核函数入口/出口、跟踪点等。文档以
seccomp为例,说明其本身就是一个附着在系统调用上的 eBPF 程序,用于在系统调用前执行安全检查。 - 安全模型:eBPF 程序由用户编写并提交给内核,这带来了安全隐患。为确保安全,内核通过 eBPF 验证器 对程序进行严格的静态分析和模拟执行检查,只有通过检查的程序才能被加载。
- 执行架构:eBPF 程序运行在一个专用的、精简的虚拟机中。这个虚拟机有自己的寄存器集、指令集和栈。为了获得接近原生代码的性能,eBPF 字节码通常会被即时编译器 编译为目标平台(如 x86-64)的本地机器码。
eBPF Pwn 的核心目标:通过构造特殊的 eBPF 程序,利用验证器逻辑的漏洞,制造验证器对程序状态的判断与程序实际运行时状态之间的差异(即“不一致寄存器”),从而绕过安全限制,实现内核态代码执行或提权。
2. eBPF 程序架构
2.1 寄存器
eBPF 虚拟机包含 11 个 64 位通用寄存器(R0-R10)和一个程序计数器(PC),一个固定大小的栈(通常为 512 字节)。它们在 x86-64 上的映射关系如下:
| eBPF 寄存器 | 映射 x86_64 寄存器 | 主要用途 |
|---|---|---|
| R0 | rax | 函数返回值,辅助函数调用结果 |
| R1 | rdi | 函数调用第一个参数(argv1),上下文指针 |
| R2 | rsi | 函数调用第二个参数(argv2) |
| R3 | rdx | 函数调用第三个参数(argv3) |
| R4 | rcx | 函数调用第四个参数(argv4) |
| R5 | r8 | 函数调用第五个参数(argv5) |
| R6 | rbx | 被调用方保存寄存器 |
| R7 | r13 | 被调用方保存寄存器 |
| R8 | r14 | 被调用方保存寄存器 |
| R9 | r15 | 被调用方保存寄存器 |
| R10(只读) | rbp | 栈帧指针,指向栈底 |
2.2 验证器
验证器是 eBPF 安全的核心。它对提交的程序执行以下检查:
- 结构验证:确保程序无不可达指令,避免死循环(早期内核直接禁止循环)。
- 模拟执行与状态跟踪:验证器模拟执行每一条指令,跟踪所有寄存器和栈的状态变化,确保没有越界读写、非法指针解引用等不安全行为。
2.3 寄存器状态跟踪
验证器为每个寄存器维护一个 struct bpf_reg_state 结构,主要包含类型信息和值范围信息。
寄存器类型
- NOT_INIT:未初始化,使用会导致验证失败。
- SCALAR_VALUE:标量值(整数),可进行算术运算,但不能直接作为指针进行内存访问。
- POINTER_TYPE:指针类型。验证器会记录指针指向的对象类型(如
PTR_TO_CTX,PTR_TO_MAP_VALUE,PTR_TO_STACK等)以及允许的偏移范围。
关键数据结构
{s,u}{min,max}_value:记录有符号和无符号整数的最小、最大值。struct tnum:跟踪寄存器中哪些位是已知的(value),哪些位是不确定的(mask)。例如(0x0; 0x1)表示低 1 位不确定(可能是 0 或 1),高位确定为 0。
2.4 验证器规则摘要
- 标量与指针:
SCALAR寄存器不能直接当指针解引用。 - 指针运算:
- 允许
ptr + known_safe_offset。 ptr + unknown_scalar之后的结果不能直接解引用,需经边界检查。- 禁止
ptr + ptr。ptr - ptr仅在两个指针指向同一内存对象时允许,结果变为SCALAR。 - 指针只能进行
Add/Sub运算以修改偏移,Mul, Div, Mod, Shift, And, Or, Xor对指针非法。
- 允许
- ALU32 操作:会清零高 32 位。
- 内核指针保护:不能将内核指针(如
sk_buff地址)直接存入 Map 让用户态读取。
2.5 ALU Sanitation
这是针对指针运算的加固机制。当对一个指针寄存器进行加减某个偏移量时,内核会插入额外的指令来验证结果偏移的合法性,确保指针不会越出其被允许的活动范围。
- 低版本绕过:
- 构造“不一致寄存器”,使验证器认为指针值未变,但实际值已变,导致活动范围计算错误,从而越界访问。
- 当指针已处于其活动范围的边界时,加固指令中的
aux->alu_limit - 1可能导致无符号数下溢回绕,使边界变得极大。
- 高版本(v5.11.17+)加强:低版本的直接指针算术越界被禁止。高版本通常需要利用
bpf_skb_load_bytes等辅助函数配合构造的越界参数来实现利用。
3. eBPF 指令与编程
3.1 指令编写辅助宏
推荐使用内核头文件中的宏(如 samples/bpf/bpf_insn.h)来编写指令,提高可读性。
3.2 主要指令类别
3.2.1 ALU 指令
| 宏 | 逻辑 | 说明 |
|---|---|---|
BPF_ALU64_REG(OP, DST, SRC) |
dst = dst OP src |
64位寄存器间运算 |
BPF_ALU32_REG(OP, DST, SRC) |
(u32)dst = dst OP src |
32位寄存器间运算,清零高32位 |
BPF_ALU64_IMM(OP, DST, IMM) |
dst = dst OP imm |
64位寄存器与立即数运算 |
BPF_ALU32_IMM(OP, DST, IMM) |
(u32)dst = dst OP imm |
32位寄存器与立即数运算 |
常见 OP:BPF_ADD/SUB/MUL/DIV/AND/OR/XOR/LSH/RSH/ARSH。 |
3.2.2 赋值指令
| 宏 | 逻辑 | 说明 |
|---|---|---|
BPF_MOV64_REG(DST, SRC) |
dst = src |
64位寄存器拷贝 |
BPF_MOV32_REG(DST, SRC) |
(u32)dst = src |
32位寄存器拷贝 |
BPF_MOV64_IMM(DST, IMM) |
dst = imm32 |
加载32位立即数到64位寄存器 |
BPF_MOV32_IMM(DST, IMM) |
(u32)dst = imm32 |
加载32位立即数 |
3.2.3 内存读写指令
| 宏 | 逻辑 | 说明 |
|---|---|---|
BPF_LDX_MEM(SIZE, DST, SRC, OFF) |
dst = *(size *)(src+off) |
从内存加载到寄存器 |
BPF_STX_MEM(SIZE, DST, SRC, OFF) |
*(size *)(dst+off) = src |
从寄存器存储到内存 |
BPF_ST_MEM(SIZE, DST, OFF, IMM) |
*(size *)(dst+off) = imm |
存储立即数到内存 |
BPF_LD_IMM64(DST, IMM) |
dst = imm64 |
加载64位立即数 |
BPF_LD_MAP_FD(DST, MAP_FD) |
dst = map_ptr |
将 Map 的文件描述符转换为指针 |
内存大小标识:BPF_DW(8字节), BPF_W(4字节), BPF_H(2字节), BPF_B(1字节)。 |
3.2.4 跳转与调用指令
| 宏 | 逻辑 | 说明 |
|---|---|---|
BPF_JMP_REG(OP, DST, SRC, OFF) |
if (dst OP src) PC += off |
寄存器间比较跳转 |
BPF_JMP_IMM(OP, DST, IMM, OFF) |
if (dst OP imm) PC += off |
寄存器与立即数比较跳转 |
BPF_CALL_FUNC(FUNC) |
r0 = helper_func(...) |
调用内核辅助函数 |
常见条件码 OP:BPF_JEQ(==), BPF_JNE(!=), BPF_JGT(> 无符号), BPF_JGE(>= 无符号), BPF_JLT(< 无符号), BPF_JLE(<= 无符号)。 |
3.2.5 其他重要指令
BPF_EXIT_INSN(): 结束程序,返回值在 R0 中。
3.3 编程注意事项
- 调用辅助函数(如
bpf_map_lookup_elem)后,必须检查返回值 R0 是否为 NULL,否则验证器会拒绝程序。BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem), BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), BPF_EXIT_INSN(), - eBPF 程序末尾通常需要设置返回值并退出:
BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN(),
4. 利用与攻击面 (eBPF Pwn)
4.1 核心目标:构造不一致寄存器
攻击的核心是找到验证器逻辑漏洞,制造一个寄存器,在验证器看来是某个值(例如 0),但在实际运行时是另一个值(例如 1)。这通常通过研究 adjust_scalar_min_max_vals 等验证器核心函数中的边界条件或错误假设来实现。
4.2 攻击原语:操控 BPF Map
创建 BPF_MAP_TYPE_ARRAY 类型的 Map 时,内核会分配一个 struct bpf_array 结构。
struct bpf_array的第一个成员是struct bpf_map map。struct bpf_map的第一个成员是const struct bpf_map_ops *ops,这是一个包含各种操作函数指针的虚表(例如map_lookup_elem,map_update_elem)。- Map 的数据(
value)存储在struct bpf_array之后的柔性数组里。
利用思路:
- 泄露地址:利用越界读,将
bpf_array->map.ops(指向内核.data段的虚表)读出来,从而计算内核基址。 - 任意地址读写:
- 在可控的
value区域伪造一个bpf_map_ops虚表。 - 利用漏洞(如越界写)将某个 Map 的
map->ops指针覆盖为指向我们伪造的虚表。 - 当内核调用该 Map 的虚函数(如
update_elem)时,实际上会跳转到我们控制的函数指针,配合map->map_type等参数的篡改,可以实现任意地址读/写。
- 在可控的
4.3 利用链概览
- 构造漏洞:编写 eBPF 程序,触发特定的验证器漏洞(如错误的位移运算处理),产生一个“不一致寄存器”。
- 越界访问:利用这个寄存器对指针进行运算,使验证器认为访问合法,实际却越界读写了相邻的
struct bpf_array结构。 - 信息泄露:越界读出
ops指针,计算出内核基址和关键函数地址。 - 篡改虚表:在 Map 的
value区布置伪造的ops,并利用越界写将目标 Map 的ops指针指向它。 - 实现任意读写:通过调用被劫持的 Map 操作函数,实现任意地址读/写原语。
- 提权:修改当前进程的 cred 结构体,或执行内核 ROP 链,获得 root 权限。
5. 调试与诊断
- /proc 接口:
/proc/sys/net/core/bpf_jit_enable:控制 JIT 编译(0 关闭,1 开启,2 开启并输出调试 trace)。/proc/sys/kernel/unprivileged_bpf_disabled:控制非特权用户调用 bpf(2) 的权限。
- 内核调试:可以通过在
array_map_alloc,sk_filter_trim_cap等关键函数下断点,结合 JIT 编译后的代码区域,单步跟踪 eBPF 程序的执行。
6. 例题分析思路
6.1 D^3CTF 2022 d3bpf
- 漏洞:内核 Patch 错误地处理了
BPF_RSH指令。当右移位数>= insn_bitness(64)时,验证器认为结果寄存器为已知的 0,但实际 CPU 执行时会将移位计数& 63,导致实际执行reg >> 0,值不变。 - 利用:利用此漏洞构造一个验证器认为值为 0,实际值很大的“不一致寄存器”,用于与指针相减,实现越界访问
struct bpf_array的ops域,进而泄露内核基址并篡改虚表。
6.2 UofTCTF 2026 extended-eBPF
- 漏洞:内核 Patch 禁用了 ALU Sanitation,并修改了
is_safe_to_compute_dst_reg_range中对于移位指令安全性的判断逻辑。 - 利用:构造验证器认为值为 1,实际值为 0 的“不一致寄存器”(通过
1 ARSH reg,其中reg的umin_val为 0)。利用此寄存器进行后续计算,实现越界读写。通过篡改 Map 的map_type和ops虚函数指针,将map_update_elem调用转化为对map_get_next_key的调用,并利用该函数的写操作实现任意地址写。
6.3 DownUnderCTF 2025 Rolling Around
- 漏洞:内核为 eBPF 新增了
ROL指令,但验证器模拟 32 位循环左移的算法 (>> (64 - imm)) 与 JIT 编译器生成的代码实际执行逻辑 (>> (32 - imm)) 不一致。 - 利用:利用此不一致性构造“不一致寄存器”,进而实现越界读写和利用。
7. 总结
eBPF Pwn 是一个深度依赖对 Linux 内核 eBPF 子系统,特别是验证器实现细节理解的攻击领域。攻击者需要:
- 精读源码:深入分析
kernel/bpf/verifier.c,尤其是adjust_scalar_min_max_vals等关键函数。 - 理解状态跟踪:掌握验证器跟踪寄存器状态(类型、范围、tnum)的模型。
- 寻找逻辑差异:在验证器的模拟逻辑与 JIT 编译后的实际执行逻辑之间寻找差异,这是漏洞的主要来源。
- 熟悉利用模式:掌握通过篡改 BPF Map 虚表实现信息泄露和任意地址读写的通用利用链。
本教学文档基于提供的链接内容整理,涵盖了 eBPF 基础、架构、安全机制、编程方法及漏洞利用的核心知识框架,为进一步研究和实战提供了详尽的基础。