影响版本: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_E1000
和CONFIG_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 queue
:watch 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)。
结构关系:
- 用户参数 —— watch_notification_filter -> watch_notification_type_filter
- 内核存储 —— watch_filter -> watch_type_filter
// (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的地址;
- 堆喷 16*128 个
- (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 版本上提权了。
参考
【kernel exploit】CVE-2021-22555 2字节堆溢出写0漏洞提权分析
【CVE.0x08】CVE-2022-0995 漏洞复现及简要分析