Бійтеся вразливостей, що приносять воркараунди. Частина 1: FragmentSmack/SegmentSmack

Бійтеся вразливостей, що приносять воркараунди. Частина 1: FragmentSmack/SegmentSmack

Всім привіт! Мене звуть Дмитро Самсонов, я працюю провідним системним адміністратором в «Однокласниках». У нас понад 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. Перезавантажити всі сервери.

Повторити всім гілок наявних в нас ядер. На даний момент це:

  • CentOS 7 3.10 - для більшості звичайних серверів;
  • Ванільне 4.19 - для нашого хмари one-cloudтому що нам потрібен BFQ, BBR і т.д.;
  • Elrepo kernel-ml 5.2 - для високонавантажених роздавачів, тому що 4.19 раніше поводився нестабільно, а фічі потрібні ті ж.

Як ви могли здогадатися, найбільше часу займає перезавантаження тисяч серверів. Оскільки не всі вразливості критичні для всіх серверів, ми перезавантажуємо тільки ті, які безпосередньо доступні з інтернету. У хмарі, щоб не обмежувати гнучкість, ми не прив'язуємо доступні ззовні контейнери до окремих серверів із новим ядром, а перезавантажуємо всі хости без винятку. На щастя, там процедура простіша, ніж зі звичайними серверами. Наприклад, stateless-контейнери можуть просто переїхати на інший сервер під час ребутування.

Тим не менш, роботи все одно багато, і вона може займати кілька тижнів, а при виникненні будь-яких проблем з новою версією — до декількох місяців. Зловмисники це чудово розуміють, тому потрібний план «Б».

FragmentSmack/SegmentSmack. Workaround

На щастя, для деяких уразливостей такий план «Б» існує, і він називається Workaround. Найчастіше це зміна налаштувань ядра/додатків, які дозволяють мінімізувати можливий ефект або повністю виключити експлуатацію вразливостей.

У випадку з FragmentSmack/SegmentSmack пропонувався такий Workaround:

«Можна змінити дефолтні значення 4MB і 3MB в net.ipv4.ipfrag_high_thresh і net.ipv4.ipfrag_low_thresh (та їх аналоги для ipv6 net.ipv6.ipfrag_high_thresh і net.ipv6.ipfrag_low_thresh) і на kb/b. Тести показують від невеликого до значного падіння використання CPU під час атаки в залежності від обладнання, налаштувань та умов. Однак може бути деякий вплив на продуктивність через ipfrag_high_thresh=256 bytes, тому що тільки два 192K-фрагменти можуть одночасно поміститися в черзі на перескладання. Наприклад, є ризик, що програми, що працюють з великими 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 є, але не значний. Ніщо не віщує - можна накочувати Workaround!

FragmentSmack/SegmentSmack. Перша кров

Перша проблема, з якою ми зіткнулися, полягала в тому, що хмарні контейнери часом застосовували нові налаштування лише частково (тільки ipfrag_low_thresh), а іноді не застосовували взагалі просто падали на старті. Стабільно відтворити проблему не вдавалося (вручну всі налаштування застосовувалися без будь-яких складнощів). Зрозуміти, чому падає контейнер на старті, теж не так просто: жодних помилок не виявлено. Одне відомо точно: відкат налаштувань вирішує проблему з падінням контейнерів.

Чому недостатньо застосувати Sysctl на хості? Контейнер живе у своєму виділеному мережному Namespace, тому принаймні частина мережевих Sysctl-параметрів у контейнері може відрізнятись від хоста.

Як саме застосовуються налаштування Sysctl у контейнері? Так як контейнери у нас непривілейовані, змінити будь-яке налаштування Sysctl, зайшовши в сам контейнер, не вийде - просто не вистачить прав. Для запуску контейнерів наша хмара на той момент використовувала Docker (зараз вже Подман). Докер через API передавали параметри нового контейнера, у тому числі потрібні налаштування Sysctl.
У ході перебору версій з'ясувалося, що API Docker не віддавало помилок (принаймні, у версії 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 і виконання команд в namespace контейнера через ip netns), і ми додали в цю частину також прописування Sysctl-параметрів. Проблема була вирішена.

FragmentSmack/SegmentSmack. Перша кров 2

Не встигли ми розібратися із застосуванням Workaround у хмарі, як почали надходити перші рідкісні скарги від користувачів. На той момент пройшло кілька тижнів від початку застосування Workaround на перших серверах. Первинне розслідування показало, що скарги надходили окремі послуги, і не всі сервери даних сервісів. Проблема знову набула вкрай невизначеного характеру.

Насамперед ми, звичайно, спробували відкотити налаштування Sysctl, але це не дало жодного ефекту. Різні маніпуляції з налаштуваннями сервера та програми теж не допомогли. Допоміг reboot. Reboot для 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 (нагадаю, що разом з анонсом у серпні вийшов фікс лише для SegmentSmack), що дало шанс відмовитися від Workaround, який завдав нам досить багато неприємностей. Частину серверів за цей час ми вже встигли перевести на нове ядро, і тепер треба було розпочинати з початку. Навіщо ми оновлювали ядро, не чекаючи фіксації FragmentSmack? Справа в тому, що процес захисту від цих уразливостей збігся (і злився) з процесом оновлення самого CentOS (що займає ще більше часу, ніж оновлення тільки ядра). До того ж SegmentSmack — небезпечніша вразливість, а фікс для нього з'явився одразу, тому сенс був у будь-якому випадку. Однак, просто оновити ядро ​​на CentOS ми не могли, тому що вразливість FragmentSmack, яка з'явилася за часів CentOS 7.5, була зафіксована лише у версії 7.6, тому нам довелося зупиняти оновлення до 7.5 і починати знову з оновленням до 7.6. І так також буває.

По-друге, до нас повернулися поодинокі скарги користувачів на проблеми. Зараз ми вже точно знаємо, що вони пов'язані з аплоадом файлів від клієнтів деякі наші сервери. Причому через ці сервери йшло дуже мало аплоадів від загальної маси.

Як ми пам'ятаємо з оповідання, відкат Sysctl не допомагав. Допомагав Reboot, але тимчасово.
Підозри з 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 відправляє його на Tunnel-інтерфейс, де з нього знімається ще один шар - Tunnel-інкапсулювання.

Складність у тому, щоб передати це все як параметрів в tcpdump.
Почнемо з кінця: чи є чисті (без зайвих заголовків) IP-пакети від клієнтів, зі знятим vlan-і tunnel-інкапсулюванням?

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

Ні, таких пакетів на сервері не було. Таким чином, проблема має бути раніше. Чи є пакети зі знятим лише Vlan-інкапсулюванням?

tcpdump ip[32:4]=0xx390x2xx

0xx390x2xx – це IP-адреса клієнта в hex-форматі.
32:4 – адреса та довжина поля, в якому записано SCR IP у Tunnel-пакеті.

Адресу поля довелося підбирати перебором, тому що в інтернеті пишуть про 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).

«Через число помилок, визначених IP-адресою algoritm (для будь-якого значення: timed out, errors, etc.)».

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

Наявність надійного індикатора проблем дуже важлива, щоб можна було точно визначити, чи допомагає відкат Sysctl, оскільки з попередньої розповіді ми знаємо, що за додатком це відразу зрозуміти не можна. Цей індикатор дозволив би виявити всі проблемні місця в продакшені, перш ніж це виявлять користувачі.
Після відкату Sysctl помилки з моніторингу припинилися, таким чином причина проблем була доведена, як і те, що допомагає відкат.

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

Найголовніші питання

Чому у нас на L3-балансувальника фрагментуються пакети? Більшість пакетів, які прилітають від користувачів на балансувальники, – це SYN та ACK. Розміри цих пакетів невеликі. Але так як частка таких пакетів дуже велика, то на їх тлі ми не помітили наявність великих пакетів, які почали фрагментуватись.

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

Чому відкат Sysctl не допомагав, а ребут допомагав? Відкат Sysctl змінював обсяг пам'яті, доступної для склеювання пакетів. При цьому, зважаючи на все, сам факт переповнення пам'яті під фрагменти приводив до гальмування з'єднань, що призводило до того, що фрагменти надовго затримувалися в черзі. Тобто процес зациклювався.
Ребут обнуляв пам'ять і все приходило до ладу.

Чи можна обійтися без Workaround? Так, але великий ризик залишити користувачів без обслуговування у разі атаки. Звичайно, застосування Workaround у результаті призвело до виникнення різних проблем, включаючи гальмування одного з сервісів у користувачів, проте ми вважаємо, що дії були виправдані.

Велике спасибі Андрію Тимофєєву (atimofeyev) за допомогу у проведенні розслідування, а також Олексію Креньову (devicex) - за титанічний працю з оновлення Centos та ядер на серверах. Процес, який у цьому випадку кілька разів довелося розпочинати з початку, через що він затягнувся на багато місяців.

Джерело: habr.com

Додати коментар або відгук