El objetivo de este proyecto es implementar protecciones para un sistema de verificación de claves implementado en VHDL.
El sistema original consiste en una máquina de estados que, tras recibir la clave introducida por el usuario, va comparando la misma con las claves almacenadas en la memoria, hasta encontrar una coincidencia o terminar de leer dicha memoria. La clave está compuesto por 4 dígitos.
La clave se compara dígito a dígito, dando por válida la clave una vez coincidan todos sus dígitos con los de una de las claves almacenadas en memoria
La máquina de estados original es la siguiente:
En la implementación original, la comparación de la clave con cada una de las almacenadas en memoria se interrumpe una vez se comprueba que uno de los dígitos no coincide. Esto provoca que, en los casos de no coincidencia, los tiempos sean diferentes en función del número de dígitos acertados.
Lo cual, mediante un análisis de señales, se podría utilizar para detectar el número de dígitos acertados y así, de esta manera, ir adivinando la clave dígito a dígito.
Además, las claves en la memoria se almacenan en texto plano, lo cual podría dar lugar a que una lectura ilegítima de la memoria expusiera todas sus claves.
La memoria está inicializada a 0, por lo que el atacante podría encontrar las claves simplemente leyendo las posiciones no nulas. Y, finalmente, las claves siempre se empiezan a comprobar desde el inicio de la memoria, por lo que el atacante sabría siempre por donde empezar a buscar.
El sistema dispone de dos implementaciones: una cableada, cuya máquina de estados se implementa directamente en VHDL; y otra programada, que está implementada sobre un microcontrolador Picoblaze embebido en el propio diseño hardware, mediante una rutina ensamblador.
El diseño del sistema permite elegir entre una implementación u otra, modificando un par de líneas de código
La mitigación para esta vulnerabilidad es simple: hay que conseguir que, ante una clave errónea, el sistema tarde siempre el mismo tiempo en activar la señal de "clave incorrecta". Idealmente, también se pueden equiparar los tiempos de las señales de "correcto" e "incorrecto"
En esta implementación, la mitigación para la vulnerabilidad de la medición de tiempos se ha hecho mediante un contador adicional, el cual, ante un dígito erróneo, realiza una espera de tantos ciclos como serían necesarios para leer el resto.
Para esconder la espera, se ha añadido dentro del mismo estado E5, correspondiente a la lectura de los dígitos, mediante una condición que permite diferenciar entre ambas situaciones
En la implementación programada, dado que el cálculo del número de ciclos es extremadamente complicado, lo que hemos hecho es forzar a que siempre se lean todos los dígitos y toda la memoria, independientemente del número de dígitos correctos e incluso de que la clave sea correcta.
Para saber si la clave es correcta, en lugar de terminar la ejecución al encontrarla o de detener la lectura de la combinación al encontrar un error, lo que hacemos es añadir dos flags: uno que indique si en la combinación actual hay algún error, y otro que indique si se ha encontrado alguna clave correcta.
Al encontrar un error en la combinación, activamos el flag de error. Al terminar la lectura de la combinación, si el flag de error no está activo, activamos el flag de clave correcta.
Al terminar la lectura de la memoria, comprobamos el flag de clave correcta para saber si se ha encontrado alguna coincidencia.
Para solucionar el problema de la lectura de las claves desde la memoria, realizamos dos acciones:
-
Completado de la memoria con información aleatoria: Esto complica el poder encontrar las claves mediante la búsqueda de posiciones de memoria no nulas
-
Cifrado de la memoria: Esto obliga a utilizar un algoritmo de decodificación para poder descifrar las claves
-
Inicio de la lectura de las claves en una posición diferente al inicio de la memoria: Esto hará mas difícil encontrar la ubicación de las claves a descifrar
-
Para generar la información aleatoria, utilizamos un script de Python que, mediante el módulo "secrets", genera números con un grado de aleatoriedad suficiente para utilizarse en claves. El script, utilizando dicho módulo, genera un conjunto de líneas con datos aleatorios en el mismo formato que requerimos para rellenar la memoria dentro de nuestro código.
-
Hecho esto, simplemente reemplazamos la memoria original por la obtenida por el script.
Para el cifrado de la memoria, como algoritmo sencillo, utilizamos un complemento a 2 "en positivo", calculando, para cada dígito (2^4 - valor).
En realidad, la memoria se asume cifrada ya de serie, por lo que el algoritmo se utilizaría únicamente para decodificar los valores ya existentes.
En el caso de la implementación cableada, la decodificación se implementa en una función, para ocultarla de forma menos evidente que con un componente adicional.
En la implementación programada, la decodificación se realiza durante la comprobación de la clave, dentro de la propia rutina ensamblador
Para realizar esto, en la versión cableada, modificamos el contador de lectura de la memoria para que, al valor indicado en la máquina de estados, le sume el desplazamiento. El desplazamiento inicia la lectura en la posición situada aproximadamente en la mitad de la fila 19 del código.
En la implementación programada, la lectura de la memoria utiliza direcciones de 10 bits, por lo que la posición indicada en la implementación cableada sería inalcanzable. Por esta razón, deshicimos los cambios de la implementación cableada, y aplicamos el desplazamiento dentro del propio código ensamblador.
La lectura comienza en una posición posterior al inicio, pero anterior a la utilizada en la versión cableada
-
sistema_clave.vhd: Fichero principal, que alberga los componentes de las implementaciones cableada y programada, y la memoria de claves común para los mismos.
La implementación a sintetizar se puede seleccionar descomentando el port map correspondiente
-
sistema_clave_tb.vhd: Fichero de simulación del sistema completo
-
ing_inv_cableada.vhd: Implementación de la máquina de estados en versión cableada
-
ing_inv_cableada_tb.vhd: Fichero de simulación para el sistema cableado
-
ing_inv_programada.vhd: Fichero de conexión del sistema programado con Picoblaze
-
ing_inv_sw.psm: Rutina ensamblador para picoblaze
-
prog.vhd: Fichero de programación de Picoblaze, con la rutina ensamblador almacenada en memoria
-
prog_sim.vhd: Fichero de simulación del sistema implementado en Picoblaze
-
puertos_in.vhd: Fichero de conexión de los puertos de entrada de nuestro proyecto para Picoblaze
-
puertos_out.vhd: Fichero de conexión de los puertos de salida con Picoblaze
-
picoblaze_pkg.vhd: Fichero de definición de los puertos de entrada y salida dentro de Picoblaze
-
interrupcion_pblaze.vhd: Fichero de definición de las interrupciones para Picoblaze
- gen_random_mem.py: Script de Python para generar la memoria aleatoria del sistema