问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2023-3390 Linux 内核 UAF 漏洞分析与利用
漏洞分析
漏洞分析 漏洞成因是 nf_tables_newrule 在异常分支会释放 rule 和 rule 引用的匿名 set ,但是没有设置 set 的状态为 inactivate,导致批处理中后面的请求还能访问已经被释放的 set. 对 nftabl...
漏洞分析 ---- 漏洞成因是 nf\_tables\_newrule 在异常分支会释放 rule 和 rule 引用的匿名 set ,但是没有设置 set 的状态为 inactivate,导致批处理中后面的请求还能访问已经被释放的 set. 对 nftables 子系统不熟悉的同学可以先看看:<https://forum.butian.net/share/2719> 漏洞相关代码如下: ```c 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 ```c 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 从链表上摘下来 ```c 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 ```c 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. ```c 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 被释放了 两次,断点代码: ```c 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) ``` 日志: ```c 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 被释放了两次 补丁分析 ```c 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 的环形链表 ![image](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-6dbcbddfaafdd6f34b14dc0bb77a250c8ed4f0ef.png) 漏洞利用思路如下: 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 指针进行信息泄露的示意图如下: ![image](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-eabf7deae0deb21fa695d79b65e4a3034fa41110.png) 泄露步骤: 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 里面的数据 ![image](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-864dbdb5aba14861103c5a805cdddf2e116af147.png) 细心观察会发现 msg\_0->m\_list.next 指针为 NULL ( msg\_msgseg 占位的副作用),正常情况下去做 unlink 会导致 panic,但是目标内核开启了 `CONFIG_DEBUG_LIST` 在 unlink 前会检查指针是否有效,如果非法就返回 false 不执行 unlink 操作 ```c 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 ![image](https://shs3.b.qianxin.com/attack_forum/2024/01/attach-6ede88115ff309f27f931bc030a7779f1c322a98.png) 总结 -- 1. 该漏洞的触发和漏洞利用都使用了 `CONFIG_DEBUG_LIST` 检测到非法链表指针时不 panic 内核的特性,在篡改对象如果需要伪造链表指针时,可以利用该特性。 2. Double Free 触发并重新占位后,freelist 可能会被链入非法的内存块,作者的做法是释放多个之前堆喷的堆块到 freelist, 避免内核申请非法内存块导致崩溃。 参考文章 ---- - [https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2023-3390\_lts\_cos\_mitigation/docs/exploit.md#2-patch-analysis](https://github.com/google/security-research/blob/master/pocs/linux/kernelctf/CVE-2023-3390_lts_cos_mitigation/docs/exploit.md#2-patch-analysis)
发表于 2024-01-25 10:00:00
阅读 ( 33752 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
请先
登录
后评论
hac425
14 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!