漏洞分析

漏洞成因是 nf_tables_newrule 在异常分支会释放 rule 和 rule 引用的匿名 set ,但是没有设置 set 的状态为 inactivate,导致批处理中后面的请求还能访问已经被释放的 set.

对 nftables 子系统不熟悉的同学可以先看看:https://www.cnblogs.com/hac425/p/17967844/cve202332233-vulnerability-analysis-and-utilization-qrjoh

漏洞相关代码如下:

static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
......
err = -ENOMEM;
rule = kzalloc(sizeof(*rule) + size + usize, GFP_KERNEL_ACCOUNT); expr = nft_expr_first(rule);
for (i = 0; i < n; i++) {
err = nf_tables_newexpr(&ctx, &expr_info[i], expr);
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);
} err_release_rule:
nf_tables_rule_release(&ctx, rule);
err_release_expr:
for (i = 0; i < n; i++) {
if (expr_info[i].ops) {
module_put(expr_info[i].ops->type->owner);
if (expr_info[i].ops->type->release_ops)
expr_info[i].ops->type->release_ops(expr_info[i].ops);
}
}
kvfree(expr_info); return err;
}

在 nf_tables_newexpr 初始化 rule 中的其中一个 expr 失败时会调用 nf_tables_rule_release 释放 rule

void nf_tables_rule_release(const struct nft_ctx *ctx, struct nft_rule *rule)
{
nft_rule_expr_deactivate(ctx, rule, NFT_TRANS_RELEASE);
nf_tables_rule_destroy(ctx, rule);
}

nft_rule_expr_deactivate --> nft_lookup_deactivate --> nf_tables_deactivate_set 函数传入 NFT_TRANS_RELEASE 参数时不会设置 set 的状态为 inactivate ,而是调用 nf_tables_unbind_set 把 set->list 从链表上摘下来

void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set,
struct nft_set_binding *binding,
enum nft_trans_phase phase)
{
switch (phase) {
case NFT_TRANS_PREPARE:
if (nft_set_is_anonymous(set))
nft_deactivate_next(ctx->net, set); // [1] set->use--;
return;
case NFT_TRANS_ABORT:
case NFT_TRANS_RELEASE:
set->use--;
fallthrough;
default:
nf_tables_unbind_set(ctx, set, binding,
phase == NFT_TRANS_COMMIT);
}
}

PS: [1] 处代码是 CVE-2023-32233 的补丁

虽然 set 从链表上被摘除,但是如果查找的 set 是在当前批处理的请求中被创建的还能通过 nft_set_lookup_global --> nft_set_lookup_byid 查找到被释放的 set

struct nft_set *nft_set_lookup_global(const struct net *net,
const struct nft_table *table,
const struct nlattr *nla_set_name,
const struct nlattr *nla_set_id,
u8 genmask)
{
struct nft_set *set; set = nft_set_lookup(table, nla_set_name, genmask);
if (IS_ERR(set)) {
if (!nla_set_id)
return set; set = nft_set_lookup_byid(net, table, nla_set_id, genmask);
}
return set;
} static struct nft_set *nft_set_lookup_byid(const struct net *net,
const struct nft_table *table,
const struct nlattr *nla, u8 genmask)
{
struct nftables_pernet *nft_net = nft_pernet(net);
u32 id = ntohl(nla_get_be32(nla));
struct nft_trans *trans; list_for_each_entry(trans, &nft_net->commit_list, list) {
if (trans->msg_type == NFT_MSG_NEWSET) {
struct nft_set *set = nft_trans_set(trans); if (id <span style="font-weight: bold;" class="mark"> nft_trans_set_id(trans) &&
set->table </span> table &&
nft_active_genmask(set, genmask))
return set;
}
}
return ERR_PTR(-ENOENT);
}

nft_set_lookup_byid 会在 commit_list 里面搜索,批处理中靠前的 NFT_MSG_NEWSET 请求中分配的 set 是否有匹配上的。

所以触发漏洞的批处理中的请求列表如下:

  1. NFT_MSG_NEWSET 创建 set_A ,set->name = 'a'

  2. NFT_MSG_NEWSET 创建 set_B ,set->name = 'b'

  3. NFT_MSG_NEWRULE 创建 rule #0​ 并在 rule 嵌入 两个 lookup expr

    1. 第一个 lookup expr 参数正确会引用 set_A
    2. 第二个 lookup expr 初始化函数传入错误的参数导致 NFT_MSG_NEWRULE 失败
    3. 错误分支调用 nf_tables_rule_release 释放两个 lookup expr 导致 set_A 被释放
  4. NFT_MSG_NEWRULE 创建 rule #1​ 并在 rule 嵌入 lookup expr, 同理导致 set_B 被释放

  5. NFT_MSG_NEWRULE 创建 rule #2​ 并在 rule 嵌入 lookup expr,传入 NFTNL_EXPR_LOOKUP_SET_ID 通过 nft_set_lookup_byid 找到 set_A

由于批处理中的 3, 4 请求处理有异常发生,nfnetlink_rcv_batch 在处理完批处理中的所有请求后会调用 ss->abort --> __nf_tables_abort 释放 rule #2​ 引用的 set_A,这会导致 set_A 的 Double Free.

static void nfnetlink_rcv_batch(struct sk_buff *skb, struct nlmsghdr *nlh,
u16 subsys_id, u32 genid)
{
done:
if (status & NFNL_BATCH_REPLAY) {
} else if (status == NFNL_BATCH_DONE) { } else {
enum nfnl_abort_action abort_action; if (status & NFNL_BATCH_FAILURE)
abort_action = NFNL_ABORT_NONE;
else
abort_action = NFNL_ABORT_VALIDATE; err = ss->abort(net, oskb, abort_action); // 调用 __nf_tables_abort 释放 set_A
if (err == -EAGAIN) {
nfnl_err_reset(&err_list);
kfree_skb(skb);
module_put(ss->owner);
status |= NFNL_BATCH_FAILURE;
goto replay_abort;
}
}

然后设置断点打印 set 的释放日志可以发现 set_A 被释放了 两次,断点代码:

def nft_lookup_destroy_cb(bp):
set_addr = get_symbol_address("priv->set")
name = gdb.parse_and_eval("priv->set->name").string()
print("[nft_lookup_destroy] delete set 0x{:x} name: {}".format(set_addr, name))
return False nft_lookup_destroy_bp = WrapperBp("net/netfilter/nft_lookup.c:178", cb=nft_lookup_destroy_cb) def nf_tables_destroy_set_cb(bp):
set_addr = get_symbol_address("set")
print("[nf_tables_destroy_set] delete set 0x{:x}".format(set_addr))
# gdb.execute("bt")
return False nf_tables_destroy_set_bp = WrapperBp("net/netfilter/nf_tables_api.c:4830", cb=nf_tables_destroy_set_cb) def nft_set_destroy_cb(bp):
set_addr = get_symbol_address("$r12")
print("[nft_set_destroy] kfree 0x{:x}".format(set_addr))
# gdb.execute("bt")
return False
nft_set_destroy_bp = WrapperBp("net/netfilter/nf_tables_api.c:4647", cb=nft_set_destroy_cb)

日志:

Continuing.
[nft_lookup_destroy] delete set 0xffff888009973400 name: a
[nf_tables_destroy_set] delete set 0xffff888009973400
[nft_set_destroy] kfree 0xffff888009973400
[nft_lookup_destroy] delete set 0xffff888009972a00 name: b
[nf_tables_destroy_set] delete set 0xffff888009972a00
[nft_set_destroy] kfree 0xffff888009972a00
[nft_lookup_destroy] delete set 0xffff888009973400 name: &
[nf_tables_destroy_set] delete set 0xffff888009973400
[nft_set_destroy] kfree 0xffff888009973400

PS: set_A 0xffff888009973400 被释放了两次

补丁分析

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=1240eb93f0616b21c675416516ff3d74798fdc97

diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 3bb0800b3849a..69bceefaa5c80 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -3844,7 +3844,8 @@ err_destroy_flow_rule:
if (flow)
nft_flow_rule_destroy(flow);
err_release_rule:
- nf_tables_rule_release(&ctx, rule);
+ nft_rule_expr_deactivate(&ctx, rule, NFT_TRANS_PREPARE);
+ nf_tables_rule_destroy(&ctx, rule);
err_release_expr:
for (i = 0; i < n; i++) {
if (expr_info[i].ops) {

先用 NFT_TRANS_PREPARE 让匿名 set 的状态设置为 inactivate,然后通过 nf_tables_rule_destroy 释放 rule 及其引用的 expr 和 匿名 set,由于 set 状态为 inactivate 后面的请求就拿不到该 set。不过单纯靠这个补丁还是会导致UAF,最新的内核代码实际上还给 set 加入了引用计数机制,来确保漏洞得到完整修复,感兴趣的读者可以看看最新内核代码对上述漏洞触发逻辑的处理。

漏洞利用

上述漏洞触发后会在 slub freelist 上形成一个 A->B->A 的环形链表

​​​​

漏洞利用思路如下:

  1. 分配多个 msg 队列,在每个队列中分配 0x400 和 0x200 两个 msg_msg,这些 msg_msg 大概率会顺序排布
  2. 释放其中两个 0x200 的 msg_msg
  3. 触发漏洞,此时 A, B 使用的是刚刚释放的两个 msg_msg
  4. 再次分配 3 个大小为0x200 的 msg_msg (0, 1, 2)分别占用的内存块为 A, B , A
  5. msg_0 和 msg_2 指向同一块内存,然后释放 msg_2,此时 msg_0 指向被释放的内存
  6. 使用 msg_msgseg 占位 msg_2 ,控制 m_ts 泄露相邻 msg_msg 中的指针
  7. 利用泄露的地址,劫持 next 指针实现任意地址读,结合堆喷泄露 pipe_buffer 里面的 ops 指针,计算内核镜像基地址
  8. 劫持 ops 做 ROP

下面对重点步骤进行介绍,使用 msg_msgseg 占位 msg_msg,劫持 m_ts 和 next 指针进行信息泄露的示意图如下:

泄露步骤:

  1. 首先篡改 msg_0->m_ts 为 0x1000 泄露相邻 msg_msg T1 的数据
  2. 从 T1 的 next 和 prev 指针中可以获取 kmalloc-cg-1k 中 T2 的地址,因为它们位于同一个队列会使用双向链表管理
  3. 修改 msg_0->next 指针泄露 T2 的数据,从 T2 数据中解析处 T2 的 idx
  4. 使用 idx 释放 T2,用 pipe_buffer 占位,就能泄露内核镜像地址​​​​

注意图中 next 指针指向的是 T2 上一个 msg_msg 的中间,然后通过 msgqids1[0] 释放 msg_0 同时会释放 msg_0->next,然后使用 msg_msg 占位就可以控制 pipe_buffer 里面的数据

​​​​​​

细心观察会发现 msg_0->m_list.next 指针为 NULL ( msg_msgseg 占位的副作用),正常情况下去做 unlink 会导致 panic,但是目标内核开启了 CONFIG_DEBUG_LIST​ 在 unlink 前会检查指针是否有效,如果非法就返回 false 不执行 unlink 操作

bool __list_del_entry_valid(struct list_head *entry)
{
struct list_head *prev, *next; prev = entry->prev;
next = entry->next; if (CHECK_DATA_CORRUPTION(next <span style="font-weight: bold;" class="mark"> NULL,
"list_del corruption, %px->next is NULL\n", entry) ||
CHECK_DATA_CORRUPTION(prev </span> NULL,
"list_del corruption, %px->prev is NULL\n", entry) ||
CHECK_DATA_CORRUPTION(next <span style="font-weight: bold;" class="mark"> LIST_POISON1,
"list_del corruption, %px->next is LIST_POISON1 (%px)\n",
entry, LIST_POISON1) ||
CHECK_DATA_CORRUPTION(prev </span> LIST_POISON2,
"list_del corruption, %px->prev is LIST_POISON2 (%px)\n",
entry, LIST_POISON2) ||
CHECK_DATA_CORRUPTION(prev->next != entry,
"list_del corruption. prev->next should be %px, but was %px. (prev=%px)\n",
entry, prev->next, prev) ||
CHECK_DATA_CORRUPTION(next->prev != entry,
"list_del corruption. next->prev should be %px, but was %px. (next=%px)\n",
entry, next->prev, next))
return false; return true; } static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return; __list_del(entry->prev, entry->next);
}

由于这种特性,可以避免内核崩溃的前提下,触​​发非对齐释放。

除了上述的漏洞利用策略,作者还实现另一种方案,主要区别在于将 UAF 转换为 user_key_payload 和 nft_set 的重叠,然后利用 user_key_payload 泄露地址&篡改 set->ops​​

总结

  1. 该漏洞的触发和漏洞利用都使用了 CONFIG_DEBUG_LIST​ 检测到非法链表指针时不 panic 内核的特性,在篡改对象如果需要伪造链表指针时,可以利用该特性。
  2. Double Free 触发并重新占位后,freelist 可能会被链入非法的内存块,作者的做法是释放多个之前堆喷的堆块到 freelist, 避免内核申请非法内存块导致崩溃。

参考文章

CVE-2023-3390 Linux 内核 UAF 漏洞分析与利用的更多相关文章

  1. 20169212《Linux内核原理与分析》课程总结

    20169212<Linux内核原理与分析>课程总结 每周作业链接汇总 第一周作业:完成linux基础入门实验,了解一些基础的命令操作. 第二周作业:学习MOOC课程--计算机是如何工作的 ...

  2. 2019-2020-1 20199329《Linux内核原理与分析》第十三周作业

    <Linux内核原理与分析>第十三周作业 一.本周内容概述 通过重现缓冲区溢出攻击来理解漏洞 二.本周学习内容 1.实验简介 注意:实验中命令在 xfce 终端中输入,前面有 $ 的内容为 ...

  3. 2019-2020-1 20199329《Linux内核原理与分析》第十二周作业

    <Linux内核原理与分析>第十二周作业 一.本周内容概述: 通过编程理解 Set-UID 的运行机制与安全问题 完成实验楼上的<SET-UID程序漏洞实验> 二.本周学习内容 ...

  4. 2019-2020-1 20199329《Linux内核原理与分析》第十一周作业

    <Linux内核原理与分析>第十一周作业 一.本周内容概述: 学习linux安全防护方面的知识 完成实验楼上的<ShellShock 攻击实验> 二.本周学习内容: 1.学习& ...

  5. 2019-2020-1 20199318《Linux内核原理与分析》第十三周作业

    <Linux内核原理与分析> 第十三周作业 一.预备知识 缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况.这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片 ...

  6. 2019-2020-1 20199318《Linux内核原理与分析》第十一周作业

    <Linux内核原理与分析> 第十一周作业 一.预备知识 什么是ShellShock? Shellshock,又称Bashdoor,是在Unix中广泛使用的Bash shell中的一个安全 ...

  7. 20169212《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 这一周学习了MOOCLinux内核分析的第一讲,计算机是如何工作的?由于本科对相关知识的不熟悉,所以感觉有的知识理解起来了有一定的难度,不过多查查资 ...

  8. Linux内核源代码情景分析系列

    http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统  5.1 概述 构成一个操作系统最重要的就 ...

  9. 20169210《Linux内核原理与分析》第二周作业

    <Linux内核原理与分析>第二周作业 本周作业分为两部分:第一部分为观看学习视频并完成实验楼实验一:第二部分为看<Linux内核设计与实现>1.2.18章并安装配置内核. 第 ...

  10. Linux内核启动代码分析二之开发板相关驱动程序加载分析

    Linux内核启动代码分析二之开发板相关驱动程序加载分析 1 从linux开始启动的函数start_kernel开始分析,该函数位于linux-2.6.22/init/main.c  start_ke ...

随机推荐

  1. 揭秘JWT:从CTF实战到Web开发,使用JWT令牌验证

    揭秘JWT:从CTF实战到Web开发,使用JWT令牌验证 介绍 JWT(JSON Web Tokens)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在网络上安全地传输信息. ...

  2. 使用阿里云 SpringBoot 仓库初始化项目

    本文基于:https://www.bilibili.com/video/BV15b4y1a7yG?p=5&vd_source=cf8900ba33d057d422c588abe5d5290d ...

  3. CF708C Centroids [树形DP,换根DP]

    Description 给定一棵树. 至多进行一次操作:删去一条边,连接一条新边,保证操作完后仍是树. 问每个点在进行操作后是否可以成为树的重心. Solution 性质\(1\):若一个点不是树的重 ...

  4. 最小代价的 SSO 单点登录方案

    现在有多个 WebApp,想用最小的代价实现 SSO 单点登录.所谓最小代价,我的理解就是对原有 WebApp 的改动最小,因此 在旁路增加一个 SsoWebApp 用于管理 SSO 的账号,进行身份 ...

  5. Java日期时间API系列15-----Jdk8中java.time包中的新的日期时间API类,java日期计算2,年月日时分秒的加减等

    通过Java日期时间API系列8-----Jdk8中java.time包中的新的日期时间API类的LocalDate源码分析 ,可以看出java8设计非常好,实现接口Temporal, Tempora ...

  6. 3个步骤轻松集成Push Kit,实现App消息推送

    推送通知作为App重要的消息传递工具,广泛应用于电子商务.社交媒体.旅游交通等领域的通知场景.比如当应用有新功能或安全补丁时,系统将推送消息提醒用户及时更新:如果是航班出行类的应用,会发送最新的班次时 ...

  7. CDQ&整体二分-三维偏序(陌上花开)

    题面 本文讲cdq,整体二分的思路与做法.=分治VS数据结构 其实维度这一方面,空间几何可以是维度,像时间这样有规定顺序的词语也可能是维度. cdq 三维偏序,一般可以用一维一维的消.可以用cdq嵌套 ...

  8. tarjan 各类板子集合

    tarjan大板子(非讲解): 1.普通缩点DGA void tarjan(int x){ dfn[x]=low[x]=++cntp; q.push(x);v[x]=1; for(int i=head ...

  9. 云原生周刊:Kubernetes v1.28 新特性一览 | 2023.8.14

    推荐一个 GitHub 仓库:Fast-Kubernetes. Fast-Kubernetes 是一个涵盖了 Kubernetes 的实验室(LABs)的仓库.它提供了关于 Kubernetes 的各 ...

  10. windows运行sentry

    原文:https://blog.51cto.com/u_15089766/2602175 解决无法转换镜像版本为v2的问题: https://blog.csdn.net/qq_33306246/art ...