Sécurité matérielle: Mémoires caches
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
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.
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.
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.
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).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 ?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.
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.
trojan veut transmettre la valeur 3.
Listez les différentes étapes pour que le programme spy puisse retrouvez cette information.main, en appelant successivement une fonction trojan puis spy.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.
Aide supplémentaire (si nécessaire)
La transmission par un canal caché de type Evict+Reload s’organise en 3 étapes:
- Le programme Cheval de Troie prépare le canal en vidant entièrement la mémoire cache (phase
Evict), - Le programme Cheval de Troie ramène une ligne dans l’ensemble N pour transmettre la valeur N,
- 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:
- Le programme
trojanprépare le canal en vidant entièrement la mémoire cache (phaseEvict), - Le programme
trojanramène une ligne dans l’ensemble N pour transmettre la valeur N, - Le programme
spyparcourt l’ensemble du tableau (phaseReload) 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).
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.
trojan puis du spy pour éviter cette fuite d’information ?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 ?
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.
Créez une fonction uint8_t victim (uint8_t input) réalisant les opérations suivantes:
- Stocke en interne un octet de clé secrète,
- Réalise un XOR entre l’octet en entrée et l’octet de la clé secrète,
- Utilse le résultat intermédiaire pour indexer une table S-BOX,
- Renvoie l’octet calculé.
spy en analysant le contenu de la mémoire cache L1D après l’exécution du programme victim.victim et spy.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.
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 ?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 ?0x00008067.