小心带来变通办法的漏洞。 第1部分:FragmentSmack / SegmentSmack



大家好! 我叫Dmitry Samsonov,我是Odnoklassniki的首席系统管理员。 我们在云中拥有超过7,000台物理服务器,11,000个容器和200个应用程序,它们以不同的配置形成700个不同的集群。 绝大多数服务器都在运行CentOS 7。
FragmentSmack漏洞信息发布于2018年8月14日
CVE-2018-5391 )和SegmentSmack( CVE-2018-5390 )。 这些是具有网络攻击向量和相当高的评级(7.5)的漏洞,由于资源枯竭(CPU)而威胁到拒绝服务(DoS)的威胁。 当时并未提出针对FragmentSmack的内核修复程序,而且,该修复程序的发布要比发布有关漏洞的信息晚得多。 为了消除SegmentSmack,建议更新内核。 更新包本身是在同一天发布的,剩下的就是安装它。
不,我们根本不反对内核更新! 但是,有细微差别...

我们如何更新产品上的核心


通常,没有什么复杂的:
  1. 下载包
  2. 将它们安装在许多服务器上(包括托管我们的云的服务器);
  3. 确保没有任何损坏;
  4. 确保所有标准内核设置均正确无误地应用;
  5. 等几天
  6. 检查服务器性能;
  7. 将新服务器的部署切换到新内核;
  8. 按数据中心更新所有服务器(一次更新一个数据中心,以在出现问题时最大程度地降低对用户的影响);
  9. 重新启动所有服务器。

对我们拥有的所有核心分支重复上述步骤。 目前,这是:

  • Stock CentOS 7 3.10-适用于大多数普通服务器;
  • Vanilla 4.19适用于我们的单一云,因为我们需要BFQ,BBR等。
  • Elrepo kernel-ml 5.2适用于高负载分发服务器 ,因为4.19以前的性能不稳定,并且功能需要相同。

您可能已经猜到了,重新启动数千台服务器花费的时间最多。 由于并非所有漏洞对于所有服务器都是至关重要的,因此我们仅重新启动可从Internet直接访问的漏洞。 在云中,为了不限制灵活性,我们不会使用新内核将外部可访问的容器绑定到单个服务器,而是会毫无例外地重新启动所有主机。 幸运的是,该过程比常规服务器要容易得多。 例如,无状态容器可以在重新引导期间简单地移至另一台服务器。

尽管如此,仍然有很多工作要做,可能要花几周的时间,如果新版本有任何问题,则可能要花几个月的时间。 攻击者很清楚这一点,因此需要计划B。

FragmentSmack / SegmentSmack。 解决方法


幸运的是,对于某些漏洞,存在这样的计划“ B”,它被称为解决方法。 通常,这是内核/应用程序设置的更改,它可以最大程度地减少可能的影响或完全消除对漏洞的利用。

对于FragmentSmack / SegmentSmack 提出以下解决方法:

您可以分别将net.ipv4.ipfrag_high_thresh和net.ipv4.ipfrag_low_thresh(及其类似的ipv6 net.ipv6.ipfrag_high_thresh和net.ipv6.ipfrag_low_thresh)和net.ipv6.ipfrag_low_thresh的默认值4MB和3MB分别更改为256 kB和192 kB。 测试表明,在攻击过程中,CPU使用率会略有下降,这要取决于设备,设置和条件。 但是,由于ipfrag_high_thresh = 262144字节,可能会影响性能,因为一次只能将两个64K片段放入重建队列中。 例如,存在使用大型UDP数据包的应用程序崩溃的风险 。”

内核文档中的参数本身描述如下:

ipfrag_high_thresh - LONG INTEGER
Maximum memory used to reassemble IP fragments.

ipfrag_low_thresh - LONG INTEGER
Maximum memory used to reassemble IP fragments before the kernel
begins to remove incomplete fragment queues to free up resources.
The kernel still accepts new fragments for defragmentation.

我们在生产服务上没有大型UDP。 LAN上没有零散的流量; WAN上有但不是很大的流量。 没什么大不了的-您可以滚动解决方法!

FragmentSmack / SegmentSmack。 第一滴血


我们遇到的第一个问题是,云容器有时仅部分应用了新设置(仅ipfrag_low_thresh),有时甚至根本没有使用它们-它们只是在开始时就崩溃了。 无法稳定地重现该问题(手动应用所有设置没有任何困难)。 理解为什么容器从一开始就掉下来也不是那么简单:没有发现错误。 可以肯定的一件事是:回滚设置解决了删除容器的问题。

为什么在主机上使用Sysctl还不够? 容器位于其专用网络命名空间中,因此容器中至少部分网络Sysctl参数可能与主机不同。

容器中的Sysctl设置如何正确应用? 由于我们拥有无特权的容器,因此通过进入容器本身来更改任何Sysctl设置都将失败-根本就没有足够的权限。 当时,我们的云使用Docker(现在为Podman )来启动容器。 码头工人通过API传递了新容器的参数,包括必要的Sysctl设置。
在枚举版本的过程中,事实证明Docker API并未引发所有错误(至少在版本1.10中)。 当试图通过“ docker run”启动容器时,我们最终至少看到了一些东西:

write /proc/sys/net/ipv4/ipfrag_high_thresh: invalid argument docker: Error response from daemon: Cannot start container <...>: [9] System error: could not synchronise with container process.

参数值无效。 但是为什么呢? 为什么它有时仅无效? 事实证明,Docker无法保证使用Sysctl参数(最新测试版本为1.13.1),因此有时ipfrag_high_thresh尝试将自身设置为256K,而ipfrag_low_thresh仍为3M,即上限低于下限,从而导致错误。

那时,我们已经使用了自己的机制来重新配置容器(通过cgroup freezer冻结容器,并通过ip netns在容器名称空间中执行命令),并且还向该部分添加了Sysctl参数。 问题已解决。

FragmentSmack / SegmentSmack。 一血2


在我们知道如何在云中使用“变通方法”之前,用户的第一个罕见投诉开始到来。 那时,自第一台服务器上开始解决方法以来,已经过去了几个星期。 初步调查显示,收到的投诉涉及单个服务,而不是有关这些服务的所有服务器。 这个问题重新变得非常模糊。

首先,当然,我们尝试回滚Sysctl设置,但这没有任何效果。 服务器和应用程序设置的各种操作也无济于事。 重启帮助。 Linux的重新启动与过去使用Windows的正常情况一样不自然。 尽管如此,它还是有帮助的,并且在Sysctl中应用新设置时,我们将所有内容注销为“内核故障”。 多么轻浮...

三个星期后,问题再次发生。 这些服务器的配置非常简单:代理/平衡器模式下的Nginx。 交通有点。 新的介绍性内容:客户端上每天出现的504个错误(“ 网关超时” )数量正在增加。 该图显示了此服务每天出现504错误的数量:



所有错误都与同一个后端有关-与云中的后端有关。 该后端上的数据包片段的内存消耗图如下:



这是操作系统图形上问题最明显的表现之一。 同时,在云中,另一个网络问题已通过QoS(流量控制)设置得以解决。 在数据包片段的内存消耗图中,它看起来完全一样:



假设很简单:如果它们在图表上看起来相同,则它们具有相同的原因。 而且,这种存储器的任何问题都极为罕见。

固定问题的实质是我们将fq数据包sheduler与QoS中的默认设置一起使用。 默认情况下,对于一个连接,它允许您将100个数据包添加到队列,并且某些连接在信道不足的情况下开始将队列阻塞到故障。 在这种情况下,数据包将丢弃。 在tc统计信息(tc -s qdisc)中,可以看到如下所示:

qdisc fq 2c6c: parent 1:2c6c limit 10000p flow_limit 100p buckets 1024 orphan_mask 1023 quantum 3028 initial_quantum 15140 refill_delay 40.0ms
Sent 454701676345 bytes 491683359 pkt (dropped 464545, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
1024 flows (1021 inactive, 0 throttled)
0 gc, 0 highprio, 0 throttled, 464545 flows_plimit


“ 464545 flow_plimit”是由于超出一个连接队列的限制而丢弃的数据包,而“丢弃464545”是此sheduler所有丢弃的数据包的总和。 在将队列长度增加到1000并重新启动容器之后,问题不再出现。 您可以坐在椅子上喝冰沙。

FragmentSmack / SegmentSmack。 最后的血


首先,在内核漏洞宣布数月后,终于出现了FragmentSmack的修复程序(我记得在8月发布的公告中,仅针对SegmentSmack发行了修复程序),这使我们有机会放弃替代方法,这给我们带来了很多麻烦。 在这段时间里,有些服务器已经设法转移到新内核,现在我们必须从头开始。 为什么我们不等待FragmentSmack修复程序就更新内核? 事实是,防范这些漏洞的过程与更新CentOS本身的过程(同时花费的时间比仅更新内核的时间)重合(并合并)。 此外,SegmentSmack是一个更危险的漏洞,并且针对它的修复程序会立即出现,因此无论如何,这一点都是重要的。 但是,我们不能简单地在CentOS上更新内核,因为在CentOS 7.5期间出现的FragmentSmack漏洞仅在7.6版中得到修复,因此我们必须停止升级到7.5,然后从头再升级到7.6。 就是这样。

其次,用户对问题的罕见抱怨又回到了我们身边。 现在我们已经确定所有这些都与将文件从客户端下载到我们的某些服务器有关。 通过这些服务器,上传的文件数量很少。

我们从上面的故事中回想起,回滚Sysctl并没有帮助。 重新启动虽然有所帮助,但暂时没有。
对Sysctl的怀疑并未消除,但是这次需要收集尽可能多的信息。 此外,由于无法更准确地检查正在发生的情况,因此极端无法再现客户上传的问题。

对所有可用统计数据和日志的分析并没有使我们更加了解正在发生的事情。 严重缺乏重现问题以“感觉”到特定连接的能力。 最后,使用Wi-Fi连接的应用程序特殊版本的开发人员设法在测试设备上实现了问题的稳定重现。 这是调查的一项突破。 客户端连接到Nginx,它代理到后端,这是我们的Java应用程序。



出现问题的对话框如下(在Nginx代理端已修复):

  1. 客户端:要求提供有关下载文件的信息。
  2. Java服务器:答案。
  3. 客户端:带文件的POST。
  4. Java服务器:错误。

同时,Java服务器将日志从客户端接收到0字节的数据写入日志,并且Nginx代理将请求花费了30秒以上(30秒是客户端应用程序的超时时间)写入日志。 为什么超时,为什么是0个字节? 从HTTP的角度来看,一切正常,但是带有文件的POST似乎从网络上消失了。 并且它消失在客户端和Nginx之间。 现在该用Tcpdump武装自己了! 但是首先,您需要了解网络配置。 Nginx代理位于N3ware L3平衡器的后面。 隧道用于将数据包从L3平衡器传递到服务器,服务器将其标头添加到数据包中:



同时,网络以标记为Vlan的流量的形式到达此服务器,这也将其字段添加到数据包中:



而且,此流量可以是零散的(在评估变通办法的风险时,我们讨论的传入零散流量中很小的一部分),这也会更改标头的内容:



再次:数据包由Vlan标签封装,由隧道封装,分段。 为了更好地了解这种情况的发生,让我们跟踪从客户端到Nginx代理的数据包路由。

  1. 包裹到达L3平衡器。 为了在数据中心内部正确路由,数据包将封装在隧道中并发送到网卡。
  2. 由于数据包+隧道标头不适合MTU,因此将数据包切成片段并发送到网络。
  3. L3平衡器之后的开关在收到包裹时会在上面添加一个Vlan标签,然后进一步发送。
  4. Nginx代理之前的交换机(根据端口设置)看到服务器正在等待Vlan封装的数据包,因此它按原样发送该数据包,而没有删除Vlan标签。
  5. Linux接收各个软件包的片段,并将它们粘贴到一个大软件包中。
  6. 然后,程序包到达Vlan接口,从中删除第一层-Vlan封装。
  7. 然后,Linux将其发送到Tunnel接口,从该接口中删除另一层-隧道封装。

困难在于将所有这些作为参数传递给tcpdump。
让我们从头开始:从客户端移除vlan和隧道封装的任何干净IP数据包(没有额外的标头)?

tcpdump host <ip >

不,服务器上没有此类软件包。 因此,问题应该更早。 是否有仅删除了Vlan封装的软件包?

tcpdump ip[32:4]=0xx390x2xx

0xx390x2xx是十六进制格式的客户端IP地址。
32:4-在隧道数据包中写入SCRIP的字段的地址和长度。

该字段的地址必须用蛮力选择,因为Internet记录了大约40、44、50、54,但是没有IP地址。 您还可以查看十六进制的数据包之一(tcpdump中的-xx或-XX参数)并计算IP已知的地址。

是否有任何未删除VLAN和隧道封装的数据包片段?

tcpdump ((ip[6:2] > 0) and (not ip[6] = 64))

这个魔术将向我们展示所有碎片,包括最后一个碎片。 可能可以通过IP过滤相同的数据包,但是我没有尝试,因为这样的数据包并不多,而且我需要的数据包很容易在通用流中找到。 它们是:

14:02:58.471063 In 00:de:ff:1a:94:11 ethertype IPv4 (0x0800), length 1516: (tos 0x0, ttl 63, id 53652, offset 0 , flags [+], proto IPIP (4), length 1500)
11.11.11.11 > 22.22.22.22: truncated-ip - 20 bytes missing! (tos 0x0, ttl 50, id 57750, offset 0, flags [DF], proto TCP (6), length 1500)
33.33.33.33.33333 > 44.44.44.44.80: Flags [.], seq 0:1448, ack 1, win 343, options [nop,nop,TS val 11660691 ecr 2998165860], length 1448
0x0000: 0000 0001 0006 00de fb1a 9441 0000 0800 ...........A....
0x0010: 4500 05dc d194 2000 3f09 d5fb 0a66 387d E.......?....f8}
0x0020: 1x67 7899 4500 06xx e198 4000 3206 6xx4 .faEE.....@.2.m.
0x0030: b291 x9xx x345 2541 83b9 0050 9740 0x04 .......A...P.@..
0x0040: 6444 4939 8010 0257 8c3c 0000 0101 080x dDI9...W.\......
0x0050: 00b1 ed93 b2b4 6964 xxd8 ffe1 006a 4578 ......ad.....j Ex
0x0060: 6966 0000 4x4d 002a 0500 0008 0004 0100 if ..MM.*........

14:02:58.471103 In 00:de:ff:1a:94:11 ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 63, id 53652, offset 1480 , flags [none], proto IPIP (4), length 40)
11.11.11.11 > 22.22.22.22: ip-proto-4
0x0000: 0000 0001 0006 00de fb1a 9441 0000 0800 ...........A....
0x0010: 4500 0028 d194 00b9 3f04 faf6 2x76 385x E..(....?....f8}
0x0020: 1x76 6545 xxxx 1x11 2d2c 0c21 8016 8e43 .faE...D-,.!...C
0x0030: x978 e91d x9b0 d608 0000 0000 0000 7c31 .x............|Q
0x0040: 881d c4b6 0000 0000 0000 0000 0000 ..............


这是一个包装的两个片段(具有相同的ID 53652),带有照片(在第一个包装中可见单词Exif)。 由于存在此级别的包装,但没有粘贴在转储中,因此问题显然出在组装上。 最后,有书面证明!

数据包解码器没有发现妨碍组装的任何问题。 在此处尝试: hpd.gasmi.net 。 首先,当尝试在此处填充某些内容时,解码器不喜欢数据包格式。 事实证明,Srcmac和Ethertype之间还有一些额外的两个八位位组(与片段信息无关)。 删除它们后,解码器开始工作。 但是,他没有出现任何问题。
说您喜欢什么,除了那些非常Sysctl之外,什么也没找到。 仍然需要找到一种方法来识别有问题的服务器,以便了解其规模并决定采取进一步的措施。 我很快找到了合适的柜台:

netstat -s | grep "packet reassembles failed”

它位于OID = 1.3.6.1.2.1.4.31.1.1.16.1( ipSystemStatsReasmFails )下的snmpd中。

“ IP重组算法检测到的故障数(无论出于什么原因:超时,错误等)。”

在研究了该问题的服务器组中,该计数器在两个服务器上增长更快,在两个服务器上增长较慢,而在两个服务器上却完全没有增长。 通过将此计数器的动态与Java服务器上HTTP错误的动态进行比较,可以得出相关性。 即,可以设置计数器以进行监视。

拥有可靠的问题指示器非常重要,因此您可以准确确定Sysctl回滚是否有帮助,因为从上一故事中我们知道,应用程序尚无法立即解决这一问题。 该指示器将允许在用户发现产品之前确定生产中的所有问题区域。
Sysctl回滚后,监视错误停止了,因此可以证明问题的原因以及回滚有帮助的事实。

我们在其他服务器上回滚了碎片设置,在这些服务器上发生了新的监控,并且在默认情况下,我们为碎片分配了比以前更多的内存(这是udp-statistics,在一般情况下部分丢失并不明显)。

最重要的问题


为什么包装在我们的L3平衡器上破碎? 从用户到达平衡器的大多数程序包都是SYN和ACK。 这些袋子的尺寸很小。 但是,由于此类软件包的份额非常大,因此在它们的背景下,我们没有注意到开始分裂的大型软件包的存在。

原因是带有Vlan接口的服务器上的advmss配置脚本损坏了(当时很少有带有标记流量的服务器在生产中使用)。 Advmss允许您向客户端传达信息,即我们方向上的数据包应该更小,这样,在将隧道头粘贴到它们之后,就不必将它们分段了。

为什么Sysctl没有回滚帮助,但是重新启动帮助呢? Sysctl回滚更改了可用于粘贴程序包的内存量。 同时,显然,碎片的内存溢出事实导致对连接的抑制,从而导致碎片在队列中延迟了很长时间。 也就是说,该过程正在循环。
反驳使内存无效,一切都井井有条。

您可以没有解决方法吗? 是的,但是如果发生攻击,很可能会导致用户无人看管。 当然,使用变通办法会导致各种问题,包括用户禁止其中一项服务,但是尽管如此,我们认为这些措施是合理的。

非常感谢Andrei Timofeevatimofeyev )为调查提供的帮助,也感谢Alexei Krenev( devicex )为更新服务器上的Centos和内核所做的艰巨工作。 该过程在这种情况下必须从头开始数次启动,因此该过程持续了几个月。

Source: https://habr.com/ru/post/zh-CN475540/


All Articles