Guide pour les développeurs de C++ sur les canaux auxiliaires d’exécution spéculative
Cet article contient des conseils pour les développeurs afin d’identifier et d’atténuer les vulnérabilités matérielles de canal côté exécution spéculative dans le logiciel C++. Ces vulnérabilités peuvent divulguer des informations sensibles au-delà des limites de confiance et affecter les logiciels qui s’exécutent sur des processeurs qui prennent en charge l’exécution spéculative et hors ordre des instructions. Cette classe de vulnérabilités a d’abord été décrite en janvier 2018 et des conseils supplémentaires sont disponibles dans l’avis de sécurité de Microsoft.
Les conseils fournis par cet article sont liés aux classes de vulnérabilités représentées par :
CVE-2017-5753, également appelé Spectre variant 1. Cette classe de vulnérabilité matérielle est liée aux canaux secondaires qui peuvent survenir en raison d’une exécution spéculative qui se produit à la suite d’une erreur de prédiction de branche conditionnelle. Le compilateur Microsoft C++ dans Visual Studio 2017 (à compter de la version 15.5.5) inclut la prise en charge du commutateur qui fournit une atténuation au moment de la
/Qspectre
compilation pour un ensemble limité de modèles de codage potentiellement vulnérables liés à CVE-2017-5753. Le/Qspectre
commutateur est également disponible dans Visual Studio 2015 Update 3 via Ko 4338871. La documentation de l’indicateur/Qspectre
fournit plus d’informations sur ses effets et son utilisation.CVE-2018-3639, également appelé Contournement de magasin spéculatif (SSB). Cette classe de vulnérabilité matérielle est liée aux canaux secondaires qui peuvent survenir en raison de l’exécution spéculative d’une charge devant un magasin dépendant en raison d’une mauvaise dépréciation de l’accès à la mémoire.
Une introduction accessible aux vulnérabilités de canal côté exécution spéculative est disponible dans la présentation intitulée The Case of Spectre et Meltdown par l’une des équipes de recherche qui ont découvert ces problèmes.
Qu’est-ce que les vulnérabilités matérielles du canal côté exécution spéculative ?
Les processeurs modernes offrent des degrés de performances plus élevés en utilisant l’exécution spéculative et hors ordre des instructions. Par exemple, cela est souvent effectué en prédisant la cible de branches (conditionnelle et indirecte) qui permet au processeur de commencer à exécuter des instructions spéculativement sur la cible de branche prédite, ce qui évite un blocage jusqu’à ce que la cible de branche réelle soit résolue. Dans le cas où l’UC découvre ultérieurement qu’une mauvaise prédiction s’est produite, l’état de l’ordinateur qui a été calculé de manière spéculative est dis carte ed. Cela garantit qu’il n’y a pas d’effets architecturalement visibles de la spéculation mal prédite.
Bien que l’exécution spéculative n’affecte pas l’état architecturalment visible, elle peut laisser des traces résiduelles dans un état non architectural, comme les différents caches utilisés par le processeur. Il s’agit de ces traces résiduelles d’exécution spéculative qui peuvent donner lieu à des vulnérabilités de canal latéral. Pour mieux comprendre cela, tenez compte du fragment de code suivant qui fournit un exemple de CVE-2017-5753 (Contournement de vérification des limites) :
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
Dans cet exemple, ReadByte
est fourni une mémoire tampon, une taille de mémoire tampon et un index dans cette mémoire tampon. Le paramètre d’index, tel que spécifié par untrusted_index
, est fourni par un contexte moins privilégié, tel qu’un processus non administratif. S’il untrusted_index
est inférieur buffer_size
à , le caractère à cet index est lu buffer
et utilisé pour indexer dans une région partagée de mémoire référencée par shared_buffer
.
D’un point de vue architectural, cette séquence de code est parfaitement sûre, car elle est garantie qu’elle untrusted_index
sera toujours inférieure à buffer_size
. Toutefois, en présence d’exécution spéculative, il est possible que l’UC ne prédicte pas la branche conditionnelle et exécute le corps de l’instruction if, même si elle untrusted_index
est supérieure ou égale à buffer_size
. Par conséquent, l’UC peut lire spéculativement un octet au-delà des limites de buffer
(qui peut être un secret) et utiliser ensuite cette valeur d’octet pour calculer l’adresse d’une charge ultérieure.shared_buffer
Bien que l’UC détecte finalement cette mauvaise prédiction, les effets secondaires résiduels peuvent être laissés dans le cache du processeur qui révèlent des informations sur la valeur d’octet qui a été lue hors limites de buffer
. Ces effets secondaires peuvent être détectés par un contexte moins privilégié s’exécutant sur le système en mettant en évidence la rapidité d’accès à chaque ligne shared_buffer
de cache. Les étapes qui peuvent être effectuées pour ce faire sont les suivantes :
Appelez
ReadByte
plusieurs fois avecuntrusted_index
une valeur inférieure àbuffer_size
. Le contexte d’attaque peut entraîner l’appelReadByte
du contexte de victime (par exemple, via RPC) de sorte que le prédicteur de branche soit entraîné à ne pas être pris comme étantuntrusted_index
inférieurbuffer_size
à .Videz toutes les lignes de cache dans
shared_buffer
. Le contexte d’attaque doit vider toutes les lignes de cache dans la région partagée de la mémoire référencée parshared_buffer
. Étant donné que la région de mémoire est partagée, cela est simple et peut être accompli à l’aide d’intrinsèques telles que_mm_clflush
.Appeler
ReadByte
avecuntrusted_index
une valeur supérieure àbuffer_size
. Le contexte d’attaque provoque l’appelReadByte
du contexte de la victime de telle sorte qu’il prédit de façon incorrecte que la branche ne sera pas prise. Cela entraîne l’exécution spéculative du corps du bloc if avecuntrusted_index
une valeur supérieurebuffer_size
à , ce qui entraîne une lecture hors limites .buffer
Par conséquent,shared_buffer
est indexé à l’aide d’une valeur potentiellement secrète qui a été lue hors limites, ce qui entraîne le chargement de la ligne de cache correspondante par l’UC.Lisez chaque ligne de cache pour
shared_buffer
voir qui est accessible le plus rapidement. Le contexte d’attaque peut lire chaque ligne de cache etshared_buffer
détecter la ligne de cache qui se charge beaucoup plus rapidement que les autres. Il s’agit de la ligne de cache susceptible d’avoir été introduite à l’étape 3. Étant donné qu’il existe une relation 1 :1 entre la valeur d’octet et la ligne de cache dans cet exemple, cela permet à l’attaquant de déduire la valeur réelle de l’octet qui a été lu hors limites.
Les étapes ci-dessus fournissent un exemple d’utilisation d’une technique appelée FLUSH+RELOAD conjointement avec l’exploitation d’une instance de CVE-2017-5753.
Quels scénarios logiciels peuvent être impactés ?
Le développement de logiciels sécurisés à l’aide d’un processus comme SDL (Security Development Lifecycle ) nécessite généralement aux développeurs d’identifier les limites d’approbation qui existent dans leur application. Une limite d’approbation existe dans les endroits où une application peut interagir avec les données fournies par un contexte moins approuvé, comme un autre processus sur le système ou un processus en mode utilisateur non administratif dans le cas d’un pilote de périphérique en mode noyau. La nouvelle classe de vulnérabilités impliquant des canaux côté exécution spéculative est pertinente pour la plupart des limites de confiance dans les modèles de sécurité logicielle existants qui isolent le code et les données sur un appareil.
Le tableau suivant fournit un résumé des modèles de sécurité logicielle dans lesquels les développeurs peuvent avoir besoin d’être préoccupés par ces vulnérabilités :
Limite de confiance | Description |
---|---|
Limite de machine virtuelle | Les applications qui isolent les charges de travail dans des machines virtuelles distinctes qui reçoivent des données non approuvées d’une autre machine virtuelle peuvent être à risque. |
Limite de noyau | Un pilote de périphérique en mode noyau qui reçoit des données non approuvées d’un processus en mode utilisateur non administratif peut être à risque. |
Limite de processus | Une application qui reçoit des données non approuvées d’un autre processus s’exécutant sur le système local, par exemple par le biais d’un appel de procédure distante (RPC), de mémoire partagée ou d’autres mécanismes de communication interprocessus (IPC) peut être à risque. |
Limite d’enclave | Une application qui s’exécute dans une enclave sécurisée (telle qu’Intel SGX) qui reçoit des données non approuvées en dehors de l’enclave peut être à risque. |
Limite de langue | Une application qui interprète ou juste-à-temps (JIT) compile et exécute du code non approuvé écrit dans un langage de niveau supérieur peut être à risque. |
Les applications qui ont une surface d’attaque exposée à l’une des limites de confiance ci-dessus doivent examiner le code sur la surface d’attaque pour identifier et atténuer les instances possibles de vulnérabilités de canal côté exécution spéculative. Il convient de noter que les limites de confiance exposées aux surfaces d’attaque à distance, telles que les protocoles de réseau distant, n’ont pas été démontrées comme étant à risque pour les vulnérabilités de canal côté exécution spéculative.
Modèles de codage potentiellement vulnérables
Les vulnérabilités de canal côté exécution spéculative peuvent survenir en conséquence de plusieurs modèles de codage. Cette section décrit les modèles de codage potentiellement vulnérables et fournit des exemples pour chacun d’eux, mais il doit être reconnu que des variations sur ces thèmes peuvent exister. Par conséquent, les développeurs sont invités à prendre ces modèles comme exemples et non comme une liste exhaustive de tous les modèles de codage potentiellement vulnérables. Les mêmes classes de vulnérabilités de sécurité de la mémoire qui peuvent exister dans le logiciel aujourd’hui peuvent également exister le long des chemins spéculatifs et hors ordre d’exécution, y compris, mais pas limité aux dépassements de mémoire tampon, aux accès aux tableaux hors limites, à l’utilisation de la mémoire non initialisée, à la confusion de type, etc. Les mêmes primitives que les attaquants peuvent utiliser pour exploiter les vulnérabilités de sécurité de la mémoire le long des chemins architecturaux peuvent également s’appliquer aux chemins spéculatifs.
En général, les canaux côté exécution spéculative liés à la mauvaise prédiction de branche conditionnelle peuvent survenir lorsqu’une expression conditionnelle fonctionne sur des données qui peuvent être contrôlées ou influencées par un contexte moins approuvé. Par exemple, cela peut inclure des expressions conditionnelles utilisées dans if
, , for
while
, ou switch
des instructions ternaires. Pour chacune de ces instructions, le compilateur peut générer une branche conditionnelle que l’UC peut ensuite prédire la cible de branche pour l’exécution.
Pour chaque exemple, un commentaire avec l’expression « SPECULATION BARRIER » est inséré où un développeur pourrait introduire une barrière comme atténuation. Ceci est abordé plus en détail dans la section sur les atténuations.
Charge hors limites spéculative
Cette catégorie de modèles de codage implique une mauvaise prédiction de branche conditionnelle qui conduit à un accès à la mémoire hors limites spéculative.
Tableau de charge hors limites alimentant une charge
Ce modèle de codage est le modèle de codage vulnérable décrit à l’origine pour CVE-2017-5753 (contournement de vérification des limites). La section d’arrière-plan de cet article explique ce modèle en détail.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
// SPECULATION BARRIER
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
De même, une charge hors limites d’un tableau peut se produire conjointement avec une boucle qui dépasse sa condition de fin en raison d’une erreur de prédiction. Dans cet exemple, la branche conditionnelle associée à l’expression x < buffer_size
peut inpréciser et exécuter de manière spéculative le corps de la for
boucle lorsqu’elle x
est supérieure ou égale à buffer_size
, ce qui entraîne une charge spéculative hors limites.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadBytes(unsigned char *buffer, unsigned int buffer_size) {
for (unsigned int x = 0; x < buffer_size; x++) {
// SPECULATION BARRIER
unsigned char value = buffer[x];
return shared_buffer[value * 4096];
}
}
Charge hors limites du tableau alimentant une branche indirecte
Ce modèle de codage implique le cas où une mauvaise prédiction de branche conditionnelle peut entraîner un accès hors limites à un tableau de pointeurs de fonction qui conduit ensuite à une branche indirecte vers l’adresse cible qui a été lu hors limites. L’extrait de code suivant fournit un exemple qui illustre cela.
Dans cet exemple, un identificateur de message non approuvé est fourni à DispatchMessage via le untrusted_message_id
paramètre. S’il untrusted_message_id
est inférieur MAX_MESSAGE_ID
à , il est utilisé pour indexer dans un tableau de pointeurs de fonction et de branche vers la cible de branche correspondante. Ce code est sécurisé de manière architecturale, mais si l’UC ne prédicte pas la branche conditionnelle, elle peut entraîner DispatchTable
l’indexation lorsque untrusted_message_id
sa valeur est supérieure ou égale à MAX_MESSAGE_ID
, ce qui entraîne un accès hors limites. Cela peut entraîner une exécution spéculative à partir d’une adresse cible de branche dérivée au-delà des limites du tableau, ce qui peut entraîner la divulgation d’informations en fonction du code exécuté de manière spéculative.
#define MAX_MESSAGE_ID 16
typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);
const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
if (untrusted_message_id < MAX_MESSAGE_ID) {
// SPECULATION BARRIER
DispatchTable[untrusted_message_id](buffer, buffer_size);
}
}
Comme dans le cas d’une charge hors limites d’un tableau qui alimente une autre charge, cette condition peut également survenir conjointement avec une boucle qui dépasse sa condition de fin en raison d’une mauvaise prédiction.
Magasin hors limites du tableau qui alimente une branche indirecte
Bien que l’exemple précédent montre comment une charge hors limites spéculative peut influencer une cible de branche indirecte, il est également possible pour un magasin hors limites de modifier une cible de branche indirecte, telle qu’un pointeur de fonction ou une adresse de retour. Cela peut entraîner une exécution spéculative à partir d’une adresse spécifiée par l’attaquant.
Dans cet exemple, un index non approuvé est passé par le untrusted_index
paramètre. Si untrusted_index
elle est inférieure au nombre d’éléments du pointers
tableau (256 éléments), la valeur de pointeur fournie est ptr
écrite dans le pointers
tableau. Ce code est sécurisé de manière architecturale, mais si l’UC ne prédicte pas la branche conditionnelle, elle peut entraîner ptr
l’écriture spéculative au-delà des limites du tableau alloué à pointers
la pile. Cela pourrait entraîner une corruption spéculative de l’adresse de retour pour WriteSlot
. Si un attaquant peut contrôler la valeur de , il peut être en mesure d’entraîner ptr
une exécution spéculative à partir d’une adresse arbitraire lorsqu’il WriteSlot
retourne le long du chemin spéculatif.
unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
void *pointers[256];
if (untrusted_index < 256) {
// SPECULATION BARRIER
pointers[untrusted_index] = ptr;
}
}
De même, si une variable locale de pointeur de fonction nommée func
a été allouée sur la pile, il peut être possible de modifier spéculativement l’adresse qui func
fait référence au moment où la mauvaise prédiction de la branche conditionnelle se produit. Cela peut entraîner une exécution spéculative à partir d’une adresse arbitraire lorsque le pointeur de fonction est appelé.
unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
void *pointers[256];
void (*func)() = &callback;
if (untrusted_index < 256) {
// SPECULATION BARRIER
pointers[untrusted_index] = ptr;
}
func();
}
Il convient de noter que ces deux exemples impliquent une modification spéculative des pointeurs indirects alloués à la pile. Il est possible que la modification spéculative puisse également se produire pour les variables globales, la mémoire allouée au tas et même la mémoire en lecture seule sur certains processeurs. Pour la mémoire allouée par pile, le compilateur Microsoft C++ prend déjà des mesures pour rendre plus difficile la modification spéculative des cibles de branche indirecte allouées par la pile, par exemple en réorganisant les variables locales de sorte que les mémoires tampons soient placées à côté d’un cookie de sécurité dans le cadre de la fonctionnalité de sécurité du /GS
compilateur.
Confusion de type spéculatif
Cette catégorie traite des modèles de codage qui peuvent donner lieu à une confusion de type spéculatif. Cela se produit lorsque la mémoire est accessible à l’aide d’un type incorrect le long d’un chemin non architectural pendant l’exécution spéculative. La mauvaise prédiction de la branche conditionnelle et la déviation spéculative du magasin peuvent potentiellement entraîner une confusion de type spéculatif.
Pour le contournement de magasin spéculatif, cela peut se produire dans les scénarios où un compilateur réutilise un emplacement de pile pour les variables de plusieurs types. Cela est dû au fait que le magasin architectural d’une variable de type A
peut être contourné, ce qui permet à la charge du type A
de s’exécuter spéculativement avant l’affectation de la variable. Si la variable précédemment stockée est d’un type différent, cela peut créer les conditions d’une confusion de type spéculatif.
Pour la mauvaise prédiction de la branche conditionnelle, l’extrait de code suivant sera utilisé pour décrire différentes conditions auxquelles la confusion de type spéculatif peut donner lieu.
enum TypeName {
Type1,
Type2
};
class CBaseType {
public:
CBaseType(TypeName type) : type(type) {}
TypeName type;
};
class CType1 : public CBaseType {
public:
CType1() : CBaseType(Type1) {}
char field1[256];
unsigned char field2;
};
class CType2 : public CBaseType {
public:
CType2() : CBaseType(Type2) {}
void (*dispatch_routine)();
unsigned char field2;
};
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ProcessType(CBaseType *obj)
{
if (obj->type == Type1) {
// SPECULATION BARRIER
CType1 *obj1 = static_cast<CType1 *>(obj);
unsigned char value = obj1->field2;
return shared_buffer[value * 4096];
}
else if (obj->type == Type2) {
// SPECULATION BARRIER
CType2 *obj2 = static_cast<CType2 *>(obj);
obj2->dispatch_routine();
return obj2->field2;
}
}
Confusion de type spéculative entraînant une charge hors limites
Ce modèle de codage implique le cas où une confusion de type spéculative peut entraîner un accès hors limites ou un accès de champ confus de type où la valeur chargée alimente une adresse de chargement ultérieure. Cela est similaire au modèle de codage hors limites du tableau, mais il est manifeste par le biais d’une séquence de codage alternative, comme illustré ci-dessus. Dans cet exemple, un contexte d’attaque peut entraîner l’exécution ProcessType
du contexte victime plusieurs fois avec un objet de type CType1
(type
le champ est égal à Type1
). Cela aura l’effet de l’entraînement de la branche conditionnelle pour la première if
instruction afin de prédire qu’elle n’est pas prise. Le contexte d’attaque peut ensuite entraîner l’exécution ProcessType
du contexte victime avec un objet de type CType2
. Cela peut entraîner une confusion de type spéculatif si la branche conditionnelle de la première if
instruction est mal prédictée et exécute le corps de l’instruction if
, en cas de conversion d’un objet de type CType2
en CType1
. Étant CType2
donné qu’il CType1
est inférieur à , l’accès à la mémoire pour CType1::field2
entraîner une charge spéculative hors limites des données qui peuvent être secrètes. Cette valeur est ensuite utilisée dans une charge à partir de shared_buffer
laquelle peut créer des effets secondaires observables, comme avec l’exemple de tableau hors limites décrit précédemment.
Confusion de type spéculatif conduisant à une branche indirecte
Ce modèle de codage implique le cas où une confusion de type spéculatif peut entraîner une branche indirecte non sécurisée pendant l’exécution spéculative. Dans cet exemple, un contexte d’attaque peut entraîner l’exécution ProcessType
du contexte victime plusieurs fois avec un objet de type CType2
(type
le champ est égal à Type2
). Cela aura pour effet d’entraîner la branche conditionnelle pour la première if
instruction à prendre et l’instruction else if
à ne pas prendre. Le contexte d’attaque peut ensuite entraîner l’exécution ProcessType
du contexte victime avec un objet de type CType1
. Cela peut entraîner une confusion de type spéculatif si la branche conditionnelle pour la première if
instruction prédite prise et que l’instruction else if
prédit non prise, exécutant ainsi le corps du corps de l’objet else if
de type en CType2
.CType1
Étant donné que le CType2::dispatch_routine
champ se chevauche avec le char
tableau CType1::field1
, cela peut entraîner une branche indirecte spéculative vers une cible de branche involontaire. Si le contexte d’attaque peut contrôler les valeurs d’octet dans le CType1::field1
tableau, ils peuvent contrôler l’adresse cible de branche.
Utilisation non initialisée spéculative
Cette catégorie de modèles de codage implique des scénarios où l’exécution spéculative peut accéder à la mémoire non initialisée et l’utiliser pour alimenter une branche indirecte ou de charge ultérieure. Pour que ces modèles de codage soient exploitables, un attaquant doit pouvoir contrôler ou influencer de manière significative le contenu de la mémoire utilisée sans être initialisé par le contexte dans lequel il est utilisé.
Utilisation non initialisée spéculative conduisant à une charge hors limites
Une utilisation non initialisée spéculative peut potentiellement entraîner une charge hors limites à l’aide d’une valeur contrôlée par un attaquant. Dans l’exemple ci-dessous, la valeur de index
celle-ci est affectée trusted_index
sur tous les chemins d’accès architecturaux et trusted_index
est supposée être inférieure ou égale à buffer_size
. Toutefois, selon le code produit par le compilateur, il est possible qu’un contournement de magasin spéculatif puisse se produire, ce qui permet à la charge des expressions dépendantes et de buffer[index]
s’exécuter avant l’affectation à index
. Si cela se produit, une valeur non initialisée pour index
sera utilisée comme décalage buffer
dans lequel un attaquant pourrait lire des informations sensibles hors limites et le transmettre via un canal latéral via la charge dépendante de shared_buffer
.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
void InitializeIndex(unsigned int trusted_index, unsigned int *index) {
*index = trusted_index;
}
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int trusted_index) {
unsigned int index;
InitializeIndex(trusted_index, &index); // not inlined
// SPECULATION BARRIER
unsigned char value = buffer[index];
return shared_buffer[value * 4096];
}
Utilisation non initialisée spéculative menant à une branche indirecte
Une utilisation non initialisée spéculative peut entraîner une branche indirecte où la cible de branche est contrôlée par un attaquant. Dans l’exemple ci-dessous, routine
est affecté à l’un ou DefaultMessageRoutine
l’autre DefaultMessageRoutine1
en fonction de la valeur de mode
. Sur le chemin architectural, cela entraîne routine
toujours l’initialisation avant la branche indirecte. Toutefois, selon le code produit par le compilateur, un contournement de magasin spéculatif peut se produire, ce qui permet à la branche indirecte d’être routine
exécutée spéculativement avant l’affectation à routine
. Si cela se produit, un attaquant peut être en mesure d’exécuter de manière spéculative à partir d’une adresse arbitraire, en supposant que l’attaquant peut influencer ou contrôler la valeur non initialisée de routine
.
#define MAX_MESSAGE_ID 16
typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);
const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
extern unsigned int mode;
void InitializeRoutine(MESSAGE_ROUTINE *routine) {
if (mode == 1) {
*routine = &DefaultMessageRoutine1;
}
else {
*routine = &DefaultMessageRoutine;
}
}
void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
MESSAGE_ROUTINE routine;
InitializeRoutine(&routine); // not inlined
// SPECULATION BARRIER
routine(buffer, buffer_size);
}
Options de correction
Les vulnérabilités de canal côté exécution spéculative peuvent être atténuées en apportant des modifications au code source. Ces modifications peuvent impliquer l’atténuation des instances spécifiques d’une vulnérabilité, par exemple en ajoutant une barrière de spéculation, ou en apportant des modifications à la conception d’une application pour rendre les informations sensibles inaccessibles à l’exécution spéculative.
Barrière de spéculation via l’instrumentation manuelle
Une barrière de spéculation peut être insérée manuellement par un développeur pour empêcher l’exécution spéculative de continuer le long d’un chemin non architectural. Par exemple, un développeur peut insérer une barrière de spéculation avant un modèle de codage dangereux dans le corps d’un bloc conditionnel, soit au début du bloc (après la branche conditionnelle) soit avant la première charge qui est préoccupante. Cela empêchera une mauvaise prédiction d’une branche conditionnelle d’exécuter le code dangereux sur un chemin non architectural en sérialisant l’exécution. La séquence de barrières de spéculation diffère par l’architecture matérielle, comme décrit dans le tableau suivant :
Architecture | Barrière de spéculation intrinsèque pour CVE-2017-5753 | Barrière de spéculation intrinsèque pour CVE-2018-3639 |
---|---|---|
x86/x64 | _mm_lfence() | _mm_lfence() |
ARM | actuellement non disponible | __dsb(0) |
ARM64 | actuellement non disponible | __dsb(0) |
Par exemple, le modèle de code suivant peut être atténué à l’aide de l’intrinsèque _mm_lfence
, comme indiqué ci-dessous.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
_mm_lfence();
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
Barrière de spéculation via l’instrumentation au moment du compilateur
Le compilateur Microsoft C++ dans Visual Studio 2017 (à compter de la version 15.5.5) inclut la prise en charge du /Qspectre
commutateur qui insère automatiquement une barrière de spéculation pour un ensemble limité de modèles de codage potentiellement vulnérables liés à CVE-2017-5753. La documentation de l’indicateur /Qspectre
fournit plus d’informations sur ses effets et son utilisation. Il est important de noter que cet indicateur ne couvre pas tous les modèles de codage potentiellement vulnérables et, comme ces développeurs, ne doivent pas s’en appuyer comme une atténuation complète pour cette classe de vulnérabilités.
Masquage des index de tableau
Dans les cas où une charge spéculative hors limites peut se produire, l’index de tableau peut être fortement lié à la fois sur le chemin architectural et non architectural en ajoutant une logique pour lier explicitement l’index de tableau. Par exemple, si un tableau peut être alloué à une taille alignée sur une puissance de deux, un masque simple peut être introduit. Ceci est illustré dans l’exemple ci-dessous, où il est supposé qu’il buffer_size
est aligné sur une puissance de deux. Cela garantit qu’il untrusted_index
est toujours inférieur buffer_size
à , même si une mauvaise prédiction de branche conditionnelle se produit et untrusted_index
a été passée avec une valeur supérieure ou égale à buffer_size
.
Il convient de noter que le masquage d’index effectué ici peut être soumis à un contournement de magasin spéculatif en fonction du code généré par le compilateur.
// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;
unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
if (untrusted_index < buffer_size) {
untrusted_index &= (buffer_size - 1);
unsigned char value = buffer[untrusted_index];
return shared_buffer[value * 4096];
}
}
Suppression des informations sensibles de la mémoire
Une autre technique qui peut être utilisée pour atténuer les vulnérabilités de canal côté exécution spéculative consiste à supprimer les informations sensibles de la mémoire. Les développeurs de logiciels peuvent rechercher des opportunités de refactorisation de leur application afin que les informations sensibles ne puissent pas être accessibles pendant l’exécution spéculative. Pour ce faire, refactorisez la conception d’une application pour isoler les informations sensibles dans des processus distincts. Par exemple, une application de navigateur web peut tenter d’isoler les données associées à chaque origine web en processus distincts, ce qui empêche un processus d’accéder à des données d’origine croisée via une exécution spéculative.
Voir aussi
Conseils pour atténuer les vulnérabilités de canal secondaire d’exécution spéculative
Atténuation des vulnérabilités matérielles du canal côté exécution spéculative