hacking computers programming cybersecurity
© Kim Hong-Ji / Reuters
Les réseaux sociaux et les blogs spécialisés en sécurité bruissaient de rumeurs depuis une semaine (pourquoi des modifications si urgentes dans le système de gestion de mémoire du noyau Linux, alors que d'habitude il faut des mois et des mois pour que le moindre changement soit accepté ?). Comme d'habitude lorsque des trous de sécurité majeurs sont découverts, ceux-ci n'étaient documentés que sous embargo, c'est-à-dire qu'on a d'abord informé les industriels ou groupes de développeurs susceptibles de fournir des correctifs, en leur laissant un délai suffisant, avant de publier les articles décrivant les problèmes.

Il y a en fait deux catégories d'attaques publiées ce jour : MELTDOWN et SPECTRE. Les attaques MELTDOWN sont celles contre lesquelles Microsoft, Apple et les développeurs de Linux ont intégré des contre-mesures. Je vais ici discuter des attaques SPECTRE, contre lesquelles il n'y a, à ma connaissance, pas de contre-mesure. Je me base pour cela sur la lecture de l'article décrivant les attaques (Kocher et al., Spectre Attacks: Exploiting Speculative Execution), article très pédagogique au moins au début. Je vais tenter de les expliquer à un niveau ne nécessitant pas de connaissances particulières en informatique.

Dans un ordinateur, un ou plusieurs processeurs exécutent des séquences d'instructions de calcul (additions, soustractions, multiplications, lecture ou écriture de données dans la mémoire). Ce sont ces instructions qui constituent les logiciels : quelle que soit la complexité ou le domaine d'application de celui-ci, ou le langage de programmation utilisé, on en revient toujours à l'exécution d'une suite de petites instructions comme cela.

On décrit parfois l'exécution de ces instructions de la façon suivante : le processeur lit l'instruction dans la mémoire, la décode (s'agit-il d'une addition, d'une soustraction, etc.), récupère éventuellement dans la mémoire les données dont elle a besoin, exécute l'opération demandée, puis écrit éventuellement son résultat dans la mémoire. C'est ainsi, en effet, que fonctionnaient les processeurs du début des années 1980 (Motorola 68000, par exemple).

Ce mode de fonctionnement est inefficace : il faut attendre que chaque étape soit achevée pour aborder la suite. On a donc fait par la suite des processeurs qui, bien qu'ils semblent, du point de vue du programmeur, exécuter successivement les instructions, les exécutent en fait comme sur une chaîne d'assemblage automobile (on parle, en terme techniques, de pipeline) : une unité du processeur décode, dès que l'instruction est décodée on la transfère aux unités qui lisent en mémoire qui la gèrent tandis que l'instruction suivante est décodée et que l'opération de calcul de l'instruction précédente est exécutée. On en est même venu à avoir des processeurs qui réordonnent l'exécution de parties d'instruction afin d'utiliser au maximum leurs unités, voire des processeurs qui tentent d'exécuter deux programmes à la fois sur les mêmes unités en tirant parti du fait que certaines sont inoccupées (hyperthreading) ! Ce qu'il faut retenir, c'est qu'il y a de nos jours, dans les processeurs à haute performance (dont ceux des PC portables, de bureau ou serveurs), des mécanismes extrêmement complexes qui essayent, grosso modo, de simuler une exécution « comme en 1980 » alors que ce n'est pas ce qui se passe dans la machine. Voyons certains de ces mécanismes.

J'ai dit plus haut qu'il fallait souvent chercher dans la mémoire de la machine (la RAM) les données nécessaires à l'exécution d'une instruction. Or, l'accès à la RAM prend du temps, beaucoup plus que l'exécution d'une instruction : cet écart entre la vitesse d'exécution des instructions et le temps nécessaire pour obtenir une donnée de la RAM a crû au cours du temps. Pour compenser, on a intégré dans les processeurs des mécanismes de mémoire cache qui, grosso modo (c'est en réalité bien plus compliqué) retiennent dans le processeur les données accédées les plus récemment et évitent le trajet vers la mémoire si la donnée recherchée est dans le cache.

Les instructions exécutées comportent souvent des branchements : si une certaine condition est vraie, on ira exécuter la suite du programme à un autre endroit spécifié, si elle est fausse on passera à l'instruction suivante. En version naïve, il faudrait donc attendre de savoir si la condition est vraie ou fausse pour pouvoir exécuter un branchement, et tout bloquer en attendant. Si cette condition dépend de données en mémoire, il faudra donc attendre que ces données soient disponibles ! On a donc introduit l'exécution spéculative : le processeur fait l'hypothèse que le branchement se fera (ou ne se fera pas) et continue l'exécution sous cette hypothèse, quitte à rétracter les instructions exécutées spéculativement si cette hypothèse se révèle fausse, c'est-à-dire à faire comme si elles n'avaient pas été exécutées - j'avoue ne pas savoir comment on peut efficacement procéder à pareille rétractation. Dans les processeurs modernes, cette hypothèse est guidée par une mémoire cache qui indique si la condition était ou non vraie la dernière fois que le processeur a exécuté cette instruction de branchement, l'idée étant que la fois suivante cela sera probablement pareil. De même, dans certains cas l'endroit où un branchement va emmener le programme est le résultat d'un calcul, et afin d'éviter de devoir attendre son résultat, une mémoire cache va donner l'endroit utilisé la dernière fois qu'on a exécuté ce branchement, qui sera spéculativement utilisé - bien entendu là encore on rétracte les instructions si l'hypothèse se révèle fausse.

Le mécanisme de rétractation n'est toutefois pas parfait, et c'est ce que l'attaque SPECTRE exploite. En effet, si les effets « fonctionnels » des instructions spéculativement exécutées sont effectivement effacés, il en reste certains effets indirects : par exemple, une instruction spéculativement exécutée a pu faire venir une donnée dans la mémoire cache. La présence ou non de cette donnée dans le cache n'est pas directement vérifiable, mais il existe des moyens indirects, notamment par des mesures de temps d'exécution (par exemple, le chargement d'une donnée dans la mémoire cache va forcer la sortie d'une autre donnée, dont l'accès suivant sera donc un peu plus lent). Les attaques SPECTRE se basent sur ces effets « non fonctionnels » de l'exécution spéculative d'instructions.

Quel est exactement le problème ? Les instructions exécutées spéculativement peuvent être des instructions qui normalement ne seraient jamais exécutées, notamment des instructions dont l'exécution est bloquée par une mesure de sécurité. Par exemple, les navigateurs Web exécutent des programmes en langage Javascript intégré dans les pages Web (donc provenant de sources quelconques auxquelles on ne fait pas confiance), mais, sauf bug dans le navigateur, interdisent à ces programmes d'aller manipuler des données auxquelles ils n'ont pas le droit de toucher. À différent moments, l'exécution du programme dans le navigateur va passer par des tests et des branchements « si l'accès aux données est autorisé, aller par ici, sinon, aller par là ». Une attaque SPECTRE consiste alors à s'arranger pour que le processeur exécute spéculativement l'accès à des données non autorisées. Bien entendu, les résultats directs de ces accès sont rétractés, mais on peut les observer indirectement par leur effet sur les mémoires cache.

En résumé, les attaques SPECTRE sont basées sur l'exploitation de mécanismes matériels destinés à l'exécution à haute performance des programmes, et qui font dans certains cas fuir des informations normalement inaccessibles via des canaux cachés, c'est-à-dire des mesures indirectes (notamment temps d'exécution de programmes). Elles permettent notamment à un programme auquel on ne fait pas confiance (par exemple, du code Javascript dans une page Web) et s'exécutant dans un environnement qui contrôle ses accès (par exemple, navigateur Web) de lire des données auxquelles il ne devrait pas avoir accès (mots de passe, clefs de signature électronique...) et ce sans qu'il n'y ait le moindre bug logiciel.

Il peut paraître surprenant que ces attaques, dont certaines des formes sont conceptuellement simples, n'apparaissent que maintenant alors qu'elles s'appuient sur des mécanismes présents depuis vingt ans dans les processeurs. Peut-être, d'ailleurs, étaient-elles connues de certaines officines ou services de renseignement ? Nous ne le saurons sans doute jamais, ou du moins pas avant longtemps. Mon opinion est que si elles sont passées si longtemps inaperçues, c'est parce qu'elles enfreignent certaines des catégories intellectuelles des programmeurs et même des spécialistes de sécurité informatique, dont le métier implique pourtant de « penser hors de la boîte ».

La vision du programmeur, même de bas niveau, ce sont des instructions dont le processeur simule une exécution séquentielle « comme du temps de papa » ; les divers mécanismes de cache, d'exécution spéculative, d'exécution dans un autre ordre sont transparents (même si les programmeurs avancés, dans des applications comme le calcul haute performance, font attention aux mémoires caches). Dans ce paradigme, les fuites de données et les trous de sécurité proviennent de fautes de conception du logiciel, qui dans certains cas fait des choses qu'il ne devrait pas. Pour prendre en compte les attaques par canaux cachés, il faut déjà sortir un peu de ces idées. Pour prendre en compte SPECTRE, il faut également tenir compte de séquences d'instructions qui ne peuvent pas être exécutées, dont l'exécution est normalement interdite, mais dont l'exécution spéculative, si elle ne donne bien entendu pas de résultats « officiels », produit des fuites d'information par des canaux cachés. Il n'est pas naturel pour le programmeur de prendre en compte l'exécution de telles séquences d'instructions fantomatiques !

Je n'ai évidemment pas de solution magique à proposer pour pallier cette catégorie d'attaques ; toute solution devra de toute façon être mûrement réfléchie. Même s'il était possible, sur les processeurs actuels, de désactiver les caches et/ou l'exécution spéculative (je ne sais pas si ça l'est), la perte de performance serait probablement intolérable. Une précaution importante est bien sûr d'éviter de lancer du code provenant de l'extérieur, y compris dans des environnements censément sécurisés (Javascript, Java...), mais cette recommandation est difficile à suivre tant le Web moderne repose justement sur l'exécution de code extérieur (désactivez Javascript dans votre navigateur, et regardez combien de sites fonctionnent encore...). Qui plus est, ces attaques suivent des principes généraux adaptables à des processeurs de diverses architectures (x86, ARM...) et de divers fabricants (Intel, AMD...).

Bref, nous vivons des temps intéressants. J'en profite pour rappeler qu'il y a un poste de maître de conférence ouvert cette année à l'ENSIMAG et au laboratoire VERIMAG, y compris sur les thématiques de sécurité informatique.

PS : J'ai écrit un billet sur l'attaque MELTDOWN.

PS² : L'avis sur le site de l'hyperviseur Xen est fort instructif, ainsi que le billet du projet Google Zéro.

PS³: Florence Maraninchi a elle aussi écrit une explication grand public de l'attaque.