CVE-2023-32233
一、漏洞简介
CVE-2023-32233 是 Linux 内核 Netfilter 子系统中的一个释放后重用(Use-After-Free,UAF)漏洞,存在于 nf_tables 组件中。该漏洞源于匿名集(anonymous set)处理不当,导致在处理批量请求更新 nf_tables 配置信息时,可能发生内存释放后仍被访问的情况。攻击者可以利用此漏洞对内核内存执行任意读写操作,从而将自身权限提升至 root,完全控制目标系统。
UAF漏洞:UAF 漏洞全称是use after free,free是指函数是在堆上动态分配空间后不再使用该改数据从而被回收。但是由于程序员的一些不适当的操作,会导致攻击者能够操控已经被释放的区域,从而执行一些byte codes。
Linux Netfilter:Netfilter 是 GNU/Linux 下第三代防火墙,从 Linux Kernel 2.4 开始,就把 Netfilter 框架作为默认防火墙加入到 Linux 内核当中,即 Netfilter 是 Linux Kernel 的一个子功能,专门用来增强网络安全。
详细知识可参考:什么是 Linux Netfilter
二、环境安装
环境下载地址,网传做好的镜像,并包含了编译好的exp:Ubuntu-23.04,密码123456
https://pan.baidu.com/s/1yQUrToQTiaDXcsHYVeKeSQ?pwd=8888
三、漏洞复现
EXP下载地址:https://github.com/Liuk3r/CVE-2023-32233?tab=readme-ov-file
执行以下的命令安装编译环境
sudo apt install gcc libmnl-dev libnftnl-dev
编译文件:
gcc -Wall -o exploit exploit.c -lmnl -lnftnl
当然我们也可以直接使用下载的镜像中已经编译好的exp
./exp
可以看见我们已经提权,拿到了root权限
四、漏洞分析
漏洞成因是 nf_tables_deactivate_set
在释放匿名 set
时没有将 set
的标记设置为 inactive
,导致它还能被此次 netlink
批处理中的其他任务访问,从而导致 UAF,介绍该漏洞需要先对 netlink
的源码进行分析。
(源码版本: linux-6.1.1.tar.gz)
1、源码分析
用户态进程可以一次提交多个 netlink
请求给内核,这些请求在内存中按顺序存储,请求的存储结构为 struct nlmsghdr
,下发请求后内核通过 nfnetlink_rcv_batch
解析每个请求并处理
用户态填充和发送请求的大致代码如下:
struct mnl_nlmsg_batch *batch = mnl_nlmsg_batch_start(mnl_batch_buffer, mnl_batch_limit);
nftnl_batch_begin(mnl_nlmsg_batch_current(batch), seq++);
table_seq = seq;
mnl_nlmsg_batch_next(batch);
// 在批处理中新建请求
struct nlmsghdr *nlh = nftnl_nlmsg_build_hdr(
mnl_nlmsg_batch_current(batch),
NFT_MSG_NEWSETELEM,
NFPROTO_INET,
NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK,
seq++
);
nftnl_set_elems_nlmsg_build_payload(nlh, set);
mnl_nlmsg_batch_next(batch);
// 发送请求给内核处理
if (mnl_socket_sendto(nl, mnl_nlmsg_batch_head(batch),
mnl_nlmsg_batch_size(batch)) < 0) {
err(1, "Cannot into mnl_socket_sendto()");
}
mnl_nlmsg_batch_stop(batch);
netlink
批处理消息的处理流程涉及两个线程,nfnetlink_rcv_batch
在进程的系统调用上下文中执行对请求处理后,将请求转换为 trans
通过 nf_tables_destroy_list
提交给 nf_tables_trans_destroy_work
内核线程做进一步处理。
nfnetlink_rcv_batch 处理流程
-
消息循环处理
- 逐条解析 skb 中的 Netlink 消息,直到消息长度不足头部大小。
- 对每条消息:通过
nfnetlink_find_client
根据type
查找对应的客户端回调表(nf_tables_cb
)。调用nc->call
执行消息对应的处理函数(如增删表/链/规则等)。
-
事务提交阶段
所有消息处理完成后,调用ss->commit
(即nf_tables_commit
)。- 遍历
commit_list
中的事务(trans
): - 按事务类型处理:例如
NFT_MSG_DELSETELEM
调用nft_setelem_remove
删除集合元素。 - 调用
nf_tables_commit_release
: - 将
commit_list
中的事务转移到nf_tables_destroy_list
。 - 调度工作队列
nf_tables_trans_destroy_work
异步处理销毁逻辑。
- 遍历
nf_tables_trans_destroy_work 销毁流程
- 事务销毁准备
- 将
nf_tables_destroy_list
合并到本地链表head
,避免并发冲突。
- 将
-
遍历
head
中的每个事务(trans
):- 调用
nft_commit_release(trans)
:根据事务类型执行最终的资源释放操作(如内存回收、引用计数更新等)。
- 调用
关键机制分析
-
异步销毁设计
- 通过工作队列延迟资源释放,避免在主处理路径(如规则配置)中阻塞,提升性能。
- 分离事务提交(commit)与销毁(destroy)阶段,确保原子性和一致性。
-
链表操作与同步
- 链表转移:
list_splice_tail_init
将commit_list
原子转移到destroy_list
,防止中间状态不一致。list_splice_init
在销毁线程中安全获取待处理事务。- 锁机制:隐含使用内核锁(如 RCU 或自旋锁)保护链表操作,需确保在并发场景下无竞态条件。
-
事务生命周期
-
创建:在
nc->call
处理阶段分配trans
并加入commit_list
。 -
提交:
nf_tables_commit
执行事务相关操作(如删除元素)。 -
释放:
nf_tables_trans_destroy_work
最终释放资源,确保无内存泄漏。
以 NFT_MSG_DELSETELEM
请求为例跟一下请求的处理路径加深理解,首先会进入 nf_tables_delsetelem
进行处理,处理后会分配 trans
并将其放到 commit_list
中
trans = nft_trans_elem_alloc(ctx, NFT_MSG_DELSETELEM, set);
if (trans == NULL)
goto fail_trans;
nft_trans_elem(trans) = elem;
nft_trans_commit_list_add_tail(ctx->net, trans);
然后 nf_tables_commit
会处理 trans
case NFT_MSG_DELSETELEM:
te = (struct nft_trans_elem *)trans->data;
nf_tables_setelem_notify(&trans->ctx, te->set,
&te->elem,
NFT_MSG_DELSETELEM);
nft_setelem_remove(net, te->set, &te->elem);
if (!nft_setelem_is_catchall(te->set, &te->elem)) {
atomic_dec(&te->set->nelems);
te->set->ndeact--;
}
break;
最后在 nf_tables_trans_destroy_work --> nft_commit_release
完成最后的处理。
static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
case NFT_MSG_DELSETELEM:
nf_tables_set_elem_destroy(&trans->ctx,
nft_trans_elem_set(trans),
nft_trans_elem(trans).priv);
break;
2、漏洞触发
接下来看一下漏洞触发的代码路径和内存变化,触发 UAF 的步骤如下:
-
创建一个匿名 set (
pwn_lookup_set
)并往set
里面插入一个elem
-
创建一个
rule
,rule
里面新建一个lookup
的expr
,lookup expr
会引用pwn_lookup_set
-
创建一个批处理其中包含两个请求:
- 使用
NFT_MSG_DELRULE
删除上一步创建的rule
- 使用
NFT_MSG_DELSETELEM
删除pwn_lookup_set
的elem
- 使用
-
在
nft_commit_release
处理NFT_MSG_DELRULE
时会释放rule
里面的expr
,然后在nft_lookup_destroy
里面会释放匿名set
-
在
nft_commit_release
处理NFT_MSG_DELSETELEM
就会访问到已经释放的set
.
下面以图和代码结合的形式分析内存状态的变化,创建匿名 set 和 rule 后的内存关系如下:
请求提交给内核后,会在 nfnetlink_rcv_batch
获取相关对象的指针(rule
、set
、elem
的指针),然后将其封装到 trans
对象中,最后在 nf_tables_trans_destroy_work --> nft_commit_release
完成具体的释放。
在 nft_commit_release
处理 NFT_MSG_DELRULE
命令时会同步释放 rule
里面的 expr
,在释放 lookup expr
时会进入 nft_lookup_destroy
释放其关联的 set
,即 pwn_lookup_set
static void nft_lookup_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_lookup *priv = nft_expr_priv(expr);
nf_tables_destroy_set(ctx, priv->set);
}
然后在处理 NFT_MSG_DELSETELEM
时就会用到已经被释放的 set
,因为内核无法知道其 trans
保存的 set
指针已经被释放
static void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set, void *elem)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem);
if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext));
kfree(elem);
}
static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
case NFT_MSG_DELSETELEM:
nf_tables_set_elem_destroy(&trans->ctx,
nft_trans_elem_set(trans),
nft_trans_elem(trans).priv);
break;
最后总结一下:在 nfnetlink_rcv_batch
处理 NFT_MSG_DELRULE
和 NFT_MSG_DELSETELEM
会把分别需要用到的对象指针(rule
指针和 set
指针)保存到 trans
,然后在 nf_tables_trans_destroy_work
处理 NFT_MSG_DELRULE
命令释放 rule
和 set
时,NFT_MSG_DELSETELEM
请求已经在队列中了,然后在处理 NFT_MSG_DELSETELEM
时就会拿到该 trans
里面保存的 set
指针,而此时该指针指向的对象已经被释放。
3、简单的总结
上面的分析可以进行一下简答的总结:
1)、核心机制
- 同步处理阶段:解析请求并生成事务对象(trans),加入提交列表。
- 异步销毁阶段:通过工作队列异步释放事务相关资源。
2)、漏洞成因
当两个存在依赖关系的请求(如 删除规则 和 删除集合元素)被放入同一批处理时,会引发以下问题:
步骤① 创建对象
用户创建匿名集合 pwn_lookup_set
和一条规则。
规则中的查找表达式(lookup expr
)会引用该集合,形成 规则 → 表达式 → 集合
的依赖链。
步骤② 提交批处理
用户提交包含两个请求的批处理:
- DELRULE:删除规则(释放规则和表达式)
- DELSETELEM:删除集合中的元素(需访问集合)
步骤③ 内核处理
-
同步阶段:两个请求被封装为事务对象,加入
commit_list
。 -
异步阶段:
- 处理
DELRULE
事务时,释放规则及其表达式。由于表达式引用了集合,集合也被释放。 - 处理
DELSETELEM
事务时,仍尝试访问已释放的集合,触发 UAF。
- 处理
3)、根本原因
内核未识别事务间的依赖关系,在异步销毁阶段错误释放仍被后续事务引用的资源。
五、补丁
官方修复补丁的关键修改位于 nf_tables_deactivate_set()
函数:
void nf_tables_deactivate_set(...) {
if (nft_set_is_anonymous(set))
nft_deactivate_next(ctx->net, set); // 标记匿名集合为“未激活”
set->use--;
}
效果:在批处理的准备阶段(NFT_TRANS_PREPARE
),匿名集合会被标记为“未激活”,阻止后续操作引用其指针
六、参考文章
https://demonlee.tech/archives/2303002
https://www.cnblogs.com/qianfu/p/17530832.html
https://www.secrss.com/articles/54704?utm_source=chatgpt.com