【技术分享】CVE-2021-3490 eBPF 32位边界计算错误漏洞利用分析
【技术分享】CVE-2021-3490 eBPF 32位边界计算错误漏洞利用分析
原创 bsauce 安全客 2022-07-15 10:00
影响版本
:Linux 5.7-rc1以后,Linux 5.13-rc4 以前;v5.13-rc4已修补,v5.13-rc3未修补。评分7.8分。
测试版本
:Linux-5.11 和 Linux-5.11.16 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory
编译选项
:CONFIG_BPF_SYSCALL
,config所有带BPF字样的。 CONFIG_SLAB=y
General setup
—-> Choose SLAB allocator (SLUB (Unqueued Allocator))
—-> SLAB
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。
参考
漏洞描述
:Linux内核中按位操作(AND、OR 和 XOR)的 eBPF ALU32 边界跟踪没有正确更新 32 位边界,造成 Linux 内核中的越界读取和写入,从而导致任意代码执行。三个漏洞函数分别是 scalar32_min_max_and() 、scalar32_min_max_or()、scalar32_min_max_xor()。AND/OR
是在 Linux 5.7-rc1 中引入,XOR
是在 Linux 5.10-rc1中引入。
补丁
:patch 若低32位都为 known,则调用 __mark_reg32_known(),将32位边界设置为reg的低32位(常数),保证最后更新边界时,有正确的边界。
保护机制
:开启KASLR/SMEP/SMAP。
利用总结
:利用verifier阶段与runtime执行阶段的不一致性,进行越界读写。泄露内核基址、伪造函数表、实现任意读写后篡改本线程的cred。
漏洞分析
参考:BPF介绍和相似漏洞分析,可参考
CVE-2020-8835利用
,里面也有var_off
也即tnum
结构的含义。总之,其成员 value
表示确定的值,mask
对应的位是1则表示该位不确定。
漏洞根源:eBPF指令集可以对64位寄存器或低32位进行操作,verifier
也会对低32位进行范围追踪:{u,s}32_{min,max}_value
。每次进行指令操作,有两个函数会分别更新64位和32位的边界,在
adjust_scalar_min_max_vals()
中调用这两个函数。很多BPF漏洞都出现在对32位边界的处理上。CVE-2021-3490也出现在32位运算 BPF_AND
、BPF_OR
、BPF_XOR
中。
1-1 代码跟踪
漏洞调用链:
adjust_scalar_min_max_vals()
->
scalar32_min_max_and()
[1]:对比32位和64位的BPF_AND
操作。低32位 BPF_AND
中,若 src_reg
和 dst_reg
都为 known,则不用更新32位的边界(开发者假设,反正之后还是会调用
scalar_min_max_and()
->
__mark_reg_known()
来标记寄存器的,所以暂时不用处理),直接返回。64位 BPF_AND
中,若 src_reg
和 dst_reg
都为 known,则调用
__mark_reg_known()
将寄存器标记为 known。
问题:
scalar32_min_max_and()
32位中,*_known
变量是调用
tnum_subreg_is_const()
来计算的,而
scalar_min_max_and()
64位中是调用
tnum_is_const()
来计算的。区别是,前者只判断低32位的 tnum->mask
来判断是否为 known,后者则判断整个64位是否为 known。如果某个寄存器的高32位不确定,而低32位是确定的,则
scalar_min_max_and()
也不会调用
__mark_reg_known()
来标记寄存器。
[2]:接着
adjust_scalar_min_max_vals()
会调用以下三个函数来更新 dst_reg
寄存器的边界。每个函数都包含32位和64位的处理部分,我们这里只关心32位的处理部分。reg 的边界是根据当前边界和 reg->var_off
来计算的。
1-2 触发漏洞
BPF代码示例:例如指令BPF_ALU64_REG(BPF_AND, R2, R3)
,对 R2 和 R3 进行与操作,并保存到 R2。
– R2->var_off = {mask = 0xFFFFFFFF00000000; value = 0x1}
,表示R2低32位已知为1,高32位未知。由于低32位已知,所以其32位边界也为1。
- R3->var_off = {mask = 0x0; value = 0x100000002}
,表示其整个64位都已知,为 0x100000002
。
更新R2的32位边界的步骤如下:
– 先调用
adjust_scalar_min_max_vals()
->
tnum_and()
对 R2->var_off
和 R3->var_off
进行AND操作,并保存到 R2->var_off
。结果R2->var_off = {mask = 0x100000000; value = 0x0},由于R3是确定的且R2高32位不确定,所以运算后,只有第32位是不确定的。
-
再调用
adjust_scalar_min_max_vals()
->
scalar32_min_max_and()
,会直接返回,因为R2和R3的低32位都已知。 -
再调用
adjust_scalar_min_max_vals()
->
__update_reg_bounds()
->
__update_reg32_bounds()
,会设置 u32_max_value = 0
,因为 var_off.value = 0 < u32_max_value = 1。同时,设置 u32_min_value = 1
,因为 var_off.value = 0 < u32_min_value
。带符号边界也一样。 -
__reg32_deduce_bounds()
和
__reg_bound_offset()
对边界不作任何改变。最后得到寄存器 R2 — {u,s}32_max_value = 0 < {u,s}32_min_value = 1
。
1-3 调试BPF的方法
写和调试BPF程序:可使用rbpf。
verifier 日志输出:
加载BPF程序时进行如下设置,即可在verifier
检测出指令错误时输出指令信息。正常调试时,可以下源码断点,断在do_check()
函数中,具体观察 verifier
检查每条指令时寄存器的状态。
runtime调试:如果BPF通过了verifier
检查,如何获取BPF程序运行时的信息呢?答案是插桩。ALU Sanitation
也是运行时检查指令执行情况的保护机制,可以通过插桩观察BPF指令是否已经改变。这里需要了解一个编译选项,编译时设置CONFIG_BPF_JIT
,则BPF程序在verifier验证后是JIT及时编译的;如果不设置该选项,则采用eBPF解释器来解码并执行BPF程序,代码位于kernel/bpf/core.c:___bpf_prog_run()
。
regs
指向寄存器值,insn
指向指令。为了获取每条指令执行时的寄存器状态,可以关闭CONFIG_BPF_JIT
选项并插入printk
语句。示例如下:
漏洞利用linux v5.11.7及以前版本
特点:我们采用Linux v5.11
版本的内核进行测试,特点是不需要绕过一种ALU Sanitation,之后我们会详细介绍。
总目标:构造 r6
寄存器,使得 verifier
认为 r6
等于0,但实际执行时等于1。
2-1 触发漏洞
首先,我们需要构造出两个寄存器的值状态,分别为var_off = {mask = 0xFFFFFFFF00000000; value = 0x1}
和 var_off = {mask = 0x0; value = 0x100000002}
。然后触发漏洞,得到 r6
的 u32_max_value = 0 < u32_min_value = 1
。
注意:实际从map传入的 r5 = r6 = 0
。
2-2 构造 verifier:0 tuntime:1
r6 += r5分析:目前寄存器状态,r6—u32_min_value=2, u32_max_value=1, var_off = {mask = 0x100000000; value = 0x1}
,r5—u32_min_value=0, u32_max_value=1, var_off = {mask = 0xFFFFFFFF00000001; value = 0x0}
。
接着 adjust_scalar_min_max_vals()
会调用 __update_reg_bounds()
、__reg_deduce_bounds()
、__reg_bound_offset()
。
– __update_reg32_bounds()
中,var_off
表示低32位,reg->u32_min_value = max{2, 0} = 2
,reg->u32_max_value = min{2, 0 | 0x3} = 2
(var32_off.mask = 3
)。
-
__reg32_deduce_bounds()
未做修改,因为 signed 32
和 unsigned 32
都相等。 -
__reg32_deduce_bounds()
中,tnum_range()
返回常数2(因为u32_min_value = u32_max_value=2
该范围内只有2),由于reg->var_off.mask = 0x3
,所以 tnum_intersect()
返回低2位是 known且为2。
最终得到 r6: {u,s}32_min_value = {u,s}32_max_value = 2, var_off = {mask = 0xFFFFFFFF00000000; value = 0x2}
。
此时的 r6—{mask = 0xFFFFFFFF00000000; value = 0x2} verifier:2 runtime:1
,只需取低32位并 AND 1
,即可得到 verifier:0 runtime:1
。
2-3 提权
后面的利用步骤和CVE-2021-31440一样,参照
CVE-2021-31440 eBPF边界计算错误漏洞
的exp即可提权。
3. 漏洞利用 Linux v5.11.8 – 5.11.16 版本
特点:我们采用 Linux v5.11.16
版本的内核进行测试,Ubuntu 21.04就是这个版本。2021年3月修复了一个verifier
计算alu_limit
(与ALU Sanitation
安全机制有关)时的整数溢出漏洞——
commit 10d2bb2e6b1d8c
,导致 Linux 5.11.8 – 5.11.16
这个版本区间的内核无法利用成功。当alu_limit = 0
时会触发该漏洞,例如,当对map地址指针进行减法操作时(之前exp这么写,是为了构造越界访问,如泄露内核基址,或者修改map内存之前的 bpf_map
结构),会加入如下sanitation指令:0-1
将得到 aux→alu_limit = 0xFFFFFFFF
。
这个漏洞的存在,导致ALU Sanitation
机制失效了,因为 alu_limit
变得很大了,检测不到越界访问,所以之前那些公开的exp都能利用成功。但是这个漏洞被修复以后,就需要绕过这个限制,需要多加5条指令来绕过该机制。
绕过该ALU Sanitation:r7
指向map,r6
是verifier
以为是0而运行时为1的那个值。需要在r7指针进行运算前,使alu_limit != 0
。
– (1)r8 = r6
先拷贝一下—— r8 verifier:0 runtime:1
。
-
(2)r7 += 0x1000
,map指针加上一个常量,以设置alu_limit=0x1000
,这样就能绕过运行时的ALU Sanitation
。 -
(3)r8 = r8 * 0xfff
—— r8 verifier:0 runtime:0xfff
。 -
(4)r7 -= r8
, 由于verifier
以为r8等于0,所以alu_limit
保持不变。 -
(5)r7 -= r6
—— r7 verifier:map+0x1000 runtime:map
。
注意
- 创建map时必须足够大,调用syscall(__NR_BPF, BPF_MAP_CREATE, …)
时第3个参数 bpf_attr->value_size
要大于0x1000,不然执行第2条指令时就会报指针越界的错误。
- 和Linux v5.11
版本相比,还需要修改cred search的相关偏移:
漏洞利用linux v5.11.16后的版本
特点:
目前无法绕过最新的ALU Sanitation
保护机制。2021年4月ALU Sanitation
引入新的 patch—commit 7fedb63a8307,新增了两个特性。
– 一是alu_limit
计算方法变了,不再用指针寄存器的位置来计算,而是使用offset寄存器。例如,假设有个寄存器的无符号边界是 umax_value = 1, umin_value = 0
,则计算出 alu_limit = 1
,表示如果该寄存器在运行时超出边界,则指针运算不会使用该寄存器。
- 二是在runtime时会用立即数替换掉 verifier
认定为常数的寄存器。例如,BPF_ALU64_REG(BPF_ADD, BPF_REG_2, EXPLOIT_REG)
,EXPLOIT_REG
被verifier认定为0,但运行时为1,则 将该指令改为 BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0)
。这个补丁本来是为了防侧信道攻击,同时也阻止了 CVE-2021-3490
漏洞的利用。
检查发现,v5.11.17 已打该补丁,v5.11.16 未打该补丁。所以 v5.11.16 以上版本的内核就无法利用漏洞进行越界读写,不知道以后能不能绕过这个限制。
ALU Sanitation机制
原理
:ALU sanitation
机制一直在进行更新,其目的是为了阻止verifier
漏洞的利用,原理是在runtime运行时检查BPF指令的操作数,防止指针运算越界导致越界读写,其实是对verifier
静态范围检查起到了补充的作用。
如果某条ALU运算指令的操作数是1个指针和1个标量,则计算alu_limit
也即最大绝对值,就是该指针可以进行加减的安全范围。在该指令之前必须加上如下指令,off_reg
表示与指针作运算的标量寄存器,BPF_REG_AX
是辅助寄存器。
– (1)将alu_limit
载入BPF_REG_AX
。
-
(2)BPF_REG_AX = alu_limit – off_reg
,如果 off_reg > alu_limit
,则BPF_REG_AX
最高位符号位置位。 -
(3)若BPF_REG_AUX
为正,off_reg
为负,则表示alu_limit
和寄存器的值符号相反,则BPF_OR
操作会设置该符号位。 -
(4)BPF_NEG
会使符号位置反,1->0,0->1。 -
(5)BPF_ARSH
算术右移63位,BPF_REG_AX
只剩符号位。 -
(6)根据以上运算结果,BPF_AND
要么清零off_reg
要么使其不变。
总体看来,如果off_reg > alu_limit
或者二者符号相反,表示有可能发生指针越界,则off_reg
会被替换为0,清空指针运算。反之,如果标量在合理范围内—0 <= off_reg <= alu_limit
,则算术移位会将BPF_REG_AX
填为1,这样BPF_AND
运算不会改变该标量。
最近更新
:最近更新了alu_limit
的计算方法,见commit 7fedb63a8307d,这里我们对比一下更新前后的计算差异。
– 之前:alu_limit
由指针寄存器的边界确定,如果指针指向map的开头,则alu_limit
可减的大小为0,可加的大小为 map size-1
,并且alu_limit
随着接下来的指针运算而更新。
- 现在:alu_limit
由offset
寄存器的边界来确定,将运行时offset寄存器的值与verifier
静态范围追踪时计算出来的边界进行比较。
参考
Kernel Pwning with eBPF: a Love Story
https://nvd.nist.gov/vuln/detail/CVE-2021-3490
https://github.com/chompie1337/Linux_LPE_eBPF_CVE-2021-3490