漏洞简介
漏洞编号: 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才行。
利用效果:
漏洞原理
漏洞触发
漏洞发生在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 就可以读到一个堆地址(用来干什么后文描述)。
泄露内核地址
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_KERNEL
flag,这样可以和漏洞结构所在同一个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处。所以我们使用如下堆布局:
-
首先申请一个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字节。
所以这里的坑是:
-
由于copy_to_user 的限制,只能读到下一个堆的8字节偏移开始0x10字节长度,所以需要选择第二第三个字段有内核地址指针的结构,并且属于kmalloc-64
-
在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,使用如下方式:
-
构造跟上一段结尾相同的堆布局的堆布局,用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内核提权