¿Cómo funciona la vulnerabilidad Spectre?

Estándar

Spectre y Meltdown explotan el mismo defecto en el microprocesador, pero usan diferentes métodos para acceder a la información que debería estar protegida. Lo que ocurre en ambos casos es que el CPU está hecho para ejecutar instrucciones que nunca deberían ejecutarse como parte de su hardware de ejecución especulativa. Una vez que el procesador descubre que no debería haber llevado a cabo tales instrucciones las elimina todas, pero los rastros de las instrucciones que se llevaron a cabo aún permanecen en el caché y pueden ser accedidos de una forma indirecta. Para encontrar los datos que están en la memoria caché solo se necesita una comparación de los tiempos de acceso para revelar cuál de un posible conjunto de datos ha sido procesado. Los datos que se almacenaron en la memoria caché le dan el valor de los datos restringidos a los que nunca debería haber tenido acceso.

El exploit Meltdown utiliza un intento de acceso al espacio de direcciones del kernel del sistema operativo para lanzar una excepción, pero no antes de que la ejecución especulativa haya recuperado y utilizado los datos fuera de límites y haya dejado un rastro en la memoria caché. Meltdown es un exploit dirigido al núcleo y, como tal, es bastante fácil de contrarrestar al mantener separadas las direcciones del kernel y del usuario, y esta es la base del arreglo que se está implementando para la mayoría de los sistemas operativos en este momento.

Sin embargo la vulnerabilidad Specter es mucho más difícil de manejar, aunque se basa en los mismos principios que Meltdown, porque se puede usar para atacar cualquier programa y ese programa no necesita tener ningún defecto para ser vulnerable.

Spectre viene en dos formas. El primero es un clásico ataque de desbordamiento de búfer. Sin embargo, en este caso, el búfer está protegido contra el desbordamiento y aún así se desborda. El mecanismo se parece mucho a Meltdown, pero hay algunas diferencias.

Considera lo siguiente:

if (x < array1_size)  
            y = array2[array1[x] ];

Siempre que x sea más pequeño que el array1_size, no pasa nada malo y la prueba es para evitar que x vaya más allá del final de array1, que es exactamente lo que todo buen programador debería hacer. Sin embargo, este análisis ignora la predicción de ramas y la ejecución especulativa. La mayoría de los procesadores modernos llevan un registro de la frecuencia con que se toma una rama y esto se usa para predecir lo que sucederá. En este caso, supongamos que la predicción de bifurcación es que x es usualmente menor que array1_size, entonces es razonable ejecutar la instrucción especulativamente antes de que la condición haya sido evaluada. Podemos hacer arreglos para que array1_size no esté en la caché y, por lo tanto, el tiempo para evaluar la condición es relativamente alto en comparación con el tiempo para ejecutar especulativamente las siguientes instrucciones. También podemos asegurarnos de que el predictor de bifurcación piense que la instrucción es la próxima instrucción que se va a producir, capacitándola con muchos ejemplos válidos de la prueba y el acceso a la matriz. Spectre necesita un poco de preparación para trabajar bien.

Cuando la condición se evalúa finalmente, el procesador se da cuenta de su error y descarta el cálculo, pero en este punto el contenido de array1[x], que está en la memoria más allá del final de la matriz, se ha utilizado para buscar un elemento de la array2 que ahora está en el caché. Tenga en cuenta que los contenidos de la array1[x] no están en la memoria caché, pero esto no tiene importancia porque, al encontrar qué elemento de la array2 está en la memoria caché, podemos deducir la array1[x]. Todo lo que necesitamos hacer es acceder a cada elemento de array2 y medir el tiempo que lleva: el acceso rápido es el que está en el caché.

Este es un ataque bastante fácil y funciona con programas que aparentemente no son vulnerables al desbordamiento de búfer. Esto hace que solucionar el problema sea mucho más difícil. La única solución directa al problema es cambiar el microcódigo del microprocesador para que el caché no tenga ningún cambio durante la ejecución especulativa y por el momento, no hay signos de esto, aunque Intel está haciendo comentarios sobre cómo solucionar el problema.

Lo sorprendente es que Spectre puede implementarse en un navegador haciendo uso nada más que de JavaScript. Esto es sorprendente porque se supone que los navegadores han sido hechos para que la medición del tiempo en alta resolución sea imposible para impedir ataques de fingerprinting y otros exploits similares. El paper donde se presenta Spectre tiene un interesante descripción del problema:

JavaScript no proporciona acceso a la instrucción rdtscp y Chrome degrada intencionalmente la precisión de su temporizador de alta resolución para disuadir ataques de tiempo usando performance.now(). Sin embargo, la función Web Worker de HTML5 simplifica la creación de un subproceso separado que disminuye repetidamente un valor en una ubicación de memoria compartida. Este enfoque produjo un temporizador de alta resolución que proporcionó una resolución suficiente.

No está claro de lo descrito anteriormente que Intel no fallara en pensar en las implicaciones de lo que sucede cuando agrega una nueva característica.

Además de un simple desbordamiento de búfer, el otro mecanismo que Specter puede hacer uso de la ejecución especulativa de instrucciones en el código de la víctima es en condiciones en las que el código nunca debe ejecutarse. Para ello, el predictor de bifurcación debe ser entrenado para esperar un salto a una ubicación posiblemente ilegal. Parece que esto es posible porque el predictor de rama ignora cualquier error, según el papel de Spectre:

El predictor de rama aprende desde saltos a destinos ilegales. Aunque se desencadena una excepción en el proceso del atacante, se puede capturar fácilmente (por ejemplo, utilizando try … catch en C++). El predictor de rama hará predicciones que envíen otros procesos al destino ilegal.

Este es un exploit mucho más difícil de implementar en el mundo real porque necesita saber mucho sobre el código que está siendo atacado. Es muy similar en enfoque a un ataque de Programación Orientada a Retorno (ROP). Sin embargo, al igual que el desbordamiento de búfer, no necesita que el código atacado sea defectuoso de ninguna manera.

Es una característica tanto de Meltdown como de Spectre que trabajan con código que está escrito de una manera absolutamente perfecta desde el punto de vista de la seguridad.

Aunque no se pretende que sea una forma de “cómo corregir” las dos vulnerabilidades, vale la pena mencionar que los intentos actuales de solucionar los problemas realmente no solucionan su causa. La única solución real es cambiar la arquitectura del procesador para que la ejecución especulativa ya no provoque ningún cambio micro arquitectónico. En otras palabras, la ejecución especulativa tiene que hacer un mejor trabajo de limpieza después de sí misma. Esto solo se puede lograr mediante cambios en el microcódigo que controla el procesador. Intel que es quien debe emitir dichas actualizaciones y por lo general, se instalan como parte de una actualización del BIOS/UEFI. ¿Es esto lo que Intel prometió como solución hace unas pocas semanas?, aún no lo sabemos.

Pero de ser así, no deberían ser necesarias las correcciones del sistema operativo y del navegador que se están trabajando actualmente. Lo cuál puede ser una pista de que probablemente necesitaremos esperar una nueva familia de CPU para estar seguros de haber dado vuelta a la página.

Sin una corrección de microcódigo, el mecanismo básico de Meltdown y Spectre sigue operativo y es solo cuestión de tiempo antes de que alguien piense de otra manera para usar la ejecución especulativa, la predicción de bifurcación y el tiempo de caché para crear una nueva amenaza.

Aquí algunos links de fuentes usadas para este post sobre Meltdown: