【kernel exploit】CVE-2022-34918 nftable堆溢出漏洞利用(list_head任意写)

2022/07/26 Kernel-exploit 共 31941 字,约 92 分钟

【kernel exploit】CVE-2022-34918 nftable堆溢出漏洞利用(list_head任意写)

影响版本:Linux v5.18.10。 v5.18.11 已修补。

测试版本:Linux-v5.18.10 失败,改用 v5.17.15 exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory 原作者在 Ubuntu 22.04(v5.15.0)上成功提权。

编译选项

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.15.tar.xz
$ tar -xvf linux-5.17.15.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述nft_set_elem_init() 函数存在堆溢出,溢出长度可达 64-16=48字节,漏洞对象可以位于 kmalloc-{64,96,128,192}(本文利用时选取 kmalloc-64 漏洞对象)。漏洞利用——首先构造堆布局 vul_obj -> user_key_payload -> percpu_ref_data,溢出篡改 user_key_payload->datalen 为 0xffff,以泄露出 percpu_ref_data->release 内核基址和 percpu_ref_data->ref physmap基址;然后构造堆布局 vul_obj -> simple_xattr,溢出篡改 simple_xattr->list 链表,利用这个有限制的任意写将 modprobe_path/sbin/modprobe 修改为 /tmp/xxxxprobe 来提权(从链表中移除xattr时触发该任意写)。该任意写的前提条件是需要泄露 physmap 地址,percpu_ref_data / shm_file_data 都既包含内核基址又包含physmap地址。

补丁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;
 	}

保护机制:KASLR / SMEP / SMAP

利用总结

  • (1)初始化
    • (1-1)设置affinity;
    • (1-2)子进程等待执行 /tmp/get_root 提权程序,该程序会被赋予root权限;
    • (1-3)设置namespace(触发漏洞需要CAP_NET_ADMIN权限);
    • (1-4)创建 netlink socket 来管理 netfilter;
  • (2)设置 nf_tables
    • (2-1)创建 table;
    • (2-2)创建 set 用于泄露地址;
    • (2-3)创建 set 用于溢出篡改 modprobe_path
  • (3)泄露内核基址与physmap基址
    • (3-1)喷射50个 user_key_payload 对象(最好位于漏洞对象后面);
    • (3-2)分配漏洞对象并触发溢出篡改 user_key_payload->datalen 为 0xffff;
    • (3-3)喷射300个 percpu_ref_data 对象(最好位于 user_key_payload 后面);
    • (3-4)泄露 percpu_ref_data->release 内核基址和 percpu_ref_data->ref physmap基址(只要读取长度为 0xffff 则表示 user_key_payload->datalen 被成功覆写);
  • (4)篡改 modprobe_path
    • (4-1)喷射300个 simple_xattr(最好位于漏洞对象后面);
    • (4-2)分配漏洞对象并触发溢出篡改 simple_xattr->list
      • simple_xattr->list.next = (physmap_base + 0x2f706d74)
      • simple_xattr->list.prev = (kaslr_base + MODPROBE_PATH_BASE + 1)
      • 为了识别被覆盖的simple_xattrsimple_xattr->name 长度为0x100,最低字节覆盖为 0xe5 = 229,这样name 恰好指向attribute结尾的某个特定字符串(例如 "security.Iwanttoberoot"),这样只要能成功移除该字符串对应的xattr,则表示该标签已被覆盖
    • (4-3)触发 unlinking attack,将 /sbin/modprobe 修改为 /tmp/xxxxprobe
    • (4-4)执行错误程序触发 modprobe 提权。

1. 背景知识

1.1 netfilter介绍

netfilter是一个开源项目,用于执行数据包过滤,也就是Linux防火墙。这个项目经常被提到iptables,它是用于配置防火墙的用户级应用程序。2014年,netfilter 防火墙添加了一个新子系统,称为nftables,可以通过nftables用户级应用程序进行配置。

netfilter_vulgarized

1.2 nftables介绍

nftables取代了流行的{ip,ip6,arp,eb}tables。该软件提供了一个新的内核数据包分类框架,该框架基于特定于网络的虚拟机 (VM) 和新的nft用户空间命令行工具。nftables重用了现有的netfilter子系统,例如现有的钩子基础设施、连接跟踪系统、NAT、用户空间队列和日志子系统。对于nftables,只需要扩展expression即可,用户自行编写expression,然后让nftables虚拟机执行它。nftables框架的数据结构如下所示:

Table{
   Chain[
     Rule
       (expression1,expression2,expression3,...)
          | | |--> expression_action
          | |--> expression_action
          |-->expression_action
     Rule
         (expression,expression,expression,...)
     ...
  ],
  Chain[
     ...
  ],
  ...
}

Table为chain的容器,chain为rule的容器,rule为expression的容器,expression响应action。构造成由 table->chain->rule->expression 四级组成的数据结构。


2. nftables代码分析

nfnetlink初始化:参见 nfnetlink_net_init() 函数,其中定义了 netlink_kernel_cfg 对象,后续如果收到 netfilter 的消息后会调用 netlink_kernel_cfg->input 函数,也即 nfnetlink_rcv() 函数,其中规定了需具备 CAP_NET_ADMIN 权限,只要支持 namespace 那么普通用户也可以访问(编译时勾选 CONFIG_USER_NS)。

static int __net_init nfnetlink_net_init(struct net *net)
{
    struct nfnl_net *nfnlnet = nfnl_pernet(net);
    struct netlink_kernel_cfg cfg = {
        .groups = NFNLGRP_MAX,
        .input  = nfnetlink_rcv,    // <===== 当收到 netfilter netlink 的消息后会调用这个 input 函数
#ifdef CONFIG_MODULES
        .bind   = nfnetlink_bind,
#endif
    };

    nfnlnet->nfnl = netlink_kernel_create(net, NETLINK_NETFILTER, &cfg);
    if (!nfnlnet->nfnl)
        return -ENOMEM;
    return 0;
}

整体调用trace:通过发送netlink消息数据包来操作nftables,从netlink到nftables的调用过程如下所示:

__sys_sendto() -> sock_sendmsg() -> sock_sendmsg_nosec() -> netlink_sendmsg -> netlink_unicast() -> netlink_unicast_kernel() -> nfnetlink_rcv() -> nfnetlink_rcv_skb_batch() -> nfnetlink_rcv_batch()

2-call-trace

nfnetlink_rcv_batch() 函数中对netlink消息进行操作。从netlink消息中剥离出nftable载荷,并依次进行对应处理。进入 nfnetlink_rcv_batch() 函数,首先根据 subsys_id (取值如下所示)获得 nfnetlink_subsystem

#define NFNL_SUBSYS_NONE 		0
#define NFNL_SUBSYS_CTNETLINK		1
#define NFNL_SUBSYS_CTNETLINK_EXP	2
#define NFNL_SUBSYS_QUEUE		3
#define NFNL_SUBSYS_ULOG		4
#define NFNL_SUBSYS_OSF			5
#define NFNL_SUBSYS_IPSET		6
#define NFNL_SUBSYS_ACCT		7
#define NFNL_SUBSYS_CTNETLINK_TIMEOUT	8
#define NFNL_SUBSYS_CTHELPER		9
#define NFNL_SUBSYS_NFTABLES		10 				// <------------ nftables
#define NFNL_SUBSYS_NFT_COMPAT		11
#define NFNL_SUBSYS_HOOK		12
#define NFNL_SUBSYS_COUNT		13

这里nftables类型为0xa。获得subsystem后,然后拿到子系统对应的回调客户端(nfnl_callback)。通过 nfnetlink_find_client() 实现该功能。

static inline const struct nfnl_callback *
nfnetlink_find_client(u16 type, const struct nfnetlink_subsystem *ss)
{
	u8 cb_id = NFNL_MSG_TYPE(type);

	if (cb_id >= ss->cb_count)
		return NULL;

	return &ss->cb[cb_id];
}

对应nftables回调客户端,在\net\netfilter\nf_tables_api.c直接找到定义——nf_tables_subsys

static const struct nfnetlink_subsystem nf_tables_subsys = {
	.name		= "nf_tables",
	.subsys_id	= NFNL_SUBSYS_NFTABLES,
	.cb_count	= NFT_MSG_MAX,
	.cb		= nf_tables_cb,
	.commit		= nf_tables_commit,
	.abort		= nf_tables_abort,
	.cleanup	= nf_tables_cleanup,
	.valid_genid	= nf_tables_valid_genid,
	.owner		= THIS_MODULE,
};

函数入口.cb 数据域便是回调客户端——nf_tables_cb。可以看到针对不同的nftables操作,定义了多个回调客户端,例如table的增删改查操作。

static const struct nfnl_callback nf_tables_cb[NFT_MSG_MAX] = {
	[NFT_MSG_NEWTABLE] = {
		.call		= nf_tables_newtable,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_TABLE_MAX,
		.policy		= nft_table_policy,
	},
	[NFT_MSG_GETTABLE] = {
		.call		= nf_tables_gettable,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_TABLE_MAX,
		.policy		= nft_table_policy,
	},
	[NFT_MSG_DELTABLE] = {
		.call		= nf_tables_deltable,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_TABLE_MAX,
		.policy		= nft_table_policy,
	},
	[NFT_MSG_NEWCHAIN] = {
		.call		= nf_tables_newchain,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_CHAIN_MAX,
		.policy		= nft_chain_policy,
	},
	...
}

nf_tables_newtable()
nf_tables_gettable()
nf_tables_deltable()
nf_tables_newchain()
nf_tables_getchain()
nf_tables_delchain()
nf_tables_newrule()
nf_tables_getrule()
nf_tables_delrule()
nf_tables_newset()
nf_tables_getset()
nf_tables_delset()
nf_tables_newsetelem()
nf_tables_getsetelem()
nf_tables_delsetelem()
nf_tables_getgen()
nf_tables_newobj()
nf_tables_getobj()
nf_tables_delobj()
nf_tables_getobj()
nf_tables_newflowtable()
nf_tables_getflowtable()
nf_tables_delflowtable()

然后再从netlink消息中剥离出netlink载荷,根据不同的消息类型(nf_tables_msg_types)进行不同的分发处理,消息类型如下所示:

enum nf_tables_msg_types {
	NFT_MSG_NEWTABLE,
	NFT_MSG_GETTABLE,
	NFT_MSG_DELTABLE,
	NFT_MSG_NEWCHAIN,
	NFT_MSG_GETCHAIN,
	NFT_MSG_DELCHAIN,
	NFT_MSG_NEWRULE,
	NFT_MSG_GETRULE,
	NFT_MSG_DELRULE,
	NFT_MSG_NEWSET,
	NFT_MSG_GETSET,
	NFT_MSG_DELSET,
	NFT_MSG_NEWSETELEM,
	NFT_MSG_GETSETELEM,
	NFT_MSG_DELSETELEM,
	NFT_MSG_NEWGEN,
	NFT_MSG_GETGEN,
	NFT_MSG_TRACE,
	NFT_MSG_NEWOBJ,
	NFT_MSG_GETOBJ,
	NFT_MSG_DELOBJ,
	NFT_MSG_GETOBJ_RESET,
	NFT_MSG_NEWFLOWTABLE,
	NFT_MSG_GETFLOWTABLE,
	NFT_MSG_DELFLOWTABLE,
	NFT_MSG_MAX,
};

依次调用 nc->call() 进一步处理。

static void nfnetlink_rcv_batch(struct sk_buff *skb, struct nlmsghdr *nlh,
				u16 subsys_id, u32 genid)
{
	struct sk_buff *oskb = skb;
	struct net *net = sock_net(skb->sk);
	const struct nfnetlink_subsystem *ss;
	const struct nfnl_callback *nc;
	struct netlink_ext_ack extack;
	LIST_HEAD(err_list);
	u32 status;
	int err;
    ...
    ss = nfnl_dereference_protected(subsys_id); 		// 根据 subsys_id 获得 nfnetlink_subsystem, 例如 nf_tables_subsys
    ...
    while (skb->len >= nlmsg_total_size(0)) {
		int msglen, type;
       	...
        type = nlh->nlmsg_type;						// <--
        ...
        nc = nfnetlink_find_client(type, ss);		// 拿到子系统对应的回调客户端, 
		if (!nc) {
			err = -EINVAL;
			goto ack;
		}
        ...
        {
			int min_len = nlmsg_total_size(sizeof(struct nfgenmsg));
			struct nfnl_net *nfnlnet = nfnl_pernet(net);
			struct nlattr *cda[NFNL_MAX_ATTR_COUNT + 1];
			struct nlattr *attr = (void *)nlh + min_len;
			u8 cb_id = NFNL_MSG_TYPE(nlh->nlmsg_type);
			int attrlen = nlh->nlmsg_len - min_len;
			struct nfnl_info info = {
				.net	= net,
				.sk	= nfnlnet->nfnl,
				.nlh	= nlh,
				.nfmsg	= nlmsg_data(nlh),
				.extack	= &extack,
			};

			/* Sanity-check NFTA_MAX_ATTR */
			if (ss->cb[cb_id].attr_count > NFNL_MAX_ATTR_COUNT) {
				err = -ENOMEM;
				goto ack;
			}

			err = nla_parse_deprecated(cda,
						   ss->cb[cb_id].attr_count,
						   attr, attrlen,
						   ss->cb[cb_id].policy, NULL);
			if (err < 0)
				goto ack;

			err = nc->call(skb, &info, (const struct nlattr **)cda);
            ...
        }
        ...
    }
}

2.1 创建 table

开始剥洋葱式分析,第一层操作创建一个table,响应函数为 nf_tables_newtable()

static int nf_tables_newtable(struct sk_buff *skb, const struct nfnl_info *info,
			      const struct nlattr * const nla[])
{
	struct nftables_pernet *nft_net = nft_pernet(info->net);
	struct netlink_ext_ack *extack = info->extack;
	u8 genmask = nft_genmask_next(info->net);
	u8 family = info->nfmsg->nfgen_family;
	struct net *net = info->net;
	const struct nlattr *attr;
	struct nft_table *table;
	struct nft_ctx ctx;
	u32 flags = 0;
	int err;
	... ...
	lockdep_assert_held(&nft_net->commit_mutex);
	attr = nla[NFTA_TABLE_NAME]; 					// [1] 查找是否存在该table
	table = nft_table_lookup(net, attr, family, genmask,
				 NETLINK_CB(skb).portid);
	if (IS_ERR(table)) {
		if (PTR_ERR(table) != -ENOENT)
			return PTR_ERR(table);
	} else {
		...
		return nf_tables_updtable(&ctx); 			// [2] 如果存在该table, 调用 nf_tables_updtable() 进行更新
	}
	...
	table = kzalloc(sizeof(*table), GFP_KERNEL_ACCOUNT);	// [3] 如果不存在就创建该表, 并进行初始化
	if (table == NULL)
		goto err_kzalloc;

	table->name = nla_strdup(attr, GFP_KERNEL_ACCOUNT);
	if (table->name == NULL)
		goto err_strdup;

	if (nla[NFTA_TABLE_USERDATA]) {
		table->udata = nla_memdup(nla[NFTA_TABLE_USERDATA], GFP_KERNEL_ACCOUNT);
		if (table->udata == NULL)
			goto err_table_udata;

		table->udlen = nla_len(nla[NFTA_TABLE_USERDATA]);
	}

	err = rhltable_init(&table->chains_ht, &nft_chain_ht_params); 
	if (err)
		goto err_chain_ht;

	INIT_LIST_HEAD(&table->chains); 			// [4] 初始化4个链表
	INIT_LIST_HEAD(&table->sets);
	INIT_LIST_HEAD(&table->objects);
	INIT_LIST_HEAD(&table->flowtables);
	table->family = family;
	table->flags = flags;
	table->handle = ++table_handle;
	if (table->flags & NFT_TABLE_F_OWNER)
		table->nlpid = NETLINK_CB(skb).portid;

	nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, nla); 	// [5] 将table加到nftbales上下文中
	err = nft_trans_table_add(&ctx, NFT_MSG_NEWTABLE);
	if (err < 0)
		goto err_trans;

	list_add_tail_rcu(&table->list, &nft_net->tables); 	 // [6] 将table链到net->nft.tables中
	...
}

2.2 创建 chain

第二步操作创建一个chain,响应函数为 nf_tables_newchain()

static int nf_tables_newchain(struct sk_buff *skb, const struct nfnl_info *info,
			      const struct nlattr * const nla[])
{
	struct nftables_pernet *nft_net = nft_pernet(info->net);
	struct netlink_ext_ack *extack = info->extack;
	u8 genmask = nft_genmask_next(info->net);
	u8 family = info->nfmsg->nfgen_family;
	struct nft_chain *chain = NULL;
	struct net *net = info->net;
	const struct nlattr *attr;
	struct nft_table *table;
	u8 policy = NF_ACCEPT;
	struct nft_ctx ctx;
	u64 handle = 0;
	u32 flags = 0;

	lockdep_assert_held(&nft_net->commit_mutex);

	table = nft_table_lookup(net, nla[NFTA_CHAIN_TABLE], family, genmask,  	// [1] 首先先找table, 无table直接退出
				 NETLINK_CB(skb).portid);
	if (IS_ERR(table)) {
		NL_SET_BAD_ATTR(extack, nla[NFTA_CHAIN_TABLE]);
		return PTR_ERR(table);
	}

	chain = NULL;
	attr = nla[NFTA_CHAIN_NAME]; 		// [2] 找chain是否存在, 存在进入update, 不存在则添加一个新chain

	if (nla[NFTA_CHAIN_HANDLE]) {		// [2-1] 两种方式寻找chain, 一是通过 nla[NFTA_CHAIN_HANDLE]
		handle = be64_to_cpu(nla_get_be64(nla[NFTA_CHAIN_HANDLE]));
		chain = nft_chain_lookup_byhandle(table, handle, genmask);
		if (IS_ERR(chain)) {
			NL_SET_BAD_ATTR(extack, nla[NFTA_CHAIN_HANDLE]);
			return PTR_ERR(chain);
		}
		attr = nla[NFTA_CHAIN_HANDLE];
	} else if (nla[NFTA_CHAIN_NAME]) {  // [2-2] 二是通过 nla[NFTA_CHAIN_NAME]
		chain = nft_chain_lookup(net, table, attr, genmask);
		if (IS_ERR(chain)) {
			if (PTR_ERR(chain) != -ENOENT) {
				NL_SET_BAD_ATTR(extack, attr);
				return PTR_ERR(chain);
			}
			chain = NULL;
		}
	} else if (!nla[NFTA_CHAIN_ID]) {
		return -EINVAL;
	}
    ...
    if (chain != NULL) {
		if (info->nlh->nlmsg_flags & NLM_F_EXCL) {
			NL_SET_BAD_ATTR(extack, attr);
			return -EEXIST;
		}
		if (info->nlh->nlmsg_flags & NLM_F_REPLACE)
			return -EOPNOTSUPP;

		flags |= chain->flags & NFT_CHAIN_BASE;
		return nf_tables_updchain(&ctx, genmask, policy, flags, attr, 		// [3] 找到 chain 则更新
					  extack);
	}

	return nf_tables_addchain(&ctx, family, genmask, policy, flags, extack);// [4] 未找到就调用nf_tables_addchain()创建
}

创建chain的过程具体看 nf_tables_addchain() 函数实现,首先分配一个chain,然后初始化chain->rules链表,并设置chain->hanle和chain->table。随后进行初始化chain->name等操作,并将chain链到table->chains中。

static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask,
			      u8 policy, u32 flags,
			      struct netlink_ext_ack *extack)
{
	const struct nlattr * const *nla = ctx->nla;
	struct nft_table *table = ctx->table;
	struct nft_base_chain *basechain;
	struct nft_stats __percpu *stats;
	struct net *net = ctx->net;
	char name[NFT_NAME_MAXLEN];
	struct nft_rule_blob *blob;
	struct nft_trans *trans;
	struct nft_chain *chain;
	unsigned int data_size;
	int err;
	...
	if (nla[NFTA_CHAIN_HOOK]) {
		...
		basechain = kzalloc(sizeof(*basechain), GFP_KERNEL_ACCOUNT); 	// [1] 分配 nft_base_chain
		...
	} else {
		if (flags & NFT_CHAIN_BASE)
			return -EINVAL;
		if (flags & NFT_CHAIN_HW_OFFLOAD)
			return -EOPNOTSUPP;

		chain = kzalloc(sizeof(*chain), GFP_KERNEL_ACCOUNT); 	// [2] 分配 nft_chain
		if (chain == NULL)
			return -ENOMEM;

		chain->flags = flags;
	}
	ctx->chain = chain;

	INIT_LIST_HEAD(&chain->rules); 					// [3] 初始化chain->rules链表
	chain->handle = nf_tables_alloc_handle(table);
	chain->table = table;

	if (nla[NFTA_CHAIN_NAME]) {						// [4] 初始化chain->name
		chain->name = nla_strdup(nla[NFTA_CHAIN_NAME], GFP_KERNEL_ACCOUNT);
	} ...

	data_size = offsetof(struct nft_rule_dp, data);	/* last rule */
	blob = nf_tables_chain_alloc_rules(data_size);
	if (!blob) {
		err = -ENOMEM;
		goto err_destroy_chain;
	}

	RCU_INIT_POINTER(chain->blob_gen_0, blob);
	RCU_INIT_POINTER(chain->blob_gen_1, blob);

	err = nf_tables_register_hook(net, table, chain);
	if (err < 0)
		goto err_destroy_chain;

	trans = nft_trans_chain_add(ctx, NFT_MSG_NEWCHAIN);
	if (IS_ERR(trans)) {
		err = PTR_ERR(trans);
		goto err_unregister_hook;
	}

	nft_trans_chain_policy(trans) = NFT_CHAIN_POLICY_UNSET;
	if (nft_is_base_chain(chain))
		nft_trans_chain_policy(trans) = policy;

	err = nft_chain_add(table, chain); 		// [5] 将chain链到table->chains
	...
}

2.3 创建 rule

第三步操作创建一个rule,响应函数为 nf_tables_newrule()

static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info,
			     const struct nlattr * const nla[])
{
	struct nftables_pernet *nft_net = nft_pernet(info->net);
	struct netlink_ext_ack *extack = info->extack;
	unsigned int size, i, n, ulen = 0, usize = 0;
	u8 genmask = nft_genmask_next(info->net);
	struct nft_rule *rule, *old_rule = NULL;
	struct nft_expr_info *expr_info = NULL;
	u8 family = info->nfmsg->nfgen_family;
	struct nft_flow_rule *flow = NULL;
	struct net *net = info->net;
	struct nft_userdata *udata;
	struct nft_table *table;
	struct nft_chain *chain;
	struct nft_trans *trans;
	u64 handle, pos_handle;
	struct nft_expr *expr;
	struct nft_ctx ctx;
	struct nlattr *tmp;
	int err, rem;

	lockdep_assert_held(&nft_net->commit_mutex);

	table = nft_table_lookup(net, nla[NFTA_RULE_TABLE], family, genmask, // [1] 获取table
				 NETLINK_CB(skb).portid);
	if (IS_ERR(table)) {
		NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_TABLE]);
		return PTR_ERR(table);
	}

	if (nla[NFTA_RULE_CHAIN]) {											// [2] 获取chain
		chain = nft_chain_lookup(net, table, nla[NFTA_RULE_CHAIN], 	
					 genmask);
		if (IS_ERR(chain)) {
			NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_CHAIN]);
			return PTR_ERR(chain);
		}
		if (nft_chain_is_bound(chain))
			return -EOPNOTSUPP;

	} else if (nla[NFTA_RULE_CHAIN_ID]) {
		chain = nft_chain_lookup_byid(net, nla[NFTA_RULE_CHAIN_ID]);
		if (IS_ERR(chain)) {
			NL_SET_BAD_ATTR(extack, nla[NFTA_RULE_CHAIN_ID]);
			return PTR_ERR(chain);
		}
	} else {
		return -EINVAL;
	}
	...

	nft_ctx_init(&ctx, net, skb, info->nlh, family, table, chain, nla);

	n = 0;
	size = 0;
	if (nla[NFTA_RULE_EXPRESSIONS]) { 			// [3] 若设置了nla[NFTA_RULE_EXPRESSIONS], 会先把所有的expression遍历出来. 计算其总值放在size中
		expr_info = kvmalloc_array(NFT_RULE_MAXEXPRS,
					   sizeof(struct nft_expr_info),
					   GFP_KERNEL);
		if (!expr_info)
			return -ENOMEM;

		nla_for_each_nested(tmp, nla[NFTA_RULE_EXPRESSIONS], rem) {
			err = -EINVAL;
			if (nla_type(tmp) != NFTA_LIST_ELEM)
				goto err_release_expr;
			if (n == NFT_RULE_MAXEXPRS)
				goto err_release_expr;
			err = nf_tables_expr_parse(&ctx, tmp, &expr_info[n]);
			if (err < 0) {
				NL_SET_BAD_ATTR(extack, tmp);
				goto err_release_expr;
			}
			size += expr_info[n].ops->size;
			n++;
		}
	}
	/* Check for overflow of dlen field */
	err = -EFBIG;
	if (size >= 1 << 12)
		goto err_release_expr;

	if (nla[NFTA_RULE_USERDATA]) { 			// [4] 若设置了nla[NFTA_RULE_USERDATA],获取userdata的大小放在usize中
		ulen = nla_len(nla[NFTA_RULE_USERDATA]);
		if (ulen > 0)
			usize = sizeof(struct nft_userdata) + ulen;
	}

	err = -ENOMEM;
	rule = kzalloc(sizeof(*rule) + size + usize, GFP_KERNEL_ACCOUNT);	// [5] 分配内存,创建一个rule,并初始化相关数据域
	if (rule == NULL)
		goto err_release_expr;

	nft_activate_next(net, rule);

	rule->handle = handle;
	rule->dlen   = size;
	rule->udata  = ulen ? 1 : 0;

	if (ulen) {
		udata = nft_userdata(rule);
		udata->len = ulen - 1;
		nla_memcpy(udata->data, nla[NFTA_RULE_USERDATA], ulen);
	}
    ...
}

2.4 创建 expression

第四步操作创建expression,其实这一步和创建rule是连在一起的。都在 nf_tables_newrule() 函数中实现。expresssion总共有如下多种类型(参见 nft_basic_types)。

static struct nft_expr_type *nft_basic_types[] = {
	&nft_imm_type,
	&nft_cmp_type,
	&nft_lookup_type,
	&nft_bitwise_type,
	&nft_byteorder_type,
	&nft_payload_type,
	&nft_dynset_type,
	&nft_range_type,
	&nft_meta_type,
	&nft_rt_type,
	&nft_exthdr_type,
	&nft_last_type,
	&nft_counter_type,
};

nf_tables_newrule() -> nf_tables_newexpr()

static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info,
			     const struct nlattr * const nla[])
{
    ...
    	expr = nft_expr_first(rule); 		// [6] 将用户层传进来的expression剥离出来后,依次放在rule中
	for (i = 0; i < n; i++) {
		err = nf_tables_newexpr(&ctx, &expr_info[i], expr); 	// [6-1] nf_tables_newexpr() 函数,会根据expression类型对其进行初始化
		if (err < 0) {
			NL_SET_BAD_ATTR(extack, expr_info[i].attr);
			goto err_release_rule;
		}

		if (expr_info[i].ops->validate)
			nft_validate_state_update(net, NFT_VALIDATE_NEED);

		expr_info[i].ops = NULL;
		expr = nft_expr_next(expr);
	}

	if (chain->flags & NFT_CHAIN_HW_OFFLOAD) {
		flow = nft_flow_rule_create(net, rule);
		if (IS_ERR(flow)) {
			err = PTR_ERR(flow);
			goto err_release_rule;
		}
	}
    ...
}

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) {
		err = ops->init(ctx, expr, (const struct nlattr **)expr_info->tb);
		if (err < 0)
			goto err1;
	}

	return 0;
err1:
	expr->ops = NULL;
	return err;
}

以上就是一个完整的table->chain->rule->expression的创建过程。


3. 漏洞分析

漏洞触发tracenf_tables_newsetelem() -> nft_add_set_elem() -> nft_set_elem_init()

nft_set 结构中表示长度的成员 udlen/klen/dlen 可能可以用来构造写原语,构造更好的溢出。

struct nft_set {
    struct list_head        list;
    struct list_head        bindings;
    struct nft_table        *table;
    possible_net_t          net;
    char                *name;
    u64             handle;
    u32             ktype;
    u32             dtype;
    u32             objtype;
    u32             size;
    u8              field_len[NFT_REG32_COUNT];
    u8              field_count;
    u32             use;
    atomic_t            nelems;
    u32             ndeact;
    u64             timeout;
    u32             gc_int;
    u16             policy;
    u16             udlen;
    unsigned char           *udata;
    /* runtime data below here */
    const struct nft_set_ops    *ops ____cacheline_aligned;
    u16             flags:14,
                    genmask:2;
    u8              klen;
    u8              dlen;
    u8              num_exprs;
    struct nft_expr         *exprs[NFT_SET_EXPR_MAX];
    struct list_head        catchall_list;
    unsigned char           data[]
        __attribute__((aligned(__alignof__(u64))));
};

3.1 nft_set_elem_init() 漏洞函数

通过观察对dlen成员的访问代码,nft_set_elem_init() 函数中 [1] 处调用了 memcpy(),并且拷贝长度用到了 set->dlen

这个memcpy() 调用用到了两个不同的对象,目的地址位于 nft_set_ext 对象,拷贝长度却来自 nft_set 结构。 nft_set_ext 对象是在 [0] 处分配的,分配长度用到了 tmpl->len ,作者想检查前面是不是用到了 set->dlen 来计算 tmpl->len

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 = kzalloc(set->ops->elemsize + tmpl->len, gfp); // [0] 调用kzalloc()函数分配内存,大小为 elemsize+tmpl->len,这里的 tmpl->len 已经包括了 desc.dlen。正常情况下desc.dlen应该是等于set->dlen的
    if (elem == NULL)
        return NULL;

    ext = nft_set_elem_ext(set, elem);
    nft_set_ext_init(ext, tmpl);

    if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY))
        memcpy(nft_set_ext_key(ext), key, set->klen);
    if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END))
        memcpy(nft_set_ext_key_end(ext), key_end, set->klen);
    if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
        memcpy(nft_set_ext_data(ext), data, set->dlen); // [1] 可疑拷贝点, 如果 set->dlen 不等于 desc.dlen, 则有可能发生溢出

    ...

    return elem;
}

3.2 nft_add_set_elem() tmpl->len初始化

目标:往上看看哪里调用了漏洞函数,看看是否用到了 set->dlen 来计算 tmpl->len

漏洞[7] 处的检查可以绕过。用户可控制 set->dlen,但是限制是 set->dlen 必须小于 64 字节,且 data type 不等于 NFT_DATA_VERDICT(如果 desc->type == NFT_DATA_VERDICT,则不会判断 desc->lenset->dlen 是否相同,并成功返回)。

如果往 NFT_DATA_VERDICT 中添加一个 data type 为 NFT_DATA_VALUE 的成员,就可以使 desc->len != set->dlendesc->len = 16 / set->dlen = 64),这样就可以在 nft_set_elem_init() 函数中 [1] 处触发OOB,溢出长度可以达到48字节。

static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,

                const struct nlattr *attr, u32 nlmsg_flags)
{
    struct nlattr *nla[NFTA_SET_ELEM_MAX + 1];
    struct nft_set_ext_tmpl tmpl;
    struct nft_set_elem elem;   		// [2]
    struct nft_data_desc desc;
    
    ...
    
    if (nla[NFTA_SET_ELEM_DATA] != NULL) { 	// 处理 nla[NFTA_SET_ELEM_DATA] 对应的数据
        err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val,   // [3] 初始化 desc
                         nla[NFTA_SET_ELEM_DATA]);
        if (err < 0)
            goto err_parse_key_end;

		dreg = nft_type_to_reg(set->dtype);
		list_for_each_entry(binding, &set->bindings, list) {
			...
		}

        nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);  // [4] 采用 desc.len 来设置 tmpl.len, 所以 tmpl.len 与 set->dlen 无关 (从 [7] 中可以看出二者可以不相等)
    }
    
    ...
    
    err = -ENOMEM;
    elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,    // [5] nft_set_elem_init() 漏洞函数,进行初始化
                      elem.key_end.val.data, elem.data.val.data,
                      timeout, expiration, GFP_KERNEL);
    if (elem.priv == NULL)
        goto err_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;

    err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr); // [6] 调用 nft_data_init(), 根据用户的输入 attr 来填充 data / desc
    if (err < 0)
        return err;

    if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {    // [7] 检查了 desc->len 和 set->dlen 是否相等, 但是只要 desc->type == NFT_DATA_VERDICT, 这个检查就形同虚设, desc->len 和 set->dlen 可以不相等
        nft_data_release(data, desc->type);
        return -EINVAL;
    }

    return 0;
}

对比:本漏洞的最上层函数是 nf_tables_newsetelem(),但是在 nf_tables_newset() 函数中,是正确将 desc->dlen 赋值给了 set->dlen,没有这个漏洞。


4. 漏洞利用

4.1 nf_tables_newsetelem() 控制溢出数据

目标:用户如何控制溢出的数据呢,这里溢出时会拷贝栈上的随机数据。

源地址来源:从[5]nft_add_set_elem() -> nft_set_elem_init() 可以看出,源地址 data 来自局部变量 elem,也即 nft_set_elem 结构——elem.data.val.data。在创建新成员时,该结构用于存储新成员的信息。以下可以看到,elem.data 只有16字节可控,所以溢出时会拷贝栈上的随机字节(elem.data.val.data 后面的数据)。

#define NFT_DATA_VALUE_MAXLEN   64

struct nft_verdict {
    u32             code;
    struct nft_chain        *chain;
};

struct nft_data {
    union {
        u32         data[4]; 		// <------------ 16字节数据可控
        struct nft_verdict  verdict;
    };
} __attribute__((aligned(__alignof__(u64))));

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)];	// NFT_DATA_VALUE_MAXLEN = 64, 所以最长为64字节
        struct nft_data val; 		// <------------
    } data;
    void            *priv;
};

栈未初始化使用[2] 处的 elem.data 没有进行初始化,现在看看上层调用中是否可以控制 elem.datanf_tables_newsetelem() -> nft_add_set_elem(),对每个新添加的成员调用 nft_add_set_elem()

static int nf_tables_newsetelem(struct sk_buff *skb,
                const struct nfnl_info *info,
                const struct nlattr * const nla[])
{
    ...
    // 遍历用户输入的 attribute, 所以用户可以控制遍历次数。由于每次调用 nft_add_set_elem() 都是紧挨着的,所以尽管 elem.data 没有初始化,所以很有可能用的就是上一次调用时的成员数据。可以忽略栈结构的随机化,所以控制溢出要取决于内核的编译。
    nla_for_each_nested(attr, nla[NFTA_SET_ELEM_LIST_ELEMENTS], rem) {
        err = nft_add_set_elem(&ctx, set, attr, info->nlh->nlmsg_flags);// 调用 nft_add_set_elem()
        if (err < 0) {
            NL_SET_BAD_ATTR(extack, attr);
            return err;
        }
    }
}

控制溢出数据:下图总结了不同阶段时,栈上的elem.data数据的变化。最开始,栈上存着随机数据,然后添加一个新的成员 NFT_DATA_VALUE 布置栈数据,最后添加第2个 NFT_DATA_VERDICT 来触发堆溢出(复用 NFT_DATA_VALUE 的数据),这样就能控制溢出数据。

2-uninit-path

4.2 漏洞对象

最后,看看漏洞对象所属的cache([0]处分配的 elem)。漏洞对象的大小取决于用户的选项( nft_add_set_elem() 函数)。这里有几个选项可以增大该对象的大小,例如 NFT_SET_ELEM_KEY / NFT_SET_ELEM_KEY_END,可以在 elem 中增大最多64字节的缓冲区,总的来说,漏洞对象可以位于 kmalloc-{64,96,128,192},注意,分配时的flag为 GFP_KERNEL

如何构造 elem 以获得最佳溢出效果呢?如下图所示,构造 elem 使之位于 kmalloc-64:

  • 20字节header;
  • 设置 NFT_SET_ELEM_KEY 选项来填充 28 字节;
  • 类型为 NFT_DATA_VERDICT 会有 16字节来存成员数据。

3-elem-build

4.3 地址泄露

泄露内核基址:由于漏洞对象位于 kmaloc-x,而msg_msg 位于 kmalloc-cg-x,所以不能用 msg_msg 来泄露了。可以采用 user_key_payload 来泄露(),该对象也包含长度变量和用户数据,在 user_preparse() 函数中分配,该对象 header 占24字节,大小可以位于 kmalloc-32kmalloc-8k

目标是溢出覆盖 user_key_payload->datalen ,然后获取更多的数据。

struct user_key_payload {
	struct rcu_head	rcu;		/* RCU destructor */
	unsigned short	datalen;	/* length of this data */
	char		data[] __aligned(__alignof__(u64)); /* actual data */
};

int user_preparse(struct key_preparsed_payload *prep)
{
	struct user_key_payload *upayload;
	size_t datalen = prep->datalen;

	if (datalen <= 0 || datalen > 32767 || !prep->data)
		return -EINVAL;

	upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);	// [6] 用户提供长度
	if (!upayload)
		return -ENOMEM;

	/* attach the data */
	prep->quotalen = datalen;
	prep->payload.data[0] = upayload;
	upayload->datalen = datalen;
	memcpy(upayload->data, prep->data, datalen); 	// [7] 将用户数据拷贝进去
	return 0;
}
EXPORT_SYMBOL_GPL(user_preparse);

限制:本方法有个缺点,就是 user_key_payload 对象的分配数量有限,sysctl 变量 kernel.keys.maxkeys 限制了key的最大分配个数,kernel.keys.maxbytes 限制了key的总长度。Ubuntu 22.04 的默认值如下:

kernel.keys.maxbytes = 20000
kernel.keys.maxkeys = 200

泄露对象kmalloc-64 中有哪些数据可以泄露呢。percpu_ref_data 对象也位于 kmalloc-64,其中包含两种有用的指针,release / confirm_switch 可以泄露内核基址(io_uring 已经整合到了内核中,io_ring_ctx_ref_free() 地址可以用于计算内核基址),ref 可以泄露 physmap 基址。

该对象在 io_ring_ctx_alloc() -> percpu_ref_init() 函数中分配,用户可以使用 io_uring_setup syscall 来触发执行该函数(在初始化 io_ring_ctx 对象时调用该函数),调用 close 可以释放该对象。

作者测试时,发现还泄露了些意料之外的 percpu_ref_data 对象,其中 percpu_ref_data->resease 指向 io_rsrc_node_ref_zero() 函数。研究后发现,这些对象也来自 io_uring_setup syscall。这个副作用也有利于内核基址泄露。所以 percpu_ref_data->release 的值有两种可能。

struct percpu_ref_data {
	atomic_long_t		count;
	percpu_ref_func_t	*release; 		// 内核基址
	percpu_ref_func_t	*confirm_switch;// 内核基址
	bool			force_atomic:1;
	bool			allow_reinit:1;
	struct rcu_head		rcu;
	struct percpu_ref	*ref; 			// physmap 地址
};

int percpu_ref_init(struct percpu_ref *ref, percpu_ref_func_t *release,
            unsigned int flags, gfp_t gfp)
{
    struct percpu_ref_data *data;
    
    ...
    
    data = kzalloc(sizeof(*ref->data), gfp);					// alloc
    
    ...
    
    data->release = release;
    data->confirm_switch = NULL;
    data->ref = ref;
    ref->data = data;
    return 0;
}

static __cold struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p)
{
    struct io_ring_ctx *ctx;
    
    ...
    
    if (percpu_ref_init(&ctx->refs, io_ring_ctx_ref_free,		// <-----------
                PERCPU_REF_ALLOW_REINIT, GFP_KERNEL))
        goto err;

    ...
}

4.4 任意写提权

任意写新方法:参考了文章 io_uring - new code, new bugs, and a new exploit technique 对 CVE-2021-41073 漏洞新的利用方法,叫做 unlinking attack,基于 list_del 操作。伪造两个地址来替代 list_head ,这样其中一个地址就会被写到另一个地址上。

(1)simple_xattr 对象

simple_xattr 对象介绍:该结构常用于存储 in-memory filesystems (例如tmpfs)的扩展属性(xattrs - extended attribute),每个文件的 simple_xattrlist_head 链表存起来。分配函数是 simple_xattr_alloc(),用户可控 simple_xattr->value,分配大小是 kmalloc-32 到很大。

缺点simple_xattr 不能修改,当对它进行编辑时,会把旧的 simple_xattr 从链表unlink,然后分配新的 simple_xattr 并链接上去。所以通过伪造size和 next指针,无法构造越界写或任意地址写。还有个问题就是非特权用户无法设置 simple_xattr,但是只要系统支持 user namespace 即可。

struct simple_xattr {
    struct list_head list;
    char *name;
    size_t size;
    char value[];
};

struct list_head {
	struct list_head *next, *prev;
};

(2)Unlinking attack

任意写__list_del() 代码如下,如果我们能够控制 prev / next 指针,可以把 next 指针设置为 modprobe_path ,这样就会在 [1] 处将 prev 值写入 next 指向的内存偏移8字节处。

问题[2] 处,next 会写往 prev,这意味着 prev 也必须是一个有效的指针,这限制了我们能写往 next 的值。解决办法是,利用 physmap 提供一个有效的 prev 值。

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev; 				// [1]
	WRITE_ONCE(prev->next, next); 	// [2]
}

physmap:physmap 是一块内核虚拟内存,物理内存页连续映射到该处。例如,如果机器有4G内存(2^32 字节),需用32 bit来索引物理内存;假设 physmap 起始地址是 0xffffffff00000000,则 0xffffffff00000000~0xffffffffffffffff 范围内的值都有效。因此,若系统有4G内存,攻击者可以控制 prev 的低4字节,只要高4字节表示physmap地址即可。

由于我们目标是修改 modprobe_path ,可以构造 prev = 0xffffxxxx2f706d74,若 next = modprobe_path+1,利用 2modprobe_path 覆写为 /tmp/xxxxprobe (其中 xxxxprev 的高4字节)。后面即可提权。

(3)注意事项

触发分配 kmalloc-64 的 simple_xattr,然后采用 unlinking attack,就能将一个溢出或错误释放转化为有限制的任意写,尽管只能写4个可控的字节和4个不可控的字节,但足以提权。

优点与限制:和 msg_msg 相比的优点是,list_head 前面没有metadata 不担心破坏。本技术需要提前泄露physmap 中的一个地址,有些结构,例如 shm_file_data 既包含text段指针又包含 physmap 地址,所以这不是问题。还要注意,所选的physmap地址必须是可写的,该地址处的数据会被覆写。

(4)识别被覆盖对象

目标:该技术需要知道哪个 simple_xattr 对象被覆盖了,否则随意移除item会导致遍历 list 时报错,可通过 name 来确定list中的item。

为了识别被覆盖的对象,可以分配长度 256 的name,这样最低字节为NULL,这样我们伪造 list_head 的同时可以覆盖 name 指针的最低字节,这样就能识别出被覆盖的对象。唯一的要求就是所有的name结尾相同。下图总结了如何构造 simple_xattr->name

4-xattr-names

提权:采用这个写原语来篡改 modprobe_path 提权。

succeed


5. 补充

5.1 问题

利用问题:从 v5.18.1 开始,就把漏洞对象放入 kmalloc-cg-* 中了(nft_add_set_elem() -> nft_set_elem_init() 分配),而弹性对象 user_key_payloaduser_preparse() 分配)和 percpu_ref_dataio_ring_ctx_alloc() -> percpu_ref_init() 分配)都是采用 GFP_KERNEL flag 分配的,导致一直不能使3个对象相邻。所以 v5.18.1 以上版本的内核无法完成利用(可能可以利用 msg_msg 对象来越界读和任意写)。

# ftrace 调试 v5.18.1 发现问题
         exploit-308     [000] .....    42.319031: kmalloc: call_site=strndup_user+0x46/0x60 ptr=ffff888106856f90 bytes_req=16 bytes_alloc=16 gfp_flags=GFP_USER|__GFP_NOWARN
         exploit-308     [000] .....    42.319033: kmalloc_node: call_site=kvmalloc_node+0x26/0xf0 ptr=ffff888106856c40 bytes_req=15 bytes_alloc=16 gfp_flags=GFP_KERNEL node=-1
         exploit-308     [000] .....    42.319037: kmalloc: call_site=user_preparse+0x36/0x70 ptr=ffff8881061b2c40 bytes_req=39 bytes_alloc=64 gfp_flags=GFP_KERNEL
         ...
         exploit-308     [000] .....    42.319186: kmalloc: call_site=nft_set_elem_init+0x46/0x290 ptr=ffff888101b56ae0 bytes_req=82 bytes_alloc=96 gfp_flags=GFP_KERNEL_ACCOUNT|__GFP_ZERO
         exploit-308     [000] .....    42.319190: kmalloc: call_site=nft_trans_alloc_gfp+0x22/0x60 ptr=ffff888100834000 bytes_req=288 bytes_alloc=512 gfp_flags=GFP_KERNEL|__GFP_ZERO
         exploit-308     [000] .....    42.319205: kmalloc: call_site=nft_set_elem_init+0x46/0x290 ptr=ffff8881017c92c0 bytes_req=64 bytes_alloc=64 gfp_flags=GFP_KERNEL_ACCOUNT|__GFP_ZERO
         ...
         exploit-308     [000] .....    42.355076: kmalloc: call_site=percpu_ref_init+0x6f/0x130 ptr=ffff8881061b2b40 bytes_req=56 bytes_alloc=64 gfp_flags=GFP_KERNEL|__GFP_ZERO

v5.18.1 中分配漏洞对象:

static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
			    const struct nlattr *attr, u32 nlmsg_flags)
{
    ...
    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_ACCOUNT);
    ...
}

v5.17.15 中分配漏洞对象:

static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
			    const struct nlattr *attr, u32 nlmsg_flags)
{
    ...
    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);
    ...
}

5.2 常用命令

liburing 安装

# 安装 liburing   生成 liburing.a / liburing.so.2.2
$ make
$ sudo make install

常用命令

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

# 编译exp
$ make CFLAGS="-I /home/hi/lib/libnftnl-1.2.2/include"
$ gcc -static ./get_root.c -o ./get_root
$ gcc -no-pie -static -pthread ./exploit.c -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

问题:原来的 ext4文件系统空间太小,很多包无法安装,现在换syzkaller中的 stretch.img 试试。

# 服务端添加用户
$ useradd hi && echo lol | passwd --stdin hi
# ssh连接
$ sudo chmod 0600 ./stretch.id_rsa
$ ssh -i stretch.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost
$ ssh -p 10021 hi@localhost
# 问题: Host key verification failed.
# 删除ip对应的相关rsa信息即可登录 $ sudo nano ~/.ssh/known_hosts
# https://blog.csdn.net/ouyang_peng/article/details/83115290

ftrace调试:注意,QEMU启动时需加上 no_hash_pointers 启动选项,否则打印出来的堆地址是hash之后的值。trace中只要用 %p 打印出来的数据都会被hash,所以可以修改 TP_printk() 处输出时的格式符,%p -> %lx

# host端, 需具备root权限
cd /sys/kernel/debug/tracing
echo 1 > events/kmem/kmalloc/enable
echo 1 > events/kmem/kmalloc_node/enable
echo 1 > events/kmem/kfree/enable

# ssh 连进去执行 exploit

cat /sys/kernel/debug/tracing/trace > /home/hi/trace.txt

# 下载 trace
scp -P 10021 hi@localhost:/home/hi/trace.txt ./ 	# 下载文件

参考

[CVE-2022-34918] A crack in the Linux firewall

exploit

https://www.openwall.com/lists/oss-security/2022/07/02/3

Linux内核nftables子系统研究与漏洞分析 —— 总结 nftables 漏洞

io_uring - new code, new bugs, and a new exploit technique —— CVE-2021-41073 利用新方法

https://github.com/star-sg/CVE/ —— CVE-2021-41073 利用新方法

文档信息

Search

    Table of Contents