Las noticia de los últimos días en los medios especializados hablan sobre las vulnerabilidades Meltdown y Spectre que parecen afectar a una amplia gama de CPU actuales, particularmente en procesadores Intel que datan desde 1995 en adelante. La parte interesante de la historia es cómo funcionan estas vulnerabilidades y saber si existen PoC (Proof Of Concept) o exploits que hagan uso de las mismas. Además es interesante saber cómo fue posible que estas vulnerabilidades pasaran desapercibidas durante tanto tiempo sin que nadie lo notara.
De las dos vulnerabilidades Metldown, es la más fácil de comprender, implementar y aparentemente protegerse contra ella. Se deriva de la forma en que un procesador moderno intentará ejecutar programas más rápido mediante el uso de la “ejecución especulativa“. Si Ud. sabe cómo funcionan las CPU en el nivel más simple, se sorprenderá al descubrir cuán sofisticado se ha vuelto un procesador moderno. Por ejemplo, la mayoría de las CPU modernas ejecutarán código en paralelo, incluido el código que quizás nunca se necesite. El ejemplo más habitual es la predicción de bifurcación. Si el flujo de control se divide en una instrucción IF, entonces puede optimizar el uso del microprocesador, ejecutando la rama que considera más probable antes de realizar la evaluación de la condición de salto. Luego de la evaluación de la condición lógica puede resultar que evaluó la rama incorrecta, entonces descarta el código ejecutado y en su lugar ejecuta la otra rama del IF.
Todo esto parece muy inocente y la idea de la ejecución especulativa no parece ser un riesgo de seguridad. ¿Qué podría salir mal?
Pero existen mentes muy creativas, especialmente la de los “hackers” pueden ser capaces de transformar hasta la característica más inocente en una vulnerabilidad y así es como ocurrió con la ejecución especulativa. Supongamos que se escribe un código que carga algunos datos que se encuentran en un área protegida del sistema operativo, el espacio del kernel y utiliza estos datos para calcular la dirección de un elemento en una matriz, un código tan inocente como este:
data = getByte (kernalAddress)
variable = probeArray (datos)
La instrucción getByte devuelve un valor en el rango de 0 a 255 y esto se usa para acceder a un elemento de probeArray, que tiene 255 elementos de longitud. Si esto funciona, habría leído con éxito un byte de datos del espacio del kernel, algo que se supone que no deberíamos hacer porque se trata de datos privados y poe lo general es alli dónde se almacenan contraseñas, claves de cifrado, etc. De hecho, no funciona porque la memoria kernel está protegida y la instrucción getByte fallará con una excepción de tiempo de ejecución y el resto del programa terminará. Por lo tanto tú nunca podrías acceder al elemento de la matriz probeArray en función del valor de la variable data.
Ahora añadamos a este simple código descrito líneas arriba la ejecución especulativa. Es muy probable que el procesador haya ejecutado el acceso probeArray especulativamente antes de que se produzca la excepción; las excepciones son complejas y lentas. Esto todavía no es un problema de seguridad porque los resultados especulativos se descartan y no están disponibles en el espacio del usuario.
No se debería producir ningun daño ya que la ejecución especulativa finalmente no ha hecho ningún cambio en el sistema.
Pero lo anterior no es del todo cierto porque se ha accedido a un elemento de probeArray y ahora está en la memoria caché del CPU. Si antes de ejecutar el programa nos aseguramos de que ninguno de los elementos de probeArray estuviera en el caché del CPU. Entonces, ahora todo lo que tenemos que hacer es leer cada elemento de probeArray y medir cuánto tarda el acceso. El acceso que es más rápido que el resto es el elemento que fue almacenado en caché por la ejecución especulativa. Una vez que sabemos qué elemento se almacenó en caché, sabremos qué valor existe en esa posición relativa del espacio de kernel.
La ejecución especulativa cambió el estado de la arquitectura del CPU y esto proporciona un canal secreto a través del cual podemos leer los datos protegidos en el espacio de kernel.
Para entenderlo mejor veamos el siguiente diagrama extraído del paper oficial de Meltdown:
Esto significa que ahora podemos leer cualquier ubicación de memoria del espacio de kernel y descubrir qué se almacena allí simplemente tratando de ejecutar un acceso ilegal y haciendo uso de la ejecución especulativa y el almacenamiento en caché para encontrar qué datos se habrían recuperado si no hubieran sido ilegales.
Con algunas adiciones al sencillo código anterior, que básicamente es agregar el código para el manejando la excepción de tiempo de ejecución de falta de privilegios, es cómo los investigadores de Google Project Zero implementaron un código que permite verificar la existencia de Meltdown. Aquí el texto que aparece en el paper:
Con el manejo de excepciones, logramos velocidades de lectura promedio de 123 KB/s cuando se filtran 12 MB de memoria del kernel. De los 12 MB de datos del kernel, solo el 0.03% se leyeron incorrectamente. Por lo tanto, con una tasa de error de 0.03%, la capacidad del canal es 122 KB/s.
Meltdown, no funciona en los procesadores AMD y ARM en el sentido de que no recupera ningún dato válido, sin embargo, se demostró que el principio general funciona en ambas arquitecturas y estas ejecutaron instrucciones más allá de la instrucción ilegal. Una de las razones por las cuales se especula que Meltdown no funciona en AMD y ARM podría ser el momento exacto de la excepción y que todo lo que se necesita para que funcione es algún ajuste del código para conseguir un mejor timing del mismo.
La solución para Meltdown es deshabilitar cualquier asignación de memoria entre kernel y memoria de usuario, aparte de las áreas que necesitan compartirse, por ejemplo, tablas de interrupción. Esto se hace mediante el parche KAISER que está disponible para Linux. Una protección más avanzada y mejor requiere el uso de la indirección para ocultar las direcciones del kernel – código de trampolín. El gran inconveniente de estas soluciones es que vuelve más lentos a los programas que hacen un uso intensivo del CPU.
Aquí cómo lo explica el equipo que descubrió esta vulnerabilidad:
Meltdown cambia la granularidad de ser comparativamente baja espacio/temporalmente hablando, por ejemplo, de 64 bytes cada pocos cientos de ciclos para los ataques de caché, a una granularidad arbitraria, permitiendo que un atacante lea cada bit. Esto es algo contra lo que ningún algoritmo (criptográfico) puede protegerse. KAISER es una solución de software a corto plazo, pero el problema que descubrimos es mucho más significativo.
Se puede culpar a Intel y a los otros fabricantes de microprocesadores si es que lo deseamos, pero diseñar hardware para que ofrezcan un gran rendimiento es un trabajo difícil. Si el enfoque del diseño es el rendimiento, entonces debemos aceptar como un efecto colateral que la seguridad se verá afectada, es tal vez por ello que los descrubridores de Meltdown nos dan finalmente un mensaje positivo de cada al futuro:
Esperamos que Meltdown y Specter abran un nuevo campo de investigación para descubrir en qué medida las optimizaciones de rendimiento cambian el estado microarquitectónico, cómo este estado se puede traducir en otro estado arquitectónico y cómo se pueden prevenir tales ataques.
Aquí algunos links de fuentes usadas para este post sobre Meltdown: