【kernel exploit】CVE-2022-32250 nftables UAF写漏洞利用

2022/11/03 Kernel-exploit 共 25833 字,约 74 分钟

【kernel exploit】CVE-2022-32250 nftables错误链表操作导致UAF写的漏洞利用

影响版本:Linux v4.19.37~v5.18.1 v5.18.2 已修补。由syzkaller发现。7.8分。原作者在 Ubuntu 22.04 Kernel 5.15上提权

测试版本:Linux-5.17.12 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory

编译选项

CONFIG_NF_TABLES=y

CONFIG_NETFILTER_NETLINK=y

CONFIG_BINFMT_MISC=y (否则启动VM时报错)

CONFIG_USER_NS=y

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.17.12.tar.xz
$ tar -xvf linux-5.17.12.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述nftables 模块的 net/netfilter/nf_tables_api.c 采用 NFT_MSG_NEWSET 功能来添加 nft_set 时,处理 lookupdynset expression 时,由于错误的 NFT_EXPR_STATEFUL 检查,nft_expr 对象释放后仍位于nft_set->binding 链表中,新加入 nft_expr 时导致UAF写(触发漏洞需要 CAP_NET_ADMIN 权限)。UAF写会往 kmalloc-64 的偏移 0x18 处写入另一个 kmalloc-64 堆块偏移 0x18 的地址值。

nft_set_elem_expr_alloc() 先调用 nft_expr_init() 初始化 expression,再检查 expression类型是否为 NFT_EXPR_STATEFUL(不是则直接释放该expression,但是忘记从nft_set->binding 链表解绑,后续链入新的 expression 时触发UAF写)。nft_expr_init() 先调用 nf_tables_expr_parse() ,再分配nft_expr对象内存,最后调用 nf_tables_newexpr() 初始化 nft_expr对象(并绑定到 nft_set->binding 链表)。

补丁patch 先检查 NFT_EXPR_STATEFUL,再分配 expression 内存。

diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 12fc9cda4a2cf..f296dfe86b622 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -2873,27 +2873,31 @@ static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
 
 	err = nf_tables_expr_parse(ctx, nla, &expr_info);
 	if (err < 0)
-		goto err1;
+		goto err_expr_parse;
+
+	err = -EOPNOTSUPP;
+	if (!(expr_info.ops->type->flags & NFT_EXPR_STATEFUL))
+		goto err_expr_stateful;
 
 	err = -ENOMEM;
 	expr = kzalloc(expr_info.ops->size, GFP_KERNEL_ACCOUNT);
 	if (expr == NULL)
-		goto err2;
+		goto err_expr_stateful;
 
 	err = nf_tables_newexpr(ctx, &expr_info, expr);
 	if (err < 0)
-		goto err3;
+		goto err_expr_new;
 
 	return expr;
-err3:
+err_expr_new:
 	kfree(expr);
-err2:
+err_expr_stateful:
 	owner = expr_info.ops->type->owner;
 	if (expr_info.ops->type->release_ops)
 		expr_info.ops->type->release_ops(expr_info.ops);
 
 	module_put(owner);
-err1:
+err_expr_parse:
 	return ERR_PTR(err);
 }
 
@@ -5413,9 +5417,6 @@ struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
 		return expr;
 
 	err = -EOPNOTSUPP;
-	if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
-		goto err_set_elem_expr;
-
 	if (expr->ops->type->flags & NFT_EXPR_GC) {
 		if (set->flags & NFT_SET_TIMEOUT)
 			goto err_set_elem_expr;

保护机制:KASLR/SMEP/SMAP/KPTI

利用总结采用 mqueue 中的 msg_msg 来泄露内核基址,因为 mqueue 中的 posix_msg_tree_node->msg_list 偏移为 0x18(且位于kmalloc-64),恰好是UAF写的偏移;另外,posix_msg_tree_node->msg_list 也能用来构造 Unlink 利用来篡改 modprobe_path利用user_key_payload泄露堆地址(便于构造unlink),老生常谈了。注意,需使用ubuntu21.04 以上的版本的libmnl 或 libnftnl才行。

  • (0)初始化
    • 设置 namespace 和 CPU 亲属性;
    • 打开5个 mqueue 用于泄露内核基址和进行Unlink;
    • 分配4个 table 和 set,本次利用需触发3次UAF写;
  • (1)泄露堆指针:利用 user_key_payload 对象读取堆地址
    • (1-1)分配漏洞对象 nft_expr1 并释放;
    • (1-2)喷射50个 user_key_payloadkmalloc-64,调用 addkey,占据漏洞对象 nft_expr1
    • (1-3)链入新的 nft_expr 触发UAF写,往 user_key_payload->data[0] (偏移0x18)写入新 nft_expr+0x18 的地址—kmalloc-64
    • (1-4)读取 user_key_payload 泄露堆地址 heap_base
  • (2)泄露内核基址:利用 mqueue 的posix_msg_tree_node->msg_list来读取 user_key_payload->rcu.func 内核函数地址
    • (2-1)堆风水:喷射 1000 个 percpu_ref_data 对象(kmalloc-64,调用 syscall(__NR_io_uring_setup, ...)来去碎片化(参考 CVE-2022-34918利用);
    • (2-2)分配漏洞对象 nft_expr2 并释放;
    • (2-3)喷射4个 posix_msg_tree_nodekmalloc-64,通过 mq_send 调用发送消息),占据漏洞对象 nft_expr2
    • (2-4)释放2个 percpu_ref_data 对象(没用,可以跳过这一步);
    • (2-5)链入新的 nft_expr 触发UAF写,往 posix_msg_tree_node->msg_list (偏移0x18)写入新 nft_expr+0x18 的地址,释放新 nft_expr
    • (2-6)喷射100个 user_key_payload,占据释放的新 nft_expr并占据其附近堆块,用于伪造 posix_msg_tree_node->msg_list 指向的 msg_msg (伪造 msg_msg->m_list = heap_basemsg_msg->m_ts = 0x28,确保 msg_msg->security = NULL);
    • (2-7)通过读取 posix_msg_tree_node->msg_list (调用 mq_receive来泄露 user_key_payload->rcu.func (偏移0x8处)
  • (3)unlink篡改 modprobe_path 提权:利用 mqueue 的posix_msg_tree_node->msg_list构造 Unlink
    • (3-1)分配漏洞对象 nft_expr3 并释放;
    • (3-2)同(2-3)喷射4个 posix_msg_tree_nodekmalloc-64,通过 mq_send 调用发送消息),占据漏洞对象 nft_expr3
    • (3-3)同(2-5)。链入新的 nft_expr 触发UAF写,往 posix_msg_tree_node->msg_list (偏移0x18)写入新 nft_expr+0x18 的地址,释放新 nft_expr
    • (3-4)喷射 1 个 user_key_payload,占据释放的新 nft_expr,用于伪造 posix_msg_tree_node->msg_list 指向的 msg_msg (伪造 msg_msg->mlist.next = modprobe_path-7msg_msg->mlist.prev = (heap_base&0xffffffff00000000)+0x2f706d74);(这里喷1个就能占据新 nft_expr,有点神奇)
    • (3-5)通过读取 posix_msg_tree_node->msg_list (调用 mq_receive来释放该 msg_msg,将该 msg_msg 从链表中取出时触发 Unlink;
  • (4)触发执行 modprobe_path 并提权。

1. 漏洞分析

1-1. 简介

Netfilter:这是Linux内核中通过用户定义handler来实现网络相关任务的框架,Netfilter 提供不同的函数用于包过滤、网络地址转换、端口转换、包日志记录,内核模块可以通过Netfilter提供的一系列 hook,将回调函数注册到内核网络栈。

nftables:属于Netfilter的一个组件,可以根据用户定义的规则来对包进行过滤或路由重定向。nftables支持sets,可以在一个规则中使用多个IP地址、端口号。在定义规则时可以用大括号表示sets(例如{22, 80, 443}),sets类型包括 ipv4_addr, ipv6_addr, ether_addr / inet_proto / inet_service / mark

nftables 采用 tables / chains / rules / expressions 来存储和处理命令,tables 包含多个 chainstables 与特定协议绑定在一起,例如 IP / IP6chains 包含多个 rules 和待处理网络流量信息的类型;rules 包含多个 expressionsexpressions 评估输入是否满足一系列条件。

关于nftables 代码和结构分析可参见 【kernel exploit】CVE-2022-1015 nftables 栈溢出漏洞分析与利用 / 【kernel exploit】CVE-2022-34918 nftable堆溢出漏洞利用(list_head任意写)

1-2. 结构关系

结构关系nft_table -> nft_set -> nft_expr set 之间是双链表链接,expression 之间也是双链表链接。

structure-relation

1-3. 代码分析

调用关系nf_tables_newset() -> nft_set_elem_expr_alloc() (漏洞函数)-> nft_expr_init() (expression初始化) -> nf_tables_newexpr() -> nft_lookup_init()lookup expression) -> nf_tables_bind_set() (将expression绑定到 set->binding

(1)入口函数 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;
	···
	set = nft_set_lookup(table, nla[NFTA_SET_NAME], genmask);	// [1] 查找现有的 set
	if (IS_ERR(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);						// [2] 分配 set 空间
	···
	INIT_LIST_HEAD(&set->bindings);								// [3] 初始化 set, 注意 bindings 成员是 list 结构
	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 字段, 则调用 nft_set_elem_expr_alloc() 处理 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++;
	} 
	...
}

(2)漏洞函数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); 	// [4-1] 调用 nft_expr_init() 初始化expr
	if (IS_ERR(expr))
		return expr;

	err = -EOPNOTSUPP;
	if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
		goto err_set_elem_expr;			// [4-2] 如果不存在 NFT_EXPR_STATEFUL 标识,则释放刚初始化的 expr
	...

err_set_elem_expr:
	nft_expr_destroy(ctx, expr);		// [4-3] 调用 nft_expr_destroy() 释放 expr
	return ERR_PTR(err);
}

(3)初始化 expression nft_expr_init() -> nf_tables_newexpr()

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);	// [4-1-1] 初始化expr_info
	if (err < 0)
		goto err1;

	err = -ENOMEM;
	expr = kzalloc(expr_info.ops->size, GFP_KERNEL);	// [4-1-2] 申请空间 `8+私有结构体长度`,本exp中是56
	if (expr == NULL)
		goto err2;

	err = nf_tables_newexpr(ctx, &expr_info, expr);		// [4-1-3] 初始化expr
	if (err < 0)
		goto err3;

	return expr;
	...
}

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;
	}
	...
}

(4)nft_lookup 漏洞expression

不同类型的 expression 对应的 nft_expr 结构不同(主要是 nft_expr->data存放的内容不同),例如,如果是lookup expression,则nft_expr->data中包含的是 nft_lookup 结构。本漏洞受到影响的 expression 有两种(结构中必须含有 binding 字段,这样才能绑定到 nft_set 结构上):look_up / nft_dynset,分别位于 net\netfilter\nft_lookup.cnet\netfilter\nft_dynset.c。作者对 nft_dynsetkmalloc-96)也完成了利用,本文以 look_upkmalloc-64)为例,初始化函数对应为 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);		// [1] 获取 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);	// [2] 找到之前创建的set
	··· // 各种初始化

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

	err = nf_tables_bind_set(ctx, set, &priv->binding);	// [3] 调用 nf_tables_bind_set() 进行绑定 <---- 将 expression 绑定到 nft_set
	if (err < 0)
		return err;

	priv->set = set;
	return 0;
}

(5)绑定 nft_set->bindings 链表

nf_tables_bind_set():主要是调用 list_add_tail_rcu() 函数将 nft_set->bindingsnft_lookup->binding->list 用双向链表链接起来。对应的结构体为 nft_set -> bindingsnft_lookup -> nft_set_binding -> list (结构关系为 nft_expr -> nft_lookup -> nft_set_binding

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() 链接链表, 将 expression 绑定到 set->binding
	nft_set_trans_bind(ctx, set);
	set->use++;

	return 0;
}
EXPORT_SYMBOL_GPL(nf_tables_bind_set);

struct nft_set {
	struct list_head		list;
	struct list_head		bindings;		// list
	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;	// list
};

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

1-4. 漏洞分析

漏洞分析nft_set_elem_expr_alloc()[4-1] 中已经分成了空间分配、链接到 set 等操作,但如果在 [4-2] 中不满足条件(nft_expr->ops->type->flags != NFT_EXPR_STATEFUL),则 [4-3] 调用 nft_expr_destroy() 释放该 nft_expr 结构体,但是却忘记将 nft_exprset->binding 双向链表中取下来,后面再加入新的 nft_expr 时就会导致UAF。

释放路径nft_expr_destroy() -> nf_tables_expr_destroy() -> nft_lookup_destroy() -> nf_tables_destroy_set()

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);		// 啥都没干, nf_tables_destroy_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))	// 不满足条件, 因为 set->bindings 包含之前赋值的 expression,所以不会调用 nft_set_destroy() 释放 set 并将 expr 从 set->binding 取出来
		nft_set_destroy(ctx, set);
}

UAF write:再次使用 SET_EXPR 功能,则会在已经释放的nft_expr后面再链接一个nft_expr,造成偏移0x18的uaf 写。根据list 操作的代码和本次参与运算的结构体,可以看出,该uaf写会篡改偏移为0x18 和偏移为0x20的两个字段指向另外两个堆地址。我们这里主要关注偏移0x18,会将其指向一个新的nft_exprkmalloc-64)的偏移0x18处。

#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);
}

2. 漏洞利用

问题:参见 nft_expr_init()v5.18.1 版本中漏洞对象是采用 GFP_KERNEL_ACCOUNT 标识分配的,v5.17.12 版本中漏洞对象是采用 GFP_KERNEL 标识分配的。 msg_msg 是采用 GFP_KERNEL_ACCOUNT 标识分配的(单独存放在 kmalloc-cg-xx slab 中)。所以 v5.17.12 版本不能使用 msg_msg 对象来利用。并且UAF写会在 kmalloc-64 的偏移 0x18 处写一个堆地址,写偏移和内容不可控。

// v5.18.1 
// https://elixir.bootlin.com/linux/v5.18.1/source/net/netfilter/nf_tables_api.c#L2879
	expr = kzalloc(expr_info.ops->size, GFP_KERNEL_ACCOUNT);
// v5.17.12
// https://elixir.bootlin.com/linux/v5.17.12/source/net/netfilter/nf_tables_api.c#L2800
	expr = kzalloc(expr_info.ops->size, GFP_KERNEL);

2-1. 泄露堆地址-user_key_payload

方法:使用 user_key_payload 来泄露(采用 GFP_KERNEL 申请),data字段用户可控。且data数据偏移正好是0x18,所以UAF写会正好往data数据上写上一个堆地址(恰好写入到 user_key_payload->data[0:8]),可以泄露出来。

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;
	...
}

2-2. 泄露内核基址-posix_msg_tree_node

2-2-1. 构造越界读

方法:采用mqueue 的posix消息队列模块,该模块和msg_msg一样是IPC进程间通信的消息队列功能。mqueue 的消息采用 posix_msg_tree_node 结构来存储,采用 GFP_KERNEL flag 分配。

步骤:利用UAF写篡改 posix_msg_tree_node->msg_listmsg_msg 链表),指向某个 kmalloc-64 的偏移 0x18 处(某个 nft_expr 对象的0x18偏移处),这样该位置就被当作一个 msg_msg 结构;然后利用 mq_timedreceive 调用来读取消息,就能读到相邻堆块偏移8开始的16字节(0x18+0x30 = 0x48,从偏移0x18开始,0x30的字节被当做 msg_msg 头,所以可读取的data从相邻堆块的第8字节开始)。因为 copy_to_user() 中有 heap_check,会检查拷贝大小是否超出内存所在slab的大小,所以这里只能读取0x10字节。

struct posix_msg_tree_node {
    struct rb_node      rb_node;		// rb_node 大小为 0x18
    struct list_head    msg_list;		// 偏移0x18,该字段管理了一个 msg_msg 链表,指向首个 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))));

分配路径SYSCALL-mq_timedsend -> do_mq_timedsend()

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); 			// [1] 调用 load_msg() 创建消息队列, 从用户空间拷贝消息
	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); 	// [2] 申请 posix_msg_tree_node 对象,采用 GFP_KERNEL flag 分配

	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; 						// [3] 将申请的 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] msg_insert() 将消息插入消息队列 
			if (ret)
				goto out_unlock;
			__do_notify(info);
		}
		inode->i_atime = inode->i_mtime = inode->i_ctime =
				current_time(inode);
	}
	...
}

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); 		// [4-1] 将 msg_msg 消息添加到 posix_msg_tree_node->msg_list 链表
	return 0;
}

读取路径SYSCALL-mq_timedreceive -> do_mq_timedreceive()

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); 							// [1] 先根据消息队列的文件描述符获取对应inode, 从 inode 中获取 mqueue_inode_info
	audit_file(f.file);
	...
	if (!info->node_cache)
		new_leaf = kmalloc(sizeof(*new_leaf), GFP_KERNEL);

	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; 					// [2] 获取 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] 将消息发给用户, copy_to_user()
			ret = -EFAULT;
		}
		free_msg(msg_ptr);									// [5] 释放消息
	}
	...
}

static inline struct msg_msg *msg_get(struct mqueue_inode_info *info)
{
	...
	} else {
		msg = list_first_entry(&leaf->msg_list,		// [3-1] 获取msg_list中第一个消息
				       struct msg_msg, m_list);
		list_del(&msg->m_list);						// [3-2] 从消息队列中删除
		if (list_empty(&leaf->msg_list)) {
			msg_tree_erase(leaf, info);
		}
	}
	info->attr.mq_curmsgs--;						// [3-3] 消息队列数减1
	info->qsize -= msg->m_ts;
	return 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);							// [5-1] 伪造 msg_msg 时, 需保证 msg->security 为0 
	msg->security = NULL;
}

2-2-2. 泄露目标对象

泄露对象:需找到一个目标对象,放在 posix_msg_tree_node 对象的后面。

  • (1)目标对象需位于 kmalloc-64,且偏移 0x8~0x18 处包含内核地址指针;
  • (2)由于 mq_timedreceive 调用会触发执行 msg_free 来释放 msg_msg->security (恰好与目标对象的前8字节重合),所以目标对象的前8字节需为0

最终还是选择 user_key_payload,前8字节 user_key_payload->rcu->next 正常为0,且第2个8字节为 user_key_payload->rcu->func 内核函数指针,指向 user_free_payload_rcu() 函数。

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);		// 偏移 0x8 可泄露内核基址
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head

步骤

  • (1)分配两个漏洞对象 nft_expr 称为 vul1 / vul2

  • (2)触发 do_mq_timedsend() 分配一个 posix_msg_tree_node 来占据 vul1

  • (3)触发UAF写漏洞(创建vul2 并链接到 vul1vul1->binding->next = vul2->binding),通过 vul1 篡改 posix_msg_tree_node

  • (4)通过 keyctl 函数在 vul2 附近布置 user_key_payload 对象。

                  vul 1 (posix_msg_tree_node)                                    vul 2
             -------------------------------      -------------------------------------------------------------------------
    0x0     |    rb color   |    rb_right   |     |           * ops                |                * set                  |
             --------------- ---------------      --------------------------------- ---------------------------------------
    0x10    |    rb_left    |  (vul 2+0x18) |     |          reg & invert          |  bingding.list.next (msg_msg->m_list) |
             --------------- ---------------      --------------------------------- ---------------------------------------
    0x20    |      ....     |      ....     |     |    X.X.prev (msg_msg->m_list)  |       chain (msg_msg->m_type)         |
             --------------- ---------------      --------------------------------- ---------------------------------------
    0x30    |      ....     |      ....     |     |     flags (msg_msg->m_ts)      |           (msg_msg->next)             |
             --------------- ---------------      --------------------------------- ---------------------------------------
                          		                                          user_key_payload
                 								  -------------------------------------------------------------------------
    0x40    								      |   rcu->next (msg_msg->security)|    rcu->func ptr (msg_msg->data[0])   |
             								      --------------------------------- ---------------------------------------
    0x50    								      |             data_len           |                data[0]                |
              								      --------------------------------- ---------------------------------------
    0x60   									      |              data[1]           |                data[2]                |
             								      --------------------------------- ---------------------------------------
    0x70    								      |              data[3]           |                data[4]                |
            								      --------------------------------- ---------------------------------------
      
    vul 1 overlap info: struct nft_expr == struct posix_msg_tree_node
    vul 2: struct nft_expr (nft_expr+0x18 开始被当做 msg_msg 结构)
    next to vul 2: struct user_key_payload (前8字节被当做 msg_msg->security
    
  • 此时, posix_msg_tree_node->msg_list->next = vul2 + 0x18 ,所以从 vul2 + 0x18 开始被当做 msg_msg;但是这个 msg_msgcopy_to_user 只能泄露0x10字节,且 msg_msg->security 必须为NULL(vul2相邻对象的前8字节),所以能泄露 vul2相邻堆块的 0x8 ~ 0x18 字节。过去有人用 percpu_ref_data 来泄露内核基址(参见 CVE-2022-34918利用,来自 io_uring),但不满足前8字节为NULL,所以只能再用 user_key_payload 来泄露。

2-3. 覆写 modprobe_path 提权

步骤:构造 msg_msg 的Unlink来篡改 modprobe_path 提权。

  • (1)利用UAF篡改 posix_msg_tree_node->msg_list 指向某个 vul2 + 0x18 处;

                          vul 1                                       vul 2
             -------------------------------             -------------------------------
    0x0     |    rb color   |    rb_right   |           |   rcu->next   | rcu->func ptr |
             --------------- ---------------             --------------- ---------------
    0x10    |    rb_left    |  (vul 2+0x18) |           |   data_len    |     data[0]   |
             --------------- ---------------             --------------- ---------------
    0x20    |      ....     |      ....     |           |     data[1]   |     data[2]   |
             --------------- ---------------             --------------- ---------------
    0x30    |      ....     |      ....     |           |     data[3]   |     data[4]   |
             --------------- ---------------             --------------- ---------------
      
    vul 1 overlap info: struct nft_expr == struct posix_msg_tree_node
    vul 2 overlap info: struct nft_expr == struct user_key_payload == fake msg_msg
      
    data[0] = m_list->next   /   data[1] = m_list->prev   /   data[2] = m_type   /   data[3] = m_ts
    
  • (2)释放该 nft_expr - vul2 ,喷射 usr_key_payload 伪造一个假的 msg_msgmsg_msg->m_list.next = &modprobe_path-7msg_msg->mlist.prev = 0xffff????2f706d74

    • &modprobe_path-7 表示修改 modprobe_path 的第 2~9 字节;
    • 0xffff????2f706d74 是 physmap 中可写的堆地址(进行 unlink 时必须为可写地址),前面已经泄露过该地址。可以将 modprobe_path 字符串从/sbin/modprobe改为 /tmp/????\xff\xffprobe
  • unlink操作:执行 mq_timedreceive 调用,触发执行 msg_get() -> list_del() -> __list_del_entry() -> __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;				// <---------- *(entry->next + 8) = entry->prev    unlink操作
    	WRITE_ONCE(prev->next, next);	// *(entry->prev) = entry->next
    }
    

2-4. 测试结果

问题:每次start.sh 启动后执行 ./exploit 总是报错 -sh: 2: ./exploit: Exec format error,只能重新传 ./exploit 进去才能执行。scp -P 10021 ./exploit.c ./exploit hi@localhost:/home/hi

succeed

3. 补充

3-1. 防护机制

  • 对特定对象的cache隔离,防止UAF对象替换:grsecurity’s autoslab / Google’s experimental mitigations
  • CFI 机制防止执行 ROP gadget,目前在x64上没有有效的机制;
  • 设置 panic_on_oops=1 来避免 unlink 利用;
  • 设置 modprobe_path 为只读—CONFIG_STATIC_USERMODHELPER
  • 关闭 unprivileged namespaces
  • 关闭 userland FUSE server 支持。

3-2. 常用命令

exp编译


libmnl / libnftl 安装

$ sudo apt-get install libcap2-bin bzip2 make pkg-config        # 安装 setcap/bzip2/make/pkg-config
$ tar   -jxvf    xx.tar.bz2
$ ./configure --prefix=/usr && make     # libmnl / libnftl
$ sudo make install

liburing 安装(本次exp不需要安装liburing)

# 安装 liburing   生成 liburing.a / liburing.so.2.2
$ apt-get install zip g++
$ apt-get update
$ apt-get install gcc-10 g++-10		# 在QEMU中编译liburing总是出错, 可能需要更新 gcc
$ git clone https://github.com/axboe/liburing
$ unzip ./liburing-master.zip
$ cd ./liburing-master
$ make
$ sudo make install
# 另一种方法
wget  https://github.com/axboe/liburing/archive/liburing-0.2.zip
unzip liburing-0.2.zip
cd liburing-liburing-0.2/
./configure --libdir=/usr/lib64 
make CFLAGS=-std=gnu99 && make install

常用命令

# ssh连接与测试
$ ssh -p 10021 hi@localhost             # password: lol
$ ./exploit

# 编译exp 				注意libmnl不支持静态编译,加 -static 就会报错; 加 -lrt 表示实时库
$ gcc ./exploit.c  -lmnl -lnftnl -lrt  -o exploit

# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi      # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./   # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root  hi@localhost:/home/hi

参考

Linux Kernel Exploit (CVE-2022-32250) with mqueue

exploit

Settlers of Netlink - Exploiting a limited kernel UAF on Ubuntu 22.04 — 学习使用 CodeQL搜寻目标对象(指定大小,指定偏移处为指针)。介绍了另一种利用方法,基于nft_dynset漏洞对象,比本文更复杂,要触发4次UAF(用到 cgroup_fs_context->release_agent 来构造任意释放,恰好和 nft_dynset->bindings->prev 重叠)。

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

CVE-2022-32250-email

nftables-details — 学习nftables的细节

团队介绍:该团队成功利用了 CVE-2022-0185 / CVE-2022-0995 / CVE-2022-32250

文档信息

Search

    Table of Contents