Les attaques par canal auxiliaire sur la micro-architecture des processeurs font l'objet de recherches depuis une quinzaine d'années. Auparavant peu connues du grand public, et concernant essentiellement les implémentations d'algorithmes cryptographiques, ces attaques ont pris le devant de la scène en ce début d'année 2018 avec les attaques Meltdown et Spectre.
On a longtemps considéré la sécurité informatique comme étant la sécurité des logiciels. Or ces logiciels s'exécutant bel et bien sur du matériel, il est possible de surveiller les effets de bord du matériel durant leur exécution, comme la consommation de courant ou encore le temps d'exécution, pour attaquer l'implémentation de certains algorithmes. Ces attaques sont appelées attaques par canal auxiliaire.
Le cache est une petite mémoire très rapide au niveau du processeur, gardant une copie des données fréquemment utilisées afin d'accélérer les calculs. En 1996, Kocher documentait la première attaque théorique sur des implémentations d'algorithmes cryptographiques en utilisant les temps d'accès au cache, suivie à partir de 2002 par des attaques pratiques. Ce type d'attaque par canal auxiliaire a montré qu'il était possible d'exploiter les effets du cache sur le temps d'exécution d'algorithmes pour extraire des informations secrètes. Similairement aux autres attaques par canal auxiliaire, l'attaquant utilise les effets de bord du cache – et non son contenu. En effet, les données qui y sont présentes ont un temps d'accès très court, contrairement aux données qui doivent être récupérées de la DRAM. Cependant, contrairement aux attaques par canal auxiliaire se basant sur la consommation de courant ou le rayonnement électromagnétique, un attaquant n'a ici pas besoin d'accès physique à la machine, mais uniquement de pouvoir exécuter du code sur la machine sans aucun privilège. On retrouve aujourd'hui cette configuration, dans laquelle la victime et l'attaquant partagent une même machine, dans de nombreux environnements, tels que les environnements virtualisés avec le cloud computing, ou tout simplement dans les ordinateurs personnels ou les appareils mobiles. Ces attaques permettent de passer outre l’isolation apportée par les hyperviseurs entre machines virtuelles [1], et même par la sandbox de JavaScript [2], et ce à la fois sur les architectures x86 et ARM.
Jusqu'à récemment, ces attaques sur le cache ne concernaient que d'une part les implémentations d'algorithmes cryptographiques, ou d'autre part des canaux cachés, c'est-à-dire des communications non autorisées entre un émetteur et un récepteur qui communiquent ainsi en utilisant des effets de bords non surveillés. En collaboration avec Daniel Gruss, Moritz Lipp, et Stefan Mangard de Graz University of Technology en Autriche, et Anders Fogh de G DATA en Allemagne, nous avons également montré, dans un papier publié en 2016, qu'il était possible de contourner la distribution aléatoire de l'espace d'adressage du noyau (KASLR) en se basant sur de tels canaux auxiliaires [3]. Une telle attaque permet de récupérer des informations nécessaires à l'exploitation d'éventuelles vulnérabilités au niveau du noyau. Enfin, les attaques Meltdown [4] et Spectre [5] ont défrayé la chronique en ce début d'année 2018.
Cet article présente une rétrospective sur ces attaques. Une première partie présentera le fonctionnement des caches ainsi que les briques de base nécessaires à ces attaques par canal auxiliaire. On détaillera ensuite le fonctionnement de l'attaque sur KASLR, qui fait fuiter des adresses de l'espace noyau. On présentera ensuite l'attaque Meltdown, qui va un cran au-dessus pour cette fois faire fuiter le contenu de l'espace noyau. Ces deux attaques sont contrées par des modifications au niveau du noyau lui-même, un correctif aujourd'hui nommé KPTI (Kernel Page-Table Isolation) dans Linux [6,7]. Nous avions initialement proposé un correctif sur lequel se base KPTI en juillet 2017, soit six mois avant la révélation publique de Meltdown, et avant même que le groupe de Graz University of Technology ne commence à investiguer cette nouvelle attaque.
On s'attachera ici à détailler le fonctionnement des éléments de micro-architecture permettant ces attaques, ainsi qu'à répondre à ces questions : Quelle est la particularité de l'attaque Meltdown –et sa jumelle Spectre – par rapport aux attaques existantes ? Et comment a-t-il été possible de développer un correctif avant même d'avoir conscience de l'attaque ?
1. Attaques par canal auxiliaire sur le cache
1.1 Fonctionnement d’un cache
Par souci de brièveté, on s'attachera ici à décrire le fonctionnement des processeurs Intel récents, basés sur l'architecture x86. Certains de ces principes, ainsi que des attaques qui en découlent, sont également applicables aux processeurs ARM. Les caches sont des petites mémoires SRAM très rapides, placées au niveau des processeurs afin de combler l'écart de vitesse entre le processeur (effectuant des calculs très rapidement) et la mémoire principale DRAM (lente relativement au processeur). Depuis la micro-architecture Nehalem (2008), les processeurs Intel sont composés de trois niveaux de cache : le plus petit (L1) est situé près des cœurs et est donc le plus rapide, tandis que le dernier niveau de cache (L3) est le plus grand, mais également le plus lent, car plus éloigné des cœurs. Il existe un L1 par cœur, divisé en cache de données et en cache d'instructions de 32Ko chacun, tandis que le L3 est partagé entre tous les cœurs et fait quelques méga-octets. La plus petite unité gérée par un cache est appelée une ligne et fait 64 octets. Les processeurs Intel sont N-associatifs, ce qui signifie que certains bits de l'adresse vont définir l'index d'un ensemble (cache set) pouvant contenir N lignes. Comme plus de N lignes de cache peuvent mapper un ensemble sur toute la mémoire, le processeur définit une politique de remplacement pour choisir la ligne à évincer pour en stocker une nouvelle.
Le fonctionnement des caches est totalement transparent pour les applications ainsi que le noyau. Il existe tout de même quelques instructions pour gérer les lignes de caches explicitement : clflush, qui prend en paramètre une adresse virtuelle et évince de tous les niveaux de cache la ligne correspondante, et les instructions prefetch, qui prennent également en paramètre une adresse virtuelle, et indiquent au processeur que la ligne de cache correspondante est susceptible d'être accédée prochainement.
1.2 Briques de base des attaques par canal auxiliaire
Les deux attaques de base sur les caches sont nommées Flush+Reload et Prime+Probe. On ne décrira ici que Flush+Reload [8]. Cette technique suppose que l'attaquant ait de la mémoire partagée avec la victime. Cela est possible par exemple en mappant une bibliothèque partagée, ou encore si le système d'exploitation ou l'hyperviseur utilise de la déduplication mémoire. L'attaquant commence par exécuter l'instruction clflush sur une ligne de cache, puis attend que la victime poursuive son exécution avant de réaccéder à cette ligne, tout en mesurant son temps d'accès. Si le temps d'accès est long, alors la ligne a du être récupérée depuis la DRAM, et n'a donc pas été accédée par la victime. Au contraire, un temps d'accès court s'explique par la présence de la ligne dans le cache, et donc par un accès de la victime. Il est donc possible pour un attaquant de connaître les accès mémoire d'un autre processus avec la granularité d'une ligne de cache, c'est-à-dire 64 octets. S'il n'est pas possible d'utiliser l'instruction clflush, on peut évincer une ligne du cache en utilisant d'autres accès mémoire localisés dans le même ensemble, forçant ainsi la politique de remplacement à évincer la ligne désirée. Cette variante est appelée Evict+Reload.
Un point important à retenir de ces attaques est qu'on ne cherche pas à obtenir le contenu d'une ligne de cache, mais bien sa présence ou non dans le cache. C'est la détection de cette présence ou non qui est à l'origine de fuites d'informations. Les nouvelles attaques présentées dans la suite de l'article se basent sur le même principe.
On distingue les canaux cachés des attaques par canaux auxiliaires sur un programme. Un canal caché est une communication non autorisée entre un émetteur et un récepteur, qui utilise donc des moyens détournés et non surveillés. Les canaux cachés sont principalement employés pour transférer des informations d’un émetteur aux privilèges élevés sur une ressource vers un récepteur qui dispose de moins de privilèges sur celle-ci. Au contraire, dans une attaque par canal auxiliaire, seul un programme a un comportement malicieux et cherche à inférer des informations provenant d’un autre programme, lui bénin. Les différences de timing apportées par la présence de lignes dans le cache ne sont exploitables que si la présence de ces lignes dépendent d'un secret. Par exemple, si les accès mémoire dépendent de bits d'une clé privée, la présence de ces lignes révèle directement ces bits.
2. Contourner KASLR avec les instructions prefetch
Cette attaque a été rendue publique en 2016, d'abord présentée à Black Hat USA puis publiée à la conférence académique ACM CCS. En addition au canal auxiliaire apporté par les instructions prefetch expliqué dans la suite de la section, cette attaque utilise le fait que, sans le patch KPTI, l'espace noyau est mappé dans l'espace d'adressage virtuel de chaque processus. Il n'est cependant normalement pas accessible si le processeur ne tourne pas en mode noyau. Cette protection est donc assurée au niveau du matériel.
2.1 Scénario d'attaque
On considère un scénario d'attaque locale où l'ASLR est en place au niveau de l'espace utilisateur ainsi que du noyau. L'attaquant peut exécuter arbitrairement du code sur le système, mais n'a aucun privilège, c'est-à-dire aucun accès au noyau ni à des interfaces telles que /proc/self/pagemap fournissant des informations sur l'espace d'adressage. Grâce aux fonctionnalités de sécurité telles que NX, SMEP et SMAP présentes sur les processeurs modernes, l'attaquant ne peut pas injecter de code sur une région mémoire inscriptible du noyau, ni sauter dans une portion de code située dans l'espace utilisateur. Le seul moyen à disposition d'un attaquant pour contourner ces fonctionnalités est donc d'utiliser des techniques de réutilisation de code comme les attaques ROP (Return-Oriented Programming). L'attaquant a pour cela besoin d'une connaissance fine de l'espace d'adressage, or KASLR rend les adresses du noyau non prédictibles, empêchant l'exploitation des vulnérabilités. C'est là qu'interviennent les fuites d'informations par canal auxiliaire, car même si le système d'exploitation ne met pas explicitement ces informations à disposition et que toutes les contre-mesures sont en place, il est possible de les obtenir de la part du processeur lui-même.
2.2 Propriétés des instructions prefetch
Le prefetching est une technique utilisée par les processeurs afin de pallier à la lenteur de la DRAM. Au lieu d'attendre que les données et les instructions soient explicitement accédées pour être chargées dans le cache, le prefetching charge ces données avant que le programme n'en ait besoin. Il existe deux types de prefetching : matériel, et logiciel. Le prefetching matériel s'effectue de façon transparente par plusieurs unités au sein du processeur, qui vont deviner les lignes de cache les plus susceptibles d'être lues prochainement. Le prefetching logiciel s'effectue à l'aide d'instructions dédiées, que le programmeur ou le compilateur doit insérer dans le programme. Cette attaque contre KASLR tire partie de ces instructions.
Au niveau des processeurs x86, on retrouve cinq instructions de prefetch (prefetcht0, prefetcht1, prefetcht2, prefetchnta et prefetchw). Elles ont pour but d'indiquer au processeur qu'une donnée va être prochainement utilisée. Il ne s'agit en fait que d'un « indice » pour le processeur, qui peut être totalement ignoré. C'est également une instruction qui ne rend jamais d'erreur. Le manuel d'Intel est cependant flou sur le comportement du processeur dans le cas où l'instruction est appelée sur une adresse à laquelle l'application n'est pas censée accéder (par exemple une adresse dans l'espace noyau). Le manuel indique uniquement « Use of software prefetch should be limited to memory addresses that are managed or owned within the application context », c'est-à-dire que l'instruction ne devrait être utilisée que sur des adresses dans le contexte de l'application. À partir de là, on peut se demander ce qui se passe si ça n'est pas le cas !
Après investigation, nous avons remarqué que les instructions prefetch avaient deux propriétés intéressantes :
- Le temps d'exécution des instructions prefetch varie dépendamment de l'état interne de différents caches internes au CPU. Ce temps est suffisamment déterministe pour être exploitable.
- Les instructions prefetch n'effectuent pas de contrôles de privilège.
2.3 Localiser un pilote sous Windows 7 et Windows 10
Pour exploiter une vulnérabilité noyau, et construire une ROP chain dans le code d'un pilote, un attaquant a besoin de l'adresse exacte à laquelle est chargé le pilote. Ces adresses sont cependant randomisées par KASLR, et non accessibles depuis un process utilisateur. Retrouver l'adresse de chargement d'un pilote permet donc de contourner KASLR.
Fig. 1 : Illustration de la traduction d'adresse virtuelle vers adresse physique sur un processeur Intel récent.
L'attaque se déroule en deux étapes. Lors de la première étape, on localise les adresses qui sont effectivement mappées en mémoire physique dans la plage d'adresses réservées au noyau et aux drivers. Pour cela, on utilise la propriété 1 décrite en section 2.2. Les instructions prefetch doivent résoudre l'adresse virtuelle en adresse physique. La traduction d'adresse se fait via la table de traduction, qui a quatre niveaux sur les processeurs Intel récents, illustrés en Figure 1 : PML4 (Page Map Level 4), PDPT (Page Directory Pointer Table), PD (Page Directory), PT (Page Table). Le niveau PML4 divise l'espace d'adressage virtuel de 48 bits en 512 régions mémoire de 512Go chacune, appelées entrées PML4. Chaque entrée PML4 mappe sur un PDPT avec 512 entrées contrôlant une région mémoire de 1Go correspondant soit à une page physique de 1Go, ou à un PD. Le PD a lui aussi 512 entrées contrôlant une région mémoire de 2Mo correspondant soit à une page physique de 2Mo, ou à un PT. Le PT a enfin 512 entrées correspondant à une page de 4Ko.
Ces structures sont stockées en mémoire, mais également dans des caches spécifiques à chaque niveau afin de rendre la traduction d'adresse plus rapide. Ainsi, la traduction d'adresse suit une procédure définie, passant par ces différents caches et s'arrêtant dès que l'entrée correspondante est trouvée. En mesurant le temps d'exécution d'une instruction de prefetch, il est donc possible de savoir si une adresse virtuelle donnée est effectivement mappée en mémoire, mais également si la page physique correspondante est une page de 4Ko, 2Mo, ou encore 1Go. Le temps d'exécution de l'instruction dépend aussi de la présence ou non de la ligne dans le cache.
En prenant en compte la propriété 2 décrite en section 2.2, il est ainsi possible de récupérer ces informations pour des adresses du noyau. Sur Windows 7, le noyau ainsi que les drivers sont mappés sur des pages consécutives dans l'espace d'adressage virtuel de 2Mo, tandis que sous Windows 10, ils sont mappés sur des pages de 4Ko. On retrouve donc les adresses valides, c'est-à-dire effectivement mappées à la mémoire physique, dans la plage d'adresses par pas de 2Mo sous Windows 7, et par pas de 4Ko sous Windows 10.
La seconde étape de l'attaque consiste à déterminer si une adresse p est utilisée par un appel système (syscall). On construit pour cela une variante d'attaque sur le cache, que l'on appelle Evict+Prefetch. Similairement à l'attaque Evict+Reload décrite en section 1.2, on commence par évincer tous les caches en accédant linéairement aux adresses d'un buffer suffisamment grand (quelques méga-octets) pour évincer le TLB, les caches des différentes tables de traduction d'adresse ainsi que le cache de données et d'instructions. On déclenche ensuite un appel à une fonction du pilote que l'on souhaite localiser. Si l'adresse p ciblée est utilisée par le pilote, alors le processeur charge la ligne correspondante dans le cache. Il est ensuite possible de savoir si la ligne a été effectivement chargée dans le cache en mesurant le temps d'exécution de l'instruction prefetch sur cette adresse p. Si le temps est plus court qu'un certain seuil défini au préalable, on en déduit que la ligne est présente dans le cache, et dans le cas contraire, qu'elle y est absente. On vérifie que la ligne est effectivement utilisée par le pilote et non un autre programme en effectuant une mesure de contrôle, sans l'appel au pilote. Si le temps d'accès est plus long sans cet appel, la ligne n'est donc pas présente dans le cache et on apprend qu'elle était bel et bien utilisée par le pilote.
Fig. 2 : Localisation du pilote ciblé, illustrée par la bande rouge.
On effectue cette étape pour chacune des lignes de cache de la région mémoire effectivement mappée, récupérée à la première étape. Les lignes de cache faisant 64 octets, on procède maintenant par pas de 64 octets. La figure 2 illustre les mesures obtenues avec la technique Evict+Prefetch sur toute la région qui correspond aux pilotes. On observe une seule mesure en dessous de 100 cycles CPU (située dans la bande rouge) : celle-ci correspond à la ligne de cache chargée par la fonction du pilote ciblé. En connaissant les plages d'adresses virtuelles des drivers (fixes sous Windows), et en apprenant le décalage (offset) par ce canal auxiliaire, on apprend donc l'adresse de départ du driver. Un attaquant peut ensuite utiliser cette information pour effectuer une attaque ROP.
Sur notre machine de test munie d'un processeur Intel i3-5005U, et une charge système peu élevée, la première étape prend moins de 300ms sous Windows 10, et 7ms sous Windows 7. La différence de temps s'explique par le nombre d'adresses à tester plus élevé sous Windows 10, du aux pages de 4Ko utilisées. Avec une charge système très élevée, il est nécessaire d'effectuer plusieurs centaines de répétitions, atteignant ainsi un temps de 2 secondes en moyenne sous Windows 7. La deuxième étape est elle plus rapide sous Windows 10, car on a déjà réduit le nombre d'adresses candidates à la première étape. Cette étape prend un temps moyen de 490 secondes sous Windows 7, et de 12 secondes sous Windows 10.
3. Meltdown
Tout comme l'attaque sur KASLR, Meltdown [4] exploite le mapping du noyau dans l'espace d'adressage virtuel de chaque processus. Cependant, contrairement aux attaques par canal auxiliaire précédentes, Meltdown exploite une autre propriété des processeurs modernes : l'exécution dans le désordre (out-of-order execution). L'exécution dans le désordre est une optimisation qui évite au processeur de devoir attendre la résolution de dépendances et donc de perdre des cycles de calcul. Au niveau architectural, cela ne pose pas de problème de sécurité, car le processeur s'assure que l'exécution est correcte, c'est-à-dire que le résultat d'une opération est le même que celui d'une exécution dans l'ordre, et que toutes les instructions avaient le droit d'être exécutées. Le problème survient au niveau micro-architectural. En effet, même si le résultat de ces calculs n'est jamais rendu, les instructions ont quand même été exécutées par le processeur et ces calculs ont laissé des traces au niveau des différents éléments de la micro-architecture, comme le cache. Ce sont ces traces que l'on va aller lire, en utilisant le canal auxiliaire du cache. Concrètement, Meltdown permet d'obtenir le contenu de toute la mémoire physique, aussi bien sous Linux, OS X que Windows, et ceci sans exploiter de faille logicielle du noyau. Cette attaque nécessite d'avoir le droit d'exécuter du code non privilégié sur la machine, et ne peut s'effectuer à distance.
Meltdown est une attaque en trois étapes. Premièrement, on charge le contenu d'un emplacement mémoire – normalement inaccessible, par exemple dans l'espace noyau – dans un registre. Cet accès génère une exception qui arrête le chemin d'exécution du programme de sorte que les instructions suivantes ne seront normalement pas exécutées. À cause de l'exécution dans le désordre, une partie des instructions suivantes peuvent avoir déjà été exécutées. On appelle ces instructions des instructions éphémères. Deuxièmement, on choisit ces instructions éphémères pour qu'elles effectuent un accès au cache encodant la valeur lue lors de la première étape. Troisièmement, on utilise la technique Flush+Reload afin de déterminer quelle ligne de cache a été accédée lors de la deuxième étape, et d'en déduire le contenu de la mémoire lu en première étape.
Lors de la première étape, la lecture d'une donnée dans l'espace noyau va provoquer une exception, qui va faire crasher le programme si elle n'est pas traitée. Il est possible de gérer ce problème soit en traitant l'exception, soit en l'empêchant. Pour traiter l'instruction, il suffit d'utiliser un signal handler qui sera exécuté en cas d'erreur de segmentation. Pour l'empêcher, il est possible de détourner l'utilisation des instructions TSX de mémoire transactionnelle. Ces instructions, que l'on retrouve sur certains processeurs Intel, permettent de définir une séquence d'instructions comme devant s'exécuter de manière atomique. En particulier le processeur restaure l'état précédent en cas d'erreur, ce qui permet dans notre cas d'éviter l'erreur de segmentation.
Les deuxièmes et troisièmes étapes représentent un canal caché. Il est possible pour un attaquant d'encoder la valeur en utilisant n'importe quel canal auxiliaire, tel que l'unité de prédiction de branchement, mais en pratique le cache – et en particulier la technique Flush+Reload – se révèle être le canal caché le plus simple, le plus robuste, et avec la transmission la plus rapide. C'est donc cette technique que l'on détaille ici. Lors de la deuxième étape, on effectue un accès mémoire qui dépend de la valeur secrète, en accédant à une ligne de cache différente par valeur. Si la lecture s'effectue octet par octet, il existe 256 possibilités. Pour éviter que l'unité de prefetch matérielle n'ajoute du bruit aux mesures en chargeant les lignes adjacentes en mémoire, on n'effectue qu'un seul accès par page de 4Ko. En effet, l'unité de prefetch ne fonctionne pas au-delà des limites des pages de 4Ko. En pratique, on alloue en mémoire un tableau d'octet de 256*4096 octets, soit 1Mo. Pour faire fuiter la valeur i, on accède à l'indice i*4096 du tableau, afin que la ligne correspondante soit chargée dans le cache. Cela correspond à l'émetteur du canal caché. On n'a maintenant plus qu'à itérer la technique Flush+Reload sur ces 256 lignes possibles, et mesurer le temps d'accès pour chaque ligne. On devrait obtenir une unique ligne avec un temps d'accès inférieur au seuil correspondant à un cache hit. Une ligne dans le cache ne peut correspondre qu'à une seule valeur de i : à la fin de cette étape, on connaît donc i. Cela correspond au récepteur du canal caché. Ce récepteur ne fait pas partie des instructions éphémères, ce peut être un thread ou un processus différent.
En répétant ces trois étapes sur toutes les adresses du noyau, il est possible d'obtenir un dump de toute la mémoire noyau, octet par octet. Sous Linux et OS X, l'espace noyau contient en outre l'intégralité de la mémoire physique. Sous Windows, un tel mapping n'existe pas, mais en pratique il existe des pools de pages ainsi qu'un cache système qui sont mappés dans l'espace noyau, et qui constituent une large portion de la mémoire physique. De façon similaire, Xen en mode paravirtualisation est impacté, et il est ainsi possible d'obtenir le dump de la mémoire de l'hyperviseur, et ainsi des autres machines virtuelles colocalisées. Si le dump octet par octet peut paraître être une opération fastidieuse et donc lente, l'utilisation du canal caché Flush+Reload rend en fait l'opération rapide : les auteurs de Meltdown ont indiqué pouvoir dumper la mémoire physique avec une vitesse allant jusqu'à 503Ko/s.
4. Le correctif : KPTI
Après avoir parlé des deux attaques, abordons maintenant le correctif. KPTI est basée sur KAISER [6,7], un patch que nous avons proposé avec Daniel Gruss, Moritz Lipp, Michael Schwarz, Richard Fellner, et Stefan Mangard de Graz University of Technology, et publié à la conférence ESSoS, en juillet 2017. KPTI est aujourd'hui cité comme correctif principal à Meltdown, mais à l'origine l'objectif était de contrer les attaques par canal auxiliaire sur KASLR – aussi bien celle décrite en section 2 que d'autres attaques de la littérature [9,10]. KPTI a finalement été intégré, avec de grosses modifications par rapport à KAISER, au noyau Linux à la version 4.15. Des correctifs similaires ont été intégrés à Windows, OS X, FreeBSD, et OpenBSD.
L'objectif de KPTI est de mieux séparer la mémoire de l'espace utilisateur et de l'espace noyau. Pour ce faire, au lieu de n'avoir qu'un seul espace d'adressage par processus, il y en a maintenant deux. Le premier, accessible en mode utilisateur, a l'espace utilisateur mappé, mais pas l'espace noyau, à l'exception d'un ensemble minimal de mappings de l'espace noyau nécessaires aux appels systèmes et interruptions. Le deuxième, accessible en mode noyau, a à la fois l'espace noyau et l'espace utilisateur mappés, avec l'espace utilisateur protégé par les fonctionnalités SMEP et SMAP.
En pratique, KPTI effectue une séparation des tables des pages pour le mode utilisateur et pour le mode noyau. Cela passe par la mise à jour du registre CR3, qui indique le début du PML4 (voir Figure 1), à chaque changement de contexte. Cette mise à jour n'est pas sans conséquence sur la performance. Le changement du registre lui-même rajoute quelques centaines de cycles à tous les appels systèmes et interruptions. Sur les processeurs plus anciens, le fait même de changer le registre CR3 implique de devoir évincer tout le TLB (Translation Lookaside Buffer), car les associations entre adresses virtuelles et adresses physiques ayant changé, les entrées du TLB sont devenues obsolètes. Cette éviction est source de la plus grande perte de performance. Heureusement, les processeurs Intel depuis la micro-architecture Westmere (2010) supportent la fonctionnalité PCID (Process-Context Identifiers), qui tague les entrées du TLB. Seules les entrées taguées avec le PCID du thread courant seront retournées lors d'une recherche, éliminant la nécessité d'évincer le TLB à chaque mise à jour du registre CR3. Le support des PCIDs a été intégré au noyau Linux à la version 4.14. Les pertes de performance sont de l'ordre de 5% sur des processeurs récents et sur des benchmarks typiques, pouvant aller jusqu'à 30% sur des processus effectuant un nombre important de changements de contexte.
Il faut rappeler pour le contexte que, si Meltdown n'a été révélé au public que début janvier 2018, Intel était déjà au courant de cette vulnérabilité en juin 2017. Le fait que KAISER ait été considéré comme patch pour le noyau à la fin de l'année 2017, au vu de la dégradation des performances et compte tenu de l'impact de l'attaque contre KASLR (somme toute faible comparé à Meltdown), a suggéré la possibilité d'une faille plus importante qu'envisagée.
5. Et Spectre ?
Les attaques Meltdown et Spectre ont été dévoilées simultanément début janvier, et sont souvent citées de pair. Les deux attaques exploitent des mécanismes similaires d'exécution spéculative, dans le sens où le processeur exécute des instructions avant de savoir si cette exécution est réellement nécessaire. Dans le cas de Spectre [5], l'attaquant utilise la prédiction de branchement pour faire fuiter des informations auxquelles il n'est pas censé avoir accès. Cette attaque est plus délicate à effectuer que Meltdown, car elle implique d'entraîner l'unité de prédiction de branchement à effectuer une mauvaise prédiction. L'étape de récupération des valeurs secrètes est identique à celle de Meltdown, utilisant un canal caché. Si les mécanismes utilisés sont similaires, il n'en va en revanche pas de même pour les contre-mesures. En effet, étant donné que Spectre ne repose pas sur la présence de l'espace noyau dans l'espace d'adressage des processus, KPTI n'est pas une contre-mesure contre Spectre. Pour contrer Spectre, il est possible d'installer une mise à jour du microcode, ou encore de patcher le logiciel vulnérable.
Conclusion
Les attaques par canal auxiliaire sur la micro-architecture se sont beaucoup développées ces dernières années, pour atteindre une réelle exposition médiatique auprès du grand public en début d'année 2018. Ces attaques reposent sur des optimisations de performance apportées par des composants de plus en plus complexes et souvent non documentés. L'impact en terme de sécurité de ces composants est encore relativement incompris, en témoigne le fait que l'exécution dans le désordre est une fonctionnalité présente dans les processeurs depuis le milieu des années 1990. Tout l'enjeu des travaux de recherche sur ce domaine repose sur le fait qu'on aimerait pouvoir se passer des attaques sans pour autant se passer de la performance.
Remerciements
Cet article est en partie basé sur des travaux de recherche effectués avec mes collaborateurs que je remercie, de l'équipe Secure Systems de Graz University of Technology ainsi qu'Anders Fogh. Il est également basé sur d'autres travaux de recherche, incluant Meltdown, référencés dans les notes. Je remercie également sincèrement mes relecteurs Pierre, Florent, et Erwan.
Références
[1] Clémentine Maurice, Manuel Weber, Michael Schwarz, Lukas Giner, Daniel Gruss, Carlo Alberto Boano, Stefan Mangard, et Kay Römer, « Hello from the Other Side : SSH over Robust Cache Covert Channels in the Cloud », Proc. of NDSS’17, 2017
[2] Yossef Oren, Vasileios P Kemerlis, Simha Sethumadhavan, et Angelos D Keromytis, « The spy in the sandbox : Practical cache attacks in javascript and their implications », Proc. of CCS’15, 2015
[3] Daniel Gruss, Clémentine Maurice, Anders Fogh, Moritz Lipp, et Stefan Mangard, « Prefetch Side-Channel Attacks : Bypassing SMAP and Kernel ASLR », Proc. of CCS’16, 2016
[4] Moritz Lipp, Michael Schwarz, Daniel Gruss, Thomas Prescher, Werner Haas, Stefan Mangard, Paul Kocher, Daniel Genkin, Yuval Yarom, et Mike Hamburg, « Meltdown ». https://arxiv.org/pdf/1801.01207.pdf, janvier 2018
[5] Paul Kocher, Daniel Genkin, Daniel Gruss, Werner Haas, Mike Hamburg, Moritz Lipp, Stefan Mangard, Thomas Prescher, Michael Schwarz, et Yuval Yarom, « Spectre attacks : Exploiting speculative execution ». https://arxiv.org/pdf/1801.01203.pdf, janvier 2018
[6] Jonathan Corbet. « Kaiser: hiding the kernel from user space ». https://lwn.net/Articles/738975/, 2017
[7] Daniel Gruss, Moritz Lipp, Michael Schwarz, Richard Fellner, Clémentine Maurice, et Stefan Mangard, « KASLR is Dead : Long Live KASLR », Proc. of ESSoS’17, 2017
[8] Yuval Yarom et Katrina Falkner, « Flush+reload : A high resolution, low noise, l3 cache side-channel attack », Proc. of USENIX Security Symposium, 2014
[9] Yeongjin Jang, Sangho Lee, et Taesoo Kim, « Breaking kernel address space layout randomization with Intel TSX», Proc. of CCS’16, 2016
[10] Ralf Hund, Carsten Willems, et Thorsten Holz. « Practical timing side channel attacks against kernel space ASLR », Proc. of S&P’13, 2013