cybersecurityEn enero de 2018 saltaba a la prensa la noticia de unos supuestos fallos de seguridad de los procesadores de Intel y ARM. Antes de describir en qué consisten estos agujeros de seguridad, necesitamos enumerar algunas características comunes a los procesadores modernos que debemos conocer para entender estas debilidades .


Una de las características que proporciona seguridad en los ordenadores actuales es el aislamiento entre los diferentes espacios de usuario y entre estos y el núcleo (kernel) del Sistema Operativo (SO). Este aislamiento se consigue mediante dos mecanismos:

El más básico es el bit de modo usuario/supervisor del procesador (MSR [PR] en PowerPC), que se activa cuando se ejecuta el código del SO, y se desactiva cuando se conmuta a un proceso de usuario.

El otro consiste en usar espacios de memoria virtuales separados para los procesos de usuario y el SO. Un espacio virtual se divide en un conjunto de páginas, normalmente de 4 KB, que se mapean en páginas de memoria física a través de unas tablas de traducción residentes en memoria (y en disco), y gestionadas por el SO, denominadas tablas de páginas.

Cada entrada en la tabla mantiene la traducción de la dirección virtual en física, así como los permisos de acceso: read/write, execute/data, user/supervisor.

Por último, para soslayar la diferencia de velocidad entre la memoria principal (DDR3/DDR4) y el núcleo procesador (core), se usa una jerarquía de cachés. Las cachés dividen el espacio de memoria físico en fragmentos, denominados líneas, de tamaño fijo (32 KB en PowerPC).

Cuando el núcleo procesador necesita un dato, en primer lugar comprueba si en la caché L1, la más rápida y próxima, existe una copia. En caso afirmativo, cache hit, el dato es obtenido de allí (en 3 ciclos en el caso del núcleo e5500 de PowerPC). En caso negativo, cache miss, el procedimiento se repite sobre el siguiente nivel, L2 (11 ciclos en e5500), y sucesivos. Si este no se encuentra en la jerarquía de caché, se lee la línea completa de la memoria principal (+40 ciclos) a la que pertenece el dato a la vez que se hace una copia en la L1, por si se necesita en un futuro próximo. La copia en L1, acelerará enormemente el siguiente acceso a ese mismo dato, y es clave para la explicación del ataque. Otras características comunes entre los procesadores modernos son la ejecución fuera de orden, la predicción de salto y la ejecución especulativa. Así, por ejemplo, si una instrucción de salto depende del resultado de una operación en curso, el predictor puede conjeturar el destino y dirigir la ejecución por el camino más probable.

Cuando finalmente está disponible el resultado de la operación de la que dependía el destino del salto, el procesador tiene que optar por descartar los resultados intermedios, si la conjetura resultó incorrecta, restituyendo el estado de la máquina al que tenía antes de la instrucción de salto, o tomarlos como válidos (commit) y continuar, si acertó. Se dice que las instrucciones ejecutadas antes de conocer si correspondían al flujo deseado del programa, se ejecutaron especulativamente.

Supongamos ahora que el siguiente fragmento de código C forma parte de un proceso de usuario atacante que se ejecuta con privilegios de usuario normal dentro del sistema atacado
struct array {
unsigned long length;
unsigned char key[…];
} *ptr = {…};
unsigned long x = …;
unsigned int y;
unsigned int probe_array[] = {…}; /* Matriz de 8K enteros /*

if (x < ptr->length)
y = probe_array[(unsigned int)ptr->key[x] << 5]; Supongamos también:

1. Que operaciones previas han preparado el predictor de salto para estimar como más probable que la condición que comprueba el if es verdadera, por ejemplo, pasando valores de x que fuesen válidos.

2. Que x es elegido maliciosamente fuera del área de datos accesibles por el usuario cuyo contenido se quiere obtener fraudulentamente, con ese objeto: x > ptr->length

3. Que el código se ha asegurado que ptr->length y la matriz probe_array[] no están presentes en la caché y que el resto de datos que se van a leer lo estén.

Cuando se ejecuta la pieza de código anterior, el procesador comienza comparando el valor malicioso x con ptr->length. En el momento de leer ptr->length, al no encontrarse en la caché y tener recuperarlo de la memoria principal, la decisión tendrá una latencia de bastantes ciclos. Mientras se resuelve, el predictor de salto asume, en base al historial reciente, que el if será verdadero por lo que procederá a calcular el offset en probe_array[] para lo que enviará una petición de lectura de ptr->key[x], que se encuentra fuera del rango de acceso permitido al proceso, al subsistema de memoria. Mientras que esta última lectura sigue su curso, el valor de ptr->length finalmente llega desde la DRAM. El procesador se da cuenta que la ejecución especulativa fue por el camino erróneo y revierte el estado de la máquina al existente antes de la bifurcación. En consecuencia, no se conserva ningún resultado intermedio y ninguna instrucción provoca una excepción (como sería el caso al acceder fuera del rango durante el flujo normal del programa). Sin embargo, aunque el valor de probe_array[offset] no es consultable por el código porque no se almacenó en la variable y, ni en ningún registro de la arquitectura, sí queda una traza sutil en la microarquitectura de la máquina pues existe una copia en la caché. Y esto es precisamente lo que explota el ataque.

¿Y cómo hacer uso de que hay una copia en la caché?.

Pues sencillamente midiendo con precisión el tiempo de acceso a cada uno de los elementos de probe_array[]. Recordemos que antes de ejecutar la pieza de código, el proceso atacante se había asegurado de que ninguno de los elementos de la matriz se encontrase en la caché. Así pues, ahora sólo uno de los tiempos medidos, el que corresponde al elemento en la caché, será significativamente menor que los demás. Y detectado el elemento, tras deshacer el cálculo del offset, se puede obtener el valor del byte ptr->key[x] que se encontraba en una zona de memoria a la que el proceso atacante nunca debió tener acceso.

Como conclusiones podemos decir que este tipo de ataque explota las características más avanzadas de los procesadores más modernos, es independiente del sistema operativo y no depende de ninguna vulnerabilidad software.


1 Todas las referencias a ciclos de reloj en el texto están referenciados a ciclos de máquina del procesador, así 3 ciclos @ 2.4 GHz (p5040) = 1.25ns.
2 Iniciando la transferencia en la dirección donde se encuentra el dato para minimizar la espera.
3 Esto se puede conseguir mediante instrucciones de control de la caché que se pueden ejecutar en modo usuario.En el PowerPC: dcbf rA, rB (data cache block flush) donde rA = 0, por ejemplo, y rB contiene la dirección física de la entrada que se desaloja de la caché, siendo rA y rB dos cualquiera de los 32 GPRs de la arquitectura.
4 Aunque la medida parezca sencilla, requiere de una cuidadosa serialización de la rutina mediante el uso de barreras de sincronización (instrucción mbar en PowerPC), para que los resultados sean fiables y no se vean afectados por la capacidad de ejecución fuera de orden del núcleo.
5 En uno de los artículos para consulta del post, los tiempos en un Intel Core i5 obtenidos por la rutina de medida eran unos 40 ciclos de reloj en el caso de encontrarse en caché, y de unos 280 en el caso de estar en la memoria principal.


Sobre el autor

Manuel Sanchez
Manuel Sanchez
Manuel Sánchez González-Pola es Ingeniero de Telecomunicación en el departamento de I+D. Dentro de dicho departamento trabaja en el grupo de Hardware como responsable de proyectos.  

Comparte este post


Nuestras Soluciones Relevantes



| Etiquetas: