CVE-2022-47929:流量控制noqueue没有问题?

来源: Cloudflare USER命名空间为我们最喜欢的工具(例如docker和podman)驱动各种功能。我们在去年6月份写过关于Linux命名空间的文章并这样解释:

640.png

USER命名空间为我们最喜欢的工具(例如docker和podman)驱动各种功能。我们在去年6月份写过关于Linux命名空间的文章并这样解释:

“许多命名空间都不存在争议的,例如UTS命名空间,其允许主机系统隐藏其主机名和时间。其他一些命名空间复杂但直接明了,例如,NET和NS(mount)命名空间令人难以理解。最后,还有一个非常特殊、很不寻常的USER命名空间。USER名称空间之所以特殊,是因为它允许通常没有特权的所有者在其中作为根运行。这样我们才能拥有非作为真正根运行的工具,例如Docker,以及无根容器之类的东西。”

由于其性质,允许非特权用户访问USER命名空间总是存在巨大的安全风险。在它的帮助下,非特权用户事实上可以运行通常需要根权限的代码。这种代码往往未经充分测试和存在bug。今天我们将研究一个此类案例,即通过USER命名空间来利用一个内核缺陷,从而导致非特权的拒绝服务攻击。

Linux流量控制队列规则

2019年,我们正在探索利用Linux流量控制的队列规则(qdisc),使用Hierarchy Token Bucket(HTB)classful qdisc策略为我们的服务之一调度数据包。Linux流量控制是一个用户配置的系统,用于调度和过滤网络数据包。队列规则是调度数据包的策略。具体而言,我们想从一个接口过滤和调度某些数据包,并将其他数据包丢入noqueue qdisc。

noqueue是qdisc的一个特例,调度到其中的数据包应该被丢弃。实践中情况并非如此。Linux对noqueue处理方式导致数据包被通过而不是被丢弃(大部分情况下)。文档也很能说明问题。它还指出,“不可能将noqueue排队规则分配给物理设备或类”。那么,当我们把noqueue分配给一个类时会发生什么呢?

让我们写一些shell命令来显示这个问题的实际情况。

1.$sudo-i2.#dev=enp0s53.#tc qdisc replace dev$dev root handle 1:htb default 14.#tc class add dev$dev parent 1:classid 1:1 htb rate 10mbit5.#tc qdisc add dev$dev parent 1:1 handle 10:noqueue

首先我们需要以根身份登录,这样我们将获得CAP_NET_ADMIN权限,从而能够配置流量控制。

然后我们将一个网络接口分配给一个变量。这些可以通过ip a找到。虚拟接口可以通过调用ls/sys/devices/virtual/net定位。这些将匹配ip a的输出。

我们的接口目前被分配给pfifo_fast qdisc,所以我们用HTB classful qdisc来代替它,并把它分配给1:的句柄。我们可以把它看作是树中的根节点。“默认1”配置的结果是,未分类的流量被路由直接通过这个qdisc,后者会回退到pfifo_fast排队。(下文将进一步说明)

接下来我们给我们的根qdisc添加一个类1:,把它分配给根1:的第一个叶节点1:1,并给它一些合理的配置默认值。

最后,我们将noqueue qdisc添加到层级结构中的第一个叶节点:1:1。这实际意味着,在这里路由的流量将被调度到noqueue。

假设我们的设置顺利执行,我们将收到类似于这个内核错误的提示:

BUG:kernel NULL pointer dereference,address:0000000000000000#PF:supervisor instruction fetch in kernel mode…Call Trace:htb_enqueue+0x1c8/0x370dev_qdisc_enqueue+0x15/0x90__dev_queue_xmit+0x798/0xd00…

我们知道该根用户负责在接口设置qdisc,如果根用户可以导致内核崩溃,那怎么办呢?我们不要将HTB qdisc应用到一个HTB qdisc的noqueue qdisc就是了。

#dev=enp0s5#tc qdisc replace dev$dev root handle 1:htb default 1#tc class add dev$dev parent 1:classid 1:2 htb rate 10mbit//A//B is missing,so anything not filtered into 1:2 will be pfifio_fast

在这里,我们利用HTB的默认情况,其中分配一个类id 1:2以被限速(A),并隐含地没有将qdisc设置另一个类(例如id 1:1)(B)。排队到(A)的数据包将被过滤到HTB_DIRECT,排队到(B)的数据包将被过滤到pfifo_fast。

因为我们不熟悉代码库的这一部分,我们通知了邮件列表并创建了一个工单。当时这个bug对我们来说似乎并不那么重要。

快进到2022年,我们正在推进USER命名空间创建强化。我们用一个新的LSM钩子扩展了Linux LSM框架:userns_create,以利用eBPF LSM提供保护,并鼓励其他人也这样做。最近,在梳理我们的积压工单时,我们重新考虑了这个bug。我们自问:"我们能不能利用USER命名空间来触发这个bug?”简短的答案是肯定的!

演示这个bug

这个漏洞可以用任何一个假设struct Qdisc.enqueue函数不为空的classful qdisc来执行(后面会详细介绍),但在本例中,我们只用HTB来演示。

$unshare-rU–net$dev=lo$tc qdisc replace dev$dev root handle 1:htb default 1$tc class add dev$dev parent 1:classid 1:1 htb rate 10mbit$tc qdisc add dev$dev parent 1:1 handle 10:noqueue$ping-I$dev-w 1-c 1 1.1.1.1

我们用“lo”接口来证明这个bug可以用虚拟接口触发。这对容器来说很重要,因为它们在大多数时候都是被提供虚拟接口,而不是物理接口。正因为如此,我们可以使用一个容器以非特权用户的身份使主机崩溃,从而执行拒绝服务攻击。

为什么有这样的结果?

为了更好地理解这个问题,我们需要回顾一下最初的补丁系列,但特别是引入了这个bug的提交。这一系列之前,在接口上实现noqueue依赖于一种hack:如果设备有tx_queue_len=0,则将设备qdisc设为noqueue。提交d66d6c3152e8("net:sched:register noqueue qdisc")通过显式允许用tc命令添加noqueue来解决这个问题,无需绕过以上限制。

内核检查我们是否处于noqueue情况的方式,就是简单地检查qdisc是否有NULL enqueue()函数。记得前面说过,noqueue在实践中不一定会丢弃数据包?在以上检查失败后,下面的逻辑处理noqueue的功能。为了不通过检查,作者不得不以欺骗方式将noop_enqueue()重新赋值为NULL,方式是在init中使enqueue=NULL,后者将在运行时在register_qdisc()后调用。

这就是classful qdiscs发挥作用的地方了。对入队函数的检查不再为NULL。在此调用路径中,它现在设置为HTB(在我们的示例中),因此允许通过调用htb_enqueue()函数,将struct skb排入队列。进入那里后,HTB会进行查找以提取分配给叶节点的qdisc,并最终尝试将struct skb排入选定的qdisc,最终到达这个函数:

include/net/sch_generic.h

static inline int qdisc_enqueue(struct sk_buff skb,struct Qdisc sch,struct sk_buff to_free){qdisc_calculate_pkt_len(skb,sch);return sch->enqueue(skb,sch,to_free);//sch->enqueue==NULL}

我们可以看到,排队过程对物理/虚拟接口是相当无关的。权限和验证检查是在向接口添加队列时进行的,这就是为什么classful qdics假定队列不是NULL。了解这一点使我们找到了一些可以考虑的解决方案。

解决方案

我们有几个解决方案,介于从我们认为最好到最坏的。

1.遵循tc-noqueue文档,不允许将noqueue分配给一个classful qdisc

2.不检查NULL,而是检查struct noqueue_qdisc_ops,并将noqueue重设为noop_enqueue

3.对于每个classful qdisc,检查是否有NULL和回退

我们最终选择了第一个选项:“disallow noqueue for qdisc classes(不允许对qdisc类的noqueue)”,而第三个选项在代码中造成了大量的混乱,且没有彻底解决问题。未来的qdiscs实现可能会忘记这个重要的检查以及维护者。然而,不采用第二个选项的原因更为有意思。

我们之所以没有采用这种方法,是因为我们需要首先回答这些问题:

为什么不允许将noqueue分配给classful qdisc?

这背离了文档。文档确实有一些在实践中不被完全遵循的先例,但我们需要对其更新,以反映目前的状况。这样做很好,但是除了删除NULL解引用错误之外,并不能解决行为变化问题。

如果我们允许将noqueue分配给qdisc,会发生什么行为变化?

这个问题更难回答,因为我们需要确定这种行为应该是什么。目前,当noqueue被作为根qdisc为一个接口应用时,其路径基本上是允许数据包被处理。声明了回退的类则是另一回事。它们可能每个都有自己的回退,我们如何知道什么是正确的回退?在HTB中,有时回退是通过HTB_DIRECT,有时是pfifo_fast。其他类又如何呢?也许我们应该回到默认的noqueue行为,就像对根qdiscos那样?

我们觉得走这条路只会给排队增加混乱和额外的复杂性。我们还可以提出一个观点,即这样的更改可以被视为特性的添加,而不一定是bug的修复。可以说,对于目前防止漏洞而言,遵守当前文档似乎是更有吸引力的方法,其他事情可以日后再解决。

要点

首先,也是最重要的,尽快应用这个补丁。同时,考虑通过设置setting sysctl-w kernel.unprivileged_userns_clone=0,只允许根在Debian内核中创建USER命名空间,sysctl-w user.max_user_namespaces=【number】用于进程层级,或者考虑回退到这两个补丁:security_create_user_ns()和SELinux实现(现在为Linux 6.1.x),允许您用eBPF或者SELinux保护自己的系统。如果确定没有使用USER命名空间和处于极端情况下,您可以考虑用CONFIG_USERNS=n关闭该功能。这只是利用命名空间进行攻击的众多例子之一,未来肯定会有更多严重程度不同的例子出现。

特别感谢Ignat Korchagin和Jakub Sitnicki的代码审查工作,并帮助在实践中演示这个bug。

免责声明:本文内容来自其他媒体或者他人投稿,请自行判断内容的正确性。若本站收录的信息无意侵犯了贵司版权,请给我们来信(info@chuhaihao.com),我们会及时处理和回复。
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
搜索