谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

大家好! 我叫 Dmitry Samsonov,是 Odnoklassniki 的首席系统管理员。 我们拥有 7 多台物理服务器、云中 11 个容器和 200 个应用程序,这些应用程序以不同的配置形成 700 个不同的集群。 绝大多数服务器运行 CentOS 7。
14 августа 2018 г. была опубликована информация об уязвимости FragmentSmack
(CVE-2018-5391) 和 SegmentSmack (CVE-2018-5390)。 这些漏洞具有网络攻击向量且得分相当高 (7.5),可能会因资源耗尽 (CPU) 而导致拒绝服务 (DoS)。 当时还没有提出针对 FragmentSmack 的内核修复方案;而且,它的发布时间比有关漏洞的信息发布要晚得多。 为了消除SegmentSmack,建议更新内核。 更新包本身已在同一天发布,剩下的就是安装它。
Нет, мы совсем не против обновления ядра! Однако есть нюансы…

Как мы обновляем ядро на проде

一般来说,没什么复杂的:

  1. Cкачать пакеты;
  2. 将它们安装在许多服务器上(包括托管我们云的服务器);
  3. 确保没有任何损坏;
  4. 确保应用所有标准内核设置且没有错误;
  5. 等几天;
  6. 检查服务器性能;
  7. Переключить деплой новых серверов на новое ядро;
  8. Обновить все серверы по дата-центрам (один дата-центр за раз, чтобы минимизировать эффект для пользователей в случае проблем);
  9. Перезагрузить все серверы.

对我们拥有的内核的所有分支重复此操作。 目前是:

  • 库存 CentOS 7 3.10 - 适用于大多数常规服务器;
  • Ванильное 4.19 — для нашего 一云云,因为我们需要BFQ、BBR等;
  • Elrepo kernel-ml 5.2 - 适用于 高负荷分销商,因为 4.19 过去表现不稳定,但需要相同的功能。

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

不过,工作量仍然很大,可能需要几周的时间,如果新版本出现任何问题,最多可能需要几个月的时间。 攻击者非常了解这一点,因此他们需要 B 计划。

FragmentSmack/SegmentSmack。 解决方法

К счастью, для некоторых уязвимостей такой план «Б» существует, и называется он Workaround. Чаще всего это изменение настроек ядра/приложений, которые позволяют минимизировать возможный эффект или полностью исключить эксплуатацию уязвимостей.

以 FragmentSmack/SegmentSmack 为例 被提议 这个解决方法:

«Можно изменить дефолтные значения 4MB и 3MB в net.ipv4.ipfrag_high_thresh и net.ipv4.ipfrag_low_thresh (и их аналоги для ipv6 net.ipv6.ipfrag_high_thresh и net.ipv6.ipfrag_low_thresh) на 256 kB и 192 kB соответственно или ниже. Тесты показывают от небольшого до значительного падения использования CPU во время атаки в зависимости от оборудования, настроек и условий. Однако может быть некоторое влияние на производительность из-за ipfrag_high_thresh=262144 bytes, так как только два 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 на хосте? Контейнер живёт в своём выделенном сетевом Namespace, поэтому по крайней мере 部分网络Sysctl参数 в контейнере может отличаться от хоста.

Как именно применяются настройки Sysctl в контейнере? Так как контейнеры у нас непривилегированные, изменить любую настройку Sysctl, зайдя в сам контейнер, не получится — просто не хватит прав. Для запуска контейнеров наше облако на тот момент использовало Docker (сейчас уже 波德曼). Докеру через 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尝试在ipfrag_low_thresh还是256M的时候设置为3K,即上限更低低于下限,从而导致错误。

当时,我们已经使用了自己的机制来在启动后重新配置容器(在启动后冻结容器) cgroup freezer и выполнение команд в namespace контейнера через ip网络),这部分我们还添加了写入Sysctl参数。 问题解决了。

FragmentSmack/SegmentSmack。 第一滴血2

还没来得及了解 Workaround 在云中的使用,第一批罕见的用户抱怨就开始到来。 当时,距离在第一批服务器上开始使用解决方案已经过去了几个星期。 初步调查显示,收到的投诉针对的是个别服务,而不是这些服务的所有服务器。 问题再次变得极其不确定。

首先,我们当然尝试回滚Sysctl设置,但这没有任何效果。 对服务器和应用程序设置的各种操作也没有帮助。 重新启动有帮助。 重新启动 Linux 与过去 Windows 中的正常现象一样不自然。 不过,它确实有帮助,我们将其归因于在 Sysctl 中应用新设置时出现的“内核故障”。 本来是多么的轻率啊……

三周后,问题再次出现。 这些服务器的配置非常简单:Nginx 处于代理/平衡器模式。 流量不大。 新介绍性说明:客户端 504 错误的数量每天都在增加(网关超时). На графике показано число 504-х ошибок в день по этому сервису:

谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

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

谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

Это одно из самых ярких проявлений проблемы на графиках операционной системы. В облаке как раз в это же время была пофикшена другая сетевая проблема с настройками QoS (Traffic Control). На графике потребления памяти под фрагменты пакетов она выглядела точно так же:

谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

假设很简单:如果它们在图表上看起来相同,那么它们就有相同的原因。 此外,这种类型的内存出现任何问题的可能性都非常小。

解决问题的本质是我们使用了 QoS 中默认设置的 fq 数据包调度程序。 默认情况下,对于一个连接,它允许您将 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 Flows_plimit”是由于超过一个连接的队列限制而丢弃的数据包,“dropped 464545”是该调度器所有丢弃的数据包的总和。 将队列长度增加到 1 并重新启动容器后,问题不再发生。 您可以坐下来喝一杯冰沙。

FragmentSmack/SegmentSmack. Последняя кровь

首先,在内核漏洞公布几个月后,针对 FragmentSmack 的修复终于出现了(提醒大家,随着 7.5 月份的公告,还发布了仅针对 SegmentSmack 的修复),这给了我们放弃 Workaround 的机会,这给我们带来了很多麻烦。 在此期间,我们已经成功地将一些服务器转移到新内核,现在我们必须从头开始。 为什么我们不等待 FragmentSmack 修复就更新内核? 事实上,防范这些漏洞的过程与更新 CentOS 本身的过程同时发生(并合并)(这比仅更新内核需要更多的时间)。 此外,SegmentSmack是一个更危险的漏洞,并且立即出现了针对它的修复程序,因此无论如何它都是有意义的。 然而,我们不能简单地更新CentOS上的内核,因为CentOS 7.6期间出现的FragmentSmack漏洞仅在7.5版本中得到修复,因此我们不得不停止7.6的更新,并从头开始更新XNUMX。 这也会发生。

其次,罕见的用户投诉问题又回到了我们这里。 现在我们已经确定它们都与从客户端到我们的某些服务器的文件上传有关。 此外,总质量中的极少数上传是通过这些服务器进行的。

正如我们从上面的故事中记得的那样,回滚 Sysctl 并没有帮助。 重新启动有帮助,但只是暂时的。
对Sysctl的怀疑并未消除,但这一次有必要收集尽可能多的信息。 还严重缺乏在客户端重现上传问题的能力,以便更准确地研究发生的情况。

Анализ всей доступной статистики и логов не приблизил нас к пониманию происходящего. Остро не хватало возможности воспроизвести проблему, чтобы «пощупать» конкретное соединение. Наконец, разработчикам на спецверсии приложения удалось добиться стабильного воспроизведения проблем на тестовом устройстве при подключении через Wi-Fi. Это стало прорывом в расследовании. Клиент подключался к Nginx, тот проксировал на бекенд, которым являлось наше приложение на Java.

谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

问题的对话是这样的(在Nginx代理端修复):

  1. 客户端:请求接收有关下载文件的信息。
  2. Java 服务器:响应。
  3. 客户端:使用文件进行 POST。
  4. Java 服务器:错误。

Java-сервер при этом пишет в лог, что от клиента получено 0 байт данных, а Nginx-прокси — что запрос занял больше 30 секунд (30 секунд — это время таймаута у клиентского приложения). Почему же таймаут и почему 0 байт? С точки зрения HTTP всё работает так, как должно работать, но POST с файлом как будто пропадает из сети. Причём пропадает между клиентом и Nginx. Пришло время вооружиться Tcpdump! Но для начала надо понять конфигурацию сети. Nginx-прокси стоит за L3-балансировщиком 网络软件。 隧道用于将数据包从 L3 平衡器传送到服务器,服务器将其标头添加到数据包中:

谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

При этом сеть на этот сервер приходит в виде Vlan-теггированного трафика, которое тоже добавляет свои поля в пакеты:

谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

А ещё этот трафик может фрагментироваться (тот самый небольшой процент входящего фрагментированного трафика, о котором мы говорили при оценке рисков от Workaround), что тоже меняет содержание хидеров:

谨防带来工作轮次的漏洞。 第 1 部分:FragmentSmack/SegmentSmack

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

  1. Пакет попадает на L3-балансировщик. Для корректной маршрутизации внутри дата-центра пакет инкапсулируется в туннель и отправляется на сетевую карту.
  2. 由于数据包 + 隧道标头不适合 MTU,因此数据包被切割成片段并发送到网络。
  3. L3 平衡器之后的交换机在收到数据包时,会为其添加 Vlan 标记并继续发送。
  4. Свитч перед Nginx-прокси видит (по настройкам порта), что сервер ожидает Vlan-инкапсулированный пакет, поэтому отправляет его, как есть, не убирая Vlan-тег.
  5. Linux получает фрагменты отдельных пакетов и склеивает их в один большой пакет.
  6. Далее пакет попадает на Vlan-интерфейс, где с него снимается первый слой — Vlan-инкапсулирование.
  7. Linux 然后将其发送到 Tunnel 接口,并从中删除另一层 - 隧道封装。

困难在于将所有这些作为参数传递给 tcpdump。
让我们从头开始:是否有来自客户端的干净(没有不必要的标头)IP 数据包,并且删除了 VLAN 和隧道封装?

tcpdump host <ip клиента>

不,服务器上没有这样的包。 所以问题一定是更早出现的。 是否有只去掉Vlan封装的报文?

tcpdump ip[32:4]=0xx390x2xx

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

Адрес поля пришлось подбирать перебором, так как в интернете пишут про 40, 44, 50, 54, но там IP-адреса не было. Также можно посмотреть один из пакетов в hex (параметр -xx или -XX в tcpdump) и посчитать, по какому адресу известный вам IP.

是否存在未去除Vlan和Tunnel封装的报文分片?

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 [email protected].
        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.....jEx
        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”

它也在 snmpd 下的 OID=1.3.6.1.2.1.4.31.1.1.16.1 下(ipSystemStatsReasmFails).

«The number of failures detected by the IP re-assembly algorithm (for whatever reason: timed out, errors, etc.)».

Среди группы серверов, на которых изучалась проблема, на двух этот счётчик увеличивался быстрее, на двух — медленнее, а ещё на двух вообще не увеличивался. Сравнение динамики этого счётчика с динамикой HTTP-ошибок на Java-сервере выявило корреляцию. То есть счётчик можно было ставить на мониторинг.

Наличие надёжного индикатора проблем очень важно, чтобы можно было точно определить, помогает ли откат Sysctl, так как из предыдущего рассказа мы знаем, что по приложению это сразу понять нельзя. Данный индикатор позволил бы выявить все проблемные места в продакшене до того, как это обнаружат пользователи.
После отката Sysctl ошибки по мониторингу прекратились, таким образом причина проблем была доказана, как и то, что откат помогает.

Мы откатили настройки фрагментации на других серверах, где загорелся новый мониторинг, а где-то под фрагменты выделили даже больше памяти, чем было до этого по умолчанию (это была udp-статистика, частичная потеря которой не была заметна на общем фоне).

最重要的问题

为什么数据包在我们的 L3 平衡器上会出现碎片? 从用户到达平衡器的大多数数据包都是 SYN 和 ACK。 这些封装的尺寸很小。 但由于此类数据包的份额非常大,因此在其背景下,我们没有注意到开始分段的大数据包的存在。

Причиной стал поломавшийся скрипт конфигурации advmss 在具有 Vlan 接口的服务器上(当时生产中很少有带有标记流量的服务器)。 Advmss 允许我们向客户端传达这样的信息:我们方向的数据包大小应该更小,以便在将隧道标头附加到它们之后,它们不必被分段。

Почему откат Sysctl не помогал, а ребут помогал? Откат Sysctl менял объём памяти, доступной для склеивания пакетов. При этом, судя по всему сам факт переполнения памяти под фрагменты приводил к торможению соединений, что приводило к тому, что фрагменты надолго задерживались в очереди. То есть процесс зацикливался.
重新启动会清除内存,一切恢复正常。

没有解决方法是否可以做到? 是的,但一旦发生攻击,用户无法获得服务的风险很高。 当然,使用解决方法会导致各种问题,包括用户的一项服务速度变慢,但我们相信这些行动是合理的。

非常感谢安德烈·季莫费耶夫(阿蒂莫费耶夫) за помощь в проведении расследования, а также Алексею Кренёву (设备x) — за титанический труд по обновлению Centos и ядер на серверах. Процесс, который в данном случае несколько раз пришлось начинать с начала, из-за чего он затянулся на много месяцев.

来源: habr.com

添加评论