Tenga cuidado con las vulnerabilidades que traen soluciones alternativas. Parte 1: FragmentSmack / SegmentSmack



Hola a todos! Mi nombre es Dmitry Samsonov, trabajo como administrador líder del sistema en Odnoklassniki. Tenemos más de 7 mil servidores físicos, 11 mil contenedores en nuestra nube y 200 aplicaciones, que en diferentes configuraciones forman 700 clústeres diferentes. La gran mayoría de los servidores ejecuta CentOS 7.
Información de vulnerabilidad de FragmentSmack lanzada el 14 de agosto de 2018
( CVE-2018-5391 ) y SegmentSmack ( CVE-2018-5390 ). Estas son vulnerabilidades con un vector de ataque de red y una calificación bastante alta (7.5), que amenaza con la denegación de servicio (DoS) debido al agotamiento de recursos (CPU). En ese momento no se propuso una solución en el kernel para FragmentSmack, además, salió mucho más tarde que la publicación de información sobre la vulnerabilidad. Para eliminar SegmentSmack, se propuso actualizar el kernel. El paquete de actualización en sí se lanzó el mismo día, todo lo que quedaba era instalarlo.
No, no estamos en absoluto en contra de la actualización del kernel. Sin embargo, hay matices ...

¿Cómo actualizamos el núcleo en el producto?


En general, nada complicado:
  1. Descargar paquetes
  2. Instálelos en varios servidores (incluidos los servidores que alojan nuestra nube);
  3. Asegúrate de que nada esté roto;
  4. Asegúrese de que todas las configuraciones estándar del núcleo se apliquen sin errores;
  5. Espera unos días;
  6. Verificar el rendimiento del servidor;
  7. Cambie la implementación de nuevos servidores a un nuevo kernel;
  8. Actualizar todos los servidores por centros de datos (un centro de datos a la vez para minimizar el efecto para los usuarios en caso de problemas);
  9. Reinicia todos los servidores.

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

  • Stock CentOS 7 3.10 - para la mayoría de los servidores comunes;
  • Vanilla 4.19 es para nuestra nube única porque necesitamos BFQ, BBR, etc.
  • Elrepo kernel-ml 5.2 es para distribuidores altamente cargados , porque 4.19 solía comportarse de manera inestable y las características necesitan las mismas.

Como habrás adivinado, reiniciar miles de servidores lleva más tiempo. Como no todas las vulnerabilidades son críticas para todos los servidores, solo reiniciamos aquellas a las 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 núcleo, sino que reiniciamos todos los hosts sin excepción. Afortunadamente, el procedimiento es más fácil allí que con los servidores normales. Por ejemplo, los contenedores sin estado pueden simplemente moverse a otro servidor durante el reinicio.

Sin embargo, todavía hay mucho trabajo y puede llevar varias semanas, y en caso de problemas con la nueva versión, hasta varios meses. Los atacantes son conscientes de esto, por lo que se necesita el plan B.

FragmentSmack / SegmentSmack. Solución alternativa


Afortunadamente, para algunas vulnerabilidades, dicho plan "B" existe, y se llama solución alternativa. Muy a menudo, este es 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 , se propuso la siguiente solución:

" Puede cambiar los valores predeterminados de 4MB y 3MB en net.ipv4.ipfrag_high_thresh y net.ipv4.ipfrag_low_thresh (y sus análogos para ipv6 net.ipv6.ipfrag_high_thresh y net.ipv6.ipfrag_low_thresh) en 256 kB y 192 kB, respectivamente. Las pruebas muestran una caída de leve a significativa en el uso de la CPU durante un ataque, dependiendo del equipo, 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 solo dos fragmentos de 64K pueden caber en la cola de reconstrucción a la vez. Por ejemplo, existe el riesgo de que las aplicaciones que funcionan con paquetes UDP grandes se rompan ”.

Los parámetros en sí mismos en la documentación del núcleo se describen a continuación:

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 los servicios de producción. No hay tráfico fragmentado en la LAN; hay tráfico, pero no significativo, en la WAN. Nada es un buen augurio: ¡puedes usar una solución alternativa!

FragmentSmack / SegmentSmack. Primera sangre


El primer problema que encontramos fue que los contenedores en la nube a veces solo aplicaban parcialmente la nueva configuración (solo ipfrag_low_thresh), y a veces no los usaban en absoluto, simplemente se bloquearon al principio. No fue posible reproducir el problema de forma estable (manualmente, todas las configuraciones se aplicaron sin ninguna dificultad). Comprender por qué el contenedor se cae al principio tampoco es tan simple: no se encontraron errores. Una cosa era segura: revertir la configuración resuelve el problema de soltar contenedores.

¿Por qué no es suficiente usar Sysctl en el host? El contenedor vive en su espacio de nombres de red dedicado, por lo que al menos parte de los parámetros Sysctl de la red en el contenedor pueden diferir del host.

¿Cómo se aplican exactamente las configuraciones de Sysctl en el contenedor? Dado que tenemos contenedores sin privilegios, cambiar cualquier configuración de Sysctl yendo al contenedor en sí fallará, simplemente no habrá suficientes derechos. En ese momento, nuestra nube usaba Docker (ahora Podman ) para lanzar contenedores. La ventana acoplable a través de la API pasó los parámetros del nuevo contenedor, incluidas las configuraciones necesarias de Sysctl.
En el curso de enumerar las versiones, resultó que la API de Docker no arrojó todos los errores (al menos en la versión 1.10). Al intentar iniciar el contenedor a través de "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 que? ¿Y por qué no es válido solo a veces? Resultó que Docker no garantizaba la aplicación de los parámetros de Sysctl (la última versión probada era 1.13.1), por lo que a veces ipfrag_high_thresh intentó establecerse en 256K cuando ipfrag_low_thresh todavía era 3M, es decir, el límite superior era más bajo que el inferior, lo que provocó un error.

En ese momento, ya usamos nuestro propio mecanismo para reconfigurar el contenedor después de comenzar (congelar el contenedor a través del congelador cgroup y ejecutar comandos en el espacio de nombres del contenedor a través de ip netns ), y también agregamos parámetros Sysctl a esta parte. El problema ha sido resuelto.

FragmentSmack / SegmentSmack. Primera sangre 2


Antes de que supiéramos cómo usar Workaround en la nube, comenzaron a llegar las primeras quejas de los usuarios. En ese momento, pasaron varias semanas desde el inicio de la solución en los primeros servidores. La investigación inicial mostró que se recibieron quejas sobre servicios individuales y no todos los servidores de estos servicios. El problema ha recuperado un carácter extremadamente vago.

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 para Linux es tan poco natural como era una condición normal para trabajar con Windows en los viejos tiempos. Sin embargo, ayudó, y descartamos todo a un "error en el núcleo" al aplicar la nueva configuración en Sysctl. Qué frívolo fue ...

Tres semanas después, el problema recurrió. La configuración de estos servidores era bastante simple: Nginx en modo proxy / equilibrador. El tráfico es un poco. Nueva introducción: el número de errores 504 ( Tiempo de espera de la puerta de enlace ) aumenta cada día en los clientes. El gráfico muestra la cantidad de 504 errores por día para este servicio:



Todos los errores son aproximadamente del mismo backend, aproximadamente el que está en la nube. El gráfico del consumo de memoria para fragmentos de paquetes en este backend fue el siguiente:



Esta es una de las manifestaciones más llamativas 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 del consumo de memoria para fragmentos de paquetes, se veía exactamente igual:



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

La esencia del problema solucionado fue que usamos el programador de paquetes fq con la configuración predeterminada en QoS. Por defecto, para una conexión, le permite agregar 100 paquetes a la cola, y algunas conexiones en una situación de escasez de canales comenzaron a obstruir la cola hasta el fallo. En este caso, los paquetes caen. En las estadísticas tc (tc -s qdisc), esto se puede ver de la siguiente manera:

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 que exceden el límite de la cola de una conexión, y "464545 descartados" es la suma de todos los paquetes descartados de este programador. Después de aumentar la longitud de la cola a 1 mil y reiniciar los contenedores, el problema dejó de aparecer. Puedes sentarte en una silla y tomar un batido.

FragmentSmack / SegmentSmack. Última sangre


Primero, unos meses después del anuncio de vulnerabilidades en el kernel, finalmente apareció una solución para FragmentSmack (recuerdo que con el anuncio en agosto se lanzó una solución solo para SegmentSmack), que nos dio la oportunidad de abandonar Workaround, lo que nos causó muchos problemas. Algunos de los servidores durante este tiempo ya logramos transferir a un nuevo kernel, y ahora tuvimos que comenzar desde el principio. ¿Por qué actualizamos el kernel sin esperar la solución FragmentSmack? El hecho es que el proceso de protección contra estas vulnerabilidades coincidió (y se fusionó) con el proceso de actualización de CentOS (que lleva incluso más tiempo que actualizar solo el núcleo). Además, SegmentSmack es una vulnerabilidad más peligrosa, y una solución apareció de inmediato, por lo que el punto fue en cualquier caso. Sin embargo, no podíamos simplemente actualizar el kernel en CentOS, porque la vulnerabilidad FragmentSmack, que apareció durante CentOS 7.5, se solucionó solo 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 asi es.

En segundo lugar, nos han devuelto quejas de usuarios poco frecuentes sobre problemas. Ahora ya sabemos con certeza que todos ellos están conectados con la descarga de archivos de clientes a algunos de nuestros servidores. Y a través de estos servidores hubo un número muy pequeño de cargas de la masa total.

Como recordamos de la historia anterior, la reversión de Sysctl no ayudó. Reiniciar ayudó, pero temporalmente.
Las sospechas con Sysctl no se levantaron, pero esta vez fue necesario recopilar la mayor cantidad de información posible. Además, había una falta extrema de la capacidad de reproducir el problema con la carga del cliente para examinar con mayor precisión lo que estaba sucediendo.

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



El diálogo con problemas fue el siguiente (corregido en el lado del proxy Nginx):

  1. Cliente: solicitud de información sobre la descarga de un archivo.
  2. Servidor Java: respuesta.
  3. Cliente: POST 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 que la solicitud tardó más de 30 segundos (30 segundos es el tiempo de espera de la aplicación del cliente). ¿Por qué tiempo de espera y por qué 0 bytes? Desde el punto de vista de HTTP, todo funciona como debería, pero la POST con el archivo parece desaparecer de la red. Y desaparece entre el cliente y Nginx. ¡Es hora de armarse con Tcpdump! Pero primero debe comprender la configuración de la red. El proxy nginx está detrás del equilibrador N3ware L3. La tunelización se usa para entregar paquetes desde el equilibrador L3 al servidor, que agrega sus encabezados a los paquetes:



Al mismo tiempo, la red llega a este servidor en forma de tráfico etiquetado con Vlan, que también agrega sus campos a los paquetes:



Y este tráfico puede estar fragmentado (el porcentaje muy pequeño de tráfico fragmentado entrante del que hablamos al evaluar los riesgos de la solución alternativa), lo que también cambia el contenido de los encabezados:



Una vez más: los paquetes están encapsulados por una etiqueta Vlan, encapsulados por un túnel, fragmentados. Para comprender mejor cómo sucede esto, rastreemos la ruta del paquete desde el cliente al proxy Nginx.

  1. El paquete llega al equilibrador L3. Para un enrutamiento correcto dentro del centro de datos, el paquete se encapsula en el túnel y se envía a la tarjeta de red.
  2. Como los encabezados de paquete + túnel no caben en la MTU, el paquete se corta en fragmentos y se envía a la red.
  3. El interruptor después del equilibrador L3 al recibir el paquete agrega una etiqueta Vlan y lo envía más.
  4. El conmutador antes de que el proxy Nginx vea (de acuerdo con 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 recibe fragmentos de paquetes individuales y los pega en un paquete grande.
  6. Luego, el paquete llega a la interfaz Vlan, donde se elimina la primera capa: encapsulación Vlan.
  7. Luego, Linux lo envía a la interfaz del túnel, donde se elimina otra capa: la encapsulación del túnel.

La dificultad es pasar todo esto como parámetros a tcpdump.
Comencemos desde el final: ¿hay algún paquete IP limpio (sin encabezados adicionales) de los clientes con vlan y encapsulación de túnel eliminados?

tcpdump host <ip >

No, no había tales paquetes en el servidor. Por lo tanto, el problema debería ser anterior. ¿Hay paquetes con solo la encapsulación Vlan eliminada?

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 se escribe SCR IP en el paquete Tunnel.

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

¿Hay fragmentos de paquetes sin la encapsulación Vlan y Tunnel eliminada?

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

Esta magia nos mostrará todos los fragmentos, incluido el último. Probablemente, lo mismo se puede filtrar por IP, pero no lo intenté, porque no hay muchos paquetes de este tipo, y los que necesitaba se encontraban fácilmente en la secuencia 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 .faEE.....@.2.m.
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.....j Ex
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 (el mismo ID 53652) con una fotografía (la palabra Exif es visible en el primer paquete). Debido al hecho de que hay paquetes a este nivel, pero no pegados en vertederos, el problema está claramente en el ensamblaje. ¡Finalmente, hay evidencia documental de esto!

El decodificador de paquetes no reveló ningún problema que impidiera el ensamblaje. Probado aquí: hpd.gasmi.net . Al principio, cuando intenta meter algo allí, al decodificador no le gusta el formato del paquete. Resultó que había algunos dos octetos adicionales entre Srcmac y Ethertype (no relacionados con la información del fragmento). Después de quitarlos, el decodificador funcionó. Sin embargo, no mostró problemas.
Di lo que quieras, excepto por esos mismos Sysctl, no se encontró nada más. Quedaba por encontrar una manera de identificar servidores problemáticos para comprender la escala y decidir sobre nuevas acciones. Rápidamente encontré el contador correcto:

netstat -s | grep "packet reassembles failed”

Está en snmpd bajo OID = 1.3.6.1.2.1.4.31.1.1.16.1 ( ipSystemStatsReasmFails ).

"El número de fallas detectadas por el algoritmo de reensamblado de IP (por cualquier razón: 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 lento, y en dos no aumentó en absoluto. Una 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, el contador podría configurarse para monitoreo.

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

Revertimos la configuración de fragmentación en otros servidores, donde el nuevo monitoreo se incendió, y en algún lugar asignamos aún más memoria para los fragmentos que antes por defecto (esto era udp-statistics, cuya pérdida parcial no era notable en el contexto general).

Las preguntas mas importantes


¿Por qué los paquetes se fragmentan en nuestro equilibrador L3? La mayoría de los paquetes que llegan de los usuarios a los balanceadores son SYN y ACK. Los tamaños de estas bolsas son pequeños. Pero dado que la proporción de dichos paquetes es muy grande, en su contexto no notamos la presencia de paquetes grandes que comenzaron a fragmentarse.

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

¿Por qué no ayudó Sysctl rollback, sino que reinició la ayuda? La reversión de Sysctl cambió la cantidad de memoria disponible para pegar paquetes. Al mismo tiempo, aparentemente el hecho mismo del desbordamiento de la memoria para los fragmentos condujo a la inhibición de las conexiones, lo que llevó al hecho de que los fragmentos se retrasaron en la cola durante mucho tiempo. Es decir, el proceso se está repitiendo.
Rebut anuló la memoria y todo estaba en orden.

¿Podría prescindir de la solución? Sí, pero existe un gran riesgo de dejar a los usuarios desatendidos en caso de un ataque. Por supuesto, el uso de Workaround como resultado condujo a varios problemas, incluida la inhibición de uno de los servicios por parte de los usuarios, pero sin embargo, creemos que las acciones estaban justificadas.

Muchas gracias a Andrei Timofeev ( atimofeyev ) por ayudar con la investigación, y a Alexei Krenev ( devicex ) por el trabajo titánico de actualizar Centos y kernels en los servidores. El proceso, que en este caso tuvo que iniciarse varias veces desde el principio, por lo que se prolongó durante muchos meses.

Source: https://habr.com/ru/post/475540/


All Articles