Пазете се от уязвимости, които водят до работа. Част 1: FragmentSmack/SegmentSmack

Пазете се от уязвимости, които водят до работа. Част 1: FragmentSmack/SegmentSmack

Здравейте всички! Казвам се Дмитрий Самсонов, работя като водещ системен администратор в Odnoklassniki. Имаме повече от 7 хиляди физически сървъра, 11 хиляди контейнера в нашия облак и 200 приложения, които в различни конфигурации образуват 700 различни клъстера. По-голямата част от сървърите работят с CentOS 7.
На 14 август 2018 г. беше публикувана информация за уязвимостта на FragmentSmack
(CVE-2018 5391-) и SegmentSmack (CVE-2018 5390-). Това са уязвимости с вектор на мрежова атака и доста висок резултат (7.5), който заплашва отказ от услуга (DoS) поради изчерпване на ресурс (CPU). Корекция на ядрото за FragmentSmack не беше предложена по това време, освен това излезе много по-късно от публикуването на информация за уязвимостта. За да се премахне SegmentSmack, беше предложено да се актуализира ядрото. Самият пакет за актуализация беше пуснат същия ден, оставаше само да го инсталирате.
Не, изобщо не сме против обновяването на ядрото! Има обаче нюанси...

Как актуализираме ядрото при производство

Като цяло, нищо сложно:

  1. Изтегляне на пакети;
  2. Инсталирайте ги на няколко сървъра (включително сървъри, хостващи нашия облак);
  3. Уверете се, че нищо не е счупено;
  4. Уверете се, че всички стандартни настройки на ядрото се прилагат без грешки;
  5. Изчакайте няколко дни;
  6. Проверете производителността на сървъра;
  7. Превключете внедряването на нови сървъри към новото ядро;
  8. Актуализирайте всички сървъри по център за данни (един център за данни наведнъж, за да минимизирате ефекта върху потребителите в случай на проблеми);
  9. Рестартирайте всички сървъри.

Повторете за всички клонове на ядрата, които имаме. В момента е:

Както може би се досещате, рестартирането на хиляди сървъри отнема най-дълго време. Тъй като не всички уязвимости са критични за всички сървъри, ние рестартираме само онези, които са директно достъпни от интернет. В облака, за да не ограничаваме гъвкавостта, ние не свързваме външно достъпни контейнери към отделни сървъри с ново ядро, а рестартираме всички хостове без изключение. За щастие процедурата там е по-опростена, отколкото при обикновените сървъри. Например, контейнерите без състояние могат просто да се преместят на друг сървър по време на рестартиране.

Въпреки това има още много работа и може да отнеме няколко седмици, а ако има проблеми с новата версия, до няколко месеца. Нападателите разбират това много добре, така че им трябва план Б.

FragmentSmack/СегментSmack. Заобиколно решение

За щастие, за някои уязвимости съществува такъв план Б и той се нарича Заобиколно решение. Най-често това е промяна в настройките на ядрото/приложението, която може да минимизира възможния ефект или напълно да елиминира използването на уязвимости.

В случай на 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 или нисък. Тестовете показват малки до значителни спадове в използването на процесора по време на атака в зависимост от хардуера, настройките и условията. Възможно е обаче да има известно въздействие върху производителността поради 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/СегментSmack. Първа кръв

Първият проблем, който срещнахме, беше, че облачните контейнери понякога прилагаха новите настройки само частично (само ipfrag_low_thresh), а понякога изобщо не ги прилагаха - просто се сриваха в началото. Не беше възможно стабилно възпроизвеждане на проблема (всички настройки бяха приложени ръчно без никакви затруднения). Разбирането защо контейнерът се срива в началото също не е толкова лесно: не бяха открити грешки. Едно нещо беше сигурно: връщането на настройките решава проблема със сривовете на контейнерите.

Защо не е достатъчно да приложите Sysctl на хоста? Контейнерът живее в собствено специално мрежово пространство от имена, така че поне част от мрежовите параметри Sysctl в контейнера може да се различава от хоста.

Как точно се прилагат настройките на Sysctl в контейнера? Тъй като нашите контейнери са непривилегировани, няма да можете да промените настройките на Sysctl, като влезете в самия контейнер - просто нямате достатъчно права. За да изпълнява контейнери, нашият облак по това време използва Docker (сега Подман). Параметрите на новия контейнер бяха предадени на 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 се опитва да бъде зададен на 256K, когато ipfrag_low_thresh все още е 3M, тоест горната граница е по-ниска от долната граница, което е довело до грешката.

По това време вече използвахме собствен механизъм за преконфигуриране на контейнера след стартиране (замразяване на контейнера след групов фризер и изпълнение на команди в пространството от имена на контейнера чрез ip netns), а също така добавихме запис на Sysctl параметри към тази част. Проблемът беше решен.

FragmentSmack/СегментSmack. Първа кръв 2

Преди да имаме време да разберем използването на Workaround в облака, започнаха да пристигат първите редки оплаквания от потребители. По това време бяха изминали няколко седмици от началото на използването на Workraround на първите сървъри. Първоначалното разследване показа, че жалби са получени срещу отделни услуги, а не срещу всички сървъри на тези услуги. Проблемът отново стана изключително несигурен.

Първо, ние, разбира се, се опитахме да върнем настройките на Sysctl, но това нямаше ефект. Различни манипулации със сървъра и настройките на приложението също не помогнаха. Рестартирането помогна. Рестартирането на Linux е толкова неестествено, колкото беше нормално за Windows в старите дни. Това обаче помогна и ние го определихме като „бъг на ядрото“ при прилагането на новите настройки в Sysctl. Колко несериозно беше...

Три седмици по-късно проблемът се повтори. Конфигурацията на тези сървъри беше доста проста: Nginx в прокси/балансиращ режим. Няма много трафик. Нова уводна бележка: броят на 504 грешки на клиенти се увеличава всеки ден (Gateway Timeout). Графиката показва броя на 504 грешки на ден за тази услуга:

Пазете се от уязвимости, които водят до работа. Част 1: FragmentSmack/SegmentSmack

Всички грешки са за един и същ бекенд - за този, който е в облака. Графиката на потреблението на памет за фрагменти от пакети в този бекенд изглеждаше така:

Пазете се от уязвимости, които водят до работа. Част 1: FragmentSmack/SegmentSmack

Това е едно от най-очевидните проявления на проблема в графиките на операционната система. В облака, точно по същото време, беше коригиран друг мрежов проблем с настройките за QoS (Контрол на трафика). На графиката на потреблението на памет за фрагменти от пакети изглеждаше точно същото:

Пазете се от уязвимости, които водят до работа. Част 1: FragmentSmack/SegmentSmack

Предположението беше просто: ако изглеждат еднакви на графиките, тогава имат същата причина. Освен това всякакви проблеми с този тип памет са изключително редки.

Същността на коригирания проблем беше, че използвахме fq планировчика на пакети с настройки по подразбиране в 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 flows_plimit“ са пакетите, отпаднали поради превишаване на лимита на опашката на една връзка, а „dropped 464545“ е сборът от всички отхвърлени пакети на този планировчик. След увеличаване на дължината на опашката до 1 и рестартиране на контейнерите, проблемът спря да се появява. Можете да седнете и да изпиете смути.

FragmentSmack/СегментSmack. Последна кръв

Първо, няколко месеца след обявяването на уязвимостите в ядрото, най-накрая се появи корекция за FragmentSmack (нека ви напомня, че заедно със съобщението през август беше пусната корекция само за SegmentSmack), което ни даде шанс да изоставим Workaround, което ни създаде доста неприятности. През това време вече успяхме да прехвърлим някои от сървърите на новото ядро ​​и сега трябваше да започнем отначало. Защо актуализирахме ядрото, без да изчакаме корекцията на FragmentSmack? Факт е, че процесът на защита срещу тези уязвимости съвпадна (и се обедини) с процеса на актуализиране на самата CentOS (което отнема дори повече време, отколкото актуализирането само на ядрото). Освен това SegmentSmack е по-опасна уязвимост и веднага се появи корекция за нея, така че все пак имаше смисъл. Не можахме обаче просто да актуализираме ядрото на CentOS, тъй като уязвимостта на FragmentSmack, която се появи по време на CentOS 7.5, беше коригирана само във версия 7.6, така че трябваше да спрем актуализацията до 7.5 и да започнем всичко отначало с актуализацията до 7.6. И това също се случва.

Второ, редки оплаквания от потребители за проблеми се върнаха при нас. Вече знаем със сигурност, че всички те са свързани с качване на файлове от клиенти на някои от нашите сървъри. Освен това много малък брой качвания от общата маса преминаха през тези сървъри.

Както си спомняме от историята по-горе, връщането на 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 балансьора NFware. Тунелирането се използва за доставяне на пакети от 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 го изпраща към интерфейса на тунела, където друг слой се премахва от него - тунелно капсулиране.

Трудността е да се предаде всичко това като параметри на tcpdump.
Да започнем от края: има ли чисти (без излишни хедъри) IP пакети от клиенти, с премахнато vlan и тунелно капсулиране?

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

Не, нямаше такива пакети на сървъра. Така че проблемът трябва да е там по-рано. Има ли пакети с премахната само Vlan капсулация?

tcpdump ip[32:4]=0xx390x2xx

0xx390x2xx е IP адресът на клиента в шестнадесетичен формат.
32:4 — адрес и дължина на полето, в което е записан SCR IP в тунелния пакет.

Адресът на полето трябваше да бъде избран чрез груба сила, тъй като в интернет пишат за 40, 44, 50, 54, но там нямаше IP адрес. Можете също така да погледнете един от пакетите в шестнадесетичен (параметъра -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).

„Броят на грешките, открити от алгоритъма за повторно сглобяване на IP (по каквато и да е причина: изтекло време, грешки и т.н.).“

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

Наличието на надежден индикатор за проблеми е много важно, за да можете точно да определите дали връщането назад на Sysctl помага, тъй като от предишната история знаем, че това не може да бъде разбрано веднага от приложението. Този индикатор ще ни позволи да идентифицираме всички проблемни области в производството, преди потребителите да го открият.
След връщане на Sysctl, грешките при наблюдение спряха, като по този начин причината за проблемите беше доказана, както и фактът, че връщането помага.

Върнахме обратно настройките за фрагментация на други сървъри, където влезе в действие нов мониторинг и някъде разпределихме дори повече памет за фрагменти, отколкото беше по-рано по подразбиране (това беше UDP статистика, чиято частична загуба не беше забележима на общия фон) .

Най-важните въпроси

Защо пакетите са фрагментирани на нашия L3 балансьор? Повечето от пакетите, които пристигат от потребителите до балансьорите, са SYN и ACK. Размерите на тези опаковки са малки. Но тъй като делът на такива пакети е много голям, на техния фон не забелязахме наличието на големи пакети, които започнаха да се фрагментират.

Причината беше повреден конфигурационен скрипт advmss на сървъри с Vlan интерфейси (по това време имаше много малко сървъри с маркиран трафик в производство). Advmss ни позволява да предадем на клиента информацията, че пакетите в нашата посока трябва да са с по-малък размер, така че след прикачване на тунелни хедъри към тях да не се налага да бъдат фрагментирани.

Защо Sysctl rollback не помогна, но рестартирането помогна? Връщането назад на Sysctl промени количеството налична памет за сливане на пакети. В същото време очевидно самият факт на препълване на паметта за фрагменти доведе до забавяне на връзките, което доведе до забавяне на фрагментите за дълго време в опашката. Тоест процесът вървеше на цикли.
Рестартирането нулира паметта и всичко се върна в ред.

Беше ли възможно да се направи без заобиколно решение? Да, но има голям риск потребителите да останат без услуга в случай на атака. Разбира се, използването на Workaround доведе до различни проблеми, включително забавяне на една от услугите за потребителите, но въпреки това смятаме, че действията са оправдани.

Много благодаря на Андрей Тимофеев (атимофеев) за съдействие при провеждане на разследването, както и Алексей Кренев (устройствоx) - за титаничната работа по актуализиране на Centos и ядра на сървъри. Процес, който в случая трябваше да започне отначало няколко пъти, поради което се проточи дълги месеци.

Източник: www.habr.com

Добавяне на нов коментар