CVE y probabilidad cuadrada

Hace aproximadamente un año, en julio de 2019, comenzamos a recibir informes de errores extraños en el kernel basado en RHEL7 en OpenVz. A primera vista, los errores eran diferentes: los nodos se bloquearon en diferentes lugares e incluso en diferentes subsistemas, pero cada vez que la investigación encontró uno u otro objeto "torcido". Los objetos eran diferentes, a veces se encontró algún tipo de basura allí, a veces un enlace a la memoria liberada, a veces se liberó el objeto en sí, pero en todos los casos la memoria para este objeto se asignó desde la caché kmalloc-192. Debajo del corte: una historia detallada sobre esta historia.



imagen



La solución de problemas habitual en tales casos es examinar cuidadosamente el ciclo de vida del objeto afectado: vea cómo se asigna la memoria para él, cómo se libera, cómo se toman y liberan correctamente los contadores de referencia, prestando especial atención a las rutas de error. Sin embargo, en nuestro caso, diferentes objetos fueron atrapados y revisando su ciclo de vida no encontraron errores.



La caché kmalloc-192 es bastante popular en el kernel, combina varias docenas de objetos diferentes. Un error en el ciclo de vida de uno de ellos es la razón más probable de este tipo de errores. Incluso enumerar todos esos objetos es bastante problemático, y no es cuestión de verificarlos todos. Continuaron llegando informes de errores, pero no logramos encontrar su causa mediante una investigación directa. Se requería una pista.



Por nuestra parte, estos errores fueron investigados por Andrey Ryabinin, un especialista en administración de memoria, ampliamente conocido en círculos estrechos de desarrolladores de kernel como el desarrollador de KASAN, una tecnología asombrosa para detectar errores de acceso a la memoria. De hecho, fue KASAN quien mejor se adaptó para descubrir las causas de nuestro error. KASAN no se incluyó en el kernel RHEL7 original, pero Andrey nos portó los parches necesarios en OpenVz. No incluimos KASAN en la versión de producción de nuestro kernel, pero está presente en la versión de depuración del kernel y ayuda activamente a nuestro QA a encontrar errores.















Además de KASAN, el kernel de depuración incluye muchas otras funciones de depuración que heredamos de Red Hat. Como resultado de la depuración, el kernel resultó ser bastante lento. QA dice que las mismas pruebas en un kernel de depuración tardan 4 veces más. Para nosotros, esto no es fundamental, no medimos el rendimiento allí, sino que buscamos errores. Sin embargo, tal desaceleración era inaceptable para los clientes, y nuestras solicitudes para poner un kernel de depuración en producción fueron invariablemente rechazadas.



Como alternativa a KASAN, se pidió a los clientes que habilitaran slub_debug en los nodos afectados... Esta tecnología también permite la detección de daños en la memoria. Usando una zona roja y el envenenamiento de la memoria para cada objeto, el asignador de memoria verifica si todo está en orden cada vez que asigna y libera memoria. Si algo sale mal, emite un mensaje de error, corrige el daño detectado si es posible y permite que el kernel continúe funcionando. Además, se almacena información sobre quién asignó y liberó por última vez un objeto, de modo que en el caso de la detección post-factum de corrupción de memoria, es posible entender "quién" era este objeto en una "vida pasada". Slub_debug se puede habilitar en la línea de comandos del kernel en un kernel de producción, pero estas comprobaciones también consumen memoria y recursos de la CPU. Para la depuración en desarrollo y control de calidad, esto es perfectamente aceptable, pero los clientes en producción lo usan sin mucho entusiasmo.



Han pasado seis meses, se acercaba el Año Nuevo. Las pruebas locales en el kernel de depuración con KASAN no detectaron el problema, no recibimos ningún informe de errores de los nodos con slub_debug habilitado, no pudimos encontrar nada en las materias primas y no encontramos el problema. Andrey estaba cargado con otras tareas, por el contrario, me salió un hueco y se me indicó que analizara el siguiente informe de error.



Después de analizar el volcado de memoria, pronto descubrí el objeto problemático kmalloc-192: su memoria estaba llena de algún tipo de basura, información perteneciente a otro tipo de objeto. Fue muy similar a las consecuencias de use-after-free, pero después de examinar cuidadosamente el ciclo de vida del objeto dañado en las materias primas, tampoco encontré nada sospechoso.



Revisé los informes de errores antiguos, traté de encontrar alguna pista allí, pero también fue en vano.



Finalmente volví a mi error y comencé a mirar el objeto anterior. También resultó estar en uso, pero era completamente incomprensible por su contenido lo que era: no había constantes, referencias a funciones u otros objetos. Después de rastrear varias generaciones de referencias a este objeto, finalmente descubrí que era un mapa de bits reductor. Este objeto era parte de la técnica de optimización para liberar memoria del contenedor. La tecnología fue desarrollada originalmente para nuestros núcleos, más tarde su autor Kirill Tkhai la incorporó a la línea principal de Linux.



"Los resultados muestran que el rendimiento aumenta al menos en 548 veces".



Varios miles de estos parches complementan el kernel RHEL7 original, estable como una roca, lo que hace que el kernel Virtuozzo sea lo más conveniente posible para los hosters. Siempre que sea posible, intentamos enviar nuestros desarrollos a la línea principal, ya que esto facilita el mantenimiento del código en buenas condiciones.



Después de seguir los enlaces, encontré una estructura que describe mi mapa de bits. El Descriptor creía que el tamaño del mapa de bits debería ser de 240 bytes, y esto no podía ser cierto de ninguna manera, ya que de hecho el objeto se asignó desde la caché kmalloc-192.



¡Bingo!



Resultó que las funciones que trabajaban con mapa de bits accedían a la memoria más allá de su límite superior y podían cambiar el contenido del siguiente objeto. En mi caso, hubo un recuento de referencia al comienzo del objeto, y cuando el mapa de bits lo anuló, la colocación posterior condujo a la liberación repentina del objeto. Más tarde, la memoria se asignó de nuevo para un nuevo objeto, cuya inicialización fue percibida como basura por el código del objeto antiguo, lo que tarde o temprano condujo inevitablemente al bloqueo del nodo.



imagen


¡Es bueno cuando puedes consultar con el autor del código!



Mirando su código con Kirill, pronto encontramos la causa raíz de la discrepancia detectada. A medida que aumentó el número de contenedores, el mapa de bits debería haber aumentado, pero dejamos de lado uno de los casos y, como resultado, a veces omitimos el mapa de bits de cambio de tamaño. En nuestras pruebas locales, esta situación no se encontró, y en la versión del parche que Kirill envió a la línea principal, el código fue reelaborado y no hubo ningún error allí.



Con 4 intentos, Kirill y yo trabajamos juntos para componer dicho parche , durante un mes lo ejecutamos en pruebas locales y a fines de febrero lanzamos una actualización con un kernel fijo. Verificamos selectivamente otros volcados de memoria, también encontramos el mapa de bits equivocado en el vecindario, celebramos la victoria y descartamos viejos errores a escondidas.



Sin embargo, las ancianas siguieron cayendo y cayendo. El goteo de este tipo de informes de errores se ha reducido, pero no se ha secado por completo.



En general, esto era lo esperado. Nuestros clientes son hosters. Odian reiniciar mucho sus nodos, porque reiniciar == tiempo de inactividad == perdió dinero. Tampoco nos gusta lanzar kernels con frecuencia. El lanzamiento oficial de la actualización es un procedimiento bastante laborioso que requiere ejecutar un montón de pruebas diferentes. Por lo tanto, los nuevos núcleos estables se lanzan aproximadamente trimestralmente.



Para garantizar la pronta entrega de correcciones de errores a los nodos de producción del cliente, utilizamos los parches en vivo ReadyKernel. En mi opinión, nadie más hace esto excepto nosotros. Virtuozzo 7 utiliza una estrategia inusual para usar parches en vivo.



Por lo general, el parche de vida es solo seguridad. En nuestro país, 3/4 de las correcciones son correcciones de errores. Correcciones de errores con los que nuestros clientes ya han tropezado o que pueden encontrar fácilmente en el futuro. Efectivamente, estas cosas solo se pueden hacer para su kit de distribución: sin los comentarios de los usuarios, no puede comprender qué es importante para ellos y qué no lo es.



Ciertamente, el parche en vivo no es una panacea. Generalmente, es imposible parchear todo en una fila, la tecnología no lo permite. Tampoco se agrega nueva funcionalidad de esta manera. Sin embargo, una parte importante de los errores se corrige con los parches de una línea más simples, que son excelentes para parches de por vida. En casos más complejos, el parche original tiene que ser "modificado creativamente con un archivo", a veces la maquinaria de parcheo en vivo tiene errores, pero nuestro asistente de la vida parcheando Zhenya Shatokhin conoce su trabajo perfectamente. Recientemente, por ejemplo, desenterróencantador error en kpatch , sobre el cual, por buenas razones , generalmente vale la pena escribir una ópera separada.



A medida que se acumulan las correcciones de errores apropiadas, generalmente una vez cada una o dos semanas, Zhenya lanza otra serie de parches en vivo ReadyKernel. Después del lanzamiento, vuelan instantáneamente a los nodos del cliente y evitan el ataque al rake que ya conocemos. Y todo esto sin reiniciar los nodos cliente. Y libera núcleos innecesariamente con frecuencia. Beneficios continuos.



Sin embargo, a menudo el parche en vivo llega a los clientes demasiado tarde: el problema que cierra ya ha ocurrido, pero el nodo, sin embargo, aún no se ha bloqueado.



Es por eso que la aparición de nuevos informes de errores con el problema que ya hemos solucionado no fue inesperada para nosotros. Analizarlos una y otra vez mostró síntomas familiares: kernel antiguo, basura en kmalloc-192, mapa de bits "incorrecto" delante y un parche en vivo descargado o cargado tardíamente con una solución.



OVZ-7188 de FastVPS , que llegó a fines de febrero, parecía uno de esos casos . “Muchas gracias por el informe de errores. Nuestras condolencias. Inmediatamente muy similar al problema conocido. Es una pena que no haya parches activos en OpenVZ. Espere una versión estable del kernel, cambie a Virtuozzo o use kernels inestables con una corrección de errores ".



Los informes de errores son una de las cosas más valiosas que nos brinda OpenVZ. Investigarlos nos da la oportunidad de detectar problemas serios antes de que intervenga cualquiera de los clientes gordos. Por lo tanto, a pesar del problema conocido, pedí que nos completaran los volcados de memoria.



Analizar el primero de ellos me desanimó un poco: no se encontró el mapa de bits "incorrecto" delante del objeto kmalloc-192 "torcido".



Un poco más tarde, el problema se reprodujo en el nuevo kernel. Y luego otro, otro y otro.



¡Ups!



¿Cómo es eso? ¿Sin arreglar? Verifiqué dos veces las materias primas: todo está bien, el parche está en su lugar, no se pierde nada.



¿De nuevo corrupción? ¿En el mismo lugar?



Tuve que resolverlo de nuevo.



imagen

(¿Qué es esto? Vea aquí )



En cada uno de los nuevos volcados de memoria, la investigación tropezó nuevamente con el objeto kmalloc-192. En general, un objeto de este tipo parecía bastante normal, pero al principio del objeto, siempre se encontraba la dirección incorrecta. Al rastrear la relación del objeto, encontré que se anularon dos bytes internos en la dirección.



in all cases corrupted pointer contains nulls in 2 middle bytes: (mask 0xffffffff0000ffff)
0xffff9e2400003d80
0xffff969b00005b40
0xffff919100007000
0xffff90f30000ccc0


En el primero de los casos enumerados, en lugar de la dirección "incorrecta" 0xffff9e2400003d80, la dirección "correcta" 0xffff9e24740a3d80 debería haber sido. En otros casos se encontró una situación similar.



Resultó que algún código extraño anuló nuestro objeto con 2 bytes. El escenario más probable es use-after-free, cuando un objeto, después de ser liberado, borra algún campo en sus primeros bytes. Revisé los objetos más utilizados, pero no encontré nada sospechoso. De nuevo un callejón sin salida.



FastVPSa petición nuestra, ejecuté el núcleo de depuración con KASAN durante una semana, pero no ayudó, el problema nunca se reprodujo. Pedimos registrar slub_debug, pero requirió reiniciar y el proceso llevó mucho tiempo. En marzo-abril, los nodos fallaron varias veces más, pero slub_debug se desactivó y esto no nos dio nueva información.



Y luego hubo una pausa, el problema dejó de reproducirse. Abril terminó, mayo pasó, no hubo nuevas caídas.



La espera terminó el 7 de junio; finalmente, el problema llegó al núcleo con slub_debug habilitado. Al comprobar la zona roja al liberar el objeto slub_debug, encontré dos bytes cero más allá de su límite superior. En otras palabras, resultó que no se usaba después de la aplicación gratuita, el objeto anterior era nuevamente el culpable. Había una estructura de aspecto normal nf_ct_ext. Esta estructura se refiere al seguimiento de la conexión, una descripción de la conexión de red que utiliza el firewall.



Sin embargo, todavía no estaba claro por qué estaba sucediendo esto.



Comencé a mirar conntrack: en uno de los contenedores, alguien llamó al puerto abierto 1720 usando ipv6. Por puerto y protocolo, encontré el nf_conntrack_helper correspondiente.



static struct nf_conntrack_helper nf_conntrack_helper_q931[] __read_mostly = {
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .data_len               = sizeof(struct nf_ct_h323_master),
                .tuple.src.l3num        = AF_INET, <<<<<<<< IPv4
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .tuple.src.l3num        = AF_INET6, <<<<<<<< IPv6
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
};


Al comparar las estructuras, noté que el helper ipv6 no definía .data_len. Entré en git para averiguar de dónde venía, descubrí un parche de 2012.



commit 1afc56794e03229fa53cfa3c5012704d226e1dec

Autor: Pablo Neira Ayuso <pablo@netfilter.org>

Fecha: Jue 7 de junio 12:11:50 2012 +0200



netfilter: nf_ct_helper: implementar datos privados de ayuda de longitud variable



Este parche usa las nuevas extensiones de conntrack de longitud variable.



En lugar de utilizar union nf_conntrack_help que contiene toda la

información de datos privados del ayudante, asignamos un

área de longitud variable para almacenar los datos del ayudante privado.



Este parche incluye la modificación de todos los ayudantes existentes.

También incluye un par de encabezados de inclusión para evitar la compilación.

advertencias.



El parche agregó un nuevo campo .data_len al asistente, que indica cuánta memoria necesitaba el controlador de conexión de red correspondiente. Se suponía que el parche definiría .data_len para todos los nf_conntrack_helpers disponibles en ese momento, pero no encontró la estructura que encontré.



Como resultado, resultó que la conexión a través de ipv6 al puerto abierto 1720 lanzó la función q931_help (), escribió en una estructura para la que nadie había asignado memoria. Un simple escaneo de puertos puso a cero un par de bytes, la transmisión de un mensaje de protocolo normal llenó la estructura con información más significativa, pero en cualquier caso, la memoria de otra persona estaba desgastada y, tarde o temprano, esto condujo al bloqueo del nodo.



Florian Westphal rediseñó el código nuevamente en 2017y eliminé .data_len, y el problema que descubrí pasó desapercibido.



A pesar de que el error ya no se encuentra en la línea principal del kernel de Linux actual, el problema fue heredado por los kernels de un montón de distribuciones de Linux, incluyendo RHEL7 / CentOS7, SLES 11 y 12, Oracle Unbreakable Enterprise Kernel 3 y 4, Debian 8 y 9 y Ubuntu 14.04 y 16.04 LTS.



El error se reprodujo trivialmente en el nodo de prueba, tanto en nuestro núcleo como en el RHEL7 original. Seguridad explícita: corrupción de memoria administrada de forma remota. Donde el puerto 1720 ipv6 está abierto, prácticamente ping de la muerte.



El 9 de junio hice un parche de una línea con una descripción vaga y lo envié a la línea principal. Envié una descripción detallada a Red Hat Bugzilla y la escribí por separado a Red Hat Security.



Otros eventos se desarrollaron sin mi participación.

El 15 de junio, Zhenya Shatokhin lanzó el parche en vivo ReadyKernel para nuestros viejos núcleos.

https://readykernel.com/patch/Virtuozzo-7/readykernel-patch-131.10-108.0-1.vl7/



El 18 de junio lanzamos un nuevo kernel estable en Virtuozzo y OpenVz.

https://virtuozzosupport.force.com/s/article/VZA-2020-043



El 24 de junio, Red Hat Security asignó una identificación de CVE al error

https://access.redhat.com/security/cve/CVE-2020-14305



Problema recibió un impacto moderado con un CVSS v3 Score 8.1 inusualmente alto y durante los días siguientes otras distribuciones de

SUSE respondieron al error de sombrero público https://bugzilla.suse.com/show_bug.cgi?id=CVE-2020-14305

Debian https: / /security-tracker.debian.org/tracker/CVE-2020-14305

Ubuntuhttps://people.canonical.com/~ubuntu-security/cve/2020/CVE-2020-14305.html



El 6 de julio, KernelCare lanzó un parche en vivo para las distribuciones afectadas.

https://blog.kernelcare.com/new-kernel-vulnerability-found-by-virtuozzo-live-patched-by-kernelcare



El 9 de julio, el problema se solucionó en los kernels estables de Linux 4.9.230 y 4.4.230.

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=linux-4.9.y&id=396ba2fc4f27ef6c44bbc0098bfddf4da76dc4c9 Las



distribuciones, sin embargo, aún no han cerrado el agujero ...



“Mira, Kostya”, le digo a mi compañero Kostya Khorenko, “¡nuestro proyectil golpeó el mismo cráter dos veces! Yo y un acceso más allá del final del objeto la última vez que conocí a nepoymi, y aquí nos visitó dos veces seguidas. Dime, ¿es como una probabilidad cuadrada? ¿O no cuadrado?

- La probabilidad es cuadrada, sí. Pero aquí tienes que mirar, ¿qué evento es la probabilidad? La probabilidad cuadrada del evento de que se hayan encontrado errores inusuales exactamente 2 veces seguidas. Está en una fila.



Bueno, Kostya es inteligente, lo sabe mejor.



All Articles