Eliminar las vulnerabilidades de seguridad de memoria desde el origen
(security.googleblog.com)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
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.
Además, aumenta el valor de los lenguajes y herramientas que pueden integrarse de forma robusta con código legado inseguro.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 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
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
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
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++
No encontré cifras recientes sobre el uso de lenguajes en versiones recientes de Windows, pero Microsoft está usando Rust tanto para desarrollo nuevo como para reescribir funcionalidades existentes [1] [2]
[0] Ver la sección “Evolution of the programming languages” https://blog.timac.org/2023/1128-state-of-appkit-catalyst-sw...
[1] https://www.theregister.com/2023/04/27/microsoft_windows_rus...
[2] https://www.theregister.com/2024/01/31/microsoft_seeks_rust_...
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