影响版本:Linux v3.17 (commit) ~v5.19.1。 v5.19.2已修补。
测试版本:Linux-5.19.1 exploit及测试环境下载地址—/bsauce/kernel-exploit-factory
编译选项:
CONFIG_BINFMT_MISC=y(否则启动VM时报错)
CONFIG_USER_NS=y(触发漏洞需要 User Namespace)
CONFIG_NET_CLS_ROUTE4=y(漏洞函数所在的模块)
CONFIG_DUMMY=yCONFIG_NET_SCH_QFQ=y(breezeO_o 提供的两个编译选项,触发poc需要用到)
CONFIG_NET_CLS_ACT/CONFIG_NET_CLS_BASIC(默认已开启)
CONFIG_NET_SCH_SFQ(exp中触发漏洞需用到 sfq随机公平队列)
CONFIG_NET_EMATCH_META(exp中堆喷对象时需要用到)
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
$ wget https://mirrors.tuna./kernel/v5.x/linux-5.19.1.tar.xz$ tar -xvf linux-5.19.1.tar.xz# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。$ make -j32$ make all$ make modules# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:和 CVE--3715 (参见 BlackHat -Europe-Your Trash Kernel Bug, My Precious 0-day 16页)类似,由于将route4_filter
对象从链表中删除和释放时的检查条件不一致,导致该对象被释放后仍存于链表中,后面可以触发Double-Free。需要User Namespaces
才能触发。采用 DirtCred 方法进行提权。
补丁:patch
diff --git a/net/sched/cls_route.c b/net/sched/cls_route.cindex a35ab8c27866e..3f935cbbaff66 100644--- a/net/sched/cls_route.c+++ b/net/sched/cls_route.c@@ -526,7 +526,7 @@ static int route4_change(struct net *net, struct sk_buff *in_skb,rcu_assign_pointer(f->next, f1);rcu_assign_pointer(*fp, f);-if (fold && fold->handle && f->handle != fold->handle) {+if (fold) {th = to_hash(fold->handle);h = from_hash(fold->handle >> 16);b = rtnl_dereference(head->table[th]);
保护机制:KASLR/SMEP/SMAP/KPTI
利用总结:exp中主要是两个函数完成漏洞利用,run_exp()
记为进程1,exploit()
记为进程2。进程之间是通过pipe进行通信,以确定运行顺序;注意每次新生成子进程,都要先绑定到 CPU0 上运行。喷射次数middle
和end
值可以适当调整,本文采用的是middle=38 / end=38+40
。DirtyCred 的详细原理可参考 【bsauce读论文】 DirtyCred-内核凭证替换利用技术 和 【kernel exploit】CVE--4154 错误释放任意file对象-DirtyCred利用。
1. 漏洞分析
1-1. 漏洞原理
漏洞分析:漏洞函数是 route4_change(),用于初始化和替换route4_filter
对象。使用handle
作为id来区分不同的route4_filter
,如果存在某个 handle 之前已被初始化过(fold
变量非空),就会移除旧的 filter,添加新的 filter;否则直接添加新的filter
。
先在[4]
处将 old filter 从 list 中删除,再在[6]
处释放 old filter。漏洞位于[4]
处,检查条件是需同时满足旧filter 的handle
非0且新旧 filter 的handle
不相等,这和[6]
处的条件不一致,[6]
只检查旧 filter 是否存在。因此,如果用户创建的 filter 的 handle 为0,则 old filter 在[4]
处不会被移除,但是在[6]
处会被释放。
漏洞触发序列:SYSCALL-write -> ksys_write() -> vfs_write() -> new_sync_write() -> call_write_iter() -> sock_write_iter() -> sock_sendmsg() -> sock_sendmsg_nosec() -> netlink_sendmsg() -> netlink_unicast() -> netlink_unicast_kernel() -> rtnetlink_rcv() -> netlink_rcv_skb() -> rtnetlink_rcv_msg() -> tc_new_tfilter() -> route4_change()
static int route4_change(struct net *net, struct sk_buff *in_skb,struct tcf_proto *tp, unsigned long base, u32 handle,struct nlattr **tca, void **arg, bool ovr,bool rtnl_held, struct netlink_ext_ack *extack){struct route4_filter *fold, *f1, *pfp, *f = NULL;fold = *arg; // fold 来自上层函数,根据句柄调用 route4_get() 找到现有的 route4_filter...f = kzalloc(sizeof(struct route4_filter), GFP_KERNEL);// [0] 分配新的 route4_filter 对象err = tcf_exts_init(&f->exts, net, TCA_ROUTE4_ACT, TCA_ROUTE4_POLICE); // route4_filter->exts.action 分配 256 字节的空间...if (fold) {// [1] fold 非空, 表示对应handle 的 filter 已存在, 将 old filter 的信息拷贝到 new filter 并在 [2] 初始化 new filterf->id = fold->id;f->iif = fold->iif;f->res = fold->res;f->handle = fold->handle;f->tp = fold->tp;f->bkt = fold->bkt;new = false;}// initialize the new filtererr = route4_set_parms(net, tp, base, f, handle, head, tb, // [2] 初始化 new filtertca[TCA_RATE], new, flags, extack);if (err < 0)goto errout;// insert the new filter to the listh = from_hash(f->handle >> 16);// [3] 将 new filter 插入到 listfp = &f->bkt->ht[h];for (pfp = rtnl_dereference(*fp);(f1 = rtnl_dereference(*fp)) != NULL;fp = &f1->next)if (f->handle < f1->handle)break;tcf_block_netif_keep_dst(tp->chain->block);rcu_assign_pointer(f->next, f1);rcu_assign_pointer(*fp, f);// remove fold filter from the list if fold existsif (fold && fold->handle && f->handle != fold->handle) {// [4] 若存在 old filter, 则从 list 中移除th = to_hash(fold->handle);h = from_hash(fold->handle >> 16);b = rtnl_dereference(head->table[th]);if (b) {fp = &b->ht[h];for (pfp = rtnl_dereference(*fp); pfp;fp = &pfp->next, pfp = rtnl_dereference(*fp)) {if (pfp == fold) {rcu_assign_pointer(*fp, fold->next); [5]// remove the old from the linked listbreak;}}}}...// free the fold filter if it exists // [6] 释放 old filterif (fold) {tcf_unbind_filter(tp, &fold->res);tcf_exts_get_net(&fold->exts);tcf_queue_work(&fold->rwork, route4_delete_filter_work); // [7] 启动内核任务,调用 oute4_delete_filter_work() 释放 old filter}...}static inline int tcf_exts_init(struct tcf_exts *exts, struct net *net,int action, int police){··· ···exts->actions = kcalloc(TCA_ACT_MAX_PRIO, sizeof(struct tc_action *), // 分配 32*8=256 大小的空间, 32个 tc_action 结构体指针GFP_KERNEL);··· ···}
1-2. 漏洞对象
漏洞对象:有两个漏洞对象,我们用的是 kmalloc-256 这个对象,因为file
对象的大小是 232,二者比较接近,便于进行cross-cache
。
route4_filter —— 大小为 144,属于kmalloc-192
;tc_action —— 大小为 192,漏洞对象并非tc_action
对象,而是存储32个tc_action
结构体指针的空间,也即大小为 256 的空间,属于kmalloc-256
。
漏洞释放链:route4_delete_filter_work() -> __route4_delete_filter() -> tcf_exts_destroy()
static void route4_delete_filter_work(struct work_struct *work){struct route4_filter *f = container_of(to_rcu_work(work),struct route4_filter,rwork);rtnl_lock();__route4_delete_filter(f);// <-------rtnl_unlock();}static void __route4_delete_filter(struct route4_filter *f){tcf_exts_destroy(&f->exts); // <-------tcf_exts_put_net(&f->exts);kfree(f); // 释放 tc_action 对象}void tcf_exts_destroy(struct tcf_exts *exts){#ifdef CONFIG_NET_CLS_ACT// 编译时勾选 CONFIG_NET_CLS_ACT —— 默认是开启的if (exts->actions) {tcf_action_destroy(exts->actions, TCA_ACT_UNBIND);kfree(exts->actions); // 释放 tc_action 对象}exts->nr_actions = 0;#endif}EXPORT_SYMBOL(tcf_exts_destroy);
另一处释放链:route4_delete() -> route4_delete_filter_work()
static int route4_delete(struct tcf_proto *tp, void *arg, bool *last,bool rtnl_held, struct netlink_ext_ack *extack){struct route4_head *head = rtnl_dereference(tp->root);struct route4_filter *f = arg;struct route4_filter __rcu **fp;struct route4_filter *nf;struct route4_bucket *b;...fp = &b->ht[from_hash(h >> 16)];for (nf = rtnl_dereference(*fp); nf;fp = &nf->next, nf = rtnl_dereference(*fp)) {if (nf == f) {...tcf_queue_work(&f->rwork, route4_delete_filter_work);// <--------...}}...}
2. 漏洞利用
思路:由于已被释放的fold
仍位于链表上,就可以再次释放fold
,触发route4_filer
对象的 Double-free,如果编译内核时开启了CONFIG_NET_CLS_ACT
,那么route4_filter->exts.actions
对象也会 Double-free。我们可以利用这两种漏洞对象来进行 DirtyCred 攻击,分别替换进程凭证(task credentials,利用 kmalloc-192)和文件凭证(open file credentials,利用 kmalloc-256)。
根据 DirtyCred 的思路,我们在文件写许可检查之后,替换file
结构,就能往只有读许可的文件写数据。理论上,exp能通杀各个漏洞版本的内核(有些老版本内核中,msg_msg
隔离在kmalloc-rcl-*
中,所以需要使用不同的堆喷对象)。作者测试后能在以下版本中提权:
CentOS 8/Stream (4.18.0-80.el8.x86_64 ~ xxx)Debian 11 (5.10.0-8-amd64 ~ xxx)Fedora 33 (5.8.15-301.fc33.x86_64 ~ xxx)Manjaro 18 (xxx ~ xxx)RHEL 8 (4.18.0-80.el8.x86_64 ~ xxx)Ubuntu 17 (4.10.0-19-generic ~ xxx)Ubuntu 18 (xxx ~ xxx)Ubuntu 19 (5.0.0-38-generic ~ xxx)Ubuntu 20 (xxx ~ xxx)
问题:Double-free 触发崩溃。我们用到的漏洞对象是kmalloc-256
,但漏洞同时也会把一个kmalloc-192
释放两次,就会触发内核 Double-free 检测,导致崩溃。解决办法是,在两次释放kmalloc-192
之间间隔释放同一页的其他 slab,就能避免崩溃。
static inline void set_freepointer(struct kmem_cache *s, void *object, void *fp){unsigned long freeptr_addr = (unsigned long)object + s->offset;#ifdef CONFIG_SLAB_FREELIST_HARDENEDBUG_ON(object == fp); // 检测 double-free#endiffreeptr_addr = (unsigned long)kasan_reset_tag((void *)freeptr_addr);*(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr);}
cross-cache:我们将释放某个kmalloc-256
cache page,将该页归还给页管理器,然后分配file
结构来复用该页(filp
cache)。步骤如下:
(1)分配一堆kmalloc-256
堆块,包含漏洞对象;(2)利用漏洞第1次释放漏洞对象,并释放一堆kmalloc-256
,以归还漏洞对象所在的页;(3)分配大量低权限file
对象来占据漏洞对象(cross-cache attack);(4)利用漏洞第2次释放漏洞对象,堆喷高权限file
对象来替换低权限file
对象。
测试截图:
3. exp适配
错误1:漏洞触发出错。在我编译的 Linux-v5.19.1 上测试exp总是失败,经过调试发现连漏洞都无法触发,在 tc_new_tfilter() tcf_proto_create()处就报错返回了,查阅后发现我这个内核版本中默认加载的流量控制队列只有以下几种:
$4 = 0xffffffff82b13f30 <pfifo_fast_ops+16> "pfifo_fast"$5 = 0xffffffff82b14470 <pfifo_qdisc_ops+16> "pfifo"$6 = 0xffffffff82b143b0 <bfifo_qdisc_ops+16> "bfifo"$7 = 0xffffffff82b142f0 <pfifo_head_drop_qdisc_ops+16> "pfifo_head_drop"$8 = 0xffffffff82b14170 <mq_qdisc_ops+16> "mq"$9 = 0xffffffff82b13ff0 <noqueue_qdisc_ops+16> "noqueue"$10 = 0xffffffff82b14230 <blackhole_qdisc_ops+16> "blackhole"$11 = 0xffffffff82b14530 <qfq_qdisc_ops+16> "qfq"
并未加载 sfq随机公平队列 (exp中就是采用的这种队列),sfq
代码实现位于 net/sched/sch_sfq.c,查看Makefile
文件才发现sfq
对应的编译选项是CONFIG_NET_SCH_SFQ
,编译时并未勾选这个选项,只有CONFIG_NET_SCH_QFQ
默认是勾选上了的,所以需重新编译内核。
错误2:堆喷出错。 tcf_em_tree_validate() -> tcf_em_validate() 这里根据传入的em_hdr->kind
(0x4)来寻找对应的 ops 结构,但是没找到。exp中赋值为hdr->kind = TCF_EM_META
,搜索源码发现TCF_EM_META
出现在 net/sched/em_meta.c,查看Makefile
发现没有勾选CONFIG_NET_EMATCH_META
选项。
参考
CVE--2588-exploit
CVE--2588-POC
[漏洞分析] CVE--2588 route4 double free内核提权
[kernel] 编译能复现指定poc的内核的排错过程