5 puntos por GN⁺ 2024-09-27 | 1 comentarios | Compartir por WhatsApp

Eliminar la causa raíz de las vulnerabilidades de seguridad de memoria

Un resultado paradójico

  • Cuando crece una base de código escrita en lenguajes sin seguridad de memoria, migrar las nuevas funcionalidades a lenguajes con seguridad de memoria reduce de forma significativa las vulnerabilidades de seguridad de memoria
  • Esto se debe a que las vulnerabilidades disminuyen de manera exponencial con el tiempo

Explicación matemática

  • La vida útil de las vulnerabilidades sigue una distribución exponencial
  • Las vulnerabilidades se generan principalmente en el código nuevo, y con el tiempo el código se vuelve más seguro
  • La densidad de vulnerabilidades en código con 5 años de antigüedad es entre 3.4 y 7.4 veces menor que en el código nuevo

Caso real en Android

  • Desde 2019, el equipo de Android comenzó a trasladar el nuevo desarrollo a lenguajes con seguridad de memoria
  • En 2024, las vulnerabilidades de seguridad de memoria se redujeron de 76% a 24%
  • A medida que disminuyeron las vulnerabilidades de seguridad de memoria, también bajó el riesgo total de seguridad

Evolución de la estrategia de seguridad de memoria

  • 1.ª generación: parches reactivos - un enfoque de encontrar y corregir vulnerabilidades
  • 2.ª generación: mitigación preventiva - un enfoque para dificultar la explotación de vulnerabilidades
  • 3.ª generación: detección preventiva de vulnerabilidades - un enfoque para encontrar vulnerabilidades por adelantado
  • 4.ª generación: prevención de alta confianza - un enfoque para prevenir que las vulnerabilidades ocurran cambiando a lenguajes con seguridad de memoria

Ventajas de la prevención de alta confianza

  • Rompe la competencia interminable entre defensores y atacantes
  • Mejora la seguridad y reduce costos mediante lenguajes con seguridad de memoria
  • Aumenta la corrección del código y la productividad de los desarrolladores

De las lecciones a la práctica

  • No es necesario desechar ni reescribir todo el código existente sin seguridad de memoria
  • Mejorar la interoperabilidad acelera la transición hacia lenguajes con seguridad de memoria
  • Desarrollo de herramientas para mejorar la interoperabilidad entre Rust y C++, y entre Rust y Kotlin

El papel de las generaciones anteriores

  • Uso selectivo de mitigación preventiva y detección
  • A medida que se avanza hacia código con seguridad de memoria, disminuye la necesidad de mitigación y detección

Conclusión

  • Usar lenguajes con seguridad de memoria en el código nuevo hace que las vulnerabilidades disminuyan de forma exponencial
  • Más de 6 años de resultados consistentes en Android demuestran la efectividad de este enfoque

Resumen de GN⁺

  • Es importante migrar a lenguajes con seguridad de memoria para reducir las vulnerabilidades de seguridad de memoria
  • El caso del equipo de Android muestra una gran reducción en las vulnerabilidades de seguridad de memoria
  • Mejorar la interoperabilidad es más práctico que reescribir por completo el código existente
  • Usar lenguajes con seguridad de memoria como Rust puede mejorar al mismo tiempo la seguridad y la productividad

1 comentarios

 
GN⁺ 2024-09-27
Opiniones en Hacker News
  • Es un artículo interesante. La idea central es que no hace falta reescribir el mundo entero.
    Solo mover el desarrollo nuevo a lenguajes con seguridad de memoria ya puede generar una mejora significativa, y es mucho más fácil y barato que portar todo para ver resultados.

    • Este resultado sugiere que, desde el punto de vista de la seguridad, los beneficios de una reescritura total son limitados. Se vuelve más rentable mantener el código legado maduro y usar lenguajes con seguridad de memoria solo para el código nuevo.
      Además, aumenta el valor de los lenguajes y herramientas que pueden integrarse de forma robusta con código legado inseguro.
    • Reescribir es tan caro que inevitablemente será muy poco frecuente, pero ir desgastando gradualmente desde los bordes es muy efectivo.
      También me pregunto si en el código muy antiguo existe algo parecido a una “curva de bañera”. Recuerdo que, cuando hubo vulnerabilidades graves de OpenSSL, quizá con Heartbleed, mucha gente miró el código y quedó horrorizada.
    • Desde la perspectiva de seguridad estoy de acuerdo, pero si quieres eliminar la recolección de basura o reducir el uso total de recursos, la historia puede ser distinta.
    • No sorprende en sí que, si todo el código nuevo se escribe en lenguajes con seguridad de memoria, bajen las vulnerabilidades de seguridad de memoria. La probabilidad de que el código nuevo introduzca problemas nuevos de seguridad de memoria es mayor que la de encontrar problemas existentes en código antiguo.
      Aun así, es impresionante que entre 2019 y 2023, sin reescrituras y solo agregando código nuevo en lenguajes con seguridad de memoria, probablemente en su mayoría Rust y algo de Kotlin, los problemas hayan bajado casi 60%. También me da curiosidad por qué hubo un periodo de estancamiento entre 2021 y 2023.
      Extrapolar las cifras de 2024 deja ver la intención de mostrar un buen número; habría sido más útil analizar y explicar por qué aumentaron de 2022 a 2023.
      También es una lástima que falte cuánto código nuevo con seguridad de memoria se escribió y en qué lenguajes. Parece que usan tanto Rust como Kotlin, pero me pregunto si Rust es el 95% o el 50%, y si Kotlin tiene una proporción grande, en qué áreas lo usan en lugar de Rust.
  • Los gráficos de este artículo se destacan por ser claros y concisos. Muestran muy bien cómo una selección cuidadosa de datos y un buen etiquetado pueden incorporar de forma natural en la prosa las ideas que se quieren transmitir.
    La conclusión del hecho de que las vulnerabilidades disminuyen exponencialmente es que hay que concentrarse en código puramente nuevo. Gastar recursos en enormes proyectos indiscriminados de “reescribir en Rust” es ineficiente incluso para el objetivo de maximizar la seguridad de memoria.
    Encaja bastante bien que la estrategia más sencilla, y la que recomiendan los expertos pragmáticos en Rust, también sea, según los datos, la mejor estrategia para minimizar vulnerabilidades de memoria.
    Me sorprendió la parte de que “el equipo de Android observó que la tasa de rollback de los cambios en Rust era menos de la mitad que la de C++”.

  • “Las vulnerabilidades disminuyen exponencialmente. Tienen una vida media. [...] Un estudio a gran escala sobre la vida útil de las vulnerabilidades, presentado en Usenix Security 2022, también confirmó este fenómeno. Los investigadores descubrieron que la gran mayoría de las vulnerabilidades existen en código nuevo o recientemente modificado”.
    Entonces, quizá para la seguridad convendría más dejar de agregar funciones nuevas que no sean estrictamente necesarias. Windows LTSC probablemente sea la versión más segura de Windows.

    • En el software mantenido, es cierto que los bugs individuales que aparecen durante el uso normal disminuyen con el tiempo. Si un bug causa problemas, alguien lo reporta, y algunos de esos bugs se corrigen. Ese es el mecanismo de disminución.
      Pero las vulnerabilidades que todavía no han sido explotadas no tienen ese mecanismo de disminución. No generan quejas de usuarios ni reportes de bugs; simplemente permanecen ahí hasta que un adversario con suficientes recursos y motivación las encuentra y las explota.
      Ahora hay más adversarios de ese nivel que antes.
    • La frase “las vulnerabilidades disminuyen exponencialmente” probablemente aplica no solo a las vulnerabilidades, sino también a todo tipo de bugs. También vi ese fenómeno en las pruebas de SBCL, un proyecto de software libre en el que participo. Los bugs nuevos tienden a aparecer en partes modificadas recientemente.
      Por supuesto, eso no significa que todos los bugs estén solo en código reciente. Todos habremos visto bugs que permanecieron sin detectarse durante años. En esos casos, hay que preguntarse por qué las pruebas no los detectaron.
      Por lo tanto, las pruebas deben concentrarse en el código modificado recientemente. En particular, las pruebas de mutación pueden aplicarse de manera muy enfocada al código modificado o al código fuertemente acoplado con él, lo que puede reducir mucho la carga de este tipo de pruebas.
      En Google existía un sistema que usaba pruebas de mutación de esta manera junto con las revisiones de código.
    • Aunque las funciones nuevas no sean estrictamente necesarias para vender software, el hardware nuevo, mejores algoritmos de seguridad y la eliminación completa de algoritmos existentes seguirán ocurriendo. Al final, entrará código nuevo.
    • También hay otro enfoque posible: compilar solo el subconjunto de funciones explícitamente necesarias.
      Claro que su practicidad varía mucho según el caso, pero debería ser una práctica más común de lo que es hoy.
    • Por eso aconsejo a la mayoría no usar de inmediato el último lanzamiento puntual de un lenguaje o biblioteca.
      Las versiones de vanguardia tienen muchas vulnerabilidades nuevas. Por lo general, el lanzamiento más antiguo que todavía recibe soporte suele ser el más seguro.
      Claro que hay excepciones cuando las funciones de una versión nueva agregan valor, pero en general creo que conviene evitar las versiones que terminan en “.0”.
  • Hay una correlación entre el código nuevo y las vulnerabilidades de memoria. La publicación del blog también ofrece una posible explicación: las vulnerabilidades tienen una vida media que disminuye rápidamente. Pero no entiendo por qué señalan que hay una relación causal entre ambos factores.
    Hay varias explicaciones razonables para esta correlación. El código nuevo suele estar relacionado con funcionalidades nuevas, y la gente se concentra en buscar vulnerabilidades en las funcionalidades nuevas. Además, el código antiguo ha pasado por más uso real y quizá haya ejecutado más condiciones de borde donde se escondían vulnerabilidades de memoria.
    No me siento cómodo diciendo que el código nuevo causa vulnerabilidades de memoria y que las vulnerabilidades tienen una vida media que disminuye rápidamente. En términos de conteo bruto puede ser, pero si pienso en vulnerabilidades de alto impacto en open source como Heartbleed o en bugs de invalidación de caché de CPU, no parece correcto desde el punto de vista del impacto.

    • Creo que puede verse como que el código más antiguo fue escrito por la mejor gente, con menos presión de tiempo.
      Mi empresa actual también es un ejemplo de eso. El código inicial lo escribieron los fundadores, y parte del código nuevo lo escriben contratistas con plazos apretados.
    • Este artículo toma datos interesantes de un proyecto y un lenguaje muy específicos e inusuales, dentro de una cultura muy específica e inusual, y los generaliza con fuerza a todo el código usando cifras tajantes.
      Si la conclusión de que “por ejemplo, usando la vida media promedio de las vulnerabilidades, el código de 5 años tiene una densidad de vulnerabilidades 3.4 veces menor que el código nuevo, y usando las vidas observadas en Android y Chromium, 7.4 veces menor” es correcta, ¿significa que si escribo código C lleno de defectos y lo dejo reposar offline durante 5 años se vuelve seguro?
      Este estudio tiene datos importantes y probablemente haya verdad debajo de lo compartido, pero molesta mucho la confianza con poco sustento y la extrapolación excesiva.
  • “Mejora de productividad: la codificación segura desplaza el descubrimiento de bugs más a la izquierda, antes del check-in del código, lo que aumenta la corrección del código y la productividad de los desarrolladores. Este cambio se refleja en métricas de tasa de rollback, como reversiones urgentes de código causadas por bugs inesperados”.
    “El equipo de Android observó que la tasa de rollback de los cambios en Rust es menos de la mitad que la de C++”.
    Llevo 20 años escribiendo código de producción a gran escala en varios lenguajes, pero cuando descubrí Rust en 2016 supe que este era el lenguaje en el que me iba a sumergir. También compré el libro de Klabnik y Carol ese mismo día, y todavía tengo el ejemplar en papel.
    Sinceramente, me devolvió el amor por programar.

    • Tiene sentido, considerando que la principal razón por la que tuve que hacer rollback de commits en C++ fueron crashes causados por haberme olvidado tontamente de verificar un puntero nulo. Si Rust evita ese problema y otros problemas de codificación igual de tontos, inevitablemente desaparece por completo una categoría de rollbacks.
  • El artículo habla de “lenguajes con seguridad de memoria (MSL)” en plural, pero el único lenguaje que menciona explícitamente como destino de la transición y para el que están mejorando la interoperabilidad es Rust.
    Kotlin también se menciona en el contexto de mejorar la interoperabilidad Rust<>Kotlin, y tiene cierto grado de seguridad de memoria, pero no sé si está al mismo nivel que Rust. Me pregunto si Google usa solo esos dos, o si también se refiere a otros lenguajes.

    • La gente interesada en este problema, especialmente en los últimos años, ha preferido el encuadre de “lenguajes con seguridad de memoria” frente a “lenguajes sin seguridad de memoria”.
      Básicamente, la raíz del problema es si son seguros por defecto o no. Es una forma de centrarse en la causa raíz, en vez de señalar o recomendar un lenguaje específico.
      En el caso de Android, que es el tema de este artículo, no conozco intentos de migrar a lenguajes con seguridad de memoria distintos de esos dos. No sigo todo el desarrollo de Android, pero sí leo bastantes artículos de este tipo, y no recuerdo que traten otros lenguajes además de Rust o Kotlin.
    • No es una historia solo de Rust. Reescribir servicios de red en C usando Java, Python o Go también es un ejemplo de transición a un lenguaje con seguridad de memoria.
      Lo central es que tu código no quede expuesto a bugs de seguridad de memoria. Si no es estrictamente necesario, quizá sea mejor elegir un lenguaje que no tenga la gestión manual de memoria al estilo Rust.
    • Android trató con más detalle los lenguajes con seguridad de memoria que usa en una publicación anterior del blog: https://security.googleblog.com/2022/12/memory-safe-language...
      Google también publicó su perspectiva sobre la seguridad de memoria en https://security.googleblog.com/2024/03/secure-by-design-goo..., donde también cubre lenguajes con seguridad de memoria que usa, como Java, Go y Rust.
    • Hay muchos lenguajes con seguridad de memoria y Google también usa varios, como Rust, Python, Java y Go. Pero el código de bajo nivel de Android históricamente fue C++, y en esa área la principal alternativa con seguridad de memoria para lo nuevo es Rust.
    • Java y Kotlin se usan para apps, y Rust se usa para nuevo software de sistema.
      En Google en general, Go se usa para parte del software de sistema, pero no he visto que se use en Android.
  • Si la vida de las vulnerabilidades sigue una distribución exponencial, el argumento es que concentrarse en valores predeterminados seguros como la seguridad de memoria en el código nuevo tiene un valor desproporcionado, tanto teóricamente como según 6 años de datos del codebase de Android.
    Sorprendente. Es la primera vez que veo usar este tipo de argumento para respaldar guardrails de seguridad que desplazan la seguridad hacia la izquierda, y es excelente. Es especialmente útil para codebases legacy grandes donde alguien podría decir: “si en 100 millones de líneas legacy de C++ no vamos a ver los beneficios de la seguridad de memoria, ¿para qué hacerlo?”.
    También parece implicar que incluso la detección ligera de vulnerabilidades puede aportar beneficios desproporcionadamente grandes. No mirando el backlog, sino solo el código nuevo y las dependencias nuevas.

  • La conclusión que se saca acá me resulta un poco incómoda. No se aborda una objeción obvia: que quizá se encuentran menos vulnerabilidades porque se revisa el código viejo con menos dedicación
    Es mucho más común mirar el historial de commits reciente que revisar una biblioteca que no cambió en 20 años

    • El código viejo tampoco se revisaba con tanta dedicación antes, y no veo que eso haya cambiado
      El texto no ofrece una teoría sobre por qué el código viejo tiene menos bugs, pero mi explicación es simple: porque ya se encontraron
      Si asumimos que cualquier código tiene una cantidad fija de bugs desconocidos por cada 1000 líneas, con el paso del tiempo se acumulan más ejecuciones en producción con entradas variadas, lo que aumenta la probabilidad de encontrarlos. Con las correcciones y las revisiones de código durante ese proceso, se puede esperar que en promedio mejore
      Por eso, con el tiempo, la cantidad de bugs por cada 1000 líneas de código existente disminuye. En cierto modo, pasó por una validación en el mundo real
      Como dice el artículo, si se siguen introduciendo bugs nuevos al mismo ritmo, no hay progreso; pero si los lenguajes con seguridad de memoria hacen que las funcionalidades nuevas introduzcan menos bugs, con el tiempo la cantidad total de bugs debería bajar
    • No entiendo bien este punto. El objeto de revisión es Android, y la gente busca vulnerabilidades de forma manual y automática basándose en el código fuente y los binarios, no en el historial de commits. No veo por qué el historial de commits sería relevante para encontrar bugs
      Los commits solo se usan para atribución. Si una biblioteca vieja que no cambió en 20 años pasó 20 años de fuzzing y revisión manual de código, es muy probable que sea bastante sólida
    • No me convenció del todo la afirmación de que el código viejo tiene menos vulnerabilidades. Podría haber factores, además de la antigüedad, que expliquen la diferencia
      Por ejemplo, durante los últimos años los ingenieros podrían haberse concentrado en reescribir las partes más riesgosas en lenguajes con seguridad de memoria, y haber tocado menos el código viejo de menor riesgo
      O cambios en procesos o personal podrían haber llevado a un aumento de defectos
      Aun así, la explicación de que cada bug tiene cierta probabilidad de ser encontrado por unidad de tiempo, y que con el tiempo quedan menos defectos, suena plausible. Si los mantenedores corrigen más vulnerabilidades de las que introducen nuevas, es probable que el código viejo tenga menos vulnerabilidades y que las que quedan sean más difíciles de encontrar
    • Lo que importa no son las vulnerabilidades teóricas, sino las que se explotan en la práctica. Si los atacantes no intentan encontrar vulnerabilidades en cierto código, en la práctica es como si no existieran
  • Me pregunto cómo se aplica esta lógica a Mac y Windows, considerando que la mayoría del código nuevo de Mac se escribe en Swift, que es seguro en memoria, mientras que Windows todavía usa principalmente C o C++

  • Si pensamos en la etapa final, mientras más raras se vuelven las vulnerabilidades, más valiosas son. Las vulnerabilidades restantes serán acumuladas cuidadosamente por actores estatales y reservadas para objetivos de alto valor
    Si esta entrada de blog describe la cuarta generación, la quinta podría parecerse al Lockdown Mode de iOS: permitir que los usuarios preocupados por la seguridad activen una casilla para aumentar la seguridad a costa de menor rendimiento
    La casilla ideal detectaría y capturaría ataques mediante algo como virtualización, y luego los enviaría para que el equipo de seguridad los analice. Eso crea un efecto disuasivo para los atacantes. No querrán quemar una vulnerabilidad escasa si existe la posibilidad de que el usuario haya activado esa casilla de seguridad, y muchos objetivos de alto valor la activarán
    Es inmunidad colectiva contra vulnerabilidades de software, no contra patógenos biológicos
    Los usuarios con alta conciencia de seguridad probablemente también tengan alta conciencia de privacidad. Por eso, en lugar de transmitir silenciosamente toda la actividad del usuario, si se detecta un ataque se debería mostrar una notificación al usuario. Mostrar unos pocos KB de actividad de red anómala podría ser suficiente para que el equipo de seguridad reconstruya el ataque, y se podría pedir la aprobación del usuario antes de compartirlo