【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.png

参考

【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 “溢出“内核提权

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 175,490评论 5 419
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 74,060评论 2 335
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 124,407评论 0 291
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 47,741评论 0 248
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 56,543评论 3 329
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 43,040评论 1 246
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 34,107评论 3 358
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 32,646评论 0 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 36,694评论 1 271
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 32,398评论 2 279
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 33,987评论 1 288
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 30,097评论 3 285
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 35,298评论 3 282
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 27,278评论 0 14
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 28,413评论 1 232
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 38,397评论 2 309
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 38,099评论 2 314

推荐阅读更多精彩内容