CVE-2020-27194-ebpf提权漏洞分析及利用
前言
前几天看到玄武公众号推了篇博文,是一位大佬发的bpf的fuzz分享和通过这个fuzz挖到的CVE-2020-27194fuzzing-for-ebpf-jit-bugs-in-the-linux-kernel。大概意思是在用户态模拟出ebpf的verifier来进行fuzz,改进了传统的在内核里fuzz的方式,大大节约了fuzz的时间。其专注于挖逻辑洞,以内存越界读写为判断标准来判定输入数据造成的效果,这个fuzz的思路非常精妙,恰好前几天GEEKPWN遇到了ebpf的题,根据poc可以看到漏洞类型和pwn2own的CVE-2020-8835非常类似,因此花了两天时间来写exp,在写这篇文章的时候已经有几个大佬发布了自己拿到root shell的视频,笔者比较菜,还是拿熟悉的modprobe_path来实现以root权限执行任意命令,后面自己会补一下拿root shell的exp,本文涉及到的文件在这里。
漏洞分析
查看漏洞patch对应的commit,可以看到patch针对scalar32_min_max_or
函数,漏洞产生的root cause在于下面的四行赋值,在进行BPF_OR操作时会将64位的smin_value/umin_value赋给32位的smin_val,并且在满足一定条件时将dst_reg->umin_value/dst_reg->umax_value
赋值给dst->s32_min_value/dst->s32_max_value
。看过CVE-2020-8835
洞的师傅应该知道其漏洞产生的核心也是错用32位函数对64位变量进行操作,在64位数给32位赋值时高位截断,进而造成对输入数据的判断失误,产生越界读写。
1 | static void scalar32_min_max_or(struct bpf_reg_state *dst_reg, |
作者给出了一种典型的漏洞触发方式,首先创建一个map,从map中可以加载值到寄存器中,不妨假设我们将某个可控值加载到r5中,通过BPF_JMP_IMM(BPF_JGT, BPF_REG_5, 0, 1)
设置r5>0
,通过BPF_LD_IMM64(BPF_REG_6, 0x600000002)
和BPF_JMP_REG(BPF_JLT, BPF_REG_5, BPF_REG_6, 1),
设置r5<r6=0x600000002
,进而让r5->umin_value=0x1,r5->umax_value=0x600000001
。再让r5和立即数0进行OR运算BPF_ALU64_IMM(BPF_OR, BPF_REG_5, 0),
,使得dst_reg->s32_min_value=dst_reg->s32_max_value
,verifier认为r5的值恒为1,使用BPF_MOV32_REG(BPF_REG_7, BPF_REG_5),
将r7赋值为r5即可让r7的值也恒为1,假如我们对r5加载的初始值为2,它首先可以绕过1<r5<0x600000001
的检查进行OR
运算,之后被verifier认为值为1.我们使用BPF_ALU64_IMM(BPF_RSH, BPF_REG_7, 1),
对r7进行右移运算,verifier中得到值为0,而实际值为1,这样就产生了检查和实际运行值的不统一,进而可以利用造成越界读写。下面是部分调试的截图。
在进行OR
运算前r5寄存器结构体的值。
在OR
运算赋值后更新的r5寄存器结构体。
在右移运算前的r7寄存器结构体。
在右移运算后的r7寄存器结构体。
从PoC到Exp
走到上面的步骤后其实后面和CVE-2020-8835
的漏洞利用都一样了。
地址泄露
内核以struct bpf_array
结构体管理map,其成员aux->value表征map的地址,我们使用BPF_ALU64_IMM(BPF_MUL,6,0x110), //r6 *= 0x110
得到r6寄存器为0x110,将exp_map的地址存储到r7寄存器后使用BPF_ALU64_REG(BPF_SUB,7,6), //r7 -= r6
让r7指向(struct bpf_array)->array_map_ops
,这是一个位于内核文件中rdata区的数据结构,再通过BPF_LDX_MEM(BPF_DW,0,7,0),
将值存储到r0寄存器,进而将这个值存储回map中,由于r6被verifier认为是0,因此r7-0再取值的结果被认为是一个合法的访问,借此我们可以计算出kaslr的值。
结构体偏移为0xc0处存储着wait_list->next
指向自身地址,借此可以泄露出map的地址以及map_element的地址。
任意地址写
任意地址写在我上篇文章以及CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析这篇文章都有详细阐述,我们利用一个利用链bpf_map_update_value->(map->ops->map_push_elem)->array_map_get_next_key
来构造任意地址写4字节。
控制流劫持
原本想尝试上篇文章的prctl提权,但是后面发现poweroff_cmd是一个不可写的地址,当我尝试进行赋值时,内核直接crash掉,之后搜了下发现有__request_module
,进而找到modprobe_path
来修改,这种思路和前面引用的rtfingc
师傅的思路是一样的,原理是内核有一个全局的变量modprobe_path设置当打开错误格式文件时将会执行的命令,我们设置为一个修改flag权限的bash文件路径即可。
踩坑
这里我分享一下自己编写exp中遇到的坑。在调用完BPF_MAP_GET(0,5)
加载ctrl_map[0]到r5之后要清空r0寄存器,即赋值为一个立即数,否则bpf认为可能存在内存泄露的风险。
在漏洞利用时有个条件需要绕过,即dst_reg->s32_min_value
需要大于等于0,初始值为0x80000000。这里我通过BPF_JMP32_IMM(BPF_JLE, 5, 0x7fffffff, 1)
来设置。
1 | if (dst_reg->s32_min_value < 0 || smin_val < 0) { |
exp.c
1 |
|
利用效果如下。
补充
在提交本文之后我参考de4dcr0w
师傅的文章写了get root shell的exp,原理在师傅的文章中已经阐释地非常清晰,这里需要注意几点:
- 因为我是编译的带符号的内核,因此前面的几个关键地址可以通过符号表直接得到,对于无符号的内核需要爆破得到地址。在构造任意读爆破的过程中有些地址空间没有分配会直接crash掉(或者有读保护),每次crash需要手动调整起始地址,这样大约十次左右可以得到目标地址
- 在exp的编写中有很多结构体成员的偏移需要手动寻找,最好找的方式是在gdb中查找,如果找不到相关的结构体需要看源码里是否define了宏,是否有函数将此类型的变量作为参数,再到IDA里对应函数处查看参数类型,
local types
里可以清楚地看到成员的名字和偏移
参考
CVE-2020-8835 pwn2own 2020 ebpf 提权漏洞分析