【kernel exploit】CVE-2022-0995 堆溢出1比特置1漏洞利用

2022/04/15 Kernel-exploit 共 7216 字,约 21 分钟

【kernel exploit】CVE-2022-0995 堆溢出1比特置1漏洞利用

影响版本:Linux 5.8~5.17-rc7 5.17-rc8已修补 / 5.16.15已修补。评分只有 7.1 分。

测试版本:Linux-5.16.14(利用失败)改用v5.11.22 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项CONFIG_WATCH_QUEUE

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

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.16.14.tar.xz
$ tar -xvf linux-5.16.20.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述watch_queue 事件通知子系统存在堆溢出,漏洞函数是watch_queue_set_filter()。内核会对用户传入的 watch_notification_type_filter 类型的 filter 进行两次有效性检查,第1次检查是为了确定分配的内存大小,第2次是为了将用户filter 存入该内存。但是两次检查不一致,导致分配空间过小,可溢出存入更多的 filter。可以利用第2次溢出,对相邻的堆块特定bit位置1,接下来的利用方法和 CVE-2021-22555 一样。

补丁patch 修改了判断条件,两处都判断type是否大于等于2。

@@ -320,7 +319,7 @@ long watch_queue_set_filter(struct pipe_inode_info *pipe,
 		    tf[i].info_mask & WATCH_INFO_LENGTH)
 			goto err_filter;
 		/* Ignore any unknown types */
-		if (tf[i].type >= sizeof(wfilter->type_filter) * 8)
+		if (tf[i].type >= WATCH_TYPE__NR)
 			continue;
 		nr_filter++;
 	}
@@ -336,7 +335,7 @@ long watch_queue_set_filter(struct pipe_inode_info *pipe,
 
 	q = wfilter->filters;
 	for (i = 0; i < filter.nr_filters; i++) {
-		if (tf[i].type >= sizeof(wfilter->type_filter) * BITS_PER_LONG)
+		if (tf[i].type >= WATCH_TYPE__NR)
 			continue;
 
 		q->type			= tf[i].type;

保护机制:KASLR / SMEP / SMAP / KPTI

利用总结:用的方法和 CVE-2021-22555 的方法一样。

1. 背景知识

内核通知机制:参见 watch_queue官方说明,内核的通用通知机制是基于pipe的,可以将内核的通知消息拼接到用户打开的管道中(编译时开启CONFIG_WATCH_QUEUE)。采用特殊mode打开pipe,即可启用该机制;内核生成的消息会被保存到 pipe_buffer 结构的 ring buffer 中;可调用 read() 来读取该消息。pipe的所有者应该告诉内核,哪些资源需要通过该管道进行观察,只有连接到该管道上的资源才会往里边插入消息,需要注意的是一个资源可能会与多个管道绑定并同时将消息插入所有管道。

用户管理 watch queuewatch queue是应用程序分配的一段缓冲区,用来记录通知,其实现代码都在pipe驱动中,用户可以通过两个API 引用和丢弃引用 pipe文件描述符fd中的缓冲区对应的 watch queue。分别是 struct watch_queue *get_watch_queue(int fd) / void put_watch_queue(struct watch_queue *wqueue)

event filter:创建好 watch queue 之后,用户可以创建 filter 来限制接收的事件。用户可传入 watch_notification_filter -> watch_notification_type_filter 结构,这样在内核中就会创建相应的filter,漏洞就出在创建 filter 过程的代码中。结构中的成员含义可见以下的漏洞分析部分。

2. 漏洞分析

用户调用 ioctl(fd, IOC_WATCH_QUEUE_SET_FILTER, &filter) 来设置filter时会触发漏洞。

调用关系ioctl -> vfs_ioctl() -> pipe_ioctl() -> watch_queue_set_filter() -> __set_bit()

注意vfs_ioctl() 中会调用 filp->f_op->unlocked_ioctl() ,该函数表的创建流程是 do_pipe2() -> __do_pipe_flags() -> create_pipe_files() -> alloc_file_pseudo() -> alloc_file()alloc_file() 分配一个 file 结构体并将其函数表设为上层调用传入的函数表,而在 create_pipe_files() 中传入的函数表为 pipefifo_fops

long watch_queue_set_filter(struct pipe_inode_info *pipe,
			    struct watch_notification_filter __user *_filter)
{
	struct watch_notification_type_filter *tf;
	struct watch_notification_filter filter;
	struct watch_type_filter *q;
	struct watch_filter *wfilter;
	struct watch_queue *wqueue = pipe->watch_queue;
	int ret, nr_filter = 0, i;
	...
	if (copy_from_user(&filter, _filter, sizeof(filter)) != 0)		// [1] 拷贝用户传入的 watch_notification_filter 结构
		return -EFAULT;
	if (filter.nr_filters == 0 ||
	    filter.nr_filters > 16 ||
	    filter.__reserved != 0)
		return -EINVAL;

	tf = memdup_user(_filter->filters, filter.nr_filters * sizeof(*tf)); // [2] 分配临时空间并拷贝用户传入的 filter
	...
	for (i = 0; i < filter.nr_filters; i++) {
		if ((tf[i].info_filter & ~tf[i].info_mask) ||
		    tf[i].info_mask & WATCH_INFO_LENGTH)
			goto err_filter;
		/* Ignore any unknown types */
		if (tf[i].type >= sizeof(wfilter->type_filter) * 8) // [3] 只计入 type 值小于 0x10*8 的数量, 后续根据 nr_filter 分配空间
			continue;
		nr_filter++;
	}

	... 
	wfilter = kzalloc(struct_size(wfilter, filters, nr_filter), GFP_KERNEL);	// [4] 根据 nr_filter 值来分配空间,存储filter
	...
	wfilter->nr_filters = nr_filter;

	q = wfilter->filters;
	for (i = 0; i < filter.nr_filters; i++) { 				// [5] 填充 wfilter->filters[]
		if (tf[i].type >= sizeof(wfilter->type_filter) * BITS_PER_LONG)	// [6] 漏洞点, 这里只要 type < 0x10*64 (0x400) 就会存入, 之前判断时是 0x80
			continue;

		q->type			= tf[i].type;						// [7] 溢出点1
		q->info_filter		= tf[i].info_filter;
		q->info_mask		= tf[i].info_mask;
		q->subtype_filter[0]	= tf[i].subtype_filter[0];
		__set_bit(q->type, wfilter->type_filter);			// [8] 溢出点2, 将wfilter->type_filter偏移q->type的bit位置为1, 可以溢出篡改指定bit
		q++;
	}
	...
}

#define BITS_PER_LONG 64
#define BIT_MASK(nr)		(UL(1) << ((nr) % BITS_PER_LONG))
#define BIT_WORD(nr)		((nr) / BITS_PER_LONG)
static inline void __set_bit(int nr, volatile unsigned long *addr)
{
	unsigned long mask = BIT_MASK(nr);				 // 1 左移 (nr % 64)
	unsigned long *p = ((unsigned long *)addr) + BIT_WORD(nr);	// nr 除以 64bit, 但p是指向8字节, 所以将修改偏移 (nr/64*8 = n/8) 处的字节

	*p  |= mask;
}

漏洞:内核会对用户传入的 watch_notification_type_filter 类型的 filter 进行两次有效性检查,第1次检查是为了确定分配的内存大小(见[3]),第2次是为了将用户filter 存入该内存(见[6])。问题在于两次检查不一致,第1次是计算type小于0x80的个数,第2次却是将type小于0x400的filter存入该内存。所以当type位于 0x80~0x400 之间时,实际存入的filter个数会大于分配的内存,导致 [7][8] 都会溢出。漏洞利用时采用的是第2处溢出,越界将指定的bit置1(将 wfilter->type_filter 偏移 q->type 的bit位置为1,而 wfilter->type_filter 位于 watch_filter 结构的开头,所以只要将 q->type 设置为固定的值,就能将相邻块的固定偏移位 置为1)。

结构关系

// (1) 用户参数结构体
struct watch_notification_filter {
	__u32	nr_filters;		/* Number of filters */
	__u32	__reserved;		/* Must be 0 */
	struct watch_notification_type_filter filters[];	// <---------
};

struct watch_notification_type_filter {
	__u32	type;			// 要过滤的事件类型, eg, WATCH_TYPE_KEY_NOTIFY
	__u32	info_filter;		/* Filter on watch_notification::info */
	__u32	info_mask;		/* Mask of relevant bits in info_filter */
	__u32	subtype_filter[8];	/* Bitmask of subtypes to filter on */
};

// (2) 内核结构体
struct watch_filter {
	union {
		struct rcu_head	rcu;
		unsigned long	type_filter[2];	/* Bitmask of accepted types */
	};
	u32			nr_filters;	/* Number of filters */
	struct watch_type_filter filters[];
};

struct watch_type_filter {		// size: 0x10
	enum watch_notification_type type;
	__u32		subtype_filter[1];	/* Bitmask of subtypes to filter on */
	__u32		info_filter;		/* Filter on watch_notification::info */
	__u32		info_mask;		/* Mask of relevant bits in info_filter */
};

3. 漏洞利用

漏洞利用:其实有两处溢出,但作者用到了第2处溢出。作者传入4个filter,其中3个有效,则在 [4] 处会申请 0x18+0x30 的内存,实际申请到 kmalloc-96。当type值为 0x30a 时(96*8+0xa),会将相邻 kmalloc-96 的第10bit 置为1,也即将 0x0000 修改为 0x0400

好处:一是只需要溢出1次,也即堆喷布置1次,提高利用成功率;二是可以直接采用CVE-2021-22555 的利用方法,篡改 msg_msg->m_list.next

利用过程

  • (1)堆布局:堆喷4096个 msg_msg,主消息和辅助消息 kmalloc-96 <-> kmalloc-1024
  • (2)触发OOB
    • 释放第0 / 1024 / 2048 / 3072 个主消息;
    • 触发OOB溢出,漏洞对象位于kmalloc-96,可能将某个 msg_msg->m_list.next 的最低两字节从 0x0000 修改为 0x0400;
    • 找到 msg_msg->m_list.next 被修改的msg_msg,下标记为 victim_qid ,指向的 msg_msg 下标记为 real_qid
  • (3)构造UAF:释放下标为 real_qid 的辅助消息B2(将下标 victim_qid 的辅助消息记为A2,下标 real_qid 的辅助消息记为B2);
  • (4)泄露UAF消息B2的地址
    • 堆喷 16*128 个 sk_buff 占据刚才释放的B2,伪造A2的 msg_msg->m_ts = 0xfd0
    • 利用A2进行OOB read,泄露相邻消息的 msg_msg->m_list.prev (记为C1,相邻辅助消息对应的主消息地址);
    • 释放 sk_buff 后再次堆喷 sk_buff ,伪造A2的 msg_msg->m_ts = 0x1fc8 / msg_msg->next = C1
    • 利用A2泄露 C1 处的 msg_msg->m_list.next (记为C2,相邻辅助消息的地址),C2-0x400 即为B2的地址;
  • (5)泄露内核基址
    • 释放 sk_buff 后再次堆喷 sk_buff ,伪造A2
      • msg_msg->m_list.next = msg_msg->m_list.prev = B+0x800
      • msg_msg->type = 0x1337
      • msg_msg->m_ts = 0xfd0
      • msg_msg->next = msg_msg->security = 0
    • 释放下标 victim_qid 的辅助消息A2;
    • 堆喷256个 pipe_buffer 占据A2;
    • 读取 sk_buff 泄露 pipe_buffer->ops 即可泄露内核基址;
  • (6)劫持控制流
    • 堆喷 sk_buff ,篡改pipe_buffer->ops 指向 A2+0x290
    • 并伪造 release 指针为 pivot gadget,剩下的ROP chain放在 pipe_buffer 上,完成提权。

问题:在内核版本 5.16.14 上始终没有办法使漏洞对象和 kmalloc-96 大小的 msg_msg 相邻,跟之前调试 CVE-2022-0185 遇到的问题一样(希望之后能弄明白,难道是account标志导致?)。无奈,只能用 5.11.22 版本上提权了。

succeed

参考

【kernel exploit】CVE-2021-22555 2字节堆溢出写0漏洞提权分析

exploit

【CVE.0x08】CVE-2022-0995 漏洞复现及简要分析

CVE-2022-0995分析(内核越界 watch_queue_set_filter)

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权

文档信息

Search

    Table of Contents