lunes, 8 de enero de 2018

SPECTRE. Recuperación del dato. Entre lo sutil y lo diabólico.

En mi entrada anterior hablé de cómo MELTDOWN y SPECTRE burlaban los mecanismos de protección de las CPU para llegar áreas protegidas pero la cuestión era ¿cómo podía recuperar el dato? -- como si a alguien le importara -- (le voy a dar a la voz en off) 

Si analizamos el comportamiento predictivo de la  CPU resulta que por un lado, puede ejecutar comandos de forma desordenada ... vale, pero si por un casual atacamos una posición de memoria protegida no la podemos dejar en ningún sitio  -- claro zoquete, por eso está protegida -- ni en un registro ni en una posición de memoria pero resulta que sí podemos averiguar su valor por medio ... sutil, como diría el maestro ... con indirectas.



Pues antes de contar cuál es ese medio, voy a explicar un poco en detalle cómo funciona una caché -- si ya sabía yo que éste si no soltaba el rollo no se quedaba a gusto --- Las CPUs son rápidas, muy rápidas pero las memorias, aunque lo parezcan, no lo son tanto y por ello metemos en medio unas memorias mas rápidas llamadas cachés que aunque no son tan rápidas como los registros son mucho más rápidas que la memoria. Con el fin de optimizar el acceso a esa memoria, cada vez que hay que ir a buscar un dato a la memoria no se copia ese dato solo a la memoria, sino que se copia una cosa llamada línea que dependiendo de la arquitectura, es un número de de bytes determinados. En el caso de las CPUs de Intel que uso ahora son 64 bytes como se puede apreciar. Es decir, yo pido un byte y si hay un fallo de caché, va a la memoria y trae 64. Como la probabilidad de que pida bytes adyacentes es muy alta, seguramente la próxima vez que pida otro dato distinto éste se encuentre en la caché.


¿Qué diferencia tenemos de que un dato esté o no en caché? Pues que tarde 400-500 ciclos de CPU en tenerlo o que tarde 200, un ahorro considerable.

Pues la idea de estas vulnerabilidades es aprovecharse de esto. Como ya he dicho previamente, la  CPU accede a datos protegidos pero no los puede dejar en ninguna parte entonces ¿cómo poder saber el valor de este dato? Pues de manera indirecta con dos cosas:
- Profundo conocimiento del funcionamiento de la CPU y cómo se maneja el manejo de páginas de memoria.
- Un array "gordo" que no quepa en una línea y que ocupe 256 páginas de memoria ¿por qué 256 páginas y no 128 bloques o cualquier otra cosas ....? Ahora lo verás. El ejemplo está sacados del whitepaper del meltdown attack.

Lo que voy a describir leerá un sólo byte de la memoria. si quieres leer por ejemplo, una clave de 2048 bits (256 bytes) pues primero tienes que averiguar dónde está y repetir el proceso de 256 veces. No es rápido pero acaba funcionando y si eres sútil, no te pillaran. Si entras a saco vas a joder todo el rendimiento del servidor y te van a pillar a la primera.

El código es el siguiente (como dije, está en el white paper de meltdown)

Previamente a hacer nada vaciaremos toda la cache mediante la instrucción CLFLUSH y cargamos en el registro RCX la dirección que queremos atacar (la protegida) y en RBX la dirección de memoria donde esté nuestro array de prueba. Como ya hemos dicho antes, tenemos que conocer el tamaño de página que se intercambia, en este ejemplo, 4096 bytes (la típica página de 4K de toda la vida)

¿Lo tenemos todo listo? -- pero ¿te crees que alguien ha llegado aquí? -- pues vamos a hacer magia. Cargamos en el byte más bajo del acumulador (recordemos que nuestros registros son de 64 bits) el byte que apunta RCX (línea 4) y multiplicamos ese valor que hemos cargado por 4096 (línea 5) -- ¿lo cualo que dices que has hecho? -- multiplicar por 4096 o lo que es lo mismo, desplazo hacia la izquierda 12 posiciones (el número hexadecimal 0x0C es el decimal 12) lo que equivale a multiplicar por 2 un total de 12 veces y como 2 elevado a 12 es 4096, ya tenemos ese valor. Y con ese valor que hemos obtenido, sumado al puntero del array que queremos acceder cargamos el dato (línea 7) Un avezado espectador diría, pero si, estás machacando el puntero original -- Oye, que estás machacando el puntero original -- (eso no vale, lo he dicho yo primero) Pues en efecto, pero el valor que tenga RBX ya no me importa porque ya he hecho la magia ¿ha quedado claro?

-- Pues no, no ha quedado nada claro -- pues te lo explico, voz en off. En este caso, estoy forzando una excepción (violación de acceso a la memoria protegida) pero me aprovecho de que esto se ejecuta de manera desordenada ... es decir, que antes de que salte la excepción, la CPU ha accedido al dato, ha ejecut a la línea 7 ... y ha deshecho todo lo que hizo porque no es válido ... no hay forma de acceder al valor RBX, ni al de RCX, ni a la posición de memoria ... o quizás sí -- ¿me estás vacilando -- Pues sí, te estoy vacilando un poco porque en realidad si ha quedado un rastro. Antes hemos dicho que tenemos un array del tamaño de 256 páginas (en este caso 4K cada una, un array en memoria de un Mbyte) y que previamente, hemos vaciado la caché. Utilizando como índice el valor protegido hemos accedido a una posición de memoria de nuestro array cuyo valor es irrelevante, la cuestión es que ha cargado en memoria una página de memoria y sólo una ¿cómo sé cual? Pues de una manera muy sencilla, voy consultando página por página (desde 0 hasta 256 leo un byte en la posición multiplicada por el tamaño de la página) y miro cuanto tiempo tarda la CPU en dármela. Dado que todas están en la memoria principal, tendrá que ir a por ellas y tardará un tiempo muy similar para todas la páginas menos para una ... aquella página cuya posición coincida con el valor leído previamente en la memoria protegida -- no me queda claro-- Pues seguro que si ves este gráfico te queda más claro.


Todos los accesos pasan de los 400 ciclos de CPU menos uno que se queda por los 200, ese el que hemos pillado antes y ¡voilá! hemos hecho magia, averiguando un valor sin poder acceder a él. si quiero más datos, pues nada, repito el proceso las veces que haga falta, que con paciencia y saliva el elefante ...

El mecanismo que uses para llegar a forzar este proceso puede ser como MELTDOWN (fuerza una excepción pero antes ya ha accedido) o jugando con la predicción (SPECTRE) o algún invento nuevo, pero lo interesante del caso es que no le veo una solución sencilla al menos sin perjudicar el rendimiento del ordenador. No sirve el separar en cachés los datos e instrucciones del anillo 0 y del 3 porque estás accediendo a los datos del anillo 3 y vas a dejar el rastro en la caché. El forzar flushes de la caché cuando se produzcan violaciones puede perjudicar a otros procesos (no olvidemos que la LLC está compartida por todos los cores y procesos) Seguramente habrá que reforzar de alguna manera el acceso a la parte protegida por HW, primando el bit de protección que al parecer ahora  no es lo suficientemente estricto.

-- ¿De qué anillos hablamos? -- De estos, que te lo hay que contar todo.




Pues ... eso es esto, amigos.

-- Anonadado me hallo ¿latita Whiskas pa celebrarlo? ---

No hay comentarios:

Armaduras.

He de reconocer que últimamente no me estiro demasiado en el tema bloguero este. Tampoco voy a molestarme en hacer propósito de enmienda so...