Sécurité matérielle: FPGA
Lors de l’introduction, nous avons vu les principes de l’analyse de la consommation des système matérielles. Ces principes ont été appliqués dans un environnement guidé (Jupyter) sur des systèmes dont l’utilisation est bien maîtrisée, mais dont la microarchitecture interne reste, au moins en partie, inconnue (un microcontrôleur STM32).
À présent, nous allons voir comment cibler un système dont toute l’organisation interne est disponible. Pour cela, l’objectif sera d’implémenter un processeur RISC-V sur une cible FPGA.
Les questions sur cette page auront plusieurs objectifs:
- Comprendre comment mener une analyse d’un système connu,
- Appréhender certaines caractéristiques des FPGA par rapport aux ASIC,
- Étudier la mise en place de contre-mesures logicielles et matérielles,
- Comprendre l’intérêt des accès libres et ouverts pour la sécurité.
0. Installation
Pour les prochaines expérimentations, nous avons besoin de télécharger le code de la plateforme RISC-V.
Celui-ci est hébergé sur GitLab.
Il faut ensuite se placer dans la branche cw.
Ces commandes ne sont à effectuer qu’une seule fois, au tout début des expérimentations:
git clone https://gitlab.com/escou64-emmk/calf.git
cd calf
git checkout cw
Enfin, pour réaliser les différentes expérimentations, les composants matériels suivants seront nécessaires:
- Un ChipWhisperer Husky pour la communication avec l’ordinateur hôte et les mesures,
- Une carte CW312-A35 contenant un FPGA Xilinx
xc7a35tcsg324-1(même famille que les carte Digilent Nexys A7), - Une carte CW313 pour réaliser l’interfaçage.
La photographie ci-dessous présente les différents branchements à effectuer pour la réalisation des mesures.

1. Analyse de la cible
Les différentes parties de ces manipulations sont effectuées en implémentant sur la carte FPGA le processeur RISC-V CALF. Afin de réaliser une analyse approfondie, notamment dans les parties Fuite en valeur et Fuite en transition, il est nécessaire de comprendre comment fonctionne le processeur ciblé.
Pour certaines étapes, il pourra être utile de simuler l’exécution d’un programme afin de visualiser l’impact sur l’intérieur du processeur. Pour cela, il est possible de réaliser une simulation à l’aide des commandes suivantes:
sw/ peut-être simulé de la même manière.
Il est cependant recommandé de directement modifier sw/src/example afin de garder un programme simple et facile à analyser. # Chargement des outils
source /net/npers/mescoutelou/share/env/HWSEC/setup.sh
# Génère un simulateur du processeur
make chisel-sim
# Compile le programme example
make -C sw example
# Simule l'exécution du programme sur 100 cycles
./obj/VSim --ram sw/hex/example.ram.mem --vcd sim/sim.vcd --ntrigger 100
# Lance la visualisation du chronogramme
gtkwave sim/sim.vcd sim/cw.gtkw &
src/main/scala/core) et/ou d’un chronogramme de simulation, retrouvez les différentes états de la FSM utilisés.src/main/scala/core correspond à du langage Chisel. Il permet de générer du SystemVerilog utilisé pour la simulation ou les outils FPGA. Vous trouverez une description équivalente en VHDL dans src/main/vhd/core.scope).Lors de l’analyse de la microarchitecture d’un processeur, on considère généralement deux grandes parties:
- le chemin de contrôle, responsable du pilotage des différents éléments afin de garantir la bonne exécution des opréations selon l’instruction lue en mémoire,
- le chemin de données, responsable du stockage et du traitement des données elles-mêmes. Ces informations peuvent s’avérer particulièrement intéressantes notamment pour l’analyses des sources de fuite.
src/main/scala/core), de la documentation et/ou d’un chronogramme de simulation, représentez sur un schéma le chemin de contrôle et le chemin de données de ce processeur. Pour simplifier, on considèrera que le processeur est directement connecté à une mémoire (sans I/O ou bootloader).2. Prise en main de la plateforme
Nous allons à présent mettre en place la pltaforme pour réaliser des mesures de consommation. Pour cela, il est nécessaire d’établir la communication entre le système ciblé intégrant le processeur (implémenté sur le FPGA) et la plateforme de mesure ChipWhisperer.
Pour la suite, nous aurons généralement besoin de 2 terminaux ouverts.
Terminal 1: génération
Le premier, placé à la racine du projet, servira à compiler les programmes et à générer le matériel.
Lancez la commande source /net/npers/mescoutelou/share/env/HWSEC/setup.sh pour le configurer.
Ensuite, les commandes suivantes seront utilisées:
# Dans calf/
# Compile le programme cw_measure
make -C sw cw_measure
# Génère le système ciblé
make chisel-fpga
Terminal 2: implémentation et analyse
Le second, placé dans fpga/, servira à implémenter le système sur le FPGA et à lancer les analyses via le ChipWhisperer.
Lancez la commande source /net/npers/mescoutelou/share/env/HWSEC/setup.sh pour le configurer.
Ensuite, une fois les commandes du Terminal 1 exécutée, utilisez:
# Dans calf/fpga/
# Implémente le système sur le FPGA
vivado -mode tcl -source cw-xpr.tcl
# Lance le script de mesure
python com/cw_measure.py
CW_SN).
Pour cela, dans fpga/, utilisez la commande python com/cw_sn.py: elle vous affichera les caractéristiques de la carte ChipWhisperer connectée à votre ordinateur.Pour réaliser une mesure, il est nécessaire que la plateforme de mesure et la cible se synchronisent.
Côté FPGA, il faut déclencher le signal de trigger lorsque un octet a été reçu après 's', puis le remettre à zéro le signal une fois le calcul terminé.
Côté mesure, l’ensemble de la fonction de capture de trace capture_trace doit être préparée et respecter la séquence suivante:
- Envoyer le caractère
's'pour déclencher la communication, - Armer le scope avec
scope.arm(), - Envoyer un nouvel octet,
- Lancer la capture avec
scope.capture(), - Attendre la réponse du système en utilisant
uart.in_waiting(), qui renvoie le nombre d’octets disponibles à la lecture, - Récupérer la trace avec
scope.get_last_trace() - Lire l’octet de réponse reçu avec
uart.read(), - Retourner la trace et l’octet.
sw/src/cw_measure.c et fpga/com/cw_measure.py et affichez une mesure de la consommation.3. Fuite du flot de contrôle
Lors de l’exécution d’un programme, l’exécution de chaque instruction et des opérations correspondantes influence directement la consommation du système. Ainsi, en effectuant des mesures de celle-ci lors du fonctionnement, il est possible d’en déduire des informations sur les calculs effectués. Dans cette partie, on se propose de voir comment cette propriété est exploitable pour récupérer un mot de passe, et les changements possibles pour protéger un tel système.
Récupération de mot de passe
Dans cette partie, nous allons tenter de reproduire une attaque permettant de retrouver le mot de passe dans l’application en C exécutée sur notre système. Voici les commandes nécessaires pour les différents éléments:
Terminal 1: génération
# Dans calf/
# Compile le programme cw_passwd
make -C sw cw_passwd
# Génère le système ciblé
make chisel-fpga
Terminal 2: implémentation et analyse
# Dans calf/fpga/
# Implémente le système sur le FPGA
vivado -mode tcl -source cw-xpr.tcl
# Lance le script de mesure
python com/cw_passwd.py
fpga/com/cw_passwd.py pour effectuer la récupération de mot de passe.
Vous devez compléter la fonction try_passwd qui envoie la tentative, mesure et récupère une trace, affiche si le mot de passe testé est correct ou non (selon la réponse du système 'y' ou 'n') et retourne la trace.
Ensuite, complétez la boucle de l’attaque.sw/src/cw_passwd et le script fpga/com/cw_passwd.py pour une longueur de 32 octets (PASSWD_LEN = 32). Quel paramètre de mesure doit être vérifié ?Protection logicielle
Lorsqu’une faille est retrouvée dans l’exécution d’un programme, l’approche la plus facile à mettre en place est généralement d’effectuer une mise à jour pour tenter de la corriger. Nous allons ici voir, au travers de l’exemple de la vérification de mot de passe, certaines considérations qui doivent être prises en compte pour permettre une xécution plus sécurisée.
sw/src/cw_passwd.
Quelle partie peut expliquer les différences entre les tentatives effectuées ?
Modifiez le code C et tester l’efficacité.
Que le résultat obtenu soit concluant ou non, passez ensuite à la question suivante.La problématique de sécurité rencontrée ici est similaire à celles due aux mesures de variations temporelles. Ainsi, en mesurant le nombre de cycles d’horloge nécessaires pour exécuter le programme, il est également possible de récupérer des informations. Considérer ce modèle va nous être utile pour tenter de renforcer notre vérification de mot de passe.
cw_trigger_set et cw_trigger_rst, des observations peuvent-elles être faites sur l’exécution du code ?
La solution proposée précédemment est-elle toujours viable ?
Vérifiez cela en simulation à l’aide du programme sw/src/example;break;.
Supprimez également l’utilisation du valid et utilisez directement output[0] pour stocker le résultat ('y'ou 'n').
Compilez le programme et analysez l’efficacité.
Expliquez le résultat en vous appuyant sur le désassemblé (sw/lst/cw_passwd.lst).cw_trigger_set et cw_trigger_rst.4. Fuite en valeur
Nous avons vu précédemment que le flot de contrôle peut influencer la consommation et donc représenté une source de fuites exploitables.
Cependant, lors de l’exécution d’un programme, la consommation est également influencée par un autre type d’informations essentielles: les données elles-mêmes.
Notamment, un des modèles régulièrement mis en place se base sur le Poids de Hamming (HW) des données pour estimer leur impact sur la consommation.
Par exemple, pour une donnée sur 8 bits avec une valeur de 0xBA, on utilisera son Poids de Hamming HW(0xba) = HW(0b10111010) = 5.
On parle ici de fuite en valeur de la donnée: comme indiquée, c’est la valeur de la donnée elle-même qui influence directement la consommation lors des différentes manipulations dans le système.
À partir de ce modèle, des tests statistiques permettent d’évaluer un système et les fuites existantes,
Notamment, le t-test permet de déterminer statistiquement si deux ensembles de données sont distinguables.
On appelle hypothèse nulle le cas où il est impossible de distinguer les deux ensembles: si l’hypothèse nulle est rejetée, alors cela signifie qu’une fuite d’informations est présente.
Dans le cadre d’une évaluation des fuites par la consommation, cela peut nous permettre d’évaluer si la consommation est influencée ou non par les données manipulées.
Généralement, les seuils retenus pour rejeter l’hypothèse nulle sont $\leq -4.5$ ou $\geq +4.5$.
Cela correspond à une probabilité supérieure à 0,99999.
Dans le cadre de ces expérimentations, nous allons tenter d’évaluer les fuites au niveau de notre processeur lors que l’on effectue une simple manipulation logique $c = a \land b$.
a et b seront deux valeurs sur 32 bits recues via UART, et c le résultat correspondant également renvoyé par la liaison UART.
Pour rappel:
- $A \oplus B$ signifieA XOR B,
- $A \land B$ signifieA AND B,
- $A \lor B$ signifieA OR B.
sw/src/cw_and pour effectuer l’opération voulue.Pour la réalisation d’un t-test, il est nécessaire de collecter 2 jeux de traces: un premier où les valeurs d’entrées sont fixées, un second où les valeurs d’entrées sont choisies aléatoirements.
Modifiez fpga/com/cw_ttest.py pour effectuer la mesure des 2 jeux de traces, chacun d’une taille N paramétrable, permettant d’analyser si la valeur de l’opérande a fuite. On utilisera:
random.getrandbits(32)uart.write(a.to_bytes(4, 'little'))pour envoyer un entier de 32 bits en le convertissant en 4bytes.
Une fois les mesures effectuées, il est nécessaire d’effectuer l’analyse elle-même.
Pour cela, la librairie Python SciPy contient une fonction ttest_ind permettant d’effectuer le calcul statistique (from scipy.stats import ttest_ind).
Pour calculer le t-test de l’échantillon E, on utilisera ttest_ind(tf_sample, ta_sample, axis=0, equal_var=False).statistic, avec tf_sample la liste des échantillons E du jeu de traces fixé et ta_sample avec la liste des échantillons E du jeu de traces aléatoire.
sw/src/cw_xor et sw/src/cw_or.
Quelle conclusion globale pouvez-vous en tirer ?Masquage booléen
Différentes stratégies peuvent être mise en place pour tenter d’empêcher cette fuite d’informations. Par exemple, un premier type d’approche peut chercher à générer du bruit (e.g. en rajoutant des faux calculs au niveau du matériel). Cependant, la fuite n’est alors pas supprimée: elle existe toujours mais est simplement cachée. Par le biais d’analyses approfondies, par exemple, en multipliant le nombre de traces de mesures, il peut donc toujours être possible de retrouver l’information souhaitée. Ainsi, un autre type d’approche plus radicale chercher à directement traiter le problème à sa source: le masquage. On retrouve ce type de stratégie très régulièrement dans l’implémentation d’algorithmes cryptographiques, aussi bien au niveau logiciel que matériel.
Le principe du masquage est simple: il vise à ne jamais manipuler directement la valeur d’une donnée. Ainsi, on s’assure (en théorie) qu’aucune fuite liée à cette valeur ne peut avoir lieu. Pour cela, chaque donnée est décomposée en plusieurs parties appelés partagés (ou shares). Dans le cas du masquage booléen, la décomposition est faite comme suit:
$$ x = s_0 \oplus x_1 $$ avec:
- $x$: la valeur de la donnée,
- $x_0$: un masque aléatoire générée pour cette donnée,
- $x_1$: la valeur masquée telle que $x_1 = x \oplus x_0$.
On note généralement une donnée x = (x0, x1). Ainsi, lorsque des calculs sur la donnée x doivent être effectuée, celle-ci n’est jamais directement manipulé: ce sont x0 et x1 qui sont utilisées. Le masque étant complètement aléatoire, les partagés sont indépendants de la valeur initiale. Si un attaquant souhaite retrouver des informations sur celle-ci, il doit être capable d’observer tous les partagés en même temps. Cependant, en multipliant le nombre de partagés et donc de masques, il est possible d’augmenter la protection offerte. On dit qu’un masquage utilisant deux partagés est dit d’ordre 1, trois partagés d’ordre 2 etc.
Une des problématiques posées par l’utilisation du masquage booléen concerne le coût opératoire. Si effectuer des opérations linéaires comme un XOR ou une permutation est simple (il suffit d’effectuer la même opération sur chaque partagé), ce n’est pas le cas pour les opérations non-linéaires telles que le OR ou le AND. Pour ces dernières, des suites d’opérations spécifiques appelés gadgets doivent être mises en place afin d’effectuer le calcul. Dans la suite de cette partie, on se propose d’étudier les fuites liées au masquage et à l’implémentation de gadgets.
sw/src/cw_xor et appelez-la sw/src/cw_masked_xor.
Modifiez le code pour qu’il prenne en entrée 4 valeurs de 32 bits en entrée (($x_0, x_1$) et ($y_0, y_1$)) et renvoie 2 valeurs de 32 bits en sortie ($z_0, z_1$).Aide supplémentaire (si nécessaire)
$z = x \land y$
$z = (x_0 \oplus x_1) \land (y_0 \oplus y_1)$
$z = (x_0 \land y_0) \oplus (x_0 \land y_1) \oplus (x_1 \land y_0) \oplus (x_1 \land y_1)$
En sachant que $z = z_0 \oplus z_1$, il y a donc plusieurs solutions possibles. On cherchera généralement à avoir $z_0 \oplus z_1$ équilibrés.
Aide supplémentaire (si nécessaire)
On notera que $x \lor y = x \oplus y \oplus (x \land y).$
sw/src/cw_masked_or et sw/src/cw_masked_and.Aide supplémentaire (si nécessaire)
On rappellera que $x \land (y \oplus z) = (x \land y) \oplus (x \land z)$.
Afin d’éviter certaines fuites, il est possible de remasquer temporairement des parties de gadget. Pour cela, on exploite le fait que $r \oplus r$ s’annule. Ainsi, si l’on considère un nouveau masque aléatoire $r$, il est possible de faire:
$z = (z_0 \oplus z_1)$
$z = (z_0 \oplus r) \oplus (z_1 \oplus r)$
sw/src/cw_masked_or et sw/src/cw_masked_and pour utiliser un masque temporaire.
Ces gadgets prendront donc 5 valeurs de 32 bits en entrée.
Vérifiez ensuite la sécurité de votre implémentation en pratique, et corrigez-la si nécessaire.5. Fuite en transition
Nous avons vu que le t-test permettait de montrer la présence d’une fuite d’information dans la consommation. En revanche, il ne donne aucune information sur la fuite elle-même, les informations qui peuvent être retrouvées et l’exploitabilité. Pour cela, on utilise généralement un autre type d’analyse appelée Correlation Power Analysis (CPA). L’idée est de montrer directement si une corrélation existe entre un groupe de valeurs et un groupe de traces. Cela peut-être particulièrement utile pour déterminer si une opérande particulière peut-être retrouvée, le résultat, une distance de Hamming dûe à une transition etc..
fpga/com/cw_cpa.py, proposez une analyse approfondie des différentes fonctions et implémentations de OR, AND et XOR.