Código y seguridad de la memoria.
La seguridad en la gestión de memoria es un problema persistente que ha afectado a la industria del software durante décadas. Se estima que más del 65% de las vulnerabilidades críticas en el software de propósito general tiene su origen en fallos de seguridad en memoria, lo que ha permitido desde infecciones masivas de malware hasta ataques dirigidos contra infraestructuras críticas.
Lenguajes como C y C++, fundamentales en la construcción del ecosistema de software actual, han sido responsables de muchas de estas vulnerabilidades debido a su gestión manual de memoria y la falta de mecanismos intrínsecos para evitar accesos indebidos (claro, más allá de los que ofrece el propio sistema operativo). A pesar de las mejoras en herramientas de mitigación y buenas prácticas, los avances en técnicas de explotación han convertido la seguridad en memoria en una carrera armamentista constante.
Sin embargo, estamos en un punto de inflexión: existen tecnologías con la capacidad de romper esta tendencia y permitir una adopción generalizada de software seguro por diseño. Para lograrlo, la industria necesita un estándar unificado y neutral que defina los requisitos para software seguro en memoria.
¿Por qué seguimos fallando en la seguridad en memoria?
El problema de seguridad en memoria no es nuevo. Desde el Morris Worm de 1988, que explotó vulnerabilidades en la gestión de buffers de UNIX, hasta ataques recientes como Spectre, Heartbleed y BlueKeep, los errores en el acceso a memoria han demostrado ser un punto de entrada privilegiado para atacantes. Hay que ser conscientes de que en las clases de ingeniería hay que prestarle más atención, y es por esto que en la asignatura de Periféricos e Interfaces se le presta mucha atención a los accesos a memoria desde la IOMMU, MMU, chipset, PCI, DMA, etc.
Pero centrándonos eminentemente en el software, algunas razones clave por las que persisten estos fallos incluyen:
Base de código heredada en C/C++: Con miles de millones de líneas de código aún activas (y depositadas en repositorios de Github – de donde aprende CoPilot), la migración a alternativas más seguras es costosa y compleja.
Estrategias de mitigación parciales: Técnicas como ASLR (Address Space Layout Randomization), DEP (Data Execution Prevention) o CFI (Control Flow Integrity) dificultan la explotación, pero no eliminan las vulnerabilidades subyacentes. Son sólo controles desde el Sistema Operativo, que recordemos que se apoya sobre el sistema de arranque (de hecho hay mucho esfuerzo invertido en hacerlo seguro, algo que también analizamos en Periféricos e Interfaces).
Ausencia de un marco estandarizado: No existe un conjunto de reglas universalmente aceptado que defina cuándo un sistema puede considerarse «seguro en memoria». Ni lo habrá. Porque la «Seguridad NO es un ESTADO es un PROCESO de continua EVOLUCIÓN«.
Tecnologías emergentes que ayudan en la seguridad en memoria
A pesar de estos desafíos, el panorama está cambiando con la maduración de diversas tecnologías enfocadas en prevenir vulnerabilidades en lugar de mitigarlas después de su explotación.
Lenguajes de programación con seguridad en memoria
- Rust: Garantiza la seguridad en memoria sin necesidad de recolección de basura, usando un sistema de propiedad y préstamos de referencias.
- Swift y SPARK: Incorporan verificaciones estáticas para evitar accesos inseguros a memoria.
- Subconjuntos seguros de C++: Iniciativas como Core Guidelines y RustBelt buscan limitar el uso inseguro de punteros y asignación dinámica en C++.
Protecciones de hardware avanzadas
- CHERI (Capability Hardware Enhanced RISC Instructions): Permite gestionar accesos a memoria de forma más granular, evitando accesos no autorizados incluso en código vulnerable.
- Memory Tagging (MTE en ARMv9): Detecta accesos indebidos en tiempo de ejecución mediante etiquetas de memoria.
Verificación formal y análisis estático
- Herramientas como Coq, Isabelle o Lean permiten demostrar matemáticamente que el código cumple propiedades de seguridad en memoria.
- CompCert (compilador formalmente verificado) garantiza que las optimizaciones del compilador no introducen errores de memoria.
Compartimentalización y aislamiento
- Técnicas como eBPF, WASM y sandboxing limitan el impacto de vulnerabilidades al restringir el acceso a recursos críticos.
Desafíos y estrategias de adopción
La implementación de un estándar global no será sencilla. Hay obstáculos importantes como:
Compatibilidad con código ya desarrollado e implantado: No es viable reescribir desde cero todo el software en lenguajes seguros, por lo que es necesario facilitar transiciones graduales.
Resistencia del ecosistema industrial: Empresas con software basado en C/C++ pueden percibir los cambios como una carga innecesaria en costes y tiempos de desarrollo.
Evaluación de costes frente a beneficios: Muchas organizaciones aún priorizan rendimiento sobre seguridad, sin considerar los costes ocultos de vulnerabilidades explotadas.
El escenario se complica con Copilot
La seguridad en memoria ya no es un lujo, sino una necesidad urgente. La tecnología está lista para el cambio, pero falta un esfuerzo coordinado entre academia, industria y gobierno para lograr una adopción efectiva. No podemos avanzar de manera descoordinada. Y un ejemplo claro es el de la iniciativa en la implantación de la IA. En el que los tres sectores, me da la impresión, que caminan sin comunicación.
Los modelos de inteligencia artificial aplicados al desarrollo de software, como GitHub Copilot, han revolucionado la productividad de los programadores. Copilot, basado en modelos de lenguaje como GPT, sugiere fragmentos de código basándose en un entrenamiento masivo con código público de GitHub. El problema es que estos modelos aprenden tanto código seguro como código plagado de vulnerabilidades.
Un estudio reciente analizó el código generado por Copilot en 89 escenarios que representaban tareas comunes de programación, evaluando su seguridad frente a los 25 principales tipos de vulnerabilidades listados por MITRE CWE. Los resultados: el 40% del código generado por Copilot contenía vulnerabilidades de seguridad.
Y es que este modelo, en particular, aprende de todo el código almacenado en Github y como cabría esperar se encuentran prácticas de programación obsoletas en el código generado.Inconsistencias en la calidad del código según el lenguaje de programación: Copilot es más seguro en Python que en C o C++, donde la gestión manual de memoria aumenta el riesgo. Falta de mecanismos de seguridad en el entrenamiento del modelo: «Garbage in, garbage out», Copilot absorbe patrones de código inseguro y los replica sin control.
Se deben usar modelos auditados. Digo yo, o al menos garantizar:
- Implementación de modelos de verificación automática para detectar vulnerabilidades antes de la producción.
- Revisión humana obligatoria en proyectos críticos, evitando una dependencia ciega de la IA.
- Regulaciones como ISO/SAE 21434 (Automoción) y Common Criteria (Infraestructura Crítica) deben incluir requisitos específicos para código generado por IA. Y esto es casi un «must».