[漏洞分析] CVE-2022-32250 netfilter UAF内核提权

渗透技巧 1年前 (2023) admin
894 0 0

漏洞简介

漏洞编号: CVE-2022-32250

漏洞产品: linux kernel – netfilter

影响范围: linux kernel 5.19

利用条件: CAP_NET_ADMIN

利用效果: 本地提权

环境搭建

调试只需要 CONFIG_NF_TABLES=y就行了

但exp中使用了NFT_SET_EXPR,必须要使用ubuntu21.04 以上的版本的libmnl 或 libnftnl才行。

利用效果:

[漏洞分析] CVE-2022-32250 netfilter UAF内核提权

漏洞原理

漏洞触发

漏洞发生在netfilter 模块的NFT_MSG_NEWSET 功能中,在特定报文(有特定成员的结构体)处理上会出现UAF问题。

按顺序分析,首先来看NFT_MSG_NEWSET 的入口函数:

netnetfilternf_tables_api.c : nf_tables_newset

 1static const struct nfnl_callback nf_tables_cb[NFT_MSG_MAX] = {
2    ··· ···
3    [NFT_MSG_NEWSET] = {
4        .call       = nf_tables_newset,
5        .type       = NFNL_CB_BATCH,
6        .attr_count = NFTA_SET_MAX,
7        .policy     = nft_set_policy,
8    },
9    ··· ···
10}
11static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
12                const struct nlattr * const nla[])
13{
14    const struct nfgenmsg *nfmsg = nlmsg_data(info->nlh);
15    u32 ktype, dtype, flags, policy, gc_int, objtype;
16    struct netlink_ext_ack *extack = info->extack;
17    u8 genmask = nft_genmask_next(info->net);
18    int family = nfmsg->nfgen_family;
19    const struct nft_set_ops *ops;
20    struct nft_expr *expr = NULL;
21    struct net *net = info->net;
22    struct nft_set_desc desc;
23    struct nft_table *table;
24    unsigned char *udata;
25    struct nft_set *set;
26    struct nft_ctx ctx;
27    size_t alloc_size;
28    u64 timeout;
29    char *name;
30    int err, i;
31    u16 udlen;
32    u64 size;
33
34    if (nla[NFTA_SET_TABLE] == NULL || //[1]一些先决条件
35        nla[NFTA_SET_NAME] == NULL ||
36        nla[NFTA_SET_KEY_LEN] == NULL ||
37        nla[NFTA_SET_ID] == NULL)
38        return -EINVAL;
39
40    ··· ···
41    ··· ···//一顿处理
42
43    set = nft_set_lookup(table, nla[NFTA_SET_NAME], genmask);//[2]寻找已经存在的set
44    if (IS_ERR(set)) {//一般是找不到,直接跳过这里,下面初始化set
45        if (PTR_ERR(set) != -ENOENT) {
46            NL_SET_BAD_ATTR(extack, nla[NFTA_SET_NAME]);
47            return PTR_ERR(set);
48        }
49    } else {
50        ··· ···
51    }
52
53    ··· ···
54    set = kvzalloc(alloc_size, GFP_KERNEL);//[3]准备初始化set
55    ··· ···
56
57    INIT_LIST_HEAD(&set->bindings);//初始化set,注意这里的bindings字段
58    INIT_LIST_HEAD(&set->catchall_list);
59    set->table = table;
60    write_pnet(&set->net, net);
61    set->ops = ops;
62    set->ktype = ktype;
63    set->klen = desc.klen;
64    set->dtype = dtype;
65    set->objtype = objtype;
66    set->dlen = desc.dlen;
67    set->flags = flags;
68    set->size = desc.size;
69    set->policy = policy;
70    set->udlen = udlen;
71    set->udata = udata;
72    set->timeout = timeout;
73    set->gc_int = gc_int;
74
75    set->field_count = desc.field_count;
76    for (i = 0; i < desc.field_count; i++)
77        set->field_len[i] = desc.field_len[i];
78
79    err = ops->init(set, &desc, nla);
80    if (err < 0)
81        goto err_set_init;
82
83    if (nla[NFTA_SET_EXPR]) {//[4]存在NFTA_SET_EXPR 情况下的处理
84        expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]);
85        if (IS_ERR(expr)) {
86            err = PTR_ERR(expr);
87            goto err_set_expr_alloc;
88        }
89        set->exprs[0] = expr;
90        set->num_exprs++;
91    } 
92
93    ··· ···
94    ··· ···
95}

[1] 首先是一些需要注意的字段,都要设置了。

[2] 如果已经建立了set,则查找已经存在的并返回,但这里第一次是找不到的,会走到下面进行初始化set。

[3] 申请空间&初始化set 的各个部分,注意这里的bindings 成员,是一个列表结构,在后面看具体信息。

[4] 如果设置了NFTA_SET_EXPR 字段,则进入到NFTA_SET_EXPR 的处理函数nft_set_elem_expr_alloc:

netnetfilternf_tables_api.c : nft_set_elem_expr_alloc

 1struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
2                     const struct nft_set *set,
3                     const struct nlattr *attr)

4
{
5    struct nft_expr *expr;
6    int err;
7
8    expr = nft_expr_init(ctx, attr); //[1]初始化expr
9    if (IS_ERR(expr))
10        return expr;
11
12    err = -EOPNOTSUPP;
13    if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
14        goto err_set_elem_expr;//[2]如果不存在NFT_EXPR_STATEFUL flag,则失败,销毁刚初始化的expr
15
16    ··· ···
17
18err_set_elem_expr:
19    nft_expr_destroy(ctx, expr);//销毁expr 函数
20    return ERR_PTR(err);
21}

[1] 首先进行expr 的初始化调用nft_expr_init 函数,下文分析

[2] 然后如果expr 没有NFT_EXPR_STATEFUL flag的话,则会被销毁,调用nft_expr_destroy,下文分析。

先分析初始化expr 的nft_expr_init 函数:

netnetfilternf_tables_api.c : nft_expr_init

 1struct nft_expr {
2    const struct nft_expr_ops   *ops;//expr 对应的回调函数表 
3    unsigned char           data[]//根据具体expr 而定
4        __attribute__((aligned(__alignof__(u64))));
5};
6
7static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
8                      const struct nlattr *nla)

9
{
10    struct nft_expr_info expr_info;
11    struct nft_expr *expr;
12    struct module *owner;
13    int err;
14
15    err = nf_tables_expr_parse(ctx, nla, &expr_info);//初始化expr_info
16    if (err < 0)
17        goto err1;
18
19    err = -ENOMEM;
20    expr = kzalloc(expr_info.ops->size, GFP_KERNEL);//申请空间 8+私有结构体长度,在该次利用是56
21    if (expr == NULL)
22        goto err2;
23
24    err = nf_tables_newexpr(ctx, &expr_info, expr);//初始化expr
25    if (err < 0)
26        goto err3;
27
28    return expr;
29    ··· ···
30    ··· ···
31}

申请的大小是56,实际申请的属于kmalloc-64,之后相当于直接调用了nf_tables_newexpr 进行struct nft_expr结构体的初始化:

netnetfilternf_tables_api.c : nf_tables_newexpr

 1static int nf_tables_newexpr(const struct nft_ctx *ctx,
2                 const struct nft_expr_info *expr_info,
3                 struct nft_expr *expr)

4
{
5    const struct nft_expr_ops *ops = expr_info->ops;
6    int err;
7
8    expr->ops = ops;
9    if (ops->init) {//调用对应expr自己的init 进行初始化
10        err = ops->init(ctx, expr, (const struct nlattr **)expr_info->tb); 
11        if (err < 0)
12            goto err1;
13    }
14··· ···
15}

实际受到影响的expr 只有look_up 和dynset 两个,分别位于netnetfilternft_lookup.c 和 netnetfilternft_dynset.c(其实是结构体中带有binding字段的),这里以look_up为例:

netnetfilternft_lookup.c : nft_lookup_init

 1static const struct nft_expr_ops nft_lookup_ops = {
2    .type       = &nft_lookup_type,
3    .size       = NFT_EXPR_SIZE(sizeof(struct nft_lookup)), //代表expr->data大小 56
4    .eval       = nft_lookup_eval,
5    .init       = nft_lookup_init,//init 是nft_lookup_init
6    .activate   = nft_lookup_activate,
7    .deactivate = nft_lookup_deactivate,
8    .destroy    = nft_lookup_destroy,
9    .dump       = nft_lookup_dump,
10    .validate   = nft_lookup_validate,
11};
12
13static inline void *nft_expr_priv(const struct nft_expr *expr)
14
{
15    return (void *)expr->data;//获取data地址
16}
17
18static int nft_lookup_init(const struct nft_ctx *ctx,
19               const struct nft_expr *expr,
20               const struct nlattr * const tb[])

21
{
22    struct nft_lookup *priv = nft_expr_priv(expr);//获取expr 的data数据段,这里是nft_lookup结构体
23    u8 genmask = nft_genmask_next(ctx->net);
24    struct nft_set *set;
25    u32 flags;
26    int err;
27
28    if (tb[NFTA_LOOKUP_SET] == NULL ||
29        tb[NFTA_LOOKUP_SREG] == NULL)
30        return -EINVAL;
31
32    set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
33                    tb[NFTA_LOOKUP_SET_ID], genmask);//找到之前创建的set
34    ··· ···//各种初始化
35    ··· ···
36
37    priv->binding.flags = set->flags & NFT_SET_MAP;
38
39    err = nf_tables_bind_set(ctx, set, &priv->binding);//调用nf_tables_bind_set进行绑定
40    if (err < 0)
41        return err;
42
43    priv->set = set;
44    return 0;
45}

先找到expr 结构体中的私有数据指针,对于lookup来说,私有结构是struct nft_lookup。然后找到lookup 报文中对应的搜索set,这里我们设置成我们刚刚创建的set,一顿初始化之后,最后调用nf_tables_bind_set 将lookup 结构和set 绑定到一起:

netnetfilternf_tables_api.c : nf_tables_bind_set

 1int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,
2               struct nft_set_binding *binding)

3
{
4    struct nft_set_binding *i;
5    struct nft_set_iter iter;
6
7    if (set->use == UINT_MAX)
8        return -EOVERFLOW;
9
10    if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))
11        return -EBUSY;
12
13    if (binding->flags & NFT_SET_MAP) {//上层函数设置的,会走入这个分支
14        /* If the set is already bound to the same chain all
15         * jumps are already validated for that chain.
16         */

17        list_for_each_entry(i, &set->bindings, list) {
18            if (i->flags & NFT_SET_MAP &&
19                i->chain == binding->chain)
20                goto bind;
21        }
22        ··· ···
23    }
24bind:
25    binding->chain = ctx->chain;
26    list_add_tail_rcu(&binding->list, &set->bindings);//调用list_add_tail_rcu 链接链表
27    nft_set_trans_bind(ctx, set);
28    set->use++;
29
30    return 0;
31}

nf_tables_bind_set 中主要是调用list_add_tail_rcu 函数将nft_set->bindings 和 nft_lookup->binding->list 用双向链表链接起来。也就是说,是将下面两个结构体的binding(s)字段通过双向链表相连:

 1struct nft_set {
2    struct list_head        list;
3    struct list_head        bindings;//列表
4    struct nft_table        *table;
5    possible_net_t          net;
6    char                *name;
7    ··· ···
8};
9
10struct nft_lookup {
11    struct nft_set          *set;
12    u8              sreg;
13    u8              dreg;
14    bool                invert;
15    struct nft_set_binding      binding;//列表
16};
17
18struct nft_set_binding {
19    struct list_head        list;
20    const struct nft_chain      *chain;
21    u32             flags;
22};

整个过程没什么问题,但回看申请expr的函数:

 1struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
2                     const struct nft_set *set,
3                     const struct nlattr *attr)

4
{
5    struct nft_expr *expr;
6    int err;
7
8    expr = nft_expr_init(ctx, attr); //[1]初始化expr
9    if (IS_ERR(expr))
10        return expr;
11
12    err = -EOPNOTSUPP;
13    if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
14        goto err_set_elem_expr;//[2]如果不存在NFT_EXPR_STATEFUL flag,则失败,销毁刚初始化的expr
15
16    ··· ···
17
18err_set_elem_expr:
19    nft_expr_destroy(ctx, expr);//销毁expr 函数
20    return ERR_PTR(err);
21}

在[1] 中完成了空间分配、链接到set 等操作,但如果在[2]中不满足,则会调用nft_expr_destroy 去销毁这个expr:

netnetfilternf_tables_api.c & netnetfilternft_lookup.c

 1void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
2
{
3    nf_tables_expr_destroy(ctx, expr);//调用nf_tables_expr_destroy
4    kfree(expr);
5}
6static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
7                   struct nft_expr *expr)

8
{
9    const struct nft_expr_type *type = expr->ops->type;
10
11    if (expr->ops->destroy)//调用lookup自己的destory函数
12        expr->ops->destroy(ctx, expr);
13    module_put(type->owner);
14}
15static void nft_lookup_destroy(const struct nft_ctx *ctx,
16                   const struct nft_expr *expr)

17
{
18    struct nft_lookup *priv = nft_expr_priv(expr);
19
20    nf_tables_destroy_set(ctx, priv->set);//基本什么也没干,调用这个函数也没啥可干的
21}
22void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
23
{
24    if (list_empty(&set->bindings) && nft_set_is_anonymous(set))//不满足条件
25        nft_set_destroy(ctx, set);
26}

可以看到整个destroy 调用栈除了free 了expr 结构体之外就没干啥事。最主要的是忘记将expr 从set 的双向链表中卸下来了,导致后面的uaf。

UAF写

如果再次使用SET_EXPR功能,则会在已经释放的堆块后面再链接一个堆块,造成偏移0x18的uaf 写:

 1#define list_add_tail_rcu        list_add_tail
2static inline void list_add_tail(struct list_head *new, struct list_head *head)
3
{
4    __list_add(new, head->prev, head);
5}
6static inline void __list_add(struct list_head *new,
7                  struct list_head *prev,
8                  struct list_head *next)
9{
10    if (!__list_add_valid(new, prev, next))
11        return;
12
13    next->prev = new;
14    new->next = next;
15    new->prev = prev;
16    WRITE_ONCE(prev->next, new);
17}

根据list 操作的代码和本次参与运算的结构体,可以看出,该uaf写实篡改偏移为0x18 和偏移为0x20的两个字段指向另外两个堆地址。我们这里主要关注偏移0x18,会将其指向一个新的expr(kmalloc-64)的偏移0x18处。

漏洞利用

限制

首先漏洞所在的堆是用GFP_KERNEL 申请的,与常用的堆利用原语如msg_msg等(使用GFP_KERNEL_ACCOUNT申请)不是在同slab中。

1expr = kzalloc(expr_info.ops->size, GFP_KERNEL);//申请空间

其次,uaf 写的限制比较明显,在0x18的地方写一个堆地址,写的偏移和内容我们不可控。

然后,漏洞所在结构体属于kmalloc-64

泄露堆地址

由于不能使用msg_msg,这里采取的是使用usr_key_payload来利用,user_key_payload 同样是可以自定义大小的内核结构体,但是用GFP_KERNEL申请,可以跟漏洞结构体申请到同slab。并且data字段是用户可控内容

 1struct user_key_payload {
2    struct rcu_head rcu;        /* RCU destructor */
3    unsigned short  datalen;    /* length of this data */
4    char        data[] __aligned(__alignof__(u64)); /* 变长数据区,用户可控数据 */
5};
6int user_preparse(struct key_preparsed_payload *prep)
7
{
8    struct user_key_payload *upayload;
9    size_t datalen = prep->datalen;
10
11    ··· ···
12    upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
13    if (!upayload)
14        return -ENOMEM;
15    ··· ···
16}

而且user_key_payload 的data 数据偏移正好是0x18,也就是说如果我们在上面expr 结构体释放之后使用usr_key_payload 占领空位,然后使用uaf ,则会改变data数据段,那么我们读取该key 就可以读到一个堆地址(用来干什么后文描述)。

[漏洞分析] CVE-2022-32250 netfilter UAF内核提权

泄露内核地址

posix 消息队列

泄露linux内核地址这里采用的是mqueue 的posix消息队列模块,该模块和msg_msg一样是IPC进程间通信的消息队列功能。我们这里使用的posix_msg_tree_node结构体内容如下:

 1struct posix_msg_tree_node {
2    struct rb_node      rb_node;
3    struct list_head    msg_list;//偏移0x18,该字段管理了一个msg_msg 链表
4    int         priority;
5};
6
7struct rb_node {//长度0x18
8    unsigned long  __rb_parent_color;
9    struct rb_node *rb_right;
10    struct rb_node *rb_left;
11} __attribute__((aligned(sizeof(long))));

该结构体的初始化与使用主要是在do_mq_timedsend函数中:

ipcmqueue.c : do_mq_timedsend

 1//[1]属于mq_timedsend系统调用
2SYSCALL_DEFINE5(mq_timedsend, mqd_t, mqdes, const char __user *, u_msg_ptr,
3        size_t, msg_len, unsigned int, msg_prio,
4        const struct __kernel_timespec __user *, u_abs_timeout)
5{
6    struct timespec64 ts, *p = NULL;
7    if (u_abs_timeout) {
8        int res = prepare_timeout(u_abs_timeout, &ts);
9        if (res)
10            return res;
11        p = &ts;
12    }
13    return do_mq_timedsend(mqdes, u_msg_ptr, msg_len, msg_prio, p);
14}
15
16static int do_mq_timedsend(mqd_t mqdes, const char __user *u_msg_ptr,
17        size_t msg_len, unsigned int msg_prio,
18        struct timespec64 *ts)

19
{
20    struct fd f;
21    struct inode *inode;
22    struct ext_wait_queue wait;
23    struct ext_wait_queue *receiver;
24    struct msg_msg *msg_ptr;
25    struct mqueue_inode_info *info;
26    ktime_t expires, *timeout = NULL;
27    struct posix_msg_tree_node *new_leaf = NULL;
28    int ret = 0;
29    DEFINE_WAKE_Q(wake_q);
30
31    ··· ···
32    ··· ···
33
34    /* First try to allocate memory, before doing anything with
35     * existing queues. */

36    msg_ptr = load_msg(u_msg_ptr, msg_len);//[2] 从用户空间获得消息
37    if (IS_ERR(msg_ptr)) {
38        ret = PTR_ERR(msg_ptr);
39        goto out_fput;
40    }
41    msg_ptr->m_ts = msg_len;
42    msg_ptr->m_type = msg_prio;
43
44    /*
45     * msg_insert really wants us to have a valid, spare node struct so
46     * it doesn't have to kmalloc a GFP_ATOMIC allocation, but it will
47     * fall back to that if necessary.
48     */

49    if (!info->node_cache)
50        new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL);//[3]申请posix_msg_tree_node结构体
51
52    spin_lock(&info->lock);
53
54    if (!info->node_cache && new_leaf) {
55        /* Save our speculative allocation into the cache */
56        INIT_LIST_HEAD(&new_leaf->msg_list);
57        info->node_cache = new_leaf;//将申请的posix_msg_tree_node结构体存入mqueue_inode_info中
58        new_leaf = NULL;
59    } else {
60        kfree(new_leaf);
61    }
62
63    ··· ···
64
65    if (info->attr.mq_curmsgs == info->attr.mq_maxmsg) {
66        ··· ···
67    } else {
68        receiver = wq_get_first_waiter(info, RECV);
69        if (receiver) {
70            pipelined_send(&wake_q, info, msg_ptr, receiver);
71        } else {
72            /* adds message to the queue */
73            ret = msg_insert(msg_ptr, info);//[4]将消息插入消息队列
74            if (ret)
75                goto out_unlock;
76            __do_notify(info);
77        }
78        inode->i_atime = inode->i_mtime = inode->i_ctime =
79                current_time(inode);
80    }
81    ··· ···
82}

[1] 该操作的主要流程比较简单,属于mq_timedsend 系统调用,并且主要逻辑发生在do_mq_timedsend 函数之中

[2] 首先该系统调用会创建一个消息队列,消息和msg_msg 一样,这里调用load_msg 函数获取用户构造的消息,关于load_msg 可以查看[kernel exploit] 消息队列msg系列在内核漏洞利用中的应用文章

[3] 然后会为struct posix_msg_tree_node结构体申请空间,使用GFP_KERNELflag,这样可以和漏洞结构所在同一个slab,并且大小相同。之后会将申请的struct posix_msg_tree_node结构体存入mqueue_inode_info中,mqueue_inode_info会记录在inode中用于后续查找

[4] 最后调用msg_insert 函数将消息添加到消息队列:

 1static int msg_insert(struct msg_msg *msg, struct mqueue_inode_info *info)
2
{
3    struct rb_node **p, *parent = NULL;
4    struct posix_msg_tree_node *leaf;
5    bool rightmost = true;
6
7    ··· ···
8    ··· ···
9insert_msg:
10    info->attr.mq_curmsgs++;
11    info->qsize += msg->m_ts;
12    list_add_tail(&msg->m_list, &leaf->msg_list); //将消息添加到msg_list
13    return 0;
14}

在msg_insert 函数中将用户传入的msg_msg 添加到posix_msg_tree_node->msg_list 链表。

可以使用do_mq_timedreceive 函数读取posix 消息队列中的消息:

 1SYSCALL_DEFINE5(mq_timedreceive, mqd_t, mqdes, char __user *, u_msg_ptr,//[1]属于mq_timedreceive系统调用
2        size_t, msg_len, unsigned int __user *, u_msg_prio,
3        const struct __kernel_timespec __user *, u_abs_timeout)
4{
5    ··· ···
6    return do_mq_timedreceive(mqdes, u_msg_ptr, msg_len, u_msg_prio, p);
7}
8
9static int do_mq_timedreceive(mqd_t mqdes, char __user *u_msg_ptr,
10        size_t msg_len, unsigned int __user *u_msg_prio,
11        struct timespec64 *ts)

12
{
13    ssize_t ret;
14    struct msg_msg *msg_ptr;
15    struct fd f;
16    struct inode *inode;
17    struct mqueue_inode_info *info;
18    struct ext_wait_queue wait;
19    ktime_t expires, *timeout = NULL;
20    struct posix_msg_tree_node *new_leaf = NULL;
21
22    ··· ···
23
24    inode = file_inode(f.file);
25    if (unlikely(f.file->f_op != &mqueue_file_operations)) {
26        ret = -EBADF;
27        goto out_fput;
28    }
29    info = MQUEUE_I(inode);//[2]从inode中获取mqueue_inode_info
30    audit_file(f.file);
31
32    ··· ···
33
34    if (!info->node_cache && new_leaf) {
35        /* Save our speculative allocation into the cache */
36        INIT_LIST_HEAD(&new_leaf->msg_list);
37        info->node_cache = new_leaf;//获取posix_msg_tree_node
38    } else {
39        kfree(new_leaf);
40    }
41
42    if (info->attr.mq_curmsgs == 0) {
43        ··· ···
44    } else {//消息队列消息数量不为0
45        DEFINE_WAKE_Q(wake_q);
46
47        msg_ptr = msg_get(info);//[3]从消息队列获取一个消息
48
49        ··· ···
50    }
51    if (ret == 0) {
52        ret = msg_ptr->m_ts;
53
54        if ((u_msg_prio && put_user(msg_ptr->m_type, u_msg_prio)) ||
55            store_msg(u_msg_ptr, msg_ptr, msg_ptr->m_ts)) {//[4]将消息发送到用户层
56            ret = -EFAULT;
57        }
58        free_msg(msg_ptr);//[5]释放消息
59    }
60out_fput:
61    fdput(f);
62out:
63    return ret;
64}

[1] 从posix消息队列接收消息属于mq_timedreceive系统调用

[2] 首先根据消息队列的文件描述符获取对应inode再获取struct posix_msg_tree_node结构

[3] 消息队列中消息数量不为0,则获取第一个消息出来:

 1static inline struct msg_msg *msg_get(struct mqueue_inode_info *info)
2
{
3    ··· ···
4    } else {
5        msg = list_first_entry(&leaf->msg_list,//获取msg_list中第一个消息
6                       struct msg_msg, m_list);
7        list_del(&msg->m_list);//然后从消息队列中删除
8        if (list_empty(&leaf->msg_list)) {
9            msg_tree_erase(leaf, info);
10        }
11    }
12    info->attr.mq_curmsgs--;//消息队列数量减少
13    info->qsize -= msg->m_ts;
14    return msg;
15}

[4] 调用store_msg 会将消息使用copy_to_user发送给用户层,具体参考[kernel exploit] 消息队列msg系列在内核漏洞利用中的应用文章

[5] 调用free_msg 释放消息,这里有一个坑:

 1void free_msg(struct msg_msg *msg)
2
{
3    struct msg_msgseg *seg;
4
5    security_msg_msg_free(msg);
6
7    seg = msg->next;
8    kfree(msg);
9    while (seg != NULL) {
10        struct msg_msgseg *tmp = seg->next;
11
12        cond_resched();
13        kfree(seg);
14        seg = tmp;
15    }
16}
17void security_msg_msg_free(struct msg_msg *msg)
18
{
19    call_void_hook(msg_msg_free_security, msg);
20    kfree(msg->security);
21    msg->security = NULL;
22}

这里会释放msg->security 字段,所以非法释放的时候必须要保证msg->security 为0。

泄露

也就是说,我们uaf写如果写到struct posix_msg_tree_node的偏移0x18处,会改写msg_list ,而msg_list 是msg_msg链表,会改写它指向一个kmalloc-64的偏移0x18处。所以我们使用如下堆布局:

[漏洞分析] CVE-2022-32250 netfilter UAF内核提权

  • 首先申请一个look_up的struct nft_expr结构,并对其进行UAF,先free

  • 使用struct posix_msg_tree_node作为被uaf目标,占领刚free的堆地址

  • 开始UAF,会将posix_msg_tree_node->msg_list字段改写指向下一个struct nft_expr的偏移0x18处

  • 而msg_list 字段原本是指向一个struct msg_msg结构体,所以会将下一个struct nft_expr的偏移0x18处开始认为成一个msg_msg。

  • 利用mq_timedreceive 读取消息,就可以读到下一个堆块的第二个字段16个字节。这是由于copy_to_user 中有heap_check,会检查拷贝大小是否超出内存所在slab 的大小,所以这里我们最多就读0x10字节。

所以这里的坑是:

  1. 由于copy_to_user 的限制,只能读到下一个堆的8字节偏移开始0x10字节长度,所以需要选择第二第三个字段有内核地址指针的结构,并且属于kmalloc-64

  2. 在mq_timedreceive 最后还会调用msg_free释放msg_msg结构,而msg_free 中会释放msg_msg->security 指针,必须要保证第一个字段为0 才行,否则会崩溃

所以这里还是选择user_key_payload:

 1struct user_key_payload {
2    struct rcu_head rcu;        /* RCU destructor */
3    unsigned short  datalen;    /* length of this data */
4    char        data[] __aligned(__alignof__(u64)); /* 变长数据区,用户可控数据 */
5};
6struct callback_head {
7    struct callback_head *next;
8    void (*func)(struct callback_head *head);
9} __attribute__((aligned(sizeof(void *))));
10#define rcu_head callback_head

user_key_payload 前0x10是struct callback_head,他的第一个字段是next指针,正常情况下就是0,满足msg_free 释放msg_msg->security 指针的绕过条件,并且第二个字段是一个函数指针func,指向user_free_payload_rcu函数。正好可以泄露user_free_payload_rcu 的地址计算出kernel 的基地址。

改写modprobe_path

接下来利用unlink 来复写modprobe_path,使用如下方式:

[漏洞分析] CVE-2022-32250 netfilter UAF内核提权


  • 构造跟上一段结尾相同的堆布局的堆布局,用posix_msg_tree_node来uaf并篡改msg_list 指向下一个nft_expr

  • 然后释放该expr,用usr_key_payload 占领,data段正好覆盖该expr 相对于msg_msg的list 头部分

  • 使用usr_key_payload 的data段覆盖msg_msg 的mlist.next与mlist.prev为&modprobe_path-7 和 0xffff????2f706d74

  • &modprobe_path-7 就是modprobe_path 的地址减7,这样后面unlink就可以篡改modprobe_path 的第二字节到第九字节这8个字节

  • 0xffff????2f706d74 是一个堆地址,其中后四字节是”tmp/”字符,前面0xffff????是堆地址的范围,其中问号表示地址随机的部分,之前我们已经泄露过堆地址了,所以是知道问号部分的。之所以覆盖为这个,为了将modprobe_path 的第二字节到第九字节 篡改为0xffff????2f706d74,这样它就可以变成字符串:”/tmp/????xffxffprobe”。并且unlink 利用的限制0xffff????2f706d74 也是一个可以被写入的地址才行,而这属于堆地址空间,可以被写入。

  • 然后使用mq_timedreceive 接收消息之后的msg_get函数中的list_del,将该msg_msg 从列表中删除触发unlink:

 1static inline void list_del(struct list_head *entry)
2
{
3    __list_del_entry(entry);
4    entry->next = LIST_POISON1;
5    entry->prev = LIST_POISON2;
6}
7static inline void __list_del_entry(struct list_head *entry)
8{
9    if (!__list_del_entry_valid(entry))
10        return;
11
12    __list_del(entry->prev, entry->next);
13}
14static inline void __list_del(struct list_head * prev, struct list_head * next)
15{
16    next->prev = prev;//unlink 写
17    WRITE_ONCE(prev->next, next);//unlink 写
18}

参考

https://blog.theori.io/research/CVE-2022-32250-linux-kernel-lpe-2022/

https://www.openwall.com/lists/oss-security/2022/05/31/1

https://github.com/theori-io/CVE-2022-32250-exploit

本文仅代表作者本人观点,用于技术探讨和交流,如有谬误,欢迎指正!

原文始发于微信公众号(华为安全应急响应中心):[漏洞分析] CVE-2022-32250 netfilter UAF内核提权

版权声明:admin 发表于 2023年1月31日 下午5:27。
转载请注明:[漏洞分析] CVE-2022-32250 netfilter UAF内核提权 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...