漏洞的发现者与原作者是ww9210,相关资料:github ,安全客文章。
本文所使用的环境与EXP下载
漏洞分析
漏洞存在于BPF模块中,该模块主要用于用户态定义数据包过滤方法,如常见的抓包工具都基于此实现,并且用户态的Seccomp功能也与此功能相似。
分析基于linux-4.20-rc3版本代码:https://elixir.bootlin.com/linux/v4.20-rc3/source
整数溢出漏洞
整数溢出漏洞存在于BPF_MAP_CREATE功能,是bpf系统调用的一部分,可参考手册:
1 | SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size) |
可以看到其处理函数是map_create,在[1]处创建了一个map结构体,并为其分配编号,此后利用编号寻找生成的map。
1 | /* called via syscall */ |
下面分析find_and_alloc_map函数,对于传入参数的含义如结构体所示,可以看到程序首先根据attr->type,寻找所对应的处理函数虚表,在[2]处。然后根据处理函数虚表的不同,调用不同的函数进行处理。
1 | struct { /* Used by BPF_MAP_CREATE */ |
本漏洞存在的虚函数位于queue_stack_map_alloc,查看内核可以计算其触发所需的type值,即(0xFFFFFFFF82028438 - 0xFFFFFFFF82028380)/8 = 0x17 :
1 | 汇编指令: |
程序在[3]处调用漏洞存在函数:queue_stack_map_alloc,在该函数中利用sizeof(bpf_queue_stack) + attr->value_size * (attr->max_entries + 1)来申请堆空间,而attr中内容均为用户输入,可以看到当max_entries 为0xffffffff时,将仅申请大小sizeof(bpf_queue_stack) 的堆块。此函数相当于申请了相邻的内存,其中前sizeof(bpf_queue_stack) 个字节为管理块,用于存储数据结构,后面的内容为数据存储结构。
1 | static struct bpf_map *queue_stack_map_alloc(union bpf_attr *attr) |
当申请完成后,初始化函数如下:bpf_map_init_from_attr,几乎为copy了用户输入的attr。
1 | void bpf_map_init_from_attr(struct bpf_map *map, union bpf_attr *attr) |
当此申请完成后,内核模块将这个堆块放入管理结构中,并生成id用于管理,并将id返回给用户。
堆溢出漏洞
由上述的整数溢出漏洞,导致内存分配时仅仅分配了管理块的大小而没有分配实际存储数据的内存。如果存在编辑功能则一定会有问题,下面的堆溢出漏洞就是由此导致的。
漏洞存在于map_update_elem函数中,即bpf系统调用的第三个功能函数。首先根据用户输入的id找到放入管理结构的map,利用kmalloc新建一个堆块根据map中存储的value_size,从用户输入拷贝。然后在map中找到存储的虚函数指针ops,然后根据ops调用相应的虚函数。
1 | static int map_update_elem(union bpf_attr *attr) |
此处,实际操作的函数由之前初始化的虚表可知是queue_stack_map_push_elem,在该函数中从之前kmalloc新建的内存中,向计算得到的地址做拷贝,大小为qs->size。
1 | static int queue_stack_map_push_elem(struct bpf_map *map, void *value, |
计算的地址,从汇编语言中更容易看出是跳过了管理块内容的地址,qs->head在新建的时候被初始化为0,此时出现堆溢出,溢出大小可以控制即初始化是输入的value_size,位置是从新建的第一个堆块以后直接溢出。
1 | .text:FFFFFFFF811AEF71 mov edx, [rbx+20h] |
其功能上很容易理解,没一个map里包含多个小块内存,value_size是每一个小块的大小,max_entries是小块的数量,每次可以写一个小块内容。
漏洞利用
[-] 利用默认仅采用smep保护,关闭smap、kaslr、kpti。
内核堆漏洞最大的问题是要看申请的堆块大小是多少,这是因为内核的堆管理是用的伙伴算法+slub算法,即相同kmem_cache的内存块是用同一个内存页切开的,所以造成内存块会相邻。
首先分析申请的内存大小和使用的kmem_cache,此处用动态分析更好。可以发现其申请的大小是0x100,并且采用了kmalloc-256进行分配。
1 | pwndbg> b *0xFFFFFFFF8119CD17 |
堆风水
所谓堆风水就是根据堆分配机制,将特定的内存块分配到特定的位置去。
此处漏洞的限定条件是:1 申请的0x100大小的堆块。 2 向相邻堆块溢出。
根据漏洞利用的常用思路,找到一个0x100大小、并存在函数指针、虚表的数据结构进行喷射都可以。
这里采用的喷射(spary)就是利用伙伴算法和slub的性质,由于其位置上相同大小的堆块相邻,因此申请大量的堆块一定存在一块与发生溢出的堆块相邻,造成指针可控的情况。
常用的ptmx由于大小问题不可用,此模块中恰好有一个数据结构可以使用就是 bpf_queue_stack。
如下,其中bpf_map_ops是一个虚函数表,数据结构中恰好包括一个虚函数表指针ops,因此,利用bpf中的BPF_MAP_CREATE功能,进行喷射就可以造成虚函数表指针可控的情况。此喷射操作恰好与整数溢出触发操作相同。
数据结构:
1 | struct bpf_queue_stack { |
劫持控制流
上述指针的偏移是0x30,当溢出超过0x30时,即可以控制该虚函数表。我们可以在用户态空间中构造一个虚函数表,将指针指向这个虚函数表。利用close函数即可以触发一个伪造的函数地址来劫持控制流。
此时的方法类似于CISCN 2017 babydriver,首先找到一个gadget来做栈迁移,预先在用户态布置好一个写好ROP的伪造内核栈空间,从而先提权,然后swapgs、iret返回用户态,打开一个shell。
选用的栈迁移gadget是:
1 | pwndbg> x /2i 0xffffffff81954dc8 |
随后在0x81954dc8+0x674+8处布置其余的ROP即可。
最终,可以在开启smep的情况下提权:
1 | / $ ./exp |
EXP
1 | // gcc -o exp exp.c -static -fno-pie |
其他
在调试ROP时,当用iret返回用户态时,遇到了一个之前没有遇到的问题,虽然跳转到了get_shell函数,但执行第一条语句时,出现Segmentation fault,拿不到shell。最后还是问了ww9210师傅,告诉我可以加一个signal函数来catch段错误,在这个处理函数中再起shell,就可以拿到shell了,虽然不太清楚为什么,但是确实有效。