Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

¡Hola a todos! Mi nombre es Dmitry Samsonov, trabajo como administrador de sistemas líder en Odnoklassniki. Contamos con más de 7 mil servidores físicos, 11 mil contenedores en nuestra nube y 200 aplicaciones, que en diversas configuraciones forman 700 clusters diferentes. La gran mayoría de servidores ejecutan CentOS 7.
El 14 de agosto de 2018 se publicó información sobre la vulnerabilidad FragmentSmack
(CVE-2018-5391) y SegmentSmack (CVE-2018-5390). Se trata de vulnerabilidades con un vector de ataque a la red y una puntuación bastante alta (7.5), que amenaza con una denegación de servicio (DoS) por agotamiento de recursos (CPU). En ese momento no se propuso una solución del kernel para FragmentSmack; además, apareció mucho más tarde que la publicación de información sobre la vulnerabilidad. Para eliminar SegmentSmack, se sugirió actualizar el kernel. El paquete de actualización en sí se lanzó el mismo día, solo quedaba instalarlo.
¡No, no estamos en absoluto en contra de actualizar el kernel! Sin embargo, hay matices...

Cómo actualizamos el kernel en producción

En general, nada complicado:

  1. Descargar paquetes;
  2. Instalarlos en varios servidores (incluidos los servidores que alojan nuestra nube);
  3. Asegúrese de que no haya nada roto;
  4. Asegúrese de que todas las configuraciones estándar del kernel se apliquen sin errores;
  5. Espere unos días;
  6. Verifique el rendimiento del servidor;
  7. Cambiar la implementación de nuevos servidores al nuevo kernel;
  8. Actualizar todos los servidores por centro de datos (un centro de datos a la vez para minimizar el efecto sobre los usuarios en caso de problemas);
  9. Reinicie todos los servidores.

Repita para todas las ramas de los núcleos que tenemos. Por el momento es:

  • Stock CentOS 7 3.10: para la mayoría de los servidores habituales;
  • Vainilla 4.19 - para los nuestros nubes de una sola nube, porque necesitamos BFQ, BBR, etc.;
  • Elrepo kernel-ml 5.2 - para distribuidores altamente cargados, porque 4.19 solía comportarse de forma inestable, pero se necesitan las mismas características.

Como habrás adivinado, reiniciar miles de servidores es lo que lleva más tiempo. Dado que no todas las vulnerabilidades son críticas para todos los servidores, solo reiniciamos aquellos a los que se puede acceder directamente desde Internet. En la nube, para no limitar la flexibilidad, no vinculamos contenedores accesibles externamente a servidores individuales con un nuevo kernel, sino que reiniciamos todos los hosts sin excepción. Afortunadamente, el procedimiento allí es más sencillo que con los servidores normales. Por ejemplo, los contenedores sin estado pueden simplemente moverse a otro servidor durante un reinicio.

Sin embargo, todavía queda mucho trabajo y puede llevar varias semanas, y si hay algún problema con la nueva versión, hasta varios meses. Los atacantes entienden esto muy bien, por lo que necesitan un plan B.

FragmentoSmack/SegmentSmack. Solución alterna

Afortunadamente, para algunas vulnerabilidades existe un plan B llamado Workaround. En la mayoría de los casos, se trata de un cambio en la configuración del kernel/aplicación que puede minimizar el posible efecto o eliminar por completo la explotación de vulnerabilidades.

En el caso de FragmentSmack/SegmentSmack fue propuesto Solución alternativa como esta:

«Puede cambiar los valores predeterminados de 4 MB y 3 MB en net.ipv4.ipfrag_high_thresh y net.ipv4.ipfrag_low_thresh (y sus contrapartes para ipv6 net.ipv6.ipfrag_high_thresh y net.ipv6.ipfrag_low_thresh) a 256 kB y 192 kB respectivamente o más bajo. Las pruebas muestran caídas de pequeñas a significativas en el uso de la CPU durante un ataque, según el hardware, la configuración y las condiciones. Sin embargo, puede haber algún impacto en el rendimiento debido a ipfrag_high_thresh=262144 bytes, ya que sólo dos fragmentos de 64 KB pueden caber en la cola de reensamblado a la vez. Por ejemplo, existe el riesgo de que las aplicaciones que funcionan con paquetes UDP grandes se rompan.".

Los parámetros mismos en la documentación del núcleo descrito de la siguiente manera:

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.

No tenemos grandes UDP en servicios de producción. No hay tráfico fragmentado en la LAN; hay tráfico fragmentado en la WAN, pero no es significativo. No hay señales: ¡puede implementar una solución alternativa!

FragmentoSmack/SegmentSmack. Primera sangre

El primer problema que encontramos fue que los contenedores de la nube a veces aplicaban la nueva configuración solo parcialmente (solo ipfrag_low_thresh), y otras veces no las aplicaban en absoluto: simplemente fallaban al principio. No fue posible reproducir el problema de manera estable (todas las configuraciones se aplicaron manualmente sin ninguna dificultad). Comprender por qué el contenedor falla al principio tampoco es tan fácil: no se encontraron errores. Una cosa era segura: revertir la configuración soluciona el problema de los fallos de los contenedores.

¿Por qué no basta con aplicar Sysctl en el host? El contenedor vive en su propio espacio de nombres de red dedicado, por lo que al menos parte de los parámetros de red Sysctl en el contenedor puede diferir del anfitrión.

¿Cómo se aplican exactamente las configuraciones de Sysctl en el contenedor? Dado que nuestros contenedores no tienen privilegios, no podrá cambiar ninguna configuración de Sysctl ingresando al contenedor; simplemente no tiene suficientes derechos. Para ejecutar contenedores, nuestra nube en ese momento usaba Docker (ahora Podman). Los parámetros del nuevo contenedor se pasaron a Docker a través de la API, incluida la configuración necesaria de Sysctl.
Mientras buscaba entre las versiones, resultó que la API de Docker no devolvía todos los errores (al menos en la versión 1.10). Cuando intentamos iniciar el contenedor mediante "docker run", finalmente vimos al 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.

El valor del parámetro no es válido. ¿Pero por qué? ¿Y por qué no es válido sólo a veces? Resultó que Docker no garantiza el orden en que se aplican los parámetros Sysctl (la última versión probada es 1.13.1), por lo que a veces ipfrag_high_thresh intentó establecerse en 256 K cuando ipfrag_low_thresh todavía era 3 M, es decir, el límite superior era inferior. que el límite inferior, lo que provocó el error.

En ese momento, ya usábamos nuestro propio mecanismo para reconfigurar el contenedor después del inicio (congelar el contenedor después congelador grupal y ejecutar comandos en el espacio de nombres del contenedor a través de redes ip), y también agregamos parámetros de escritura Sysctl a esta parte. El problema fue resuelto.

FragmentoSmack/SegmentSmack. Primera sangre 2

Antes de que tuviéramos tiempo de comprender el uso de Workaround en la nube, comenzaron a llegar las primeras quejas poco comunes de los usuarios. En ese momento, habían pasado varias semanas desde el inicio de la utilización de Workaround en los primeros servidores. La investigación inicial mostró que las quejas se recibieron contra servicios individuales, y no contra todos los servidores de estos servicios. El problema se ha vuelto nuevamente extremadamente incierto.

En primer lugar, por supuesto, intentamos revertir la configuración de Sysctl, pero esto no tuvo ningún efecto. Varias manipulaciones con el servidor y la configuración de la aplicación tampoco ayudaron. Reiniciar ayudó. Reiniciar Linux es tan antinatural como lo era en Windows en los viejos tiempos. Sin embargo, ayudó y lo atribuimos a un "fallo del kernel" al aplicar la nueva configuración en Sysctl. Que frívolo fue...

Tres semanas después, el problema volvió a aparecer. La configuración de estos servidores era bastante sencilla: Nginx en modo proxy/equilibrador. No hay mucho tráfico. Nueva nota introductoria: el número de errores 504 en los clientes aumenta cada día (Tiempo de espera de puerta de enlace). El gráfico muestra el número de errores 504 por día para este servicio:

Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

Todos los errores se refieren al mismo backend: el que está en la nube. El gráfico de consumo de memoria para fragmentos de paquetes en este backend se veía así:

Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

Esta es una de las manifestaciones más obvias del problema en los gráficos del sistema operativo. En la nube, al mismo tiempo, se solucionó otro problema de red con la configuración de QoS (Control de tráfico). En el gráfico de consumo de memoria para fragmentos de paquetes, se veía exactamente igual:

Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

La suposición era simple: si se ven iguales en los gráficos, entonces tienen la misma razón. Además, los problemas con este tipo de memoria son extremadamente raros.

La esencia del problema solucionado fue que utilizamos el programador de paquetes fq con la configuración predeterminada en QoS. De forma predeterminada, para una conexión, le permite agregar 100 paquetes a la cola, y algunas conexiones, en situaciones de escasez de canales, comenzaron a obstruir la cola al máximo de su capacidad. En este caso, los paquetes se descartan. En tc stats (tc -s qdisc) se puede 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 los paquetes descartados debido a exceder el límite de la cola de una conexión, y "drop 464545" es la suma de todos los paquetes descartados de este programador. Después de aumentar la longitud de la cola a mil y reiniciar los contenedores, el problema dejó de ocurrir. Puedes sentarte y tomar un batido.

FragmentoSmack/SegmentSmack. última sangre

En primer lugar, varios meses después del anuncio de las vulnerabilidades en el kernel, finalmente apareció una solución para FragmentSmack (permítanme recordarles que junto con el anuncio en agosto, se lanzó una solución solo para SegmentSmack), lo que nos dio la oportunidad de abandonar la solución alternativa. lo que nos causó bastantes problemas. Durante este tiempo, ya habíamos logrado transferir algunos de los servidores al nuevo kernel y ahora teníamos que empezar desde el principio. ¿Por qué actualizamos el kernel sin esperar la solución de FragmentSmack? El hecho es que el proceso de protección contra estas vulnerabilidades coincidió (y se fusionó) con el proceso de actualización del propio CentOS (que lleva incluso más tiempo que actualizar solo el kernel). Además, SegmentSmack es una vulnerabilidad más peligrosa y apareció una solución de inmediato, por lo que tenía sentido de todos modos. Sin embargo, no pudimos simplemente actualizar el kernel en CentOS porque la vulnerabilidad FragmentSmack, que apareció durante CentOS 7.5, solo se solucionó en la versión 7.6, por lo que tuvimos que detener la actualización a 7.5 y comenzar de nuevo con la actualización a 7.6. Y esto también sucede.

En segundo lugar, nos han llegado raras quejas de usuarios sobre problemas. Ahora ya sabemos con seguridad que todos están relacionados con la carga de archivos de los clientes a algunos de nuestros servidores. Además, una cantidad muy pequeña de cargas de la masa total pasó por estos servidores.

Como recordamos de la historia anterior, revertir Sysctl no ayudó. Reiniciar ayudó, pero temporalmente.
Las sospechas sobre Sysctl no se eliminaron, pero esta vez fue necesario recopilar la mayor cantidad de información posible. También había una gran falta de capacidad para reproducir el problema de carga en el cliente para poder estudiar con mayor precisión lo que estaba sucediendo.

El análisis de todas las estadísticas y registros disponibles no nos acercó más a comprender lo que estaba sucediendo. Había una grave falta de capacidad para reproducir el problema para “sentir” una conexión específica. Finalmente, los desarrolladores, utilizando una versión especial de la aplicación, lograron lograr una reproducción estable de los problemas en un dispositivo de prueba cuando se conecta a través de Wi-Fi. Este fue un gran avance en la investigación. El cliente se conectó a Nginx, que se dirigió al backend, que era nuestra aplicación Java.

Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

El diálogo para problemas era así (corregido en el lado del proxy Nginx):

  1. Cliente: solicitud para recibir información sobre la descarga de un archivo.
  2. Servidor Java: respuesta.
  3. Cliente: PUBLICAR con archivo.
  4. Servidor Java: error.

Al mismo tiempo, el servidor Java escribe en el registro que se recibieron 0 bytes de datos del cliente y el proxy Nginx escribe que la solicitud tardó más de 30 segundos (30 segundos es el tiempo de espera de la aplicación cliente). ¿Por qué el tiempo de espera y por qué 0 bytes? Desde una perspectiva HTTP, todo funciona como debería, pero el POST con el archivo parece desaparecer de la red. Además, desaparece entre el cliente y Nginx. ¡Es hora de armarte con Tcpdump! Pero primero necesitas entender la configuración de la red. El proxy Nginx está detrás del equilibrador L3 NFware. El túnel se utiliza para entregar paquetes desde el equilibrador L3 al servidor, que agrega sus encabezados a los paquetes:

Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

En este caso, la red llega a este servidor en forma de tráfico etiquetado con Vlan, que también añade sus propios campos a los paquetes:

Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

Y este tráfico también se puede fragmentar (ese mismo pequeño porcentaje de tráfico entrante fragmentado del que hablamos al evaluar los riesgos de Workaround), lo que también cambia el contenido de los encabezados:

Tenga cuidado con las vulnerabilidades que generan rondas de trabajo. Parte 1: FragmentSmack/SegmentSmack

Una vez más: los paquetes se encapsulan con una etiqueta Vlan, se encapsulan con un túnel y se fragmentan. Para comprender mejor cómo sucede esto, rastreemos la ruta del paquete desde el cliente hasta el proxy Nginx.

  1. El paquete llega al equilibrador L3. Para un enrutamiento correcto dentro del centro de datos, el paquete se encapsula en un túnel y se envía a la tarjeta de red.
  2. Dado que los encabezados del paquete + túnel no caben en la MTU, el paquete se corta en fragmentos y se envía a la red.
  3. El conmutador después del equilibrador L3, al recibir un paquete, le agrega una etiqueta Vlan y lo envía.
  4. El conmutador frente al proxy Nginx ve (según la configuración del puerto) que el servidor espera un paquete encapsulado en Vlan, por lo que lo envía tal cual, sin quitar la etiqueta Vlan.
  5. Linux toma fragmentos de paquetes individuales y los fusiona en un paquete grande.
  6. A continuación, el paquete llega a la interfaz Vlan, donde se elimina la primera capa: la encapsulación Vlan.
  7. Luego, Linux lo envía a la interfaz Tunnel, donde se elimina otra capa: la encapsulación de Tunnel.

La dificultad es pasar todo esto como parámetros a tcpdump.
Comencemos desde el final: ¿hay paquetes IP limpios (sin encabezados innecesarios) de los clientes, sin la encapsulación de vlan y túnel?

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

No, no había tales paquetes en el servidor. Entonces el problema debe estar ahí antes. ¿Hay algún paquete al que solo se le haya eliminado la encapsulación Vlan?

tcpdump ip[32:4]=0xx390x2xx

0xx390x2xx es la dirección IP del cliente en formato hexadecimal.
32:4: dirección y longitud del campo en el que está escrita la IP del SCR en el paquete del túnel.

La dirección del campo tuvo que ser seleccionada por fuerza bruta, ya que en Internet escriben alrededor de 40, 44, 50, 54, pero allí no había ninguna dirección IP. También puede mirar uno de los paquetes en hexadecimal (el parámetro -xx o -XX en tcpdump) y calcular la dirección IP que conoce.

¿Se eliminan fragmentos de paquetes sin encapsulación Vlan y Tunnel?

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

Esta magia nos mostrará todos los fragmentos, incluido el último. Probablemente, lo mismo se pueda filtrar por IP, pero no lo intenté, porque no hay muchos paquetes de este tipo y los que necesitaba se encontraban fácilmente en el flujo general. 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 ..............

Estos son dos fragmentos de un paquete (mismo ID 53652) con una fotografía (la palabra Exif es visible en el primer paquete). Debido al hecho de que hay paquetes en este nivel, pero no en forma fusionada en los volcados, el problema está claramente en el ensamblaje. ¡Por fin hay pruebas documentales de ello!

El decodificador de paquetes no reveló ningún problema que impidiera la compilación. Lo probé aquí: hpd.gasmi.net. Al principio, cuando intentas meter algo allí, al decodificador no le gusta el formato del paquete. Resultó que había dos octetos adicionales entre Srcmac y Ethertype (no relacionados con la información del fragmento). Después de quitarlos, el decodificador empezó a funcionar. Sin embargo, no mostró problemas.
Digan lo que digan, no se encontró nada más excepto esos Sysctl. Todo lo que quedaba era encontrar una manera de identificar los servidores problemáticos para comprender la escala y decidir acciones futuras. El contador necesario se encontró con bastante rapidez:

netstat -s | grep "packet reassembles failed”

También está en snmpd bajo OID=1.3.6.1.2.1.4.31.1.1.16.1 (ipSystemStatsReasmFails).

"La cantidad de fallas detectadas por el algoritmo de reensamblaje de IP (por cualquier motivo: tiempo de espera agotado, errores, etc.)".

Entre el grupo de servidores en los que se estudió el problema, en dos este contador aumentó más rápido, en dos más lentamente y en dos más no aumentó en absoluto. La comparación de la dinámica de este contador con la dinámica de los errores HTTP en el servidor Java reveló una correlación. Es decir, se podría monitorear el medidor.

Tener un indicador confiable de problemas es muy importante para que pueda determinar con precisión si revertir Sysctl ayuda, ya que por la historia anterior sabemos que esto no se puede entender de inmediato desde la aplicación. Este indicador nos permitiría identificar todas las áreas problemáticas en producción antes de que los usuarios las descubran.
Después de revertir Sysctl, los errores de monitoreo cesaron, por lo que se demostró la causa de los problemas, así como el hecho de que la reversión ayuda.

Revertimos la configuración de fragmentación en otros servidores, donde entró en juego un nuevo monitoreo, y en algún lugar asignamos incluso más memoria para los fragmentos de lo que antes era el valor predeterminado (estas eran estadísticas UDP, cuya pérdida parcial no se notaba en el fondo general) .

Las preguntas más importantes

¿Por qué los paquetes están fragmentados en nuestro balanceador L3? La mayoría de los paquetes que llegan de los usuarios a los balanceadores son SYN y ACK. Los tamaños de estos paquetes son pequeños. Pero como la proporción de estos paquetes es muy grande, en su contexto no notamos la presencia de paquetes grandes que comenzaron a fragmentarse.

El motivo fue un script de configuración roto. advmss en servidores con interfaces Vlan (había muy pocos servidores con tráfico etiquetado en producción en ese momento). Advmss nos permite transmitir al cliente la información de que los paquetes en nuestra dirección deben ser más pequeños para que después de adjuntarles encabezados de túnel no tengan que fragmentarse.

¿Por qué la reversión de Sysctl no ayudó, pero el reinicio sí? Revertir Sysctl cambió la cantidad de memoria disponible para fusionar paquetes. Al mismo tiempo, aparentemente el hecho mismo del desbordamiento de la memoria de los fragmentos provocó una ralentización de las conexiones, lo que provocó que los fragmentos se retrasaran durante mucho tiempo en la cola. Es decir, el proceso fue en ciclos.
El reinicio borró la memoria y todo volvió al orden.

¿Era posible prescindir de la solución alternativa? Sí, pero existe un alto riesgo de dejar a los usuarios sin servicio en caso de ataque. Por supuesto, el uso de Workaround generó varios problemas, incluida la ralentización de uno de los servicios para los usuarios, pero aun así creemos que las acciones estaban justificadas.

Muchas gracias a Andrey Timofeev (atimofeyev) por su ayuda en la realización de la investigación, así como a Alexey Krenev (dispositivox) - por el trabajo titánico de actualización de Centos y kernels en los servidores. Un proceso que en este caso hubo que iniciar desde el principio varias veces, por lo que se prolongó durante muchos meses.

Fuente: habr.com

Añadir un comentario