EXP
首先贴一下完整的payload
1 | /* |
EXP分析
首先我们看一下prep
函数,首先此函数调用了bpf_create_map
函数创建了一个大小为3的map结构,并返回map的fd赋值给了mapfd
参数(测试后fd值为3)
之后,调用bpf_prog_load
函数申请加载一个自定义的bpf指令集,类型为BPF_PROG_TYPE_SOCKET_FILTER
,需要注意的是,在linux 4.4
之前,bpf这个系统调用是需要CAP_SYS_ADMIN
权限的,但是在linux 4.4
之后,非特权用户可以使用BPF_PROG_TYPE_SOCKET_FILTER
类型和相应的map创建受限的程序,所以这里我们作为非特权用户也只能创建这样的类型
在加载指令集的过程中,实际上就已经将我们的exp虚拟执行验证后加载了,上面一篇blog讲了exp的前四句,也即触发漏洞的部分,接着我们来讲讲exp的剩余指令,也即利用部分
在利用部分的一开头是如下两行指令
1 | "\x18\x19\x00\x00\x03\x00\x00\x00" |
我们来看一下这两句指令在加载的过程中做了些什么
首先,来看看对应的系统调用处理函数的源码
1 | /* last field in 'union bpf_attr' used by this command */ |
可以看到实际上我们的指令在虚拟加载时,并不是像前文一样直接到了do_check
函数,而是先到了bpf_check
函数(前文为了直接讨论核心部分,省略了此部分)
那么来看看bpf_check
函数
1 | int bpf_check(struct bpf_prog **prog, union bpf_attr *attr) |
这里我们主要关注replace_map_fd_with_map_ptr
函数,这个函数主要是将我们之前得到的mapfd转化为map的指针,以保证我们申请的map可以和我们申请的bpf指令联系起来
来看看这个函数的实现
1 | static int replace_map_fd_with_map_ptr(struct verifier_env *env) |
可以从上面的源代码以及我的相应注释中看到,此函数检查我们的指令中是否存在BPF_LD_IMM64
指令,如果存在的话,将指令中的立即数所表示的fd转化为指针,保存指针值在我们的exp的第五行以及第六行的IMM区域
我们第五行的立即数IMM为0x3
,我上面有说过,我们一开始申请的map的fd就是3,所以这里就相当于我们将我们之前申请的map与我们的指令进行了绑定
在注释中我提到为何第六行必须为空,实际上有两个原因,首先是BPF_LD_IMM64
指令的格式就是这么要求的
1 |
|
可以看到,实际上调用BPF_LD_IMM64
指令就是调用了BPF_LD_IMM64_RAW
指令,此指令需要占据普通指令的2倍大小的空间
另外,由于指针的值需要被分为两部分储存,故而确实需要第六条指令为空来做存储
好了,我们接着看下面第七条指令之后的指令
将其解码之后,指令是这样的
1 | // 上面两句payload执行完成后会将map的指针赋值给BPF_REG_9寄存器中 |
另附上参考表
1 | R0 – rax |
先来看看6~13,翻译出来应该是
1 | [6]: r1=r9 |
其中编号9的语句,为ST_MEM_W
类型的指令,表达的意思是*(uint *) (dst_reg + off16) = imm32
,那么带入到此语句即向$rbp-4处赋值0,此举是为了准备参数(r2寄存器),准备调用BPF_FUNC_map_lookup_elem
编号为11的语句,BPF_FUNC_map_lookup_elem
返回的是指针,所以r0中存放的是&map[0]
1 | static u64 bpf_map_lookup_elem(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5) |
在6~13语句执行完成后就相当于将r6赋值为map中第一个元素的值
之后的14-21,22-29都是一样的,分别表示的意思是r7 = map[1]
,r8 = map[2]
从30行开始,翻译如下
1 | [30]: ALU64_MOV_X(0,2,0x0,0x0) r2=r0 // 此时r0为map[2]的地址 |
可以看到,这是最终执行利用的地方
在经过了6~29语句之后,r6 = map[0]
,r7 = map[1]
,r8 = map[2]
而且map中的值是由用户定义的,用户可以通过BPF_MAP_UPDATE_ELEM
这一bpf的调用type来改变map中每一项的值
那么根据上面的翻译
- 如果用户定义
map[0] = 0
,那么此时将会执行33-35语句,也就是尝试读map[1]
中所保存的值所指向的地址中的值,并把读出的值赋值给map[2]
- 如果用户定义
map[0] = 1
,那么此时将会执行37-38语句,也就是尝试将r10寄存器的值赋值给map[2]
,而r10在这里代表的是rbp - 如果用户定义
map[0] = 2
,那么此时将会执行39-40语句,也就是尝试将map[2]
中的值赋值给map[1]
中所保存的值所指向的地址
简而言之,当map[0]
为1、2、3时,分别代表了任意地址读,泄露内核栈地址、任意地址写这三个功能
知道了这些后,exp就十分容易明白了
贴出我修改的exp,在我的机器上运行成功
1 | // Tested on: |
另外贴一份注释中提到的BPF_MAP_LOOKUP_ELEM
的实现
1 | static int map_lookup_elem(union bpf_attr *attr) |