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

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

文章目录

  • [漏洞分析] CVE-2022-32250 netfilter UAF内核提权
    • 漏洞简介
    • 环境搭建
    • 漏洞原理
      • 漏洞触发
      • UAF写
    • 漏洞利用
      • 限制
      • 泄露堆地址
      • 泄露内核地址
        • posix 消息队列
        • 泄露
      • 改写modprobe_path
    • 参考

漏洞简介

漏洞编号: 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内核提权_第1张图片

漏洞原理

漏洞触发

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

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

net\netfilter\nf_tables_api.c : nf_tables_newset

static const struct nfnl_callback nf_tables_cb[NFT_MSG_MAX] = {
	··· ···
	[NFT_MSG_NEWSET] = {
		.call		= nf_tables_newset,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_SET_MAX,
		.policy		= nft_set_policy,
	},
	··· ···
}
static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
			    const struct nlattr * const nla[])
{
	const struct nfgenmsg *nfmsg = nlmsg_data(info->nlh);
	u32 ktype, dtype, flags, policy, gc_int, objtype;
	struct netlink_ext_ack *extack = info->extack;
	u8 genmask = nft_genmask_next(info->net);
	int family = nfmsg->nfgen_family;
	const struct nft_set_ops *ops;
	struct nft_expr *expr = NULL;
	struct net *net = info->net;
	struct nft_set_desc desc;
	struct nft_table *table;
	unsigned char *udata;
	struct nft_set *set;
	struct nft_ctx ctx;
	size_t alloc_size;
	u64 timeout;
	char *name;
	int err, i;
	u16 udlen;
	u64 size;

	if (nla[NFTA_SET_TABLE] == NULL || //[1]一些先决条件
	    nla[NFTA_SET_NAME] == NULL ||
	    nla[NFTA_SET_KEY_LEN] == NULL ||
	    nla[NFTA_SET_ID] == NULL)
		return -EINVAL;
    
	··· ···
    ··· ···//一顿处理
        
	set = nft_set_lookup(table, nla[NFTA_SET_NAME], genmask);//[2]寻找已经存在的set
	if (IS_ERR(set)) {//一般是找不到,直接跳过这里,下面初始化set
		if (PTR_ERR(set) != -ENOENT) {
			NL_SET_BAD_ATTR(extack, nla[NFTA_SET_NAME]);
			return PTR_ERR(set);
		}
	} else {
		··· ···
	}

	··· ···
	set = kvzalloc(alloc_size, GFP_KERNEL);//[3]准备初始化set
	··· ···

	INIT_LIST_HEAD(&set->bindings);//初始化set,注意这里的bindings字段
	INIT_LIST_HEAD(&set->catchall_list);
	set->table = table;
	write_pnet(&set->net, net);
	set->ops = ops;
	set->ktype = ktype;
	set->klen = desc.klen;
	set->dtype = dtype;
	set->objtype = objtype;
	set->dlen = desc.dlen;
	set->flags = flags;
	set->size = desc.size;
	set->policy = policy;
	set->udlen = udlen;
	set->udata = udata;
	set->timeout = timeout;
	set->gc_int = gc_int;

	set->field_count = desc.field_count;
	for (i = 0; i < desc.field_count; i++)
		set->field_len[i] = desc.field_len[i];

	err = ops->init(set, &desc, nla);
	if (err < 0)
		goto err_set_init;

	if (nla[NFTA_SET_EXPR]) {//[4]存在NFTA_SET_EXPR 情况下的处理
		expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]);
		if (IS_ERR(expr)) {
			err = PTR_ERR(expr);
			goto err_set_expr_alloc;
		}
		set->exprs[0] = expr;
		set->num_exprs++;
	} 
    
    ··· ···
    ··· ···
}

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

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

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

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

net\netfilter\nf_tables_api.c : nft_set_elem_expr_alloc

struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
					 const struct nft_set *set,
					 const struct nlattr *attr)
{
	struct nft_expr *expr;
	int err;

	expr = nft_expr_init(ctx, attr); //[1]初始化expr
	if (IS_ERR(expr))
		return expr;

	err = -EOPNOTSUPP;
	if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
		goto err_set_elem_expr;//[2]如果不存在NFT_EXPR_STATEFUL flag,则失败,销毁刚初始化的expr

	··· ···

err_set_elem_expr:
	nft_expr_destroy(ctx, expr);//销毁expr 函数
	return ERR_PTR(err);
}

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

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

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

net\netfilter\nf_tables_api.c : nft_expr_init

struct nft_expr {
	const struct nft_expr_ops	*ops;//expr 对应的回调函数表 
	unsigned char			data[]//根据具体expr 而定
		__attribute__((aligned(__alignof__(u64))));
};

static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
				      const struct nlattr *nla)
{
	struct nft_expr_info expr_info;
	struct nft_expr *expr;
	struct module *owner;
	int err;

	err = nf_tables_expr_parse(ctx, nla, &expr_info);//初始化expr_info
	if (err < 0)
		goto err1;

	err = -ENOMEM;
	expr = kzalloc(expr_info.ops->size, GFP_KERNEL);//申请空间 8+私有结构体长度,在该次利用是56
	if (expr == NULL)
		goto err2;

	err = nf_tables_newexpr(ctx, &expr_info, expr);//初始化expr
	if (err < 0)
		goto err3;

	return expr;
	··· ···
    ··· ···
}

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

net\netfilter\nf_tables_api.c : nf_tables_newexpr

static int nf_tables_newexpr(const struct nft_ctx *ctx,
			     const struct nft_expr_info *expr_info,
			     struct nft_expr *expr)
{
	const struct nft_expr_ops *ops = expr_info->ops;
	int err;

	expr->ops = ops;
	if (ops->init) {//调用对应expr自己的init 进行初始化
		err = ops->init(ctx, expr, (const struct nlattr **)expr_info->tb); 
		if (err < 0)
			goto err1;
	}
··· ···
}

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

net\netfilter\nft_lookup.c : nft_lookup_init

static const struct nft_expr_ops nft_lookup_ops = {
	.type		= &nft_lookup_type,
	.size		= NFT_EXPR_SIZE(sizeof(struct nft_lookup)), //代表expr->data大小 56
	.eval		= nft_lookup_eval,
	.init		= nft_lookup_init,//init 是nft_lookup_init
	.activate	= nft_lookup_activate,
	.deactivate	= nft_lookup_deactivate,
	.destroy	= nft_lookup_destroy,
	.dump		= nft_lookup_dump,
	.validate	= nft_lookup_validate,
};

static inline void *nft_expr_priv(const struct nft_expr *expr)
{
	return (void *)expr->data;//获取data地址
}

static int nft_lookup_init(const struct nft_ctx *ctx,
			   const struct nft_expr *expr,
			   const struct nlattr * const tb[])
{
	struct nft_lookup *priv = nft_expr_priv(expr);//获取expr 的data数据段,这里是nft_lookup结构体
	u8 genmask = nft_genmask_next(ctx->net);
	struct nft_set *set;
	u32 flags;
	int err;

	if (tb[NFTA_LOOKUP_SET] == NULL ||
	    tb[NFTA_LOOKUP_SREG] == NULL)
		return -EINVAL;

	set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
				    tb[NFTA_LOOKUP_SET_ID], genmask);//找到之前创建的set
	··· ···//各种初始化
    ··· ···

	priv->binding.flags = set->flags & NFT_SET_MAP;

	err = nf_tables_bind_set(ctx, set, &priv->binding);//调用nf_tables_bind_set进行绑定
	if (err < 0)
		return err;

	priv->set = set;
	return 0;
}

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

net\netfilter\nf_tables_api.c : nf_tables_bind_set

int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,
		       struct nft_set_binding *binding)
{
	struct nft_set_binding *i;
	struct nft_set_iter iter;

	if (set->use == UINT_MAX)
		return -EOVERFLOW;

	if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))
		return -EBUSY;

	if (binding->flags & NFT_SET_MAP) {//上层函数设置的,会走入这个分支
		/* If the set is already bound to the same chain all
		 * jumps are already validated for that chain.
		 */
		list_for_each_entry(i, &set->bindings, list) {
			if (i->flags & NFT_SET_MAP &&
			    i->chain == binding->chain)
				goto bind;
		}
		··· ···
	}
bind:
	binding->chain = ctx->chain;
	list_add_tail_rcu(&binding->list, &set->bindings);//调用list_add_tail_rcu 链接链表
	nft_set_trans_bind(ctx, set);
	set->use++;

	return 0;
}

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

struct nft_set {
	struct list_head		list;
	struct list_head		bindings;//列表
	struct nft_table		*table;
	possible_net_t			net;
	char				*name;
	··· ···
};

struct nft_lookup {
	struct nft_set			*set;
	u8				sreg;
	u8				dreg;
	bool				invert;
	struct nft_set_binding		binding;//列表
};

struct nft_set_binding {
	struct list_head		list;
	const struct nft_chain		*chain;
	u32				flags;
};

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

struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
					 const struct nft_set *set,
					 const struct nlattr *attr)
{
	struct nft_expr *expr;
	int err;

	expr = nft_expr_init(ctx, attr); //[1]初始化expr
	if (IS_ERR(expr))
		return expr;

	err = -EOPNOTSUPP;
	if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
		goto err_set_elem_expr;//[2]如果不存在NFT_EXPR_STATEFUL flag,则失败,销毁刚初始化的expr

	··· ···

err_set_elem_expr:
	nft_expr_destroy(ctx, expr);//销毁expr 函数
	return ERR_PTR(err);
}

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

net\netfilter\nf_tables_api.c & net\netfilter\nft_lookup.c

void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
{
	nf_tables_expr_destroy(ctx, expr);//调用nf_tables_expr_destroy
	kfree(expr);
}
static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
				   struct nft_expr *expr)
{
	const struct nft_expr_type *type = expr->ops->type;

	if (expr->ops->destroy)//调用lookup自己的destory函数
		expr->ops->destroy(ctx, expr);
	module_put(type->owner);
}
static void nft_lookup_destroy(const struct nft_ctx *ctx,
			       const struct nft_expr *expr)
{
	struct nft_lookup *priv = nft_expr_priv(expr);
	
	nf_tables_destroy_set(ctx, priv->set);//基本什么也没干,调用这个函数也没啥可干的
}
void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
{
	if (list_empty(&set->bindings) && nft_set_is_anonymous(set))//不满足条件
		nft_set_destroy(ctx, set);
}

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

UAF写

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

#define list_add_tail_rcu		list_add_tail
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}
static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	if (!__list_add_valid(new, prev, next))
		return;

	next->prev = new;
	new->next = next;
	new->prev = prev;
	WRITE_ONCE(prev->next, new);
}

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

漏洞利用

限制

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

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

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

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

泄露堆地址

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

struct user_key_payload {
	struct rcu_head	rcu;		/* RCU destructor */
	unsigned short	datalen;	/* length of this data */
	char		data[] __aligned(__alignof__(u64)); /* 变长数据区,用户可控数据 */
};
int user_preparse(struct key_preparsed_payload *prep)
{
	struct user_key_payload *upayload;
	size_t datalen = prep->datalen;

	··· ···
	upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
	if (!upayload)
		return -ENOMEM;
	··· ···
}

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

在这里插入图片描述

泄露内核地址

posix 消息队列

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

struct posix_msg_tree_node {
    struct rb_node      rb_node;
    struct list_head    msg_list;//偏移0x18,该字段管理了一个msg_msg 链表
    int         priority;
};

struct rb_node {//长度0x18
    unsigned long  __rb_parent_color;
    struct rb_node *rb_right;
    struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));

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

ipc\mqueue.c : do_mq_timedsend

//[1]属于mq_timedsend系统调用
SYSCALL_DEFINE5(mq_timedsend, mqd_t, mqdes, const char __user *, u_msg_ptr,
		size_t, msg_len, unsigned int, msg_prio,
		const struct __kernel_timespec __user *, u_abs_timeout)
{
	struct timespec64 ts, *p = NULL;
	if (u_abs_timeout) {
		int res = prepare_timeout(u_abs_timeout, &ts);
		if (res)
			return res;
		p = &ts;
	}
	return do_mq_timedsend(mqdes, u_msg_ptr, msg_len, msg_prio, p);
}

static int do_mq_timedsend(mqd_t mqdes, const char __user *u_msg_ptr,
		size_t msg_len, unsigned int msg_prio,
		struct timespec64 *ts)
{
	struct fd f;
	struct inode *inode;
	struct ext_wait_queue wait;
	struct ext_wait_queue *receiver;
	struct msg_msg *msg_ptr;
	struct mqueue_inode_info *info;
	ktime_t expires, *timeout = NULL;
	struct posix_msg_tree_node *new_leaf = NULL;
	int ret = 0;
	DEFINE_WAKE_Q(wake_q);

	··· ···
    ··· ···

	/* First try to allocate memory, before doing anything with
	 * existing queues. */
	msg_ptr = load_msg(u_msg_ptr, msg_len);//[2] 从用户空间获得消息
	if (IS_ERR(msg_ptr)) {
		ret = PTR_ERR(msg_ptr);
		goto out_fput;
	}
	msg_ptr->m_ts = msg_len;
	msg_ptr->m_type = msg_prio;

	/*
	 * msg_insert really wants us to have a valid, spare node struct so
	 * it doesn't have to kmalloc a GFP_ATOMIC allocation, but it will
	 * fall back to that if necessary.
	 */
	if (!info->node_cache)
		new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL);//[3]申请posix_msg_tree_node结构体
    
	spin_lock(&info->lock);
    
	if (!info->node_cache && new_leaf) {
		/* Save our speculative allocation into the cache */
		INIT_LIST_HEAD(&new_leaf->msg_list);
		info->node_cache = new_leaf;//将申请的posix_msg_tree_node结构体存入mqueue_inode_info中
		new_leaf = NULL;
	} else {
		kfree(new_leaf);
	}

	··· ···

	if (info->attr.mq_curmsgs == info->attr.mq_maxmsg) {
		··· ···
	} else {
		receiver = wq_get_first_waiter(info, RECV);
		if (receiver) {
			pipelined_send(&wake_q, info, msg_ptr, receiver);
		} else {
			/* adds message to the queue */
			ret = msg_insert(msg_ptr, info);//[4]将消息插入消息队列
			if (ret)
				goto out_unlock;
			__do_notify(info);
		}
		inode->i_atime = inode->i_mtime = inode->i_ctime =
				current_time(inode);
	}
	··· ···
}

[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 函数将消息添加到消息队列:

static int msg_insert(struct msg_msg *msg, struct mqueue_inode_info *info)
{
	struct rb_node **p, *parent = NULL;
	struct posix_msg_tree_node *leaf;
	bool rightmost = true;

	··· ···
    ··· ···
insert_msg:
	info->attr.mq_curmsgs++;
	info->qsize += msg->m_ts;
	list_add_tail(&msg->m_list, &leaf->msg_list); //将消息添加到msg_list
	return 0;
}

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

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

SYSCALL_DEFINE5(mq_timedreceive, mqd_t, mqdes, char __user *, u_msg_ptr,//[1]属于mq_timedreceive系统调用
		size_t, msg_len, unsigned int __user *, u_msg_prio,
		const struct __kernel_timespec __user *, u_abs_timeout)
{
	··· ···
	return do_mq_timedreceive(mqdes, u_msg_ptr, msg_len, u_msg_prio, p);
}

static int do_mq_timedreceive(mqd_t mqdes, char __user *u_msg_ptr,
		size_t msg_len, unsigned int __user *u_msg_prio,
		struct timespec64 *ts)
{
	ssize_t ret;
	struct msg_msg *msg_ptr;
	struct fd f;
	struct inode *inode;
	struct mqueue_inode_info *info;
	struct ext_wait_queue wait;
	ktime_t expires, *timeout = NULL;
	struct posix_msg_tree_node *new_leaf = NULL;

	··· ···

	inode = file_inode(f.file);
	if (unlikely(f.file->f_op != &mqueue_file_operations)) {
		ret = -EBADF;
		goto out_fput;
	}
	info = MQUEUE_I(inode);//[2]从inode中获取mqueue_inode_info
	audit_file(f.file);

	··· ···

	if (!info->node_cache && new_leaf) {
		/* Save our speculative allocation into the cache */
		INIT_LIST_HEAD(&new_leaf->msg_list);
		info->node_cache = new_leaf;//获取posix_msg_tree_node
	} else {
		kfree(new_leaf);
	}

	if (info->attr.mq_curmsgs == 0) {
		··· ···
	} else {//消息队列消息数量不为0
		DEFINE_WAKE_Q(wake_q);

		msg_ptr = msg_get(info);//[3]从消息队列获取一个消息

		··· ···
	}
	if (ret == 0) {
		ret = msg_ptr->m_ts;

		if ((u_msg_prio && put_user(msg_ptr->m_type, u_msg_prio)) ||
			store_msg(u_msg_ptr, msg_ptr, msg_ptr->m_ts)) {//[4]将消息发送到用户层
			ret = -EFAULT;
		}
		free_msg(msg_ptr);//[5]释放消息
	}
out_fput:
	fdput(f);
out:
	return ret;
}

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

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

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

static inline struct msg_msg *msg_get(struct mqueue_inode_info *info)
{
	··· ···
	} else {
		msg = list_first_entry(&leaf->msg_list,//获取msg_list中第一个消息
				       struct msg_msg, m_list);
		list_del(&msg->m_list);//然后从消息队列中删除
		if (list_empty(&leaf->msg_list)) {
			msg_tree_erase(leaf, info);
		}
	}
	info->attr.mq_curmsgs--;//消息队列数量减少
	info->qsize -= msg->m_ts;
	return msg;
}

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

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

void free_msg(struct msg_msg *msg)
{
	struct msg_msgseg *seg;

	security_msg_msg_free(msg);

	seg = msg->next;
	kfree(msg);
	while (seg != NULL) {
		struct msg_msgseg *tmp = seg->next;

		cond_resched();
		kfree(seg);
		seg = tmp;
	}
}
void security_msg_msg_free(struct msg_msg *msg)
{
	call_void_hook(msg_msg_free_security, msg);
	kfree(msg->security);
	msg->security = NULL;
}

这里会释放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内核提权_第2张图片

  • 首先申请一个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:

struct user_key_payload {
	struct rcu_head	rcu;		/* RCU destructor */
	unsigned short	datalen;	/* length of this data */
	char		data[] __aligned(__alignof__(u64)); /* 变长数据区,用户可控数据 */
};
struct callback_head {
    struct callback_head *next;
    void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));
#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内核提权_第3张图片

  • 构造跟上一段结尾相同的堆布局的堆布局,用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/???\xff\xffprobe"。并且unlink 利用的限制0xffff???2f706d74 也是一个可以被写入的地址才行,而这属于堆地址空间,可以被写入。
  • 然后使用mq_timedreceive 接收消息之后的msg_get函数中的list_del,将该msg_msg 从列表中删除触发unlink:
static inline void list_del(struct list_head *entry)
{
	__list_del_entry(entry);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}
static inline void __list_del_entry(struct list_head *entry)
{
	if (!__list_del_entry_valid(entry))
		return;

	__list_del(entry->prev, entry->next);
}
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;//unlink 写
	WRITE_ONCE(prev->next, next);//unlink 写
}

参考

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

你可能感兴趣的:(漏洞分析,二进制,#,linux,kernel,1024程序员节,linux,kernel,内核提权,漏洞利用,网络安全)