Condividi tramite


Materiale sussidiario per sviluppatori C++ - Minacce Speculative Execution Side Channel

Questo articolo contiene indicazioni per gli sviluppatori per facilitare l'identificazione e la mitigazione delle vulnerabilità hardware del canale di esecuzione speculativo nel software C++. Queste vulnerabilità possono divulgare informazioni riservate attraverso limiti di attendibilità e possono influire sul software in esecuzione su processori che supportano l'esecuzione speculativa e non ordinata delle istruzioni. Questa classe di vulnerabilità è stata descritta per la prima volta a gennaio 2018 e altre informazioni e indicazioni sono disponibili nell'avviso sulla sicurezza di Microsoft.

Le indicazioni fornite da questo articolo sono correlate alle classi di vulnerabilità rappresentate da:

  1. CVE-2017-5753, noto anche come Spectre variant 1. Questa classe di vulnerabilità hardware è correlata ai canali laterali che possono verificarsi a causa di un'esecuzione speculativa che si verifica a causa di un errore di prespredizione del ramo condizionale. Il compilatore Microsoft C++ in Visual Studio 2017 (a partire dalla versione 15.5.5) include il supporto per il /Qspectre commutatore che fornisce una mitigazione in fase di compilazione per un set limitato di modelli di codifica potenzialmente vulnerabili correlati a CVE-2017-5753. L'opzione /Qspectre è disponibile anche in Visual Studio 2015 Update 3 a KB 4338871. La documentazione per il /Qspectre flag fornisce altre informazioni sugli effetti e sull'utilizzo.

  2. CVE-2018-3639, noto anche come Bypass dell'archivio speculativo (SSB). Questa classe di vulnerabilità hardware è correlata ai canali laterali che possono verificarsi a causa dell'esecuzione speculativa di un carico prima di un archivio dipendente in seguito a una mancata prepredizione dell'accesso alla memoria.

Un'introduzione accessibile alle vulnerabilità del canale laterale dell'esecuzione speculativa è disponibile nella presentazione intitolata The Case of Spectre and Meltdown da uno dei team di ricerca che hanno scoperto questi problemi.

Che cosa sono le vulnerabilità hardware del canale laterale di esecuzione speculativa?

Le CPU moderne offrono livelli di prestazioni superiori usando l'esecuzione speculativa e out-of-order delle istruzioni. Ad esempio, questa operazione viene spesso eseguita stimando la destinazione dei rami (condizionali e indiretti) che consente alla CPU di iniziare a eseguire in modo speculativo le istruzioni nella destinazione prevista del ramo, evitando così uno stallo fino alla risoluzione della destinazione effettiva del ramo. Nel caso in cui la CPU rilevi in un secondo momento che si è verificata un'errata preselezione, tutto lo stato della macchina che è stato calcolato in modo speculativo viene rimosso. In questo modo si garantisce che non vi siano effetti architetturali visibili della speculazione errata.

Anche se l'esecuzione speculativa non influisce sullo stato visibile dall'architettura, può lasciare tracce residui in stato non architetturale, ad esempio le varie cache usate dalla CPU. Si tratta di tracce residui di esecuzione speculativa che possono causare vulnerabilità del canale laterale. Per comprendere meglio questo problema, prendere in considerazione il frammento di codice seguente che fornisce un esempio di CVE-2017-5753 (bounds check bypass):

// 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];
    }
}

In questo esempio viene ReadByte fornito un buffer, una dimensione del buffer e un indice in tale buffer. Il parametro di indice, come specificato da untrusted_index, viene fornito da un contesto meno privilegiato, ad esempio un processo non amministrativo. Se untrusted_index è minore di buffer_size, il carattere in corrispondenza di tale indice viene letto da buffer e usato per indicizzare in un'area condivisa di memoria a shared_buffercui fa riferimento .

Dal punto di vista dell'architettura, questa sequenza di codice è perfettamente sicura perché è garantita che untrusted_index sarà sempre minore di buffer_size. Tuttavia, in presenza di esecuzione speculativa, è possibile che la CPU predicherà erroneamente il ramo condizionale ed eseguirà il corpo dell'istruzione if anche quando untrusted_index è maggiore o uguale a buffer_size. Di conseguenza, la CPU può leggere in modo speculativo un byte oltre i limiti di buffer (che potrebbe essere un segreto) e potrebbe quindi usare tale valore di byte per calcolare l'indirizzo di un carico successivo tramite shared_buffer.

Anche se alla fine la CPU rileverà questa prespredizione errata, gli effetti collaterali residui potrebbero essere lasciati nella cache della CPU che rivelano informazioni sul valore di byte letto fuori dai limiti da buffer. Questi effetti collaterali possono essere rilevati da un contesto meno privilegiato in esecuzione nel sistema eseguendo il probe della velocità di accesso a ogni riga della cache in shared_buffer . I passaggi che è possibile eseguire per eseguire questa operazione sono i seguenti:

  1. Richiamare ReadByte più volte con untrusted_index un valore minore di buffer_size. Il contesto di attacco può causare il richiamo del contesto ReadByte della vittima ,ad esempio tramite RPC, in modo che il predictor del ramo venga sottoposto a training come untrusted_index non è minore di buffer_size.

  2. Scaricare tutte le righe della cache in shared_buffer. Il contesto di attacco deve scaricare tutte le righe della cache nell'area condivisa di memoria a shared_buffercui fa riferimento . Poiché l'area di memoria è condivisa, questa operazione è semplice e può essere eseguita usando oggetti intrinseci come _mm_clflush.

  3. Richiamare ReadByte con untrusted_index maggiore di buffer_size. Il contesto di attacco fa sì che il contesto vittima richiami ReadByte in modo che predica erroneamente che il ramo non verrà eseguito. In questo modo il processore esegue speculativamente il corpo del blocco if con untrusted_index maggiore di buffer_size, causando così una lettura out-of-bounds di buffer. Di conseguenza, shared_buffer viene indicizzato usando un valore potenzialmente segreto che è stato letto out-of-bounds, causando il caricamento della rispettiva riga della cache dalla CPU.

  4. Leggere ogni riga della cache in shared_buffer per vedere quali sono gli accessi più rapidamente. Il contesto di attacco può leggere ogni riga della cache in shared_buffer e rilevare la riga della cache che viene caricata in modo significativo più veloce rispetto alle altre. Si tratta della riga della cache che probabilmente è stata inserita nel passaggio 3. Poiché in questo esempio è presente una relazione 1:1 tra il valore di byte e la riga della cache, ciò consente all'utente malintenzionato di dedurre il valore effettivo del byte letto fuori limite.

I passaggi precedenti forniscono un esempio di uso di una tecnica nota come FLUSH+RELOAD insieme all'uso di un'istanza di CVE-2017-5753.

Quali scenari software possono essere interessati?

Lo sviluppo di software sicuro usando un processo come sdl (Security Development Lifecycle ) richiede in genere agli sviluppatori di identificare i limiti di attendibilità presenti nell'applicazione. Esiste un limite di attendibilità nelle posizioni in cui un'applicazione può interagire con i dati forniti da un contesto meno attendibile, ad esempio un altro processo nel sistema o un processo in modalità utente non amministrativa nel caso di un driver di dispositivo in modalità kernel. La nuova classe di vulnerabilità che coinvolgono canali lato esecuzione speculativa è rilevante per molti dei limiti di attendibilità nei modelli di sicurezza software esistenti che isolano codice e dati in un dispositivo.

La tabella seguente fornisce un riepilogo dei modelli di sicurezza software in cui gli sviluppatori potrebbero dover preoccuparsi di queste vulnerabilità:

Limite di attendibilità Descrizione
Limite della macchina virtuale Le applicazioni che isolano i carichi di lavoro in macchine virtuali separate che ricevono dati non attendibili da un'altra macchina virtuale potrebbero essere a rischio.
Limite del kernel Un driver di dispositivo in modalità kernel che riceve dati non attendibili da un processo in modalità utente non amministrativa potrebbe essere a rischio.
Limite del processo Un'applicazione che riceve dati non attendibili da un altro processo in esecuzione nel sistema locale, ad esempio tramite una chiamata rpc (Remote Procedure Call), la memoria condivisa o altri meccanismi di comunicazione tra processi (IPC) potrebbero essere a rischio.
Limite dell'enclave Un'applicazione eseguita all'interno di un enclave sicuro (ad esempio Intel SGX) che riceve dati non attendibili dall'esterno dell'enclave può essere a rischio.
Limite della lingua Un'applicazione che interpreta o JIT (Just-In-Time) compila ed esegue codice non attendibile scritto in un linguaggio di livello superiore può essere a rischio.

Le applicazioni che hanno una superficie di attacco esposta a uno dei limiti di attendibilità precedenti devono esaminare il codice sulla superficie di attacco per identificare e attenuare le possibili istanze di vulnerabilità del canale lato esecuzione speculativo. Si noti che i limiti di attendibilità esposti alle superfici di attacco remote, ad esempio i protocolli di rete remota, non sono stati dimostrati a rischio di vulnerabilità del canale lato esecuzione speculativo.

Modelli di codifica potenzialmente vulnerabili

Le vulnerabilità del canale laterale dell'esecuzione speculativa possono verificarsi in seguito a più modelli di codifica. In questa sezione vengono descritti modelli di codifica potenzialmente vulnerabili e vengono forniti esempi per ognuno di essi, ma è consigliabile riconoscere che possono esistere variazioni su questi temi. Di conseguenza, gli sviluppatori sono invitati a prendere questi modelli come esempi e non come un elenco completo di tutti i modelli di codifica potenzialmente vulnerabili. Le stesse classi di vulnerabilità di sicurezza della memoria che possono esistere oggi nel software possono esistere anche lungo percorsi speculativi e out-of-order di esecuzione, tra cui, a titolo esemplificativo, sovraccarichi del buffer, accessi di matrici out-of-bounds, uso non inizializzato della memoria, confusione dei tipi e così via. Le stesse primitive che gli utenti malintenzionati possono usare per sfruttare le vulnerabilità di sicurezza della memoria lungo percorsi architetturali possono essere applicate anche ai percorsi speculativi.

In generale, i canali lato esecuzione speculativa correlati alla prespredizione condizionale del ramo possono verificarsi quando un'espressione condizionale opera sui dati che possono essere controllati o influenzati da un contesto meno attendibile. Ad esempio, può includere espressioni condizionali usate nelle ifistruzioni ternarie , switchforwhile, , o . Per ognuna di queste istruzioni, il compilatore può generare un ramo condizionale per cui la CPU può quindi prevedere la destinazione del ramo in fase di esecuzione.

Per ogni esempio, viene inserito un commento con la frase "SPECULATION BARRIER" in cui uno sviluppatore potrebbe introdurre una barriera come mitigazione. Questo argomento viene illustrato in modo più dettagliato nella sezione sulle mitigazioni.

Carico fuori limite speculativo

Questa categoria di modelli di codifica implica un errore di preselezione del ramo condizionale che comporta un accesso speculativo alla memoria out-of-bounds.

Caricamento out-of-bounds della matrice che alimenta un carico

Questo modello di codifica è il modello di codifica vulnerabile descritto originariamente per CVE-2017-5753 (Bounds Check Bypass). La sezione di sfondo di questo articolo illustra in dettaglio questo modello.

// 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];
    }
}

Analogamente, un carico di matrice fuori limite può verificarsi in combinazione con un ciclo che supera la condizione di terminazione a causa di una preselezione errata. In questo esempio, il ramo condizionale associato all'espressione x < buffer_size può eseguire erroneamente ed eseguire speculativamente il corpo del for ciclo quando x è maggiore o uguale a buffer_size, determinando così un carico speculativo fuori limite.

// 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];
    }
}

Caricamento di array out-of-bounds che alimenta un ramo indiretto

Questo modello di codifica implica il caso in cui un ramo condizionale non predidizione può portare a un accesso out-of-bounds a una matrice di puntatori a funzione che quindi porta a un ramo indiretto all'indirizzo di destinazione che è stato letto fuori limite. Il frammento di codice seguente fornisce un esempio che illustra questa operazione.

In questo esempio viene fornito un identificatore di messaggio non attendibile a DispatchMessage tramite il untrusted_message_id parametro . Se untrusted_message_id è minore di MAX_MESSAGE_ID, viene usato per indicizzare in una matrice di puntatori a funzione e ramo alla destinazione del ramo corrispondente. Questo codice è indipendente dall'architettura, ma se la CPU predice erroneamente il ramo condizionale, potrebbe comportare DispatchTable l'indicizzazione da untrusted_message_id quando il valore è maggiore o uguale a MAX_MESSAGE_ID, causando così un accesso out-of-bounds. Ciò potrebbe comportare l'esecuzione speculativa da un indirizzo di destinazione del ramo derivato oltre i limiti della matrice che potrebbe portare alla divulgazione di informazioni a seconda del codice eseguito speculativamente.

#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);
    }
}

Come nel caso di un carico fuori limite di matrice che alimenta un altro carico, questa condizione può verificarsi anche in combinazione con un ciclo che supera la condizione di terminazione a causa di una prespredizione errata.

Array out-of-bounds store che alimenta un ramo indiretto

Anche se l'esempio precedente ha mostrato come un carico speculativo out-of-bounds possa influenzare una destinazione di ramo indiretto, è anche possibile che un archivio out-of-bounds modifichi una destinazione di ramo indiretto, ad esempio un puntatore a funzione o un indirizzo restituito. Ciò può causare potenzialmente l'esecuzione speculativa da un indirizzo specificato da un utente malintenzionato.

In questo esempio viene passato un indice non attendibile tramite il untrusted_index parametro . Se untrusted_index è minore del numero di elementi della pointers matrice (256 elementi), il valore del puntatore specificato in ptr viene scritto nella pointers matrice. Questo codice è indipendente dall'architettura, ma se la CPU prepredice il ramo condizionale, potrebbe comportare ptr la scrittura speculativa oltre i limiti della matrice allocata pointers dallo stack. Ciò potrebbe causare un danneggiamento speculativo dell'indirizzo restituito per WriteSlot. Se un utente malintenzionato può controllare il valore di ptr, potrebbe essere in grado di causare l'esecuzione speculativa da un indirizzo arbitrario quando WriteSlot restituisce lungo il percorso speculativo.

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
}

Analogamente, se una variabile locale del puntatore a funzione denominata func è stata allocata nello stack, potrebbe essere possibile modificare in modo speculativo l'indirizzo che func fa riferimento a quando si verifica un errore di prespredizione del ramo condizionale. Ciò potrebbe comportare l'esecuzione speculativa da un indirizzo arbitrario quando viene chiamato il puntatore di funzione.

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();
}

Si noti che entrambi questi esempi comportano la modifica speculativa dei puntatori indiretti allo stack allocati. È possibile che la modifica speculativa possa verificarsi anche per variabili globali, memoria allocata dall'heap e anche memoria di sola lettura in alcune CPU. Per la memoria allocata dallo stack, il compilatore Microsoft C++ esegue già passaggi per rendere più difficile modificare speculativamente le destinazioni di ramo indiretto allocate dallo stack, ad esempio riordinando le variabili locali in modo che i buffer vengano posizionati accanto a un cookie di sicurezza come parte della funzionalità di sicurezza del /GS compilatore.

Confusione di tipo speculativo

Questa categoria riguarda i modelli di codifica che possono dare origine a una confusione di tipo speculativo. Ciò si verifica quando si accede alla memoria usando un tipo non corretto lungo un percorso non architetturale durante l'esecuzione speculativa. Sia la prespredizione del ramo condizionale che il bypass dell'archivio speculativo possono potenzialmente causare confusione di tipo speculativo.

Per il bypass dell'archivio speculativo, ciò può verificarsi negli scenari in cui un compilatore riutilizza una posizione dello stack per le variabili di più tipi. Ciò è dovuto al fatto che l'archivio architetturale di una variabile di tipo A può essere ignorato, consentendo così l'esecuzione speculativa del carico di tipo A prima dell'assegnazione della variabile. Se la variabile archiviata in precedenza è di un tipo diverso, è possibile creare le condizioni per una confusione di tipo speculativo.

Per la mancata preselezione del ramo condizionale, il frammento di codice seguente verrà usato per descrivere diverse condizioni che possono dare origine a confusione di tipo speculativo.

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;
    }
}

Confusione di tipo speculativo che comporta un carico out-of-bounds

Questo modello di codifica implica il caso in cui una confusione di tipo speculativo può comportare un accesso fuori limite o confuso del tipo in cui il valore caricato genera un indirizzo di caricamento successivo. Questo comportamento è simile al modello di codifica out-of-bounds della matrice, ma viene visualizzato tramite una sequenza di codifica alternativa, come illustrato in precedenza. In questo esempio, un contesto di attacco potrebbe causare l'esecuzione ProcessType del contesto vittima più volte con un oggetto di tipo CType1 (type il campo è uguale a Type1). Questo avrà l'effetto di eseguire il training del ramo condizionale per la prima if istruzione per stimare non eseguita. Il contesto di attacco può quindi causare l'esecuzione ProcessType del contesto vittima con un oggetto di tipo CType2. Ciò può comportare una confusione di tipo speculativo se il ramo condizionale per la prima if istruzione erroneamente e esegue il corpo dell'istruzione if , eseguendo così il cast di un oggetto di tipo CType2 su CType1. Poiché CType2 è minore di CType1, l'accesso alla memoria a CType1::field2 comporterà un carico speculativo di dati non vincolati che potrebbero essere segreti. Questo valore viene quindi usato in un carico da shared_buffer cui è possibile creare effetti collaterali osservabili, come nell'esempio di matrice out-of-bounds descritto in precedenza.

Confusione di tipo speculativo che porta a un ramo indiretto

Questo modello di codifica implica il caso in cui una confusione di tipo speculativo può causare un ramo indiretto non sicuro durante l'esecuzione speculativa. In questo esempio, un contesto di attacco potrebbe causare l'esecuzione ProcessType del contesto vittima più volte con un oggetto di tipo CType2 (type il campo è uguale a Type2). Questo avrà l'effetto di eseguire il training del ramo condizionale per la prima if istruzione da prendere e l'istruzione else if da non eseguire. Il contesto di attacco può quindi causare l'esecuzione ProcessType del contesto vittima con un oggetto di tipo CType1. Ciò può comportare una confusione di tipo speculativo se il ramo condizionale per la prima if istruzione prevede che l'istruzione else if non venga eseguita, eseguendo così il corpo dell'oggetto else if e eseguendo un oggetto di tipo CType1 su CType2. Poiché il CType2::dispatch_routine campo si sovrappone alla char matrice CType1::field1, questo potrebbe comportare un ramo indiretto speculativo a una destinazione di ramo non intenzionale. Se il contesto di attacco può controllare i valori di byte nella CType1::field1 matrice, potrebbe essere in grado di controllare l'indirizzo di destinazione del ramo.

Uso non inizializzato speculativo

Questa categoria di modelli di codifica prevede scenari in cui l'esecuzione speculativa può accedere alla memoria non inizializzata e usarla per inserire un carico o un ramo indiretto successivo. Affinché questi modelli di codifica siano sfruttabili, un utente malintenzionato deve essere in grado di controllare o influenzare significativamente il contenuto della memoria usata senza essere inizializzato dal contesto in cui viene usato.

Uso non inizializzato speculativo che comporta un carico out-of-bounds

Un uso speculativo non inizializzato può potenzialmente portare a un carico out-of-bounds usando un valore controllato da un utente malintenzionato. Nell'esempio seguente il valore di viene assegnato trusted_index in tutti i percorsi architetturali e trusted_index si presuppone che sia minore o uguale a buffer_size.index Tuttavia, a seconda del codice prodotto dal compilatore, è possibile che si verifichi un bypass dell'archivio speculativo che consenta l'esecuzione del carico da buffer[index] e delle espressioni dipendenti prima dell'assegnazione a index. In questo caso, un valore non inizializzato per index verrà usato come offset in buffer cui potrebbe consentire a un utente malintenzionato di leggere le informazioni riservate fuori dai limiti e trasmetterlo tramite un canale laterale tramite il carico dipendente di 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];
}

Uso speculativo non inizializzato che porta a un ramo indiretto

Un uso speculativo non inizializzato può potenzialmente portare a un ramo indiretto in cui la destinazione del ramo è controllata da un utente malintenzionato. Nell'esempio seguente, routine viene assegnato a DefaultMessageRoutine1 o DefaultMessageRoutine a seconda del valore di mode. Nel percorso dell'architettura, ciò comporterà routine l'inizializzazione sempre prima del ramo indiretto. Tuttavia, a seconda del codice prodotto dal compilatore, può verificarsi un bypass dell'archivio speculativo che consente al ramo indiretto di routine essere eseguito speculativamente prima dell'assegnazione a routine. In questo caso, un utente malintenzionato può essere in grado di eseguire speculativamente da un indirizzo arbitrario, presupponendo che l'utente malintenzionato possa influenzare o controllare il valore non inizializzato di 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);
}

Opzioni di mitigazione

Le vulnerabilità del canale laterale dell'esecuzione speculativa possono essere attenuate apportando modifiche al codice sorgente. Queste modifiche possono comportare la mitigazione di istanze specifiche di una vulnerabilità, ad esempio aggiungendo una barriera di speculazione o apportando modifiche alla progettazione di un'applicazione per rendere le informazioni riservate inaccessibili all'esecuzione speculativa.

Barriera di speculazione tramite strumentazione manuale

Una barriera di speculazione può essere inserita manualmente da uno sviluppatore per impedire l'esecuzione speculativa di procedere lungo un percorso non architettonico. Ad esempio, uno sviluppatore può inserire una barriera di speculazione prima di un modello di codifica pericoloso nel corpo di un blocco condizionale, all'inizio del blocco (dopo il ramo condizionale) o prima del primo carico preoccupante. In questo modo si impedisce a un ramo condizionale di eseguire il codice pericoloso in un percorso non architetturale serializzando l'esecuzione. La sequenza di barriere di speculazione è diversa dall'architettura hardware, come descritto nella tabella seguente:

Architettura Barriera di speculazione intrinseca per CVE-2017-5753 Barriera di speculazione intrinseca per CVE-2018-3639
x86/x64 _mm_lfence() _mm_lfence()
ARM Attualmente non disponibile __dsb(0)
ARM64 Attualmente non disponibile __dsb(0)

Ad esempio, il modello di codice seguente può essere mitigato usando l'intrinseco _mm_lfence come illustrato di seguito.

// 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];
    }
}

Barriera di speculazione tramite strumentazione in fase di compilazione

Il compilatore Microsoft C++ in Visual Studio 2017 (a partire dalla versione 15.5.5) include il supporto per l'opzione /Qspectre che inserisce automaticamente una barriera di speculazione per un set limitato di modelli di codifica potenzialmente vulnerabili correlati a CVE-2017-5753. La documentazione per il /Qspectre flag fornisce altre informazioni sugli effetti e sull'utilizzo. È importante notare che questo flag non copre tutti i modelli di codifica potenzialmente vulnerabili e, di conseguenza, gli sviluppatori non devono basarsi su di esso come mitigazione completa per questa classe di vulnerabilità.

Mascherare gli indici di matrice

Nei casi in cui può verificarsi un carico esterno ai limiti speculativi, l'indice di matrice può essere fortemente limitato sia sul percorso architetturale che non architetturale aggiungendo logica per associare in modo esplicito l'indice della matrice. Ad esempio, se una matrice può essere allocata a una dimensione allineata a una potenza di due, è possibile introdurre una maschera semplice. Questo è illustrato nell'esempio seguente in cui si presuppone che buffer_size sia allineato a una potenza di due. In questo modo si garantisce che untrusted_index sia sempre minore di buffer_size, anche se si verifica un errore di preselezione del ramo condizionale ed untrusted_index è stato passato con un valore maggiore o uguale a buffer_size.

Si noti che la maschera dell'indice eseguita qui potrebbe essere soggetta a bypass dell'archivio speculativo a seconda del codice generato dal compilatore.

// 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];
    }
}

Rimozione di informazioni riservate dalla memoria

Un'altra tecnica che può essere usata per attenuare le vulnerabilità del canale laterale dell'esecuzione speculativa consiste nel rimuovere informazioni riservate dalla memoria. Gli sviluppatori di software possono cercare opportunità di effettuare il refactoring dell'applicazione in modo che le informazioni riservate non siano accessibili durante l'esecuzione speculativa. A tale scopo, è possibile effettuare il refactoring della progettazione di un'applicazione per isolare le informazioni riservate in processi separati. Ad esempio, un'applicazione Web browser può tentare di isolare i dati associati a ogni origine Web in processi separati, impedendo così a un processo di accedere ai dati tra origini tramite l'esecuzione speculativa.

Vedi anche

Linee guida per attenuare le vulnerabilità del canale laterale dell'esecuzione speculativa
Mitigazione delle vulnerabilità hardware del canale laterale dell'esecuzione speculativa