Dans les tutoriels sur la récupération du flot d’exécution par la consommation, nous avons vu que des décalages temporels entre plusieurs exécutions pouvaient être observés par le biais des canaux auxiliaires. Notamment, cela engendrait de grandes différences dans l’évolution de la consommation ou du temps global d’exécution.

Dans ce tutoriel, nous allons à présent nous intéresser à l’observation des variations temporelles dûes à la microarchitecture des processeurs. Notamment, nous allons voir comment les mémoires caches, pourtant essentielles pour les performances de nombreux systèmes, peuvent aussi représenter une source de fuite.

Installation

Pour les prochaines expérimentations, nous avons besoin de télécharger les simulateurs de microrchitectures RISC-V. Ils sont hébergés sur GitLab.

Ces commandes ne sont à effectuer qu’une seule fois au tout début des expérimentations:

  git clone https://gitlab.com/escou64-emmk/uarch-sim.git
  cd uarch-sim

Prise en main de l’environnement

Cette commande doit être lancée à chaque ouverture de terminal :

  source /net/npers/mescoutelou/share/env/HWSEC/setup.sh
  cd uarch-sim/pw/hwsec/cache

Ensuite, les commandes suivantes seront utilisées:

  # Compile et exécute le programme
  make exec
  # Affiche le chronogramme
  make view

Certaines options sont disponibles pour changer la version du processeur, le temps de simulation etc.:

  # Compile et exécute le programme avec le processeur core_4 durant 5.000 cycles
  make exec CORE_NAME=core_4 NTRIGGER=5000
  # Affiche le chronogramme généré avec le processeur core_4
  make view CORE_NAME=core_4
Question 1

Lancez une première simulation. À l’aide du chronogramme de la simulation, identifiez les caractéristiques principales de la mémoire L1D:

  • le nombre d’ensembles,
  • le nombre de lignes par ensemble,
  • la taille de chaque ligne,
  • la taille totale.

Détection d’une variation

Pour être capable de détecter des variations temporelles lors de l’exécution, on utilise généralement des compteurs de cycles d’exécution. La plupart des processeurs contiennent ce type de mécanismes afin d’avoir une notion de temps. C’est par exemple utilise pour instaurer des délais sans mécanisme plus complexe comme les timers et les interruptions.

Dans l’architecture RISC-V, le registre spécial mcycle compte le nombre de cycles écoulés depuis le dernier reset du système. Le registre étant sur 64 bits, l’opération de lecture s’effectue en deux fois sur des implémentations 32 bits. Il est possible de lire ce registre à l’aide des instructions csrr rd, cycleh / csrr rd, cycle (ou des pseudo-instructions rdcycleh rd / rdcycle rd). Dans notre environnement de simulation, la fonction uint64_t __HAL_read_cycle() permet d’effectuer cette lecture dans un programme en langage C.

Question 2 Créez un tableau global share d’une taille suffisante pour remplir la mémoire cache L1D. Forcez son placement en mémoire pour que l’adresse de base soit alignée sur la taille du tableau.
Aide supplémentaire (si nécessaire)

volatile __attribute__ ((aligned (4))) uint8_t share[8]; permet de créer en mémoire un tableau volatile appelé share d’une taille de 8 octets dont l’adresse de base est aligné sur (multiple de) 4.

Lors du démarrage du système, certaines fonctions sont responsables de l’initialisation du contenu mémoire. Par exemple, elles doivent mettre à 0 certaines régions ou copier des valeurs dans d’autres. Ainsi, lorsque la fonction main est exécutée, les mémoires caches ne sont pas vides mais contiennent des lignes utilisées lors de l’initialisation.

Question 3 Au début de la fonction main, ajoutez une boucle parcourant deux fois ligne par ligne un tableau init de taille 2048 octets.

Une fois le système placé dans un état connu et maîtrisé, il va alors être possible d’effectuer des mesures de temps plus précises.

Question 4 Mesurez le temps d’exécution pour un cache miss lors d’un accès à la première ligne du tableau share. Écrivez la valeur correspondante dans un fichier output.csv en utilisant la fonction HAL_csv_u32(uint32_t data). Pour des mesures plus précises, mesurez le temps pour un chargement mémoire (load) plutôt que pour un rangement mémoire (store).
Question 5 De la même manière, mesurez le temps d’exécution pour un cache hit. Est-il possible de distinguer les deux états en fonction du résultat de la mesure ?
Question 6 Toujours dans la fonction main, réalisez plusieurs mesures successives sur des lignes différentes de share, en copiant / collant la séquence d’opérations utilisées dans les questions précédentes. Qu’observez-vous sur les différentes valeurs mesurées ?
Question 7 Concevez à présent une fonction uint32_t measure_access_time(uint8_t *addr); renvoyant le temps d’accès pour lire la valeur située à l’adresse addr. Qu’observez-vous au niveau des mesures ? Comment l’expliquez-vous ?

Conception d’un canal caché

On considère maintenant un scénario où deux programmes P1 et P2 sont exécutés à tour de rôle sur un même processeur. C’est par exemple le cas dans des systèmes plus complexes lorsqu’un système d’exploitation organise les différentes exécutions. Dans ce cas précis, les deux programmes ont donc accès à tour de rôle aux différentes ressources du processeur pour leur exécution. C’est notamment le cas des mémoires caches.

Une méthode pour évaluer les informations qui peuvent transiter entre ces deux programmes est la mise en place d’un canal caché de communication. L’objectif est de permettre à ces deux programmes de communiquer en utilisant uniquement des informations détournées telles que des temps d’exécution. On appelle généralement le programme responsable d’envoyer l’information un trojan(Cheval de Troie) et le programme responsable de récupérer l’information un spy.

Pour les prochaines questions, nous allons tenter de concevoir un canal caché de communication exploitant les variations temporelles dues à la mémoire cache L1D. Pour faciliter les mesures et la compréhension, trojan et spy seront deux fonctions appelées à tour de rôle dans la fonction main.

Cette organisation avec deux appels de fonctions sert à émuler un mécanisme réellement présent dans les systèmes d’exploitation: l’ordonnanceur des tâches. C’est lui qui donne l’autorisation aux différents programmes de s’exécuter à tour de rôle, en partageant le temps d’exécution. On parle alors de partage temporel des ressources matérielles.

Une méthode pour valuer une fuite est la conception d’un canal caché de communication. Dans le cas d’une récupération d’information, l’attaquant ne contrôle que la partie mesure et analyse.

Dans cette partie, l’objectif sera de faire communiquer deux programmes distincts: un Cheval de Troie trojan et un espion spy. Dans notre cas, ils seront modélisés par deux fonctions du même nom appelées à tour de rôle.

Pour mettre en place le canal caché de communication, nous avons besoin de modéliser la forme que prendront les informations transmises. Dans notre cas, nous allons nous appuyer sur les ensembles du cache L1D et une zone mémoire partagée par les deux programmes appelée share. Afin d’exploiter uniquement les variations temporelles, aucune information ne pourra être transmise en écrivant / lisant une valeur directement dans la zone mémoire partagée. Ainsi, pour encoder une donnée dans l’état de la mémoire cache, on considèrera qu’un hit dans l’ensemble N du cache correspond à la transmission de la valeur N.

Question 8 Combien de valeurs différentes peuvent être transmises par le L1D en utilisant ce canal caché ? À combien de bits d’informations transmises cela correspond-il ?
Question 9 On considère que le programme trojan veut transmettre la valeur 3. Listez les différentes étapes pour que le programme spy puisse retrouvez cette information.
Question 10 Implémentez ce cas dans la fonction main, en appelant successivement une fonction trojan puis spy.
Question 11 Quelle étape supplémentaire est nécessaire si le programme trojan souhaite ensuite transmettre la valeur 2.

Pour modéliser un canal caché, une des représentations possibles est une matrice de taille N x N avec N le nombre de valeurs différentes pouvant être transmises. L’axe des abscisses représente alors la valeur encodée par le trojan, l’axe des ordonnées la valeur testée par le spy et la valeur le nombre correspondant de cycles d’horloge mesurés.

Question 12 Adaptez votre programme pour qu’il génère dans le fichier output.csv l’ensemble des valeurs nécessaires à la modélisation du canal caché par une matrice. Quelle forme prend la matrice finalement générée ?
Aide supplémentaire (si nécessaire)

La transmission par un canal caché de type Evict+Reload s’organise en 3 étapes:

  1. Le programme Cheval de Troie prépare le canal en vidant entièrement la mémoire cache (phase Evict),
  2. Le programme Cheval de Troie ramène une ligne dans l’ensemble N pour transmettre la valeur N,
  3. Le programme espion parcourt l’ensemble du tableau (phase Reload) et détecte la ligne ramenée précédemment, puis en déduit N.

Les attaques exploitant les mémoires caches sont nombreuses et largement référencées dans la littérature. La stratégie employée précédemment s’apparente à la variante Evict+Reload, organisée en 3 phases:

  1. Le programme trojan prépare le canal en vidant entièrement la mémoire cache (phase Evict),
  2. Le programme trojan ramène une ligne dans l’ensemble N pour transmettre la valeur N,
  3. Le programme spy parcourt l’ensemble du tableau (phase Reload) et détecte la ligne ramenée précédemment, puis en déduit N.

De nombreuses variantes utilisent cependant des approches différentes. Flush+Reload a par exemple une stratégie similaire, à la différence que l’éviction est remplacée par un effacement (Flush). Cette variante est cependant dépendante des jeux d’instructions et implémentations qui ne mettent pas toujours à disposition d’instruction ou de mécanisme pour vider une mémoire cache (opération de flush). Une autre des variantes connues s’appelle Prime+Probe. À l’inverse de Evict+Reload, la stratégie consiste à d’abord remplir le cache (phase Prime) avant de détecter si des lignes ont été évincées (phase Probe).

Question 13 Proposez une autre version de votre canal caché utilisant maintenant la stratégie Prime+Probe.

Contre-mesure

Les mémoires caches sont des mécanismes largement répandus dans les processeurs modernes. Ainsi, les possibles fuites d’informations peuvent toucher un grand nombre de système. Heureusement, différentes contre-mesures sont possibles en traitant le problème à la source.

Question 14 Reprenez votre implémentation de canal caché de la partie précédente. Que faudrait-il faire entre l’exécution du trojan puis du spy pour éviter cette fuite d’information ?
Question 15 Implémentez votre protection en ajoutant une fonction void isolate() qui sera exécutée après chaque appel de spy ou de trojan.
Aide supplémentaire (si aucune idée)

Dans la fonction isolate, copiez votre boucle initial utilisez pour parcourir deux fois ligne par ligne le tableau init de taille 2048 octets. Qu’observez-vous ? Pourquoi ?

Question 16 De manière générale, quelle bonne pratique permettrait donc d’isoler un programme sensible ?
Question 17 Estimez l’impact sur les performances d’une telle approche.
Question 18 Réfléchissez à d’autres stratégies qui pourraient être mises en place pour atténuer / supprimer ce canal caché.

Exploitation d’un canal auxiliaire

Nous avons vu qu’un canal caché permettait d’évaluer les fuites d’informations entre deux programmes trojan et spy en tentant de les faire communiquer. Lors d’une attaque cherchant à récupérer de manière non-autorisée des informations sur un programme, on parle alors généralement de canal auxiliaire. Le scénario est proche de celui d’un canal caché: un programme spy cherche toujours à récupérer des informations mais maintenant transmises involontairement par un autre programme qu’on appellera victim.

Pour cette partie, on considère que le programme ciblé réalise une opération sensible bien connue: l’utilisation d’une S-BOX comme dans un calcul pour un chiffrement AES. Notre objectif va être de voir comment, à partir des variations temporelles dues aux mémoires caches, il peut être possible de retrouver un octet de la clé secrète.

Question 19

Créez une fonction uint8_t victim (uint8_t input) réalisant les opérations suivantes:

  1. Stocke en interne un octet de clé secrète,
  2. Réalise un XOR entre l’octet en entrée et l’octet de la clé secrète,
  3. Utilse le résultat intermédiaire pour indexer une table S-BOX,
  4. Renvoie l’octet calculé.
Question 20 Définissez les informations que pourraient récupérer le programme spy en analysant le contenu de la mémoire cache L1D après l’exécution du programme victim.
Question 21 Mettez en place une stratégie pour retrouver l’octet en exécutant successivement à plusieurs reprises les programmes victim et spy.
Question 22 On souhaite modifiez le programme victim pour éviter cette fite d’information. Proposez deux stratégies simples à mettre en place.

Généralisation

Les fuites d’informations ne sont évidemment pas propre au cache L1D. Ainsi, toutes les mémoires caches sont susceptibles de faire fuire des informations lorsqu’elles sont partagées librement entre plusieurs programmes. Dans le système matériel simulé ici, un cache pour les instructions L1I et un deuxième niveau de cache partagé L2 sont également présents.

Question 23 Similairement à ce qui a été fait pour le cache L1D, modélisez le canal caché entre deux programmes trojan et spy exploitant la mémoire cache L2. Quelles différences sont à considérer ? Quel impact cela peut-il avoir sur les différentes contre-mesures ?
Question 24 Similairement à ce qui a été fait pour le cache L1D et le L2, modélisez le canal caché entre deux programmes trojan et spy exploitant la mémoire cache L1I. Quelles différences sont à considérer ? Quel impact cela peut-il avoir sur les différentes contre-mesures ?
Pour notre analyse, on considèrera que n’importe quelle zone mémoire du système peut être librement lue, écrite et exécutée. En langage d’assemblage RISC-V, la valeur d’une instruction de retour de fonction est 0x00008067.