V8: un año con Specter

El 3 de enero de 2018, Google Project Zero y otros revelaron los primeros tres de una nueva clase de vulnerabilidades que afectan a los procesadores de ejecución especulativa. Se llamaron Spectre (1 y 2) y Meltdown . Mediante el uso de mecanismos especulativos de ejecución de CPU, un atacante puede omitir temporalmente las verificaciones de seguridad de software explícitas e implícitas que evitan que los programas lean datos inaccesibles en la memoria. Si bien la ejecución especulativa se diseñó como parte de la microarquitectura, invisible a nivel arquitectónico, los programas cuidadosamente diseñados podían leer información inaccesible en un bloque especulativo y revelarla a través de canales laterales, como el tiempo de ejecución de un fragmento de programa.

Cuando se demostró que los ataques de Spectre son posibles usando JavaScript, el equipo V8 participó en la solución del problema. Formamos un equipo de respuesta de emergencia y trabajamos estrechamente con otros equipos de Google, nuestros otros socios de navegador y socios de hardware. Junto con ellos, llevamos a cabo de manera proactiva tanto la investigación ofensiva (la construcción de módulos de ataque para probar el concepto) como la defensiva (mitigando posibles ataques).

El ataque Spectre consta de dos partes:

  • Fuga de datos inaccesibles al estado latente de la CPU . Todos los ataques de Spectre conocidos utilizan la especulación para transferir bits de datos inaccesibles a los cachés de la CPU.
  • Recuperando un estado oculto para restaurar datos inaccesibles. Para esto, un atacante necesita un reloj con suficiente precisión. (Sorprendentemente baja precisión, especialmente con métodos como el umbral de borde, en comparación con un umbral a lo largo de un contorno seleccionado).

Teóricamente, sería suficiente para bloquear cualquiera de los dos componentes del ataque. Como no sabemos cómo bloquear completamente ninguno de ellos, desarrollamos e implementamos mitigaciones que reducen significativamente la cantidad de información que se filtra a las cachés de CPU y mitigaciones que dificultan la recuperación de un estado oculto.

Temporizadores de alta precisión.


Los pequeños cambios de estado que quedan después de la ejecución especulativa producen diferencias temporales muy pequeñas, casi imposiblemente pequeñas, del orden de una billonésima de segundo. Para detectar directamente tales diferencias individuales, el atacante necesita un temporizador de alta precisión. Los procesadores ofrecen dichos temporizadores, pero la plataforma web no los configura. El temporizador más preciso en la plataforma web performance.now() tenía una resolución de varios microsegundos, que inicialmente se consideró inadecuado para este propósito. Sin embargo, hace dos años, un grupo de investigación especializado en ataques de microarquitectura publicó un artículo sobre temporizadores en una plataforma web. Llegaron a la conclusión de que la memoria compartida mutable simultánea y varios métodos de recuperación de resolución permiten la creación de temporizadores de resolución aún mayor, hasta el nanosegundo. Dichos temporizadores son lo suficientemente precisos como para detectar aciertos y errores individuales del caché L1. Es él quien generalmente se utiliza para capturar información en los ataques de Spectre.

Protección del temporizador


Para interrumpir la capacidad de detectar pequeñas diferencias en el tiempo, los desarrolladores de navegadores han elegido un enfoque multilateral. En todos los navegadores, la resolución de performance.now() se redujo (en Chrome de 5 microsegundos a 100) y se introdujo la fluctuación de fase aleatoria para evitar la restauración de la resolución. Después de las consultas entre los desarrolladores de todos los navegadores, juntos decidimos dar un paso sin precedentes: deshabilitar de forma inmediata y retroactiva la API SharedArrayBuffer en todos los navegadores para evitar la creación de un temporizador de nanosegundos.

Ganar


Al comienzo de nuestra investigación ofensiva, quedó claro que las mitigaciones de temporizador por sí solas no son suficientes. Una de las razones es que un atacante puede simplemente ejecutar su código repetidamente para que la diferencia de tiempo acumulativa sea mucho más que un acierto o un error de caché. Pudimos construir "dispositivos" confiables que usan muchas líneas de caché a la vez, hasta la capacidad total de caché, lo que da una diferencia de tiempo de hasta 600 microsegundos. Más tarde, descubrimos métodos de amplificación arbitrarios que no están limitados por la capacidad de caché. Dichos métodos de amplificación se basan en intentos repetidos de leer datos secretos.

Protección JIT


Para leer datos inaccesibles utilizando Spectre, un atacante obliga a la CPU a ejecutar especulativamente código que lee datos normalmente inaccesibles y los coloca en la memoria caché. La protección puede considerarse desde dos lados:

  1. Prevenir la ejecución de código especulativo.
  2. Prevención de la lectura de datos inaccesibles de la tubería especulativa.

Experimentamos con la primera opción insertando instrucciones recomendadas para evitar la especulación, como LFENCE de Intel, en cada rama condicional crítica y usando retpolins para ramas indirectas. Desafortunadamente, tales mitigaciones pesadas reducen significativamente la productividad (desaceleración 2-3x en el punto de referencia Octane). En cambio, tomamos el segundo enfoque al insertar secuencias de mitigación que evitan que se lean datos confidenciales debido a especulaciones inadecuadas. Permítanme ilustrar la técnica con el siguiente fragmento de código:

 if (condition) { return a[i]; } 

Por simplicidad, asumimos que la condición 0 o 1 . El código anterior es vulnerable si la CPU lee especulativamente de a[i] cuando i fuera de rango, obteniendo acceso a datos normalmente inaccesibles. Una observación importante es que en este caso, la especulación intenta leer a[i] cuando la condición es 0 . Nuestra mitigación reescribe este programa para que se comporte exactamente igual que el programa original, pero no permita que se filtren datos cargados especulativamente.

Reservamos un registro de CPU, que llamamos "veneno", para realizar un seguimiento de si el código se está ejecutando en una rama malinterpretada. El registro de envenenamiento es compatible con todas las ramas y llamadas del código generado, por lo que cualquier rama interpretada incorrectamente hace que el registro de envenenamiento se convierta en 0 . Luego medimos todos los accesos a la memoria para que enmascaren incondicionalmente el resultado de todas las descargas con el valor actual del registro de envenenamiento. Esto no impide que el procesador prediga (o malinterprete) las ramas, pero destruye la información (potencialmente fuera de los límites) de los valores cargados debido a las ramas interpretadas incorrectamente. El código de la herramienta se muestra a continuación ( a es una matriz de números).

 let poison = 1; // … if (condition) { poison *= condition; return a[i] * poison; } 

El código adicional no afecta el comportamiento normal (definido por la arquitectura) del programa. Solo afecta el estado micro-arquitectónico cuando se trabaja en una CPU con ejecución especulativa. Si instrumenta un programa en el nivel del código fuente, las optimizaciones avanzadas en compiladores modernos pueden eliminar dicha instrumentación. En V8, evitamos que el compilador elimine las mitigaciones al insertarlas en una etapa muy tardía de la compilación.

También utilizamos esta técnica de envenenamiento para evitar fugas de ramas indirectas en el bucle de código de bytes del intérprete y en la secuencia de llamadas a funciones de JavaScript. En el intérprete, establecemos el veneno en 0 si el controlador de bytecode (es decir, una secuencia de código de máquina que interpreta un bytecode) no coincide con el bytecode actual. Para las llamadas de JavaScript, pasamos la función de destino como un parámetro (en el registro) y establecemos el veneno en 0 al comienzo de cada función si la función de destino entrante no coincide con la función actual. Con este ablandamiento, vemos una desaceleración de menos del 20% en el índice de referencia de octano.

La mitigación para WebAssembly es más simple, ya que la comprobación de seguridad principal es garantizar que el acceso a la memoria esté dentro de los límites. Para las plataformas de 32 bits, además de las comprobaciones de límites habituales, llenamos toda la memoria a la siguiente potencia de dos y enmascaramos incondicionalmente los bits superiores del índice de memoria del usuario. Las plataformas de 64 bits no necesitan esa mitigación, ya que la implementación utiliza protección de memoria virtual para las comprobaciones de fronteras. Experimentamos compilando declaraciones de interruptor / caso en código de búsqueda binario en lugar de usar una rama indirecta potencialmente vulnerable, pero es demasiado costoso para algunas cargas de trabajo. Las llamadas indirectas están protegidas por retpolins.

Protección de software: no confiable


Afortunada o desafortunadamente, nuestra investigación ofensiva progresó mucho más rápido que la defensiva, y rápidamente encontramos imposible mitigar mediante programación todas las posibles fugas durante los ataques de Spectre. Hay varias razones para esto. Primero, los esfuerzos de ingeniería para combatir a Spectre son desproporcionados al nivel de amenaza. En V8, nos encontramos con muchos otros riesgos de seguridad que son mucho peores, desde leer directamente fuera de las fronteras debido a errores comunes (que es más rápido y más fácil que Spectre), escribir fuera de las fronteras (esto es imposible con Spectre y peor) y el control remoto potencial ejecución de código (imposible con Spectre y mucho, mucho peor). En segundo lugar, las medidas de mitigación cada vez más sofisticadas que desarrollamos e implementamos conllevaron una complejidad significativa, que es una obligación técnica y en realidad puede aumentar la superficie de ataque y la sobrecarga de rendimiento. En tercer lugar, probar y mantener la mitigación de las fugas microarquitectónicas es aún más difícil que diseñar los propios dispositivos para un ataque, ya que es difícil asegurarse de que las mitigaciones continúen funcionando de la manera en que fueron diseñadas. Al menos una vez, las mitigaciones importantes se deshicieron efectivamente mediante optimizaciones posteriores del compilador. Cuarto, descubrimos que mitigar efectivamente algunas opciones de Spectre, especialmente la opción 4, simplemente no es posible en el software, incluso después de los heroicos esfuerzos de nuestros socios de Apple para tratar el problema en su compilador JIT.

Aislamiento del sitio


Nuestra investigación condujo a la conclusión: en principio, el código no confiable puede leer todo el espacio de direcciones de un proceso utilizando Spectre y canales laterales. Las mitigaciones de software reducen la efectividad de muchos dispositivos potenciales, pero no son efectivas ni integrales. La única medida efectiva es mover datos confidenciales fuera del espacio de direcciones del proceso. Afortunadamente, Chrome ha estado intentando durante muchos años separar los sitios en diferentes procesos para reducir la superficie de ataque debido a vulnerabilidades comunes. Estas inversiones dieron sus frutos, y para mayo de 2018 llegamos a la etapa de preparación y expandimos el aislamiento de los sitios en la cantidad máxima de plataformas. Por lo tanto, el modelo de seguridad de Chrome ya no asume la privacidad del idioma durante el proceso de representación.

Specter ha recorrido un largo camino y enfatizó los méritos de la colaboración de desarrolladores en la industria y la academia. Hasta ahora, los sombreros blancos están por delante de los negros. Todavía no conocemos un solo ataque real, con la excepción de experimentadores curiosos e investigadores profesionales que desarrollan dispositivos para probar el concepto. Nuevas variantes de estas vulnerabilidades continúan apareciendo y esto continuará por algún tiempo. Continuamos monitoreando estas amenazas y tomándolas en serio.

Al igual que muchos programadores, también pensamos que los lenguajes seguros proporcionan el borde correcto para la abstracción, evitando que los programas bien escritos lean memoria arbitraria. Es triste que esto haya resultado ser un error: esta garantía no corresponde al equipo actual. Por supuesto, todavía creemos que los lenguajes seguros tienen más ventajas de ingeniería, y el futuro depende de ellos, pero ... en el equipo de hoy se filtran un poco.

Los lectores interesados ​​pueden profundizar en el tema y obtener información más detallada en nuestro artículo científico .

Source: https://habr.com/ru/post/449546/


All Articles