源码分析
文档解析
https://docs.ebpf.io/linux/concepts/dynptrs/
dyn-ptr意为动态指针,它有很多其他的meta data用于在运行时辅助安全检查;
dynptr是一个不透明指针,verifier不允许直接解引用这个指针,程序要使用helper function或者kfunc来访问它指向的内存。
源码引入
辅助函数原型定义:
https://elixir.bootlin.com/linux/v6.0/source/kernel/bpf/helpers.c#L1497
eBPF对于辅助函数的检查与调用
首先在do_check中检查到CALL指令以及是辅助函数:
进入到check_help_call函数:
由于一个函数有多个参数,因此创建一个结构meta,用它来存放多个参数的分析结果:
之后通过check_func_arg依次检查5个参数:
check_func_arg函数:
直接看参数类型是 ARG_PTR_TO_DYNPTR 的情况,如果可以是uninit的,就要检查一下当前reg是不是没有init的dynptr,然后将meta的uninit_dynptr_regno记为regno:
退出check_func_arg之后,
进入到check_mem_access函数,reg->type应该是PTR_TO_STACK:
check_stack_access_within_bounds
update_stack_depth
check_stack_write
dynptr相关的辅助函数
源码链接如下:
https://elixir.bootlin.com/linux/v5.19/source/kernel/bpf/helpers.c#L1497
先看bpf_dynptr_from_mem函数,其代码如下:
函数原型定义如下:
stack
eBPF的栈和分配的空间是反向顺序的:
在eBPF程序中,我们关于栈的offset一定是一个负数,因此一个-8,对应的spi则为
在eBPF的栈中则是first在上边,seconde在下边了;
漏洞分析
部分写dynptr的slot
部分写栈上的synptr类型的slot之后,导致重新分配dynptr,是得原来的dynptr能够oob-read,oob-write。
当我们通过bpf_dynptr_from_mem函数得到一个dynptr,其中arg4是一个栈指针,其是未初始化的,此时在check_func_arg中得到uinit_dynptr_regno:
之后在check_helper_call函数中,会通过mark_stack_slots_dynptr函数设置标记:
mark_stack_slots_dynptr:
这样的存放在栈上的dynptr会得到如下标记,可以看到栈上的16个字节都被标记为dynptr:
此时被标记为dynptr的栈空间不能被read了,但是却可以被write:
因此,如果我们在一个栈空间中:
fp-24_w=dddddddd
fp-16_w=dddddddd
fp-8_w =mmmmmmmm
然后利用write覆盖写fp-16:
fp-24_w=dddddddd
fp-16_w=mmmmmmmm
fp-8_w =mmmmmmmm
那么此时如果我们以fp-16作为一个dynptr去调用bpf_dynptr_from_mem函数,该函数会检查这个dynptr是否为合法的uninit的,检查到16个字节都是m,就会通过verifier的检查,然后标记16个字节的d:
fp-24_w=dddddddd
fp-16_w=dddddddd
fp-8_w =dddddddd
此时,如果我们用fp-24作为dynptr去调用bpf_dynptr_read函数,那么就会在verifier中检查fp-24是否是valid_init的,也就是检查从fp-24开始的16个字节是否都是标记为d,这是可以通过的,因此这个dynptr的offset和size就会是fp-16中存放的data指针,这样就会有oob-read了!
同理,我们也就会有oob-write的能力了。