问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2024-41010 linux内核中网络调度相关的net/sched部分存在释放后使用漏洞 漏洞利用分析
漏洞分析
该漏洞存在于 Linux 内核的流量控制和调度部分。通过特定的操作,攻击者能够触发 UAF(Use-After-Free)漏洞。攻击者可以通过合理的构造条件,在内核内存中写入任意数据,从而造成系统崩溃或进一步的安全威胁。
### CVE-2024-41010 漏洞利用分析 #### 根本原因分析 观察 `tcx_entry` 的生命周期,我们可以看到它首先在 `ingress_init` 等函数初始化新建的 ingress/clsact qdisc 时分配: ```c static int ingress_init(struct Qdisc *sch, struct nlattr *opt, struct netlink_ext_ack *extack) { struct ingress_sched_data *q = qdisc_priv(sch); struct net_device *dev = qdisc_dev(sch); struct bpf_mprog_entry *entry; bool created; int err; if (sch->parent != TC_H_INGRESS) return -EOPNOTSUPP; net_inc_ingress_queue(); entry = tcx_entry_fetch_or_create(dev, true, &created); // [1] if (!entry) return -ENOMEM; tcx_miniq_set_active(entry, true); // [2] mini_qdisc_pair_init(&q->miniqp, sch, &tcx_entry(entry)->miniq); // [3] if (created) tcx_entry_update(dev, entry, true); q->block_info.binder_type = FLOW_BLOCK_BINDER_TYPE_CLSACT_INGRESS; q->block_info.chain_head_change = clsact_chain_head_change; q->block_info.chain_head_change_priv = &q->miniqp; // [4] err = tcf_block_get_ext(&q->block, sch, &q->block_info, extack); if (err) return err; mini_qdisc_pair_block_init(&q->miniqp, q->block); return 0; } ``` 在 \[1\] 处,调用了 `tcx_entry_fetch_or_create` 来分配或获取已存在的 `tcx_entry`(被 `bpf_mprog_entry` 包裹): ```c static inline struct bpf_mprog_entry * tcx_entry_fetch_or_create(struct net_device *dev, bool ingress, bool *created) { struct bpf_mprog_entry *entry = tcx_entry_fetch(dev, ingress); // [5] *created = false; if (!entry) { entry = tcx_entry_create(); if (!entry) return NULL; *created = true; } return entry; } ``` 在 \[2\] 处,调用 `tcx_miniq_set_active` 将 `tcx_entry->miniq_active` 设置为 `true`,然后在 \[3\] 处,将 `&tcx_entry(entry)->miniq`(基本上是指向 `tcx_entry` 第一个字段的指针)存储到 `&q->miniqp`,然后在 \[4\] 处将其复制到 `q->block_info.chain_head_change_priv`。 如果我们现在再看看释放 ingress qdisc 的过程,会发现一些有趣的地方: ```c static void ingress_destroy(struct Qdisc *sch) { struct ingress_sched_data *q = qdisc_priv(sch); struct net_device *dev = qdisc_dev(sch); struct bpf_mprog_entry *entry = rtnl_dereference(dev->tcx_ingress); if (sch->parent != TC_H_INGRESS) return; tcf_block_put_ext(q->block, sch, &q->block_info); if (entry) { tcx_miniq_set_active(entry, false); if (!tcx_entry_is_active(entry)) { tcx_entry_update(dev, NULL, true); tcx_entry_free(entry); } } net_dec_ingress_queue(); } ``` 在 `ingress_destroy` 中,如果存在与网络设备绑定的 `tcx_entry`,它会将 `tcx_entry->miniq_active` 设置为 `false`,然后调用 `tcx_entry_is_active(entry)` 来判断 `tcx_entry` 是否仍在使用。如果没有使用,则从网络设备中移除引用并释放 `tcx_entry`。接下来看看 `tcx_entry_is_active` 的实现: ```c static inline bool tcx_entry_is_active(struct bpf_mprog_entry *entry) { ASSERT_RTNL(); return bpf_mprog_total(entry) || tcx_entry(entry)->miniq_active; } ``` 它会检查该条目是否有任何附加的 bpf 程序,因为这些程序会引用该条目,或者检查 `miniq_active` 是否为 `true`,表示 qdisc 视图仍然存在。但是,销毁 qdisc 时会将其设置为 `false`,如果没有附加任何程序,这个函数总是会返回 `false`,从而进入释放路径。 这个问题引发了一个问题:如果另一个 qdisc 获取了相同的条目并引用了它,然后我们删除第一个 qdisc,导致该条目也被删除,会发生什么?值得注意的是,在我的理解中,不能在同一网络设备上共存两个 ingress qdisc,创建一个新的 ingress qdisc 会删除旧的 qdisc。然而,令人感兴趣的是,创建新 ingress qdisc 会在调用 `ingress_destroy` 删除旧的 qdisc 之前调用 `ingress_init()`,因此可以通过连续创建两个 ingress qdisc 来复制引用。在第一个 qdisc 被删除之后,第二个 qdisc 仍然持有指向 `tcx_entry` 的指针,这个指针会在随后的调用链中的 `mini_qdisc_pair_swap()` 函数中被解引用: ```c [...] static void tcf_chain0_head_change(struct tcf_chain *chain, struct tcf_proto *tp_head) { struct tcf_filter_chain_list_item *item; struct tcf_block *block = chain->block; if (chain->index) return; mutex_lock(&block->lock); list_for_each_entry(item, &block->chain0.filter_chain_list, list) tcf_chain_head_change_item(item, tp_head); mutex_unlock(&block->lock); } [...] static void tcf_chain_head_change_item(struct tcf_filter_chain_list_item *item, struct tcf_proto *tp_head) { if (item->chain_head_change) item->chain_head_change(tp_head, item->chain_head_change_priv); } [...] static void clsact_chain_head_change(struct tcf_proto *tp_head, void *priv) { struct mini_Qdisc_pair *miniqp = priv; mini_qdisc_pair_swap(miniqp, tp_head); }; ``` 这可以通过向该 qdisc 添加或删除流量控制过滤器来触发。 #### 漏洞触发 1. 创建一个新的 ingress/clsact qdisc 2. 创建第二个 ingress/clsact qdisc,这将导致以下事件发生: - 重用 `tcx_entry` 引用 - 删除 `tcx_entry` - 删除第一个 qdisc 3. 向第二个 qdisc 添加过滤器,这会解引用悬空指针,并将 UAF 对象的第一个 qword 覆盖为指向 qdisc 结构体或 NULL 的指针 悬空指针在 `mini_qdisc_pair_swap` 函数中被解引用: ```c void mini_qdisc_pair_swap(struct mini_Qdisc_pair *miniqp, struct tcf_proto *tp_head) { /* 被 chain0->filter_chain_lock 保护。 * 不能直接访问链表,因为 tp_head 可能为 NULL。 */ struct mini_Qdisc *miniq_old = rcu_dereference_protected(*miniqp->p_miniq, 1); // [1] struct mini_Qdisc *miniq; if (!tp_head) { RCU_INIT_POINTER(*miniqp->p_miniq, NULL); // [2] } else { miniq = miniq_old != &miniqp->miniq1 ? &miniqp->miniq1 : &miniqp->miniq2; /* 确保读者在我们修改 miniq 时看不到它。 * 确保至少经过一个 RCPU 协调周期,确保 miniq 被标记为非活跃状态。 */ if (IS_ENABLED(CONFIG_PREEMPT_RT)) cond_synchronize_rcu(miniq->rcu_state); else if (!poll_state_synchronize_rcu(miniq->rcu_state)) synchronize_rcu_expedited(); miniq->filter_list = tp_head; rcu_assign_pointer(*miniqp->p_miniq, miniq); // [3] } if (miniq_old) /* 这是与上面的 rcu 同步的对等操作。我们需要阻止潜在的新用户 * 访问 miniq_old,直到所有的读者都不再看到它。 */ miniq_old->rcu_state = start_poll_synchronize_rcu(); // [4] } ``` #### 有限的 UAF 需要注意的是,`miniqp` 是 `struct tcx_entry` 的第一个字段,`miniq` 是 `struct Qdisc` 的一个字段,因此 `miniqp->p_miniq` 是指向 qdisc 中部的一个指针,而 `tcx_entry` 位于 kmalloc-2k 中。 注意到,在 \[2\] 处,如果 `tp_head` 为 NULL,它会将 NULL 写入 `miniqp->p_miniq`,这正好位于 `tcx_entry` 的开始位置。如果 `tp_head` 不为 NULL,则会在 \[3\]中写入 `miniq`,即将实际的结构体写入悬空指针位置。 通过触发 UAF 攻击,可以绕过验证并修改内核内存的特定部分,进而对系统安全造成威胁。 ### 升级为任意释放 (Arbitrary Free) 我的方法是通过跨缓存(cross-caching)从 `kmalloc-2k` 到 `kmalloc-cg-2k`,借此访问 `msg_msgseg` 对象,并将 `msg_msgseg->next` 指针替换为 `miniq` 指针。这在我们的案例中特别有趣,因为 `struct mini_Qdisc` 的第一个字段是 `filter_list`,它是一个绑定到过滤器的 `struct tcf_proto` 对象的链表,而这些对象我们可以随意分配和释放。结合 `msg_msgseg` 的特性,读取 `msg_msgseg` 会释放 `msg_msgseg->next` 直到它找到 NULL,这使得我们有了一个非常强大的任意释放工具。 ```c void free_msg(struct msg_msg *msg) { struct msg_msgseg *seg; security_msg_msg_free(msg); seg = msg->next; kfree(msg); while (seg != NULL) { struct msg_msgseg *tmp = seg->next; cond_resched(); kfree(seg); seg = tmp; } } ``` 通过这个方法,我们可以做以下操作: - 使 `tcx_entry` 变为悬空指针 - 将它替换为 `msg_msgseg` - 向 qdisc 添加过滤器(这会把一个 `tcf_proto` 添加到以 `miniq` 为头的链表中,并将 `miniq` 指针写入 `msg_msgseg->next`) - 读取 `msg_msgseg`,这会触发释放,并遍历 `filter_list`,直到它找到 NULL,释放所有的对象,从而释放我们位于 `kmalloc-128` 区域的 `tcf_proto`。 ### 达到 kmalloc-cg-1k 我的漏洞利用的最终目标是获得一个在 `kmalloc-cg-1k` 中的双重释放(double free),从而使 `pipe_buffer` 与 `skbuf->data` 重叠,达到控制内存的目的。为此,我需要通过一些中间步骤,跨缓存,逐步接近 `kmalloc-cg-1k`。 #### 获取 kmalloc-cg-512 指针 一旦我在 `kmalloc-128` 中获得了任意释放的能力,我决定再次跨缓存,这次从 `kmalloc-128` 到 `kmalloc-cg-128`,通过喷洒 `msg_msgseg` 并重用我们的任意释放原语来释放 `kmalloc-cg-128` 中的一个 `msg_msgseg` 对象。在这一点上,我的目标是获得一个指向 `kmalloc-cg-1k` 的对象,以便将我们现在完全可控的 `msg_msgseg->next` 与其重叠。我的第一个想法是将其与一个 `msg_msg` 对象重叠,但这会导致 `msg_msgseg` 被链接到 `msg_msg` 的循环链表中,在释放时会无限迭代,导致死锁。后来,我通过分析结构体,发现了 `struct in_ifaddr`: ```c struct hlist_node hash; struct in_ifaddr __rcu *ifa_next; struct in_device *ifa_dev; struct rcu_head rcu_head; __be32 ifa_local; __be32 ifa_address; __be32 ifa_mask; __u32 ifa_rt_priority; __be32 ifa_broadcast; unsigned char ifa_scope; unsigned char ifa_prefixlen; unsigned char ifa_proto; __u32 ifa_flags; char ifa_label[IFNAMSIZ]; __u32 ifa_valid_lft; __u32 ifa_preferred_lft; unsigned long ifa_cstamp; /* created timestamp */ unsigned long ifa_tstamp; /* updated timestamp */ ``` 这个结构体特别有趣的原因主要有两个: - 我可以读取 `ifa_dev` 指针,这将允许我泄露 `kmalloc-512` 区域中一个 `struct in_device` 对象的指针。 - 在喷洒过程中,我可以使用 `ifa_address` 字段作为标识符,从而知道哪个喷洒的对象与 `msg_msgseg` 重叠。 虽然 `kmalloc-512` 指针并不是我最初的目标,但它已经足够好。因为 `kmalloc-cg-512` 和 `kmalloc-512` 属于同一个页面级别,所以它们通常在同一页面附近。基于这一点,我在分配 `in_device` 之前,先分配了大量的 `kmalloc-cg-512` 类型的 `skbuf->data`,这样,当我收到泄露的指针时,可以通过减去一个任意的偏移量,将指针定位到 `skbuf->data` 喷洒区域的中间。 #### 从 kmalloc-cg-128 到 kmalloc-cg-512 现在,我们有了 `skbuf->data` 对象在 `kmalloc-cg-512` 中的指针,我们需要通过某种方式将它写入我们可控的 `msg_msgseg`,以便释放它。虽然许多方便的数据喷洒技术似乎已经失效,但我最终发现了一个(看起来是新颖的)对象,能够完成这个工作: ```c static int rtnl_alt_ifname(int cmd, struct net_device *dev, struct nlattr *attr, bool *changed, struct netlink_ext_ack *extack) { char *alt_ifname; size_t size; int err; err = nla_validate(attr, attr->nla_len, IFLA_MAX, ifla_policy, extack); [...] alt_ifname = nla_strdup(attr, GFP_KERNEL_ACCOUNT); [...] kfree(alt_ifname); if (!err) *changed = true; return err; } ``` `alt_ifname` 对象是一个临时缓冲区,用来存储用户数据,分配时使用了 `GFP_KERNEL_ACCOUNT`。需要注意的是,`rtnl_alt_ifname` 只在获得 rtnl 锁的上下文中被调用,因此我们不能通过多线程来喷洒和竞争临时缓冲区。我们必须确保要覆盖的对象先于其他对象进入空闲链表。 一种方法是逐步分配其他 `msg_msgseg` 对象,并检查我们被篡改的 `msg_msgseg`,使用 `MSG_COPY` 读取它,而不释放它,从而知道哪个新分配的 `msg_msgseg` 对象刚好覆盖了目标对象,然后用 `alt_ifname` 缓冲区替换它,最终使我们可以将 `msg_msgseg->next` 指向 `kmalloc-cg-512` 中的 `skbuf->data` 对象,并释放它。 #### 从 kmalloc-cg-512 到 kmalloc-cg-1k `kmalloc-cg-512` 已经比 `kmalloc-cg-128` 更好。我们现在可以将 `msg_msg` 与我们的 `skbuf->data` 对象重叠。如果我们在与该 `msg_msg` 对象相同的队列中发送另一个消息,它会将第二个消息的指针放在第一个消息的第一个 qword 中。通过发送一个在 `kmalloc-cg-1k` 中分配的消息,我们可以泄露一个指针,并通过读取我们的 `skbuf->data` 来获取这个指针。如果我们稍微增加指针,它会指向另一个由我们的喷洒操作分配的 `msg_msg`,该对象属于一个不同的队列,这样我们就得到了一个指向 `kmalloc-cg-1k` 中 `msg_msg` 的重复引用,可以用来进行双重释放。关于如何操作 `msg_msg` 来实现这个目标,已经在这里详细讨论过:[Google CVE-2021-22555 writeup](https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html)。 ### pipe\_buffer->page 物理读写 > 获取权限 一旦我们到达 `kmalloc-cg-1k`,我们可以将 `skbuf->data` 与 `pipe_buffer` 重叠,控制 `pipe_buffer->page` 指针,进而实现物理读写。这项技术已经在 [Pipe Buffer](https://www.interruptlabs.co.uk/articles/pipe-buffer) 中详细讨论过。 使用这个原语,我可以覆盖 `modprobe_path` 字符串,将其指向一个通过 `memfd_create` 创建的内存文件,该文件位于 `/proc/<pid>/fd/<n>` 下。我还暴力破解了 exploit 进程的 pid,绕过了沙盒化命名空间的限制。有关更多细节,可以参考 [Nftables techniques](https://pwning.tech/nftables/#4-techniques)。 ### 参考文章 [https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2024-41010\_lts/docs/exploit.md](https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2024-41010_lts/docs/exploit.md)
发表于 2025-02-10 10:02:57
阅读 ( 785 )
分类:
Web应用
0 推荐
收藏
0 条评论
请先
登录
后评论
吃不饱的崽
2 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!