Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

Ola a todos! Chámome Dmitry Samsonov, traballo como administrador de sistemas líder en Odnoklassniki. Contamos con máis de 7 mil servidores físicos, 11 mil contedores na nosa nube e 200 aplicacións, que en varias configuracións forman 700 clústeres diferentes. A gran maioría dos servidores executan CentOS 7.
O 14 de agosto de 2018 publicouse información sobre a vulnerabilidade FragmentSmack
(CVE-2018-5391) e SegmentSmack (CVE-2018-5390). Trátase de vulnerabilidades cun vector de ataque á rede e unha puntuación bastante alta (7.5), que ameaza con denegación de servizo (DoS) debido ao esgotamento dos recursos (CPU). Nese momento non se propuxo unha corrección do núcleo para FragmentSmack; ademais, saíu moito máis tarde que a publicación da información sobre a vulnerabilidade. Para eliminar SegmentSmack, suxeriuse actualizar o núcleo. O paquete de actualización en si foi lanzado o mesmo día, só quedaba instalalo.
Non, non estamos en contra de actualizar o núcleo. Non obstante, hai matices...

Como actualizamos o núcleo en produción

En xeral, nada complicado:

  1. Descargar paquetes;
  2. Instálaos nun número de servidores (incluídos os que aloxan a nosa nube);
  3. Asegúrese de que non se rompe nada;
  4. Asegúrese de que todas as opcións estándar do núcleo se apliquen sen erros;
  5. Agarda uns días;
  6. Comproba o rendemento do servidor;
  7. Cambiar o despregamento de novos servidores ao novo núcleo;
  8. Actualiza todos os servidores por centro de datos (un centro de datos á vez para minimizar o efecto sobre os usuarios en caso de problemas);
  9. Reinicie todos os servidores.

Repita para todas as ramas dos núcleos que temos. De momento é:

  • Stock CentOS 7 3.10 - para a maioría dos servidores normais;
  • Vainilla 4.19 - para os nosos nubes dunha soa nube, porque necesitamos BFQ, BBR, etc.;
  • Elrepo kernel-ml 5.2 - para distribuidores altamente cargados, porque 4.19 adoitaba comportarse inestable, pero necesítanse as mesmas características.

Como podes adiviñar, reiniciar miles de servidores leva o tempo máis longo. Dado que non todas as vulnerabilidades son críticas para todos os servidores, só reiniciamos aqueles aos que se pode acceder directamente desde Internet. Na nube, para non limitar a flexibilidade, non vinculamos contedores accesibles externamente a servidores individuais cun novo núcleo, senón que reiniciamos todos os hosts sen excepción. Afortunadamente, o procedemento alí é máis sinxelo que cos servidores normais. Por exemplo, os contedores sen estado poden simplemente moverse a outro servidor durante un reinicio.

Non obstante, aínda queda moito traballo, e pode levar varias semanas, e se hai algún problema coa nova versión, ata varios meses. Os atacantes entenden isto moi ben, polo que necesitan un plan B.

FragmentSmack/SegmentSmack. Solución alternativa

Afortunadamente, para algunhas vulnerabilidades existe un plan B deste tipo e chámase solución alternativa. Na maioría das veces, trátase dun cambio na configuración do núcleo/aplicación que pode minimizar o posible efecto ou eliminar completamente a explotación de vulnerabilidades.

No caso de FragmentSmack/SegmentSmack foi proposto esta solución:

«Podes cambiar os valores predeterminados de 4MB e 3MB en net.ipv4.ipfrag_high_thresh e net.ipv4.ipfrag_low_thresh (e os seus homólogos para ipv6 net.ipv6.ipfrag_high_thresh e net.ipv6.ipfrag_low_thresh) a 256 kB e respectivamente 192 kB inferior. As probas mostran caídas pequenas ou significativas no uso da CPU durante un ataque dependendo do hardware, a configuración e as condicións. Non obstante, pode haber algún impacto no rendemento debido a ipfrag_high_thresh=262144 bytes, xa que só poden caber dous fragmentos de 64K á vez na cola de montaxe. Por exemplo, existe o risco de que se rompan as aplicacións que funcionan con paquetes UDP grandes».

Os propios parámetros na documentación do núcleo descrito do seguinte xeito:

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.

Non temos grandes UDP nos servizos de produción. Non hai tráfico fragmentado na LAN; hai tráfico fragmentado na WAN, pero non significativo. Non hai sinais: podes lanzar a solución alternativa!

FragmentSmack/SegmentSmack. Primeira sangue

O primeiro problema que atopamos foi que os contedores na nube ás veces aplicaban a nova configuración só parcialmente (só ipfrag_low_thresh) e ás veces non as aplicaban en absoluto: simplemente fallaban ao comezo. Non foi posible reproducir o problema de forma estable (todos os axustes aplicáronse manualmente sen ningunha dificultade). Entender por que o contedor falla ao comezo tampouco é tan sinxelo: non se atoparon erros. Unha cousa era certa: revertir a configuración resolve o problema dos fallos de contedores.

Por que non é suficiente con aplicar Sysctl no host? O contedor vive no seu propio espazo de nomes de rede dedicado, polo menos parte dos parámetros Sysctl da rede no recipiente pode diferir do anfitrión.

Como se aplica exactamente a configuración de Sysctl no contedor? Dado que os nosos contedores non teñen privilexios, non poderás cambiar ningunha configuración de Sysctl accedendo ao propio contenedor; simplemente non tes suficientes dereitos. Para executar contedores, a nosa nube daquela usaba Docker (agora podman). Os parámetros do novo contedor pasáronse a Docker a través da API, incluíndo a configuración de Sysctl necesaria.
Ao buscar entre as versións, resultou que a API de Docker non devolveu todos os erros (polo menos na versión 1.10). Cando tentamos iniciar o contedor mediante "docker run", finalmente vimos polo menos algo:

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.

O valor do parámetro non é válido. Pero por qué? E por que non é válido só ás veces? Resultou que Docker non garante a orde na que se aplican os parámetros Sysctl (a última versión probada é a 1.13.1), polo que ás veces ipfrag_high_thresh intentou establecerse en 256K cando ipfrag_low_thresh aínda era 3M, é dicir, o límite superior era inferior que o límite inferior, o que provocou o erro.

Nese momento, xa utilizabamos o noso propio mecanismo para reconfigurar o recipiente despois do inicio (conxelar o recipiente despois conxelador de grupo e executando comandos no espazo de nomes do contedor vía ip netns), e tamén engadimos a escritura de parámetros Sysctl a esta parte. O problema foi resolto.

FragmentSmack/SegmentSmack. Primeiro sangue 2

Antes de ter tempo para entender o uso de Workaround na nube, comezaron a chegar as primeiras queixas raras dos usuarios. Nese momento, pasaran varias semanas desde o inicio do uso de Workaround nos primeiros servidores. A investigación inicial mostrou que se recibiron denuncias contra servizos individuais, e non todos os servidores destes servizos. O problema volveuse a ser extremadamente incerto.

Primeiro de todo, por suposto, tentamos revertir a configuración de Sysctl, pero isto non tivo ningún efecto. Tampouco axudaron varias manipulacións coa configuración do servidor e da aplicación. Reiniciar axudou. Reiniciar Linux é tan antinatural como era normal para Windows nos vellos tempos. Non obstante, axudou, e considerámolo como un "fallo do núcleo" ao aplicar a nova configuración en Sysctl. Que frívolo foi...

Tres semanas despois o problema reapareceu. A configuración destes servidores foi bastante sinxela: Nginx en modo proxy/equilibrador. Non hai moito tráfico. Nova nota introductoria: o número de erros 504 nos clientes aumenta cada día (Tempo de espera da pasarela). O gráfico mostra o número de 504 erros ao día para este servizo:

Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

Todos os erros son máis ou menos o mesmo backend, sobre o que está na nube. O gráfico de consumo de memoria para fragmentos de paquetes neste backend tiña o seguinte aspecto:

Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

Esta é unha das manifestacións máis obvias do problema nos gráficos do sistema operativo. Na nube, ao mesmo tempo, solucionouse outro problema de rede coa configuración de QoS (Control de tráfico). No gráfico de consumo de memoria para fragmentos de paquetes, parecía exactamente o mesmo:

Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

A suposición era sinxela: se parecen igual nos gráficos, entón teñen o mesmo motivo. Ademais, calquera problema con este tipo de memoria é extremadamente raro.

A esencia do problema solucionado foi que usamos o programador de paquetes fq con configuración predeterminada en QoS. Por defecto, para unha conexión, permite engadir 100 paquetes á cola, e algunhas conexións, en situacións de escaseza de canles, comezaron a atascar a cola ao máximo. Neste caso, os paquetes son eliminados. En estatísticas tc (tc -s qdisc) pódese ver así:

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" son os paquetes eliminados debido a que se superou o límite de cola dunha conexión, e "dropped 464545" é a suma de todos os paquetes eliminados deste planificador. Despois de aumentar a lonxitude da cola a 1 e reiniciar os contedores, o problema deixou de producirse. Podes sentarte e beber un batido.

FragmentSmack/SegmentSmack. Último Sangue

En primeiro lugar, varios meses despois do anuncio de vulnerabilidades no núcleo, finalmente apareceu unha corrección para FragmentSmack (recordovos que xunto co anuncio de agosto, lanzouse unha corrección só para SegmentSmack), o que nos deu a oportunidade de abandonar a solución. que nos causou bastantes problemas. Durante este tempo, xa conseguimos transferir algúns dos servidores ao novo núcleo, e agora tiñamos que comezar dende o principio. Por que actualizamos o núcleo sen esperar a corrección de FragmentSmack? O caso é que o proceso de protección contra estas vulnerabilidades coincidiu (e fusionouse) co propio proceso de actualización de CentOS (que leva aínda máis tempo que actualizar só o núcleo). Ademais, SegmentSmack é unha vulnerabilidade máis perigosa, e unha solución para ela apareceu inmediatamente, polo que tiña sentido de todos os xeitos. Non obstante, non puidemos simplemente actualizar o núcleo en CentOS porque a vulnerabilidade FragmentSmack, que apareceu durante CentOS 7.5, só se solucionou na versión 7.6, polo que tivemos que deter a actualización á 7.5 e comezar de novo coa actualización á 7.6. E isto tamén pasa.

En segundo lugar, as queixas raras dos usuarios sobre problemas volvéronse a nós. Agora xa sabemos con certeza que todos eles están relacionados coa carga de ficheiros dos clientes a algúns dos nosos servidores. Ademais, un número moi reducido de cargas da masa total pasaron por estes servidores.

Como lembramos da historia anterior, facer retroceder Sysctl non axudou. Axudou o reinicio, pero temporalmente.
Non se eliminaron as sospeitas sobre Sysctl, pero esta vez foi necesario recoller a maior cantidade de información posible. Tamén houbo unha enorme falta de capacidade para reproducir o problema de carga no cliente para estudar con máis precisión o que estaba a suceder.

A análise de todas as estatísticas e rexistros dispoñibles non nos achegou a comprender o que estaba a suceder. Había unha aguda falta de capacidade para reproducir o problema para "sentir" unha conexión específica. Finalmente, os desenvolvedores, utilizando unha versión especial da aplicación, conseguiron conseguir unha reprodución estable dos problemas nun dispositivo de proba cando se conectaba mediante Wi-Fi. Este foi un avance na investigación. O cliente conectouse a Nginx, que proxy ao backend, que era a nosa aplicación Java.

Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

O diálogo para problemas foi así (fixado no lado do proxy de Nginx):

  1. Cliente: solicitude para recibir información sobre a descarga dun ficheiro.
  2. Servidor Java: resposta.
  3. Cliente: POST con ficheiro.
  4. Servidor Java: erro.

Ao mesmo tempo, o servidor Java escribe no rexistro que se recibiron 0 bytes de datos do cliente e o proxy Nginx escribe que a solicitude levou máis de 30 segundos (30 segundos é o tempo de espera da aplicación cliente). Por que o tempo de espera e por que 0 bytes? Desde a perspectiva de HTTP, todo funciona como debería, pero o POST co ficheiro parece desaparecer da rede. Ademais, desaparece entre o cliente e Nginx. É hora de armarse con Tcpdump! Pero primeiro cómpre comprender a configuración da rede. O proxy Nginx está detrás do equilibrador L3 NFware. O túnel utilízase para entregar paquetes desde o equilibrador L3 ao servidor, que engade as súas cabeceiras aos paquetes:

Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

Neste caso, a rede chega a este servidor en forma de tráfico etiquetado con Vlan, que tamén engade os seus propios campos aos paquetes:

Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

E este tráfico tamén se pode fragmentar (esa mesma pequena porcentaxe de tráfico fragmentado entrante da que falamos ao avaliar os riscos de Workaround), o que tamén cambia o contido das cabeceiras:

Coidado coas vulnerabilidades que traen traballo. Parte 1: FragmentSmack/SegmentSmack

Unha vez máis: os paquetes son encapsulados cunha etiqueta Vlan, encapsulados cun túnel, fragmentados. Para comprender mellor como ocorre isto, imos rastrexar a ruta do paquete desde o cliente ata o proxy Nginx.

  1. O paquete chega ao equilibrador L3. Para un enrutamento correcto dentro do centro de datos, o paquete encápsulase nun túnel e envíase á tarxeta de rede.
  2. Dado que as cabeceiras de paquete + túnel non encaixan na MTU, o paquete córtase en fragmentos e envíase á rede.
  3. O interruptor despois do equilibrador L3, ao recibir un paquete, engádelle unha etiqueta Vlan e envíao.
  4. O interruptor diante do proxy Nginx ve (en función da configuración do porto) que o servidor está esperando un paquete encapsulado en Vlan, polo que o envía como está, sen eliminar a etiqueta Vlan.
  5. Linux toma fragmentos de paquetes individuais e fusionaos nun paquete grande.
  6. A continuación, o paquete chega á interface Vlan, onde se elimina a primeira capa - encapsulación Vlan.
  7. A continuación, Linux envíao á interface do túnel, onde se elimina outra capa: encapsulación do túnel.

A dificultade é pasar todo isto como parámetros a tcpdump.
Comecemos polo final: hai paquetes IP limpos (sen cabeceiras innecesarias) dos clientes, con vlan e encapsulamento de túneles eliminados?

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

Non, non había tales paquetes no servidor. Polo tanto, o problema debe estar aí antes. Hai algún paquete que só teña eliminado a encapsulación de Vlan?

tcpdump ip[32:4]=0xx390x2xx

0xx390x2xx é o enderezo IP do cliente en formato hexadecimal.
32:4 — enderezo e lonxitude do campo no que se escribe a IP SCR no paquete Tunnel.

O enderezo de campo tivo que ser seleccionado pola forza bruta, xa que en Internet escriben uns 40, 44, 50, 54, pero alí non había enderezo IP. Tamén podes mirar un dos paquetes en hexadecimal (o parámetro -xx ou -XX en tcpdump) e calcular o enderezo IP que coñeces.

Hai fragmentos de paquetes sen eliminar a encapsulación de Vlan e Tunnel?

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

Esta maxia amosaranos todos os fragmentos, incluído o último. Probablemente, o mesmo se pode filtrar por IP, pero non o intentei, porque non hai moitos paquetes deste tipo, e os que necesitaba foron facilmente atopados no fluxo xeral. Aquí están:

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 ..............

Trátase de dous fragmentos dun paquete (o mesmo ID 53652) cunha fotografía (a palabra Exif é visible no primeiro paquete). Debido ao feito de que hai paquetes a este nivel, pero non na forma combinada nos vertedoiros, o problema está claramente na montaxe. Por fin hai unha proba documental diso!

O decodificador de paquetes non revelou ningún problema que impedise a compilación. Probao aquí: hpd.gasmi.net. Ao principio, cando intentas encher algo alí, ao descodificador non lle gusta o formato do paquete. Resultou que había dous octetos adicionais entre Srcmac e Ethertype (non relacionados coa información de fragmentos). Despois de eliminalos, o descodificador comezou a funcionar. Non obstante, non mostrou problemas.
Diga o que se diga, non se atopou nada máis que aqueles Sysctl. Só quedaba atopar un xeito de identificar os servidores problemáticos para comprender a escala e decidir sobre outras accións. O contador necesario atopouse con suficiente rapidez:

netstat -s | grep "packet reassembles failed”

Tamén está en snmpd baixo OID=1.3.6.1.2.1.4.31.1.1.16.1 (ipSystemStatsReasmFails).

"O número de fallos detectados polo algoritmo de re-ensamblaxe de IP (por calquera motivo: tempo de espera, erros, etc.)".

Entre o grupo de servidores nos que se estudou o problema, en dous este contador aumentou máis rápido, en dous máis lentamente e en dous máis non aumentou nada. A comparación da dinámica deste contador coa dinámica dos erros HTTP no servidor Java revelou unha correlación. É dicir, poderíase controlar o contador.

Ter un indicador fiable de problemas é moi importante para que poida determinar con precisión se a recuperación de Sysctl axuda, xa que pola historia anterior sabemos que isto non se pode entender inmediatamente desde a aplicación. Este indicador permitiríanos identificar todas as áreas problemáticas na produción antes de que os usuarios o descubran.
Despois de retroceder Sysctl, os erros de seguimento detivéronse, polo que se comprobou a causa dos problemas, así como o feito de que a recuperación axuda.

Revertimos a configuración de fragmentación noutros servidores, onde entrou en xogo un novo monitoreo, e nalgún lugar asignamos aínda máis memoria para fragmentos da que era o predeterminado anteriormente (estas eran estatísticas UDP, cuxa perda parcial non se notaba no contexto xeral) .

As preguntas máis importantes

Por que están fragmentados os paquetes no noso equilibrador L3? A maioría dos paquetes que chegan dos usuarios aos equilibradores son SYN e ACK. Os tamaños destes paquetes son pequenos. Pero dado que a participación deste tipo de paquetes é moi grande, no seu contexto non notamos a presenza de paquetes grandes que comezaron a fragmentarse.

O motivo foi un script de configuración roto advmss en servidores con interfaces Vlan (nese momento había moi poucos servidores con tráfico etiquetado en produción). Advmss permítenos transmitir ao cliente a información de que os paquetes na nosa dirección deberían ser de menor tamaño para que despois de anexarlles as cabeceiras de túnel non teñan que fragmentarse.

Por que a recuperación de Sysctl non axudou, pero o reinicio si? Ao retroceder Sysctl cambiou a cantidade de memoria dispoñible para combinar paquetes. Ao mesmo tempo, ao parecer, o feito mesmo de desbordar a memoria dos fragmentos provocou unha desaceleración das conexións, o que provocou que os fragmentos se atrasaran durante moito tempo na cola. É dicir, o proceso foi en ciclos.
O reinicio borrou a memoria e todo volveu á orde.

Era posible prescindir da solución alternativa? Si, pero existe un alto risco de deixar sen servizo aos usuarios en caso de ataque. Por suposto, o uso de Workaround deu lugar a diversos problemas, entre eles a ralentización dun dos servizos para os usuarios, pero non obstante cremos que as accións estaban xustificadas.

Moitas grazas a Andrey Timofeev (atimofeyev) para asistencia na realización da investigación, así como Alexey Krenev (dispositivox) - polo titánico traballo de actualización de Centos e núcleos nos servidores. Un proceso que neste caso tivo que iniciarse dende o principio varias veces, polo que se prolongou durante moitos meses.

Fonte: www.habr.com

Engadir un comentario