• 【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。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    漏洞描述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;
     	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    保护机制: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用户级应用程序进行配置。

    请添加图片描述

    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[
         ...
      ],
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    整体调用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()

    请添加图片描述

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这里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];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    对应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,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    函数入口.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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    然后再从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,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    依次调用 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);
                ...
            }
            ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    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中
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    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()创建
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    创建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
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    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);
    	}
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107

    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,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    以上就是一个完整的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))));
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    对比:本漏洞的最上层函数是 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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    栈未初始化使用[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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    请添加图片描述

    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字节来存成员数据。

    请添加图片描述

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

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

    kernel.keys.maxbytes = 20000
    kernel.keys.maxkeys = 200
    
    • 1
    • 2

    泄露对象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;
    
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (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]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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-32 的 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

    请添加图片描述

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

    请添加图片描述


    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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);
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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);
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    5.2 常用命令

    liburing 安装

    # 安装 liburing   生成 liburing.a / liburing.so.2.2
    $ make
    $ sudo make install
    
    • 1
    • 2
    • 3

    常用命令

    # 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    问题:原来的 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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 ./ 	# 下载文件
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    参考

    [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 利用新方法

  • 相关阅读:
    网络安全(黑客)自学笔记
    Wireshark TS | 网络路径不一致传输丢包问题
    HugeGraph Hubble 配置 https 协议的操作步骤
    Vue2 + Echarts实现3D地图下钻
    Java项目在linux上部署步骤
    Maven 从入门到精通
    基于注意力机制卷积神经网络结合门控单元CNN-GRU-SAM-Attention实现柴油机故障诊断附matlab代码
    前端工程化知识系列(6)
    C++特性:继承,封装,多态
    Ajax的概念及jQuery中的Ajax的3种方法,模仿jQuery封装自己的Ajax函数
  • 原文地址:https://blog.csdn.net/panhewu9919/article/details/126002565