CVE-2022-23222 Linux内核eBPF框架漏洞分析
0x00.前置知识
本篇涉及的linux源码,版本为5.9.10。
eBPF框架介绍
eBPF全称extended Berkeley Packet Filter,中译为扩展的伯克利-包过滤器。其主要功能是以用户身份来对linux内核的一些函数进行扩展以及hook,从而实现对socket网络封包进行过滤之类的功能。
eBPF本质上是一个沙盒虚拟机,用户可以通过编写bpf指令,并利用sys_bpf系统调用将指令附加到一些内核函数上,不过大部分功能都只有特权用户才能使用,普通用户只能实现极少数的功能。
bpf指令可以直接对内核内存进行操作,可扩展性高,所以内核需要对用户提供的bpf指令进行验证,保证其不会进行越界等违规操作。
1.verifier验证器
bpf指令的验证代码主要位于kernel/bpf/verfier.c文件中。
它会对bpf指令进行的检查大致如下:
- 权限校验,只有关闭unprivileged_bpf_disabled,普通用户才有资格对socket或cgroup_skb进行bpf操作.
- Call、Jmp等校验,bpf程序要求不能有循环、不能有函数嵌套(直接间接都不行),函数参数校验,bpf函数只允许拥有5个参数。
- verifier程序会模拟执行bpf指令,但不会对内存进行实际操作,它会遍历所有的指令路径,并对寄存器进行范围检查。例如基于if语句对寄存器值进行范围界定,若寄存器范围不符合指针所能操作的范围,某指令却将指针寄存器与该寄存器相加,将会把指令定义为非法,导致bpf程序载入失败。
- 为了防止路径爆炸,verfier也会对指令执行次数进行限制,并对路径状态进行记录,当执行到某个路径时,发现曾以相同的状态执行到该路径,便会进行剪枝操作。
要注意的点是,bpf程序默认初始的内存指针只有,main函数一开始传入给BPF_REG_1的skb指针,以及BPF_REG_10存储的栈指针(bpf的栈是直接使用的内核栈,即如果能发生一些越界,则可以直接进行返回地址修改)。其他所有的内存指针都必须通过函数调用获得,寄存器存储指针会进行标记,所以直接赋值寄存器一个指针是无效的。
2.bpf指令执行
bpf指令载入后,可以使用BPF_PROG_TEST_RUN的bpf系统调用功能点进行执行。
其实际执行bpf指令的函数则是位于kernel/bpf/core.c文件下的___bpf_prog_run。
verfier验证之后,内核会认为其bpf指令足够安全,几乎不会再进行任何校验,不过会有个简单的校验,后文会提到。
3.bpf map相关知识
这个可以看看我之前有关bpf的文章,里面讲了那几个map函数,实际就是可以创建一个内存区域供bpf程序使用。
0x01.漏洞成因
bpf寄存器拥有如下这些类型:
1 | //include/linux/bpf_verifier.h:43 |
其中NOT_INIT表示未初始化,SCALAR_VALUE表示标量,即普通数字,其他类型基本上就是与内存指针相关,而这些类型之下还有一个OR_NULL的类别。
当通过bpf指令调用bpf_map_lookup_elem函数去获取相应索引的value时,会有两种可能,一种是返回正常的指向value的指针,另一种则是索引不在范围内或者其他变故导致返回NULL,而OR_NULL类型的寄存器就表示存储类似bpf_map_lookup_elem函数返回值的寄存器,在没有进行if null校验前,这种寄存器的值和类型都是不确定的。
理论上来说,就不能对该寄存器进行任何alu操作,但实际上,alu函数的检查不严导致部分OR_NULL类型的寄存器仍然可以被操作。
漏洞函数为adjust_ptr_min_max_vals:
1 | //kernel/bpf/verifier.c:5203 |
adjust_ptr_min_max_vals函数由do_check->check_alu_op->adjust_reg_min_max_vals->adjust_ptr_min_max_vals链调用。
本意是对部分指针寄存器进行alu操作,所以利用以上的switch语句来进行过滤,但过滤并不严格。
查看前文的bpf_reg_type,能够发现如下类型未被包括。
- PTR_TO_BTF_ID_OR_NULL
- PTR_TO_MEM_OR_NULL
- PTR_TO_RDONLY_BUF_OR_NULL
- PTR_TO_RDWR_BUF_OR_NULL
这就导致了我们可以对一些OR_NULL寄存器进行alu操作。
接下来我们需要知道模拟执行的以下几点:
1.每个寄存器都有一个id,当使用mov指令将一个寄存器传递给另一个寄存器时,会直接进行*dst_reg = *src_reg;的操作,故id也会被复制过去。
1 | //kernel/bpf/verifier.c:6247 |
2.当执行JNE或者JEQ判断一个OR_NULL类型的寄存器是否为NULL时,寄存器为NULL的分支将会把所有同id的寄存器状态进行修改,包括值与类型。
先是__mark_reg_known(reg, val);将其值设置为val即NULL,不过此时也还只清空了比较的寄存器。
1 | //kernel/bpf/verifier.c:6675 |
后续调用了mark_ptr_or_null_regs,循环处理所有符合标记的reg。
1 | //kernel/bpf/verifier.c:7220 |
1 | //kernel/bpf/verifier.c:6964 |
最后mark_ptr_or_null_reg会进行reg->off清空以及reg->type的修改。
1 |
|
因此在verifier程序,所有和该OR_NULL寄存器同id的寄存器全部设置为值为0的标量。
3.实际运行过程中,寄存器是没有类型区分的,而这些相关的寄存器的值也不会在比较之后设置为0。毕竟实际运行并非模拟执行,寄存器的值都是动态的。
综上可以了解到,OR_NULL寄存器在模拟执行中是可以被alu操作的,并且在if判断后verifier会认为所有有关寄存器的值都是0,但实际运行过程并非如此,从而产生差异。
0x02.漏洞验证
在了解漏洞成因之后,就可以进行简易的POC编写进行漏洞验证了。
1 | //POC |
执行就可以泄露数据了。
前文中有提到要先减0x108再加BPF_REG_7,后面还得再去加0x100,这里加0x100的原因是,bpf它有个动态的指针check,会去修改指令,这里会将指针寄存器+标量寄存器的指令进行了修改.
BPF_ALU64_REG(BPF_ADD, BPF_REG_2, BPF_REG_1),指令实际如下:
MAX_BPF_REG = 0x107 (栈底基于当前指针的最大偏移)**
MAX_BPF_REG -= BPF_SRC_REG
MAX_BPF_REG |= BPF_SRC_REG
MAX_BPF_REG = -MAX_BPF_REG
MAX_BPF_REG >>= 0x3f
MAX_BPF_REG &= BPF_SRC_REG
BPF_DST_REG += __MAX_BPF_REG
这里实际就是将BPF_SRC_REG与MAX_BPF_REG进行比较,大于就直接归0了。
所有的指针都会添加这层检查,可以有效杜绝直接越界。
感兴趣可以在___bpf_prog_run函数这里下断点,跟一下,就会发现这里的指令被修改了。
1 | //kernel/bpf/core.c:1679 |
绕过方式也很简单,这里只会对指针寄存器+标量寄存器的指令进行动态检测,但是指针寄存器+立即数并不会进行检测,所以我们就可以先加上一个寄存器让指针仍在范围内,再去加上立即数实现绕过。
0x03.漏洞利用
前文中,我们构造栈上的越界读取从而泄露了栈上数据,内核基地址被我们泄露出来了,实际这里还可以进行越界写,所以我们可以修改栈的返回地址打ROP。
具体怎么操作我就不细写了,就是kernel的简单rop了,不过是bpf指令形式的构造。
1 |
|
这里要注意的点就是,传入的user_sp最好是32位地址,因为bpf独特的mov操作,它的imm都是s32的类型,如果使用high<<32+low的形式,也会出现问题,如果low是32位负数,加上去会进行符号扩展,然后高32位就莫名其妙减一了,就很淦。
0x04.总结
有的时候分析CVE是真的难,这个bpf框架我审了好久,再加上资料不多,还是英文文档,就搞得很折磨人。
这个漏洞成因主要是switch进行黑名单过滤,并过滤不严导致的,也给我带来了新的思考,还是很不错的。
0x05.参考资料
CVE-2022-23222 eBPF verifier 提权漏洞分析
- Title: CVE-2022-23222 Linux内核eBPF框架漏洞分析
- Author: 0rb1t
- Created at : 2025-02-16 21:09:26
- Updated at : 2025-02-17 12:30:48
- Link: https://redefine.ohevan.com/2025/02/16/CVE-2022-23222-Linux内核eBPF框架漏洞分析/
- License: This work is licensed under CC BY-NC-SA 4.0.