4.20 Linux Kernel中 bpf 模块存在一个整数溢出漏洞,并且通过整数溢出漏洞可以构造一个堆溢出漏洞。由于没有开启PIE,所以可以通过在slub中堆喷 bpf_map结构体,最后通过采用类似 ptmx的方法 实现 ROP提权
漏洞分析
漏洞存在于 BPF
模块,该模块主要用于用户态定义数据包过滤方法,如常见的抓包工具都基于此实现,并且用户态的 Seccomp
功能也与此功能相似。可参考该文档。
其本质上是一种内核代码注入技术:
- 内核中实现了一个
cBPF/eBPF
虚拟机; - 用户态可以用C来写运行的代码,再通过一个
Clang&LLVM
的编译器将C代码编译成BPF
目标码; - 用户态通过系统调用
bpf()
将BPF
目标码注入到内核当中; - 内核通过
JIT(Just-In-Time)
将BPF
编码转换成本地指令码;如果当前架构不支持JIT
转换内核则会使用一个解析器(Interpreter)来模拟运行,这种运行效率较低; - 内核在
packet filter
和tracing
等应用中提供了一系列的钩子来运行BPF
代码
整数溢出漏洞
整数溢出漏洞存在于 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
函数中:
1 | static int map_update_elem(union bpf_attr *attr) |
即 bpf
系统调用的第三个功能函数。首先根据用户输入的 id
找到放入管理结构的 map
,利用 kmalloc
新建一个堆块根据 Map
中存储的 value_size
,从用户输入拷贝。然后在map
中找到存储的虚函数指针 ops
,然后根据 ops
调用相应的虚函数。这里调用的函数为 queue_stack_map_push_elem
函数:
1 | static int queue_stack_map_push_elem(struct bpf_map *map, void *value, |
queue_stack_map_push_elem
函数会从上一步存储用户输入的堆块拷贝数据到 利用数据存储管理结构体计算的地址,大小为 qs->size
。此时,qs->head
在新建的时候被初始化为0,此时出现堆溢出,溢出大小可以控制即初始化是输入的 value_size
,位置是从新建的第一个堆块以后直接溢出:
每一个map
里包含多个小块内存,value_size
是每一个小块的大小,max_entries
是小块的数量,每次可以写一个小块的内容。
总结
在
bpf
系统中,会调用map_create
来创建一个map
结构体;在
map_create
函数中会根据用户传入的attr
参数调用find_and_alloc_map
函数来创建一个map
结构体;在
find_and_alloc_map
函数中,会根据传入的参数attr->type
从虚表中寻找对应的函数指针,然后会调用对应的函数根据attr
生成一个map
结构体;漏洞的虚函数指针为
queue_stack_map_alloc
,其触发所需的type
值为0x17
,在queue_stack_map_alloc
中,函数利用sizeof(bpf_queue_stack)+attr->value_size*(attr->max_entires+1)
来申请堆空间,而attr
中内容均为用户输入。所以这里可以构造attr->max_entries
为0xffffffff
造成整数溢出漏洞,使得申请的堆块仅包含bpf_queue_stack
这个管理结构体;当申请
map
完成后,会从用户输入的attr
中拷贝对应的参数到map
中,也即该结构体用户也可以控制。其次用户可以调用漏洞函数
map_update_elem
,首先根据用户输入的id
找到 管理结构体map
,随后根据map->value_size
调用kmalloc
新建一个堆块,并将用户输入拷贝到map
中。然后在map
中找到存储的虚函数指针ops
,根据ops
调用相应的虚函数。调用的虚函数这里为
queue_stack_map_push_elem
函数,在该函数中会调用memcpy
函数向 计算得到的地址 将 第6步中得到的map
拷贝到 目标地址中,大小为qs->size
。而这里计算得到的地址是 在管理结构体之后,而qs->size
是我们可以自己输入的,也就是这里存在一个 堆溢出漏洞。
漏洞利用
由于默认仅采用smep
保护,关闭 smap
、kaslr
、kpti
。
首先经过动态分析,会发现 通过 kmalloc
申请的大小为 0x100
,且是从 kmalloc-256
上分配。那么利用思路即考虑在 kmalloc-256
链上布置堆风水。
总结一下上面的漏洞利用要求:
- 申请的堆块大小固定为 0x100
- 堆溢出仅能向相邻的堆块溢出
这里的想法是:
- 现在
kmalloc-256
上堆喷射大量含有函数指针的堆块; - 利用堆溢出漏洞,修改相邻的堆块里的函数指针,实现劫持控制流
堆喷
这里最开始想到的就是 tty_struct
,但是这里受到大小限制,不可能将 tty_struct
喷射到 kmalloc-256
的链上。
这里选用的是 bpf
自带的一个结构体 bpf_queue_stack
,如下所示:
1 | struct bpf_queue_stack { |
在 bpf_queue_stack
中,含有 bpf_map
,而 在 bpf_map
中的 含有一个 bpf_map_ops
包含大量的结构体指针。而我们可以调用 BPF_MAP_CREATE
功能来申请 bpf_map_ops
结构,那么这里就可以实现堆喷射。
劫持控制流
通过 BPF_MAP_CREATE
函数创建的 bpf__map
结构体大小为 0xd0
,而系统分配的 slub
大小为 0x100
,而 bpf_map
的第一个结构体即为 bpf_map_ops
结构体。所以总体堆块布局应该如下,所以我们堆溢应该溢出 0x30
,就刚好能够修改紧邻的 bpf_map
结构体的 bpf_map_ops
结构体。
1 | | | | | |
实现函数指针改写后,又如何来触发调用该函数指针呢?由于 ops
内含有很多虚表指针,就有可能通过 dereference
这些函数指针中的任何一个实现控制流劫持,获得 rip
的控制权。为了找到使用的这些函数指针的方法,原文提到了一种 under-context fuzzing + symbolic execution
的方法,通过 fuzzing
来寻找,这一个技术我目前还不太了解,希望后续可以接触到吧。最终,其找到了 map_release
的 dereference
如下所示:
1 | /* called from workqueue */ |
当我们对 bpf_map
调用 close
函数时,会将 bpf_map_free_deferred()
添加到队列并随后执行,通过 ops.map_free
设为我们自己的 gadget
,就能实现 控制流劫持。
这里我分别尝试 ROP
和 modprobe_path
的方法来进行提权。
ROP
这个方法和之前所作的 TokyoWesterns-gnote
很相似。首先利用一个栈迁移的 gadget
,这里当然还是 xchg esp, eax
,这里选择如下所示:
1 | 0xffffffff81954dc8: xchg esp,eax |
那么我们的ROP
需要布置在 (0xffffffff81954dc8&0xffffffff)+0x674
处。布置的 ROP
就和正常 Kernel-rop
提权相同。ROP
如下:
1 | p_rax_r, |
漏洞调试
如下所示 rax=0xffffffff
,加上1后发生整数溢出。
1 | *RAX 0xffffffff |
当我们使用分配了 bpf_map
结构体如下 0xffff88807f934600
所示。从 0xd0
溢出0x30
修改后一个 bpf_map
的 bpf_map_ops
指向 0xa000000000
用户态伪造的 fake_bpf_map_ops
。
1 | pwndbg> x/20xg 0xffff88807f934600 |
而在 0xa000000000
中伪造 map_release
,然后将其指向 栈迁移的 gadget
,如下所示:
1 | 0xffffffff81954dc8 xchg eax, esp |
这里 ROP
需要注意绕过 smep
,修改 cr4
寄存器。
1 | / $ ./exp-rop |
modprobe_path
如果想要使用 modprobe_path
其实也是要利用 ROP
来完成,不过 ROP
执行的内容不再是提权,而是修改 modprobe_path
的内容。ROP
如下:
1 | p_rax_r, |
这里就是先利用 rop
执行 memcpy(modprobe_path, target, 0x20)
函数。然后手动执行 /tmp/ll
,就能够将 flag
的 权限进行修改。
1 | -rwxrwxrwx 1 root root 11 Apr 17 08:01 flag |
EXP
ROP:
1 | //gcc exp.c -static -masm=intel -fno-pie -o exp |
modprobe_path:
1 | //gcc exp.c -static -masm=intel -fno-pie -o exp |
参考
Linux kernel 4.20 BPF 整数溢出-堆溢出漏洞及其利用
Linux kernel 4.20 BPF 整数溢出漏洞分析
Linux kernel 4.20 BPF 整数溢出漏洞分析
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2021/04/17/Linux-Kernle-4-20-整数溢出和堆溢出漏洞分析/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!