Partager via


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 :

  1. 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 kb 4338871. La documentation de l’indicateur /Qspectre fournit plus d’informations sur ses effets et son utilisation.

  2. 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 ignoré. 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 :

  1. Appelez ReadByte plusieurs fois avec untrusted_index une valeur inférieure à buffer_size. Le contexte d’attaque peut entraîner l’appel ReadByte du contexte de victime (par exemple, via RPC) de sorte que le prédicteur de branche soit entraîné à ne pas être pris comme étant untrusted_index inférieur buffer_sizeà .

  2. 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 par shared_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.

  3. Appeler ReadByte avec untrusted_index une valeur supérieure à buffer_size. Le contexte d’attaque provoque l’appel ReadByte 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 avec untrusted_index une valeur supérieure buffer_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.

  4. 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 et shared_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, , forwhile, ou switchdes 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 ptrune 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 CType1est 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