【kernel exploit】CVE-2022-2588 Double-free 漏洞 DirtyCred 利用

2022/10/21 Kernel-exploit 共 9286 字,约 27 分钟

【kernel exploit】CVE-2022-2588 Double-free 漏洞 DirtyCred 利用

影响版本:Linux v3.17 (commit) ~v5.19.1。 v5.19.2已修补。

测试版本:Linux-5.19.1 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项

CONFIG_BINFMT_MISC=y (否则启动VM时报错)

CONFIG_USER_NS=y (触发漏洞需要 User Namespace)

CONFIG_NET_CLS_ROUTE4=y (漏洞函数所在的模块)

CONFIG_DUMMY=y CONFIG_NET_SCH_QFQ=ybreezeO_o 提供的两个编译选项,触发poc需要用到)

CONFIG_NET_CLS_ACT / CONFIG_NET_CLS_BASIC (默认已开启)

CONFIG_NET_SCH_SFQ (exp中触发漏洞需用到 sfq随机公平队列

CONFIG_NET_EMATCH_META (exp中堆喷对象时需要用到)

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/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-2021-3715 (参见 BlackHat 2021-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.c
index 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 上运行。喷射次数 middleend 值可以适当调整,本文采用的是 middle=38 / end=38+40。DirtyCred 的详细原理可参考 【bsauce读论文】 DirtyCred-内核凭证替换利用技术【kernel exploit】CVE-2021-4154 错误释放任意file对象-DirtyCred利用

进程1进程2
0. 绑定到CPU 0 上运行,设置子进程内存、工作目录、Namespace,启动进程2; 
1. 去碎片化,打开10000个文件,消耗 filp cache,为 cross-cache 作准备; 
2. 喷射 (middle+3)*32 kmalloc-192 & kmalloc-256;(和漏洞对象位于同一cache,便于进行 cross-cache 被 file 对象复用) 
 3. 分配1个 route4_filter 漏洞对象,还有1个kmalloc-256 的漏洞对象;
4. 再喷射 (end-middle-2)*32 kmalloc-192 & kmalloc-256; 
5. 释放 (end-24)*32 kmalloc-192 & kmalloc-256; 
 6. 第1次释放漏洞对象 kmalloc-192 & kmalloc-256;
7. 释放 (end-middle+1) kmalloc-192 & kmalloc-256;(避免连续释放同一对象,触发内核 double-free 的检测) 
 8. 喷射 4000 个低权限 file 对象(通过打开 exp_dir/data 文件);
 9. 第2次释放漏洞对象 kmalloc-192 & kmalloc-256
 10. 喷射 5000 个低权限 file 对象,采用 kcmp 调用检查是否和前4000个 file 重合,重合的两个 file 记为 overlap_a / overlap_b
 11. 发起3个利用线程,线程1写入大量数据来占用文件锁;线程2往 overlap_a 写入恶意数据;
12. 线程3关闭 overlap_a / overlap_b,喷射 4096*2 个高权限 file 对象(通过打开 /etc/passwd 文件);未区分CPU 
 13. 最后检查 /etc/passwd 文件是否被写入恶意数据。

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 filter
        f->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 filter
    err = route4_set_parms(net, tp, base, f, handle, head, tb, // [2] 初始化 new filter
                   tca[TCA_RATE], new, flags, extack);
    if (err < 0)
        goto errout;

    // insert the new filter to the list
    h = from_hash(f->handle >> 16);                    		// [3] 将 new filter 插入到 list
    fp = &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 exists
    if (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 list
                    break;
                }
            }
        }
    }
	...
    // free the fold filter if it exists                   	// [6] 释放 old filter
    if (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_HARDENED
	BUG_ON(object == fp); 			// 检测 double-free
#endif

	freeptr_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 对象。

测试截图

succeed

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-2022-2588-exploit

CVE-2022-2588-POC

[漏洞分析] CVE-2022-2588 route4 double free内核提权

[kernel] 编译能复现指定poc的内核的排错过程

文档信息

Search

    Table of Contents