Le 3 janvier 2018, Google Project Zero et d'autres ont
révélé les trois premiers d'une nouvelle classe de vulnérabilités qui affectent les processeurs d'exécution spéculative. Ils s'appelaient
Spectre (1 et 2) et
Meltdown . À l'aide de mécanismes d'
exécution spéculatifs du processeur, un attaquant peut contourner temporairement les contrôles de sécurité logiciels explicites et implicites qui empêchent les programmes de lire des données inaccessibles en mémoire. Alors que l'exécution spéculative était conçue dans le cadre de la microarchitecture, invisible au niveau architectural, des programmes soigneusement conçus pouvaient lire des informations inaccessibles dans un bloc spéculatif et les révéler par des canaux latéraux, tels que le temps d'exécution d'un fragment de programme.
Lorsqu'il a été démontré que les attaques Spectre sont possibles en utilisant JavaScript, l'équipe V8 a participé à la résolution du problème. Nous avons formé une équipe d'intervention d'urgence et travaillé en étroite collaboration avec d'autres équipes Google, nos autres partenaires de navigation et nos partenaires matériels. Avec eux, nous avons mené de manière proactive à la fois des recherches offensives (conception de modules d'attaque pour prouver le concept) et défensives (atténuation des attaques potentielles).
L'attaque Spectre se compose de deux parties:
- Fuite de données autrement inaccessibles dans l'état latent du CPU . Toutes les attaques Spectre connues utilisent la spéculation pour transférer des bits de données inaccessibles vers des caches CPU.
- Récupération d'un état masqué pour restaurer des données inaccessibles. Pour cela, un attaquant a besoin d'une montre d'une précision suffisante. (Précision étonnamment faible, en particulier avec des méthodes telles que le seuillage des bords - comparer avec un seuil le long d'un contour sélectionné).
Théoriquement, il suffirait de bloquer l'une des deux composantes de l'attaque. Étant donné que nous ne savons comment bloquer complètement aucun d'entre eux, nous avons développé et déployé des atténuations qui réduisent considérablement la quantité d'informations qui fuient dans les caches de processeur et les atténuations qui rendent difficile la récupération d'un état masqué.
Minuteries de haute précision
De minuscules changements d'état qui subsistent après l'exécution spéculative produisent des différences temporelles minuscules, presque incroyablement minuscules - de l'ordre du milliardième de seconde. Pour détecter directement ces différences individuelles, l'attaquant a besoin d'une minuterie de haute précision. Les processeurs proposent de tels temporisateurs, mais la plate-forme Web ne les définit pas. Le minuteur le plus précis de la plate-forme Web
performance.now()
avait une résolution de plusieurs microsecondes, ce qui était initialement considéré comme inadapté à cette fin. Cependant, il y a deux ans, un groupe de recherche spécialisé dans les attaques microarchitecturales a publié
un article sur les minuteries sur une plateforme web. Ils ont conclu que la mémoire partagée mutable simultanée et diverses méthodes de récupération de résolution permettent la création de minuteries de résolution encore plus élevée, jusqu'à la nanoseconde. Ces temporisateurs sont suffisamment précis pour détecter les coups sûrs et manqués du cache L1. C'est lui qui est généralement utilisé pour capturer des informations dans les attaques Spectre.
Protection minuterie
Pour perturber la capacité de détecter de petites différences de temps, les développeurs de navigateurs ont choisi une approche multilatérale. Dans tous les navigateurs, la résolution de
performance.now()
été réduite (dans Chrome de 5 microsecondes à 100) et une gigue aléatoire a été introduite pour empêcher la restauration de la résolution. Après des consultations entre les développeurs de tous les navigateurs, nous avons décidé ensemble de franchir une étape sans précédent: désactiver immédiatement et rétroactivement l'API
SharedArrayBuffer
dans tous les navigateurs pour empêcher la création d'une minuterie nanoseconde.
Gain
Au début de nos recherches offensives, il est devenu clair que les atténuations de temporisation à elles seules ne suffisent pas. L'une des raisons est qu'un attaquant peut simplement exécuter son code à plusieurs reprises, de sorte que la différence de temps cumulée est bien plus qu'un hit ou un cache manquant. Nous avons pu construire des «gadgets» fiables qui utilisent plusieurs lignes de cache à la fois, jusqu'à la capacité de cache entière, ce qui donne une différence de temps allant jusqu'à 600 microsecondes. Plus tard, nous avons découvert des méthodes d'amplification arbitraires qui ne sont pas limitées par la capacité du cache. Ces méthodes d'amplification sont basées sur des tentatives répétées de lecture de données secrètes.
Protection JIT
Pour lire des données inaccessibles à l'aide de Spectre, un attaquant force le CPU à exécuter de manière spéculative du code qui lit des données normalement inaccessibles et les place dans le cache. La protection peut être envisagée de deux côtés:
- Empêcher l'exécution de code spéculatif.
- Prévention de la lecture des données inaccessibles du pipeline spéculatif.
Nous avons expérimenté la première option en insérant des instructions recommandées pour empêcher la spéculation, comme le
LFENCE
d'Intel, sur chaque branche conditionnelle critique et en utilisant des
retpolins pour les branches indirectes. Malheureusement, de telles mesures d'atténuation réduisent considérablement la productivité (ralentissement de 2 à 3 fois par rapport à l'indice de référence Octane). Au lieu de cela, nous avons adopté la deuxième approche en insérant des séquences d'atténuation qui empêchent la lecture des données sensibles en raison d'une spéculation incorrecte. Permettez-moi d'illustrer la technique avec l'extrait de code suivant:
if (condition) { return a[i]; }
Pour simplifier, nous supposons que la condition
0
ou
1
. Le code ci-dessus est vulnérable si le CPU lit spéculativement à partir d'
a[i]
lorsque
i
est hors de portée, accédant à des données normalement inaccessibles. Une observation importante est que dans ce cas, la spéculation essaie de lire
a[i]
lorsque la condition est
0
. Notre atténuation réécrit ce programme afin qu'il se comporte exactement de la même manière que le programme d'origine, mais ne laisse aucune fuite de données spéculativement chargées.
Nous réservons un registre CPU, que nous appelons «poison», pour savoir si le code s'exécute dans une branche mal interprétée. Le registre anti-poison est pris en charge dans toutes les branches et tous les appels du code généré, de sorte que toute branche incorrectement interprétée fait que le registre anti-poison devient
0
. Ensuite, nous mesurons tous les accès à la mémoire afin qu'ils masquent inconditionnellement le résultat de tous les téléchargements avec la valeur actuelle du registre antipoison. Cela n'empêche pas le processeur de prédire (ou d'interpréter de manière erronée) les branches, mais cela détruit les informations (potentiellement hors des limites) des valeurs chargées en raison de branches mal interprétées. Le code de l'outil est illustré ci-dessous (
a
est un tableau de nombres).
let poison = 1;
Le code supplémentaire n'affecte pas le comportement normal (défini par l'architecture) du programme. Il n'affecte l'état micro-architectural que lorsque vous travaillez sur un processeur avec une exécution spéculative. Si vous instrumentez un programme au niveau du code source, les optimisations avancées des compilateurs modernes peuvent supprimer une telle instrumentation. Dans la version 8, nous empêchons le compilateur de supprimer les atténuations en les insérant à un stade très avancé de la compilation.
Nous utilisons également cette technique d'empoisonnement pour éviter les fuites de branches indirectes dans la boucle de bytecode de l'interpréteur et dans la séquence d'appels de fonction JavaScript. Dans l'interpréteur, nous mettons le poison à
0
si le gestionnaire de bytecode (c'est-à-dire une séquence de code machine qui interprète un bytecode) ne correspond pas au bytecode actuel. Pour les appels JavaScript, nous transmettons la fonction cible en tant que paramètre (dans le registre) et définissons le poison sur
0
au début de chaque fonction si la fonction cible entrante ne correspond pas à la fonction actuelle. Avec cet assouplissement, nous observons un ralentissement de moins de 20% de l'indice de référence Octane.
L'atténuation pour WebAssembly est plus simple, car le contrôle de sécurité principal consiste à s'assurer que l'accès à la mémoire est dans les limites. Pour les plates-formes 32 bits, en plus des vérifications de limites habituelles, nous remplissons toute la mémoire à la puissance suivante de deux et masquons inconditionnellement tous les bits supérieurs de l'index de mémoire utilisateur. Les plates-formes 64 bits n'ont pas besoin d'une telle atténuation, car l'implémentation utilise la protection de la mémoire virtuelle pour les contrôles de bordure. Nous avons expérimenté la compilation d'instructions switch / case en code de recherche binaire au lieu d'utiliser une branche indirecte potentiellement vulnérable, mais c'est trop cher pour certaines charges de travail. Les appels indirects sont protégés par des retpolins.
Protection logicielle - peu fiable
Heureusement ou malheureusement, nos recherches offensives ont progressé beaucoup plus rapidement que sur la défensive, et nous avons rapidement constaté qu'il était impossible d'atténuer par programme toutes les fuites possibles lors des attaques Spectre. Il y a plusieurs raisons à cela. Premièrement, les efforts d'ingénierie pour combattre Spectre sont disproportionnés par rapport au niveau de menace. Dans la V8, nous rencontrons de nombreux autres risques de sécurité qui sont bien pires, de la lecture directement en dehors des frontières en raison de bogues courants (ce qui est plus rapide et plus facile que Spectre), de l'écriture en dehors des frontières (cela est impossible avec Spectre et pire) et du potentiel distant exécution de code (impossible avec Spectre et bien pire). Deuxièmement, les mesures d'atténuation de plus en plus sophistiquées que nous avons développées et mises en œuvre comportaient une complexité importante, ce qui est une obligation technique et peut en fait augmenter la surface d'attaque et les frais généraux de performance. Troisièmement, tester et maintenir l'atténuation des fuites microarchitecturales est encore plus difficile que de concevoir les gadgets eux-mêmes pour une attaque, car il est difficile d'être sûr que les atténuations continuent de fonctionner comme elles ont été conçues. Au moins une fois, d'importantes atténuations ont été effectivement annulées par les optimisations ultérieures du compilateur. Quatrièmement, nous avons constaté que l'atténuation efficace de certaines options de Spectre, en particulier l'option 4, n'est tout simplement pas possible dans le logiciel, même après les efforts héroïques de nos partenaires Apple pour résoudre le problème dans leur compilateur JIT.
Isolement du site
Nos recherches ont abouti à la conclusion: en principe, le code non fiable peut lire l'intégralité de l'espace d'adressage d'un processus en utilisant Spectre et les canaux latéraux. Les atténuations logicielles réduisent l'efficacité de nombreux gadgets potentiels, mais ne sont ni efficaces ni complètes. La seule mesure efficace consiste à déplacer les données sensibles en dehors de l'espace d'adressage du processus. Heureusement, Chrome essaie depuis de nombreuses années de séparer les sites en différents processus afin de réduire la surface d'attaque en raison de vulnérabilités courantes. Ces investissements ont porté leurs fruits et, en mai 2018, nous sommes passés au stade de préparation et avons étendu l'
isolement des sites sur le nombre maximal de plates-formes. Ainsi, le modèle de sécurité Chrome n'assume plus la confidentialité de la langue pendant le processus de rendu.
Spectre a parcouru un long chemin et a souligné les mérites de la collaboration des développeurs dans l'industrie et le monde universitaire. Jusqu'à présent, les chapeaux blancs sont en avance sur les chapeaux noirs. Nous ne connaissons toujours pas une seule véritable attaque, à l'exception des expérimentateurs curieux et des chercheurs professionnels qui développent des gadgets pour prouver le concept. De nouvelles variantes de ces vulnérabilités continuent d'apparaître et cela continuera pendant un certain temps. Nous continuons de surveiller ces menaces et de les prendre au sérieux.
Comme de nombreux programmeurs, nous avons également pensé que les langages sûrs fournissent la bonne frontière pour l'abstraction, empêchant les programmes bien typés de lire la mémoire arbitraire. Il est triste que cela se soit avéré être une erreur - cette garantie ne correspond pas à l'équipement d'aujourd'hui. Bien sûr, nous pensons toujours que les langages sûrs ont plus d'avantages en matière d'ingénierie, et l'avenir leur appartient, mais ... sur les équipements d'aujourd'hui, ils fuient un peu.
Les lecteurs intéressés peuvent approfondir le sujet et obtenir des informations plus détaillées dans notre
article scientifique .