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.

Branchements des composants ChipWhisperer Husky, CW313 et CW312.

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:

En théorie, n’importe quel programme dans 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 &
Question 1 Le processeur RISC-V utilisé est basé sur une simple FSM afin d’effectuer les différentes étapes de l’exécution. À partir du code (src/main/scala/core) et/ou d’un chronogramme de simulation, retrouvez les différentes états de la FSM utilisés.
Le code disponible dans 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.
Question 2 À partir de cette information, déduisez le nombre d’échantillons (samples) nécessaires pour garantir que l’exécution d’une instruction est entièrement mesurée. On considèrera que la fréquence de l’ADC est 4x supérieure à celle du processeur (plus d’informations sont disponibles lors du lancement des scripts Python pour communiquer avec le ChipWhisperer, via l’affichage du 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.
Question 3 À partir du code (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
La première fois, il sera nécessaire de modifier le numéro de série (sn) de votre carte au début du script (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:

  1. Envoyer le caractère 's' pour déclencher la communication,
  2. Armer le scope avec scope.arm(),
  3. Envoyer un nouvel octet,
  4. Lancer la capture avec scope.capture(),
  5. Attendre la réponse du système en utilisant uart.in_waiting(), qui renvoie le nombre d’octets disponibles à la lecture,
  6. Récupérer la trace avec scope.get_last_trace()
  7. Lire l’octet de réponse reçu avec uart.read(),
  8. Retourner la trace et l’octet.
Question 4 Complétez les fichiers 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
Question 5 Modifiez le script 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.
Question 6 Modifiez le code C dans 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.

Question 7 Reprenez le code C dans 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.

Question 8 On considère que chaque instruction demande un même nombre de cycles $N$ d’horloge pour exécuter entièrement une instruction. Pour exécuter $I$ instruction sur notre processeur avec FSM, il faudrait donc $N * I$ cycles d’horloge. En considérant qu’un attaquant serait capable d’effectuer une mesure au cycle près du temps écoulé entre 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;
Question 9 (Si vous ne l’avez pas déjà fait) Reprenez la version initiale et supprimez le 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).
Question 10 Proposez une solution robuste face à l’analyse de la consommation mais aussi des variations temporelles, toujours en ne modifiant uniquement le code C entre cw_trigger_set et cw_trigger_rst.
Question 11 On parle de programmation en temps constant pour parler des techniques où le temps d’exécution est indépendant des données manipulées. En quoi ces stratégies peuvent être efficaces ? Si l’on considère le système dans son ensemble, et donc également la microarchitecture réelle du processeur, en quoi est-ce complexe à mettre en place ? Finalement, pourquoi ces stratégies sont orthogonales à la recherche absolue de performances ?

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.

Le t-test permet de savoir si un système présente une fuite en différenciant deux populations. En revanche, d’un point de vue sécurité et mise en place d’une attaque, il ne prouve en rien l’exploitabilité de cette fuite. Ainsi, il est possible qu’une fuite existe mais qu’elle soit inutilisable en pratique.

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.
Question 12 Modifiez 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.

Question 13

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 4 bytes.

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.

Question 14 Calculer la valeur du t-test point par point. Sur votre graphique, faites également apparaître les seuils de rejet de l’hypothèse nulle à +/-4.5. Que pouvez-vous conclure ?
Question 15 Évaluez maintenant les fuites pour $c = a \oplus b$ et $c = a \lor b$ en modifiant 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.

Question 16 On souhaite concevoir un gadget réalisant l’opération masquée $z = x \oplus y$. Détaillez les opérations logiques nécessaires.
Question 17 Faites une copie de 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$).
Question 18 En sachant que $x \land (y \oplus z) = (x \land y) \oplus (x \land z)$ et que $(x \oplus y) \land z = (x \land z) \oplus (y \land z)$, détaillez les opérations logiques nécessaires pour $z = x \land y$.
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.

Question 19 À partir du calcul de $C = A \land B$, déduisez le calcul permettant d’obtenir $C = A \lor B$.
Aide supplémentaire (si nécessaire)

On notera que $x \lor y = x \oplus y \oplus (x \land y).$

Question 20 En sachant que $x \land (y \oplus z) = (x \land y) \oplus (x \land z)$ et que $(x \oplus y) \land z = (x \land z) \oplus (y \land z)$, détaillez les opérations logiques nécessaires pour $z = x \land y$.
Question 21 Implémentez les gadgets correspondants dans sw/src/cw_masked_or et sw/src/cw_masked_and.
Question 22 À l’aide d’un t-test, évaluez les fuites sur ces différents gadgets. Quel résultat obtenez-vous ?
Question 23 Quelque soit le choix effectué précédemment, on considère à présent que $z_0 = (x_0 \land y_0) \oplus (x_0 \land y_1)$ et $z_1 = (x_1 \land y_0) \oplus (x_1 \land y_1)$. Réécrivez ces égalités directement en fonction de $x$ et $y$. Quel problème cela pose-t-il ?
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)$

Question 24 Modifiez les gadgets 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

Question 25 On considère $x = x_0 \oplus x_1$. Quelle relation existe entre le Poids de Hamming de $x$ et la distance de Hamming entre $x_0$ et $x_1$.
Question 26 Quelle règle générale peut-on en déduire pour l’implémentations sécurisée d’algorithmes masqués ?
Question 27 À partir des résultats des questions précédentes, peut-on conclure que l’implémentation de vos gadgets est sécurisée ? Si oui, pourquoi ? Si non, quelles modifications faudrait-il effectuer en matériel et/ou logiciel ?

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..

Question 28 Après avoir analysé le script fpga/com/cw_cpa.py, proposez une analyse approfondie des différentes fonctions et implémentations de OR, AND et XOR.