cosa fallisce nell'esecuzione speculativa che consente la lettura della memoria fuori dai limiti della vulnerabilità di Spectre rispetto al normale comportamento della CPU?

4

Dopo progetto di blog zero di google per Spectre / Meltdown , c'è questo pezzo di codice che esemplifica l'attacco:

struct array {
    unsigned long length;
    unsigned char data[];
};
struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */
/* >0x400 (OUT OF BOUNDS!) */
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
    unsigned char value = arr1->data[untrusted_offset_from_caller];
    unsigned long index2 = ((value&1)*0x100)+0x200;
    if (index2 < arr2->length) {
        unsigned char value2 = arr2->data[index2];
    }
}

Viene spiegato che l'esecuzione speculativa seguita dalla CPU, proverà ad eseguire fino a

arr2->data[index2];

prima di raggiungere la condizione

if (untrusted_offset_from_caller < arr1->length) {

che impedirebbe l'accesso a una zona fuori dalla memoria dei limiti.

La mia domanda è:

  • cosa avrebbe impedito l'accesso a quella zona di memoria in una normale esecuzione se il codice avrebbe cercato di farlo in modo esplicito?

Suppongo che in qualche posto i controlli di accesso alla memoria del sistema operativo e / o della cpu debbano fermarlo e che l'exec speculativo salti semplicemente sopra (?).

Sembra che correggere (non effettivamente fatto) il controllo ( se la mia precedente ipotesi è corretta ... ) non sarebbe sufficiente o non è l'approccio corretto , come è già stato affermato che sono necessarie altre due condizioni : svuotamento del predittore di ramo & prendere in considerazione l'indirizzo completo dell'istruzione branch (sembra che le CPU non lo facciano oggigiorno), ma:

  • non verrebbero i controlli (se la risposta alla prima domanda è che sono necessari controlli aggiuntivi) o quell'impatto "flushing" nelle prestazioni? e se sì, è stato stimato quanto? (quando quelle presunte CPU sarebbero / saranno fatte).
posta circulosmeos 06.01.2018 - 01:01
fonte

6 risposte

3

Semplificando un po 'le cose, il problema è che qualcosa di simile

if (*p1) x = p2[256 * *p3];

può essere elaborato come:

start loading *p1 into t1.
load *p3 into t2, and set t3 to 0 if it's a valid fetch, 1 otherwise.
load *(p2 + t2*256) into t4
wait for t1..t4 to be ready
if t1 was set, then...
  if t3 is set [access was invalid] then fire an invalid access trap.
  otherwise copy t4 into x.
discard t1..t4.

Se la lettura di *p1 produce un valore zero, il fatto che *p3 non sia valido non deve causare una trap (poiché il codice non chiederebbe effettivamente di leggere *p3 ). Per qualsiasi ragione i progettisti di Intel pensavano che fosse più facile ritardare il controllo della validità della prima lettura della memoria fino a quando il valore recuperato era usato per calcolare l'indirizzo di un'altra lettura speculativa, piuttosto che avere una memoria non valida letta immediatamente forzare lo speculatore ad assumere la sua previsione era sbagliata.

Si noti che il problema non è che il processore recuperi in modo speculativo da * p3. Il problema è che il processore utilizza questo valore senza considerare se sia stato acquisito legittimamente. Mentre gli attacchi attuali si concentrano sull'utilizzo del valore recuperato per calcolare un indirizzo, e quindi utilizzando la cache per scoprire quale indirizzo viene recuperato, il problema fondamentale è che i dati vengono letti e bloccati senza riguardo per l'accesso legittimo. Ogni volta che un dispositivo recupera fisicamente i dati che dovrebbero essere inaccessibili creerà un potenziale per gli attacchi di canale laterale. Il modo migliore per prevenire tali attacchi è evitare che il dispositivo acquisisca tali dati in primo luogo.

    
risposta data 06.01.2018 - 01:40
fonte
3

Il concetto chiave è che nessun chip è obbligato a eseguire con precisione il codice in base alle istruzioni. L'obbligo è invece quello di eseguire "come se" il codice fosse eseguito esattamente secondo le istruzioni. Tutti i processori moderni approfittano di questa libertà.

In teoria, il processore è libero di eseguire qualsiasi istruzione in modo speculativo in qualsiasi momento, a patto che il risultato finale sia "come se" l'istruzione non sia mai stata eseguita. Questo è usato per aumentare le prestazioni, usando parti inattive del chip per fare lavori speculativi, nella speranza che gli effetti di queste istruzioni siano necessari.

Nei bug Meltdown / Spectre, è stato rivelato che tale esecuzione speculativa in realtà NON è "come se" non fosse mai accaduta. Questa speculazione ha cambiato lo stato della cache, caricando i dati che altrimenti non potrebbero essere memorizzati nella cache. Questo cambia i tempi che Meltdown / Spectre fanno leva per leggere la memoria da luoghi che non avrebbe dovuto leggere.

L'errore chiave è che il chip non funziona più "come se" stesse seguendo perfettamente gli ordini. Sta marciando leggermente fuori linea. In un caso come:

if (cursorIdx < cursors.size())
    y = buffer[cursors[cursorIdx]];

un programmatore può aspettarsi che non si possa leggere alcuna memoria dal buffer. Questo è logico perché le istruzioni scritte nel programma richiedevano che il controllo della dimensione dell'array del cursore si verificasse prima della lettura del buffer. Finché il chip funziona "come se" stava seguendo gli ordini, è possibile dimostrare che nessuno dovrebbe essere in grado di osservare una lettura del buffer che si verifica prima del controllo della dimensione. Con questo exploit, vediamo che un osservatore può leggere dal buffer, e forse anche un'altra memoria al di fuori del bugger.

Quindi, il codice che sembrava essere sicuro, anche contro gli attacchi di canale laterale, è improvvisamente molto pericoloso perché non funziona più "come se" fosse eseguito correttamente. Il problema non è che la memoria viene letta in modo speculativo. Il processore è stato autorizzato a farlo per tutto il tempo ed è ancora tecnicamente autorizzato a farlo. Il problema è che questo exploit dimostra che tali letture speculative non sono "come se" non siano mai state lette, perché possiamo osservare se si sono verificate. Ora perdono informazioni sullo spazio di memoria che non avrebbero dovuto essere perso.

E sì, per rispondere alla tua domanda, le correzioni apportate a questo hanno un impatto negativo sulle prestazioni. Uno dei motivi per cui questo exploit è un grosso problema è che è molto difficile risolvere queste correzioni senza impatti sulle prestazioni.

    
risposta data 12.01.2018 - 20:06
fonte
2

A partire dalla fine degli anni '70 / primi anni '80, le CPU iniziarono a scomporre l'esecuzione delle istruzioni in una serie di passaggi più piccoli, chiamati "esecuzione in pipeline". Ad esempio, la CPU potrebbe leggere contemporaneamente un'istruzione dalla memoria, eseguire un secondo e memorizzare i risultati da un terzo. Fare le cose in questo modo rende le CPU più veloci: avendo più istruzioni contemporaneamente in diverse fasi di elaborazione, una CPU può essere molte volte più veloce con la stessa velocità di clock.

Ciò si verifica in un problema quando l'istruzione eseguita è un ramo (come un'istruzione "if"): quale istruzione è l'istruzione "next" che dovrebbe essere caricata dalla memoria? La soluzione era la previsione dei rami e l'esecuzione speculativa: la CPU fa un'ipotesi plausibile su quale istruzione è successiva, la esegue, ma non rende i risultati permanenti finché non sa se l'ipotesi era giusta o meno. Questo accelera di nuovo le cose, perché la CPU non ha bisogno di aspettare che i risultati di un ramo siano noti a meno che non indovinino in che modo il ramo andrà.

Le moderne CPU sono veloci, l'accesso alla memoria è lento e le pipeline sono molto più lunghe della semplice pipe a tre stadi che ho descritto sopra. Osservando il tuo codice dal punto di vista della CPU mentre sta eseguendo questa linea:

unsigned long untrusted_offset_from_caller = ...;

La CPU vede le due affermazioni "se" in arrivo e dice: "in base all'esperienza passata, entrambe queste affermazioni" if "si riveleranno vere. Pertanto, dovrò recuperare arr1->data[untrusted_offset_from_caller] e arr2->data[index2] dalla memoria. "

if (untrusted_offset_from_caller < arr1->length) {
    unsigned char value = arr1->data[untrusted_offset_from_caller];
    unsigned long index2 = ((value&1)*0x100)+0x200;
    if (index2 < arr2->length) {
        unsigned char value2 = arr2->data[index2];
    }
}

Quindi procede all'emissione delle richieste di memoria e all'esecuzione speculativa del codice. Ora, questa volta, risulta che if (index2 < arr2->length) era falso e la CPU scarta il lavoro che ha fatto.

Tuttavia, non scarta tutti di esso. La memoria, i registri e il puntatore dell'istruzione mostrano tutti ciò che ci si aspetterebbe dall'asserzione falsa, ma il recupero preventivo di arr2->data[index2] è ancora nella cache dei dati della CPU. Un programma può capirlo dal fatto che leggere quella parte di memoria è più veloce del normale e può dedurre il valore di arr1->data[untrusted_offset_from_caller] .

    
risposta data 06.01.2018 - 01:27
fonte
0

"Cosa avrebbe impedito l'accesso a quella zona di memoria in una normale esecuzione"? Niente lo avrebbe impedito. Ma normalmente l'indirizzo utilizzato sarebbe stato basato su dati legalmente disponibili per il codice in esecuzione. Quindi i dati verrebbero letti speculativamente, una linea di cache verrebbe espulsa e questo ci dirà quale byte è stato letto, ma sarebbe un byte che avremmo avuto il diritto di sapere comunque. Quindi nulla sarebbe stato rivelato che era segreto.

Ecco la mia proposta per la gestione della situazione fornita dal codice di esempio (in hardware):

Un'operazione di lettura non è un problema. Quindi un metodo semplice sarebbe quello di consentire solo l'operazione di lettura speculativa one . Una seconda lettura speculativa dovrebbe aspettare che il primo non sia più speculativo.

Primo miglioramento: una lettura speculativa che non modifica la cache (o perde le informazioni in qualche altro modo) va bene. Quindi permettiamo sempre una lettura speculativa, e quindi consentiamo più letture purché non espellano una linea cache (o altrimenti perdono informazioni).

Secondo miglioramento: ulteriori letture vanno bene finché non hanno un indirizzo dipendente dalla prima lettura. Quindi teniamo traccia di quali registri sono il risultato di una lettura speculativa, e di come questo si propaga e consente ulteriori letture purché l'indirizzo non sia basato su una lettura speculativa. Questo permette a "if (x > 0) z = a [0] + a [1] + a [2];" procedere.

Terzo miglioramento: modifichiamo la cache L1 in modo che qualsiasi tentativo di lettura dei dati appartenenti a un altro processo produrrà un errore di cache. Ora sappiamo che se un'operazione di lettura colpisce la cache L1, allora ci è stato permesso di leggere i dati. Pertanto ignoriamo tutte le letture che sono state hit della cache L1.

    
risposta data 07.01.2018 - 01:43
fonte
0

Capisco cosa sta chiedendo il circulosmio, anche questa domanda è nella mia mente: se la lettura della memoria non autorizzata non è consentita nell'esecuzione normale, perché può accadere in modalità speculativa?

Un'aspettativa logica sarebbe che fallirebbe anche nella speculazione e fare abortire il ramo speculativo o altro. In effetti l'errore di accesso è registrato, ma viene applicato solo in un secondo momento se il codice diventa "reale".

La mia ipotesi istruita è che è stata una scelta per mantenere il design più semplice e veloce: se tutti gli effetti del ramo speculativo vengono scartati e non visti dal resto del codice, perché preoccuparsi di rendere la logica più complessa del necessario? Ogni transistor e ogni nanosecondo contano quando si progetta una tale bestia.

Il problema è che, a causa della cache, le ipotesi non erano esatte: non tutti gli effetti sono stati ripristinati e gli ingegneri non hanno notato questo fatto, o non erano a conoscenza del fatto che gli attacchi di canale laterale possono rilevare tali effetti o non hanno realizzato che quegli effetti possono effettivamente rivelare contenuti di memoria se usati in modi elaborati.

    
risposta data 10.01.2018 - 10:33
fonte
0

Dopo aver studiato attentamente tutte le risposte molto utili e aver ordinato tutte le informazioni che ho raccolto, faccio qui un veloce riassunto delle conoscenze che ho ottenuto e che mi sono state di aiuto come risposta soddisfacente.

Prima di tutto sono molto grato al commento di @ cort-ammon:

"The key concept is that no chip is obliged to precisely execute code according to the instructions. The obligation is instead that it must execute "as if" the code was precisely executed according to the instructions. All modern processors take advantage of this freedom."

Questo concetto è davvero necessario per capire perché tutte queste caratteristiche della CPU (come Branch Target Prediction, Speculative Execution e così via) sono state implementate in primo luogo e perché rimangono ugualmente valide (e ora sappiamo: come pericoloso!) come sempre.

Ora come per la domanda:

  • cosa avrebbe impedito l'accesso a quella zona di memoria in una normale esecuzione se il codice avrebbe cercato di farlo in modo esplicito?

Niente (come sottolineato da @immibis & @ gnasher729!): perché questa versione della vulnerabilità di Spectre consente solo di leggere i dati a cui il processore ha accesso.

Non c'è differenza nel codice, ad esempio, una lettura da un overflow del buffer all'interno dello stesso spazio di memoria del processo ( Vedi ad esempio questo codice ).

Cioè: il codice indicato nella domanda viene estratto da Variante 1: Limiti controllo bypass , in cui il generico Viene mostrato Proof of Concept (PoC) . Questo PoC viene utilizzato in vari modi: per utilizzare il codice in Variante 1 per attaccare il kernel, è necessario l'accesso a un interprete bytecode eBPF , in modo che "Il codice di spazio utente non privilegiato può fornire codice bytecode al kernel" . Questo è il comportamento caratteristico (!) Di eBPF (" esteso Berkeley Packet Filter ", una funzione programmabile dei kernel Linux) che l'attacco fa pieno uso di: in questo modo l'attacco passa inalterato al kernel, dove può leggere le posizioni di memoria che non dovrebbe (saranno fuori dalla sua memoria riservata, ma nello stesso spazio di processo) senza attivare alcun allarme - e non sarebbe nemmeno se il codice fosse esaminato dal compilatore eBPF (e non lo fosse), perché non viene effettuata alcuna lettura diretta della memoria fuori dai limiti.

Ma il PoC può anche essere usato in Variante 2: Iniezione target del ramo in cui, con altri trucchi, costringe la CPU a eseguire salti diretti in un'esecuzione di altri processi speculata erroneamente per ottenere l'accesso in lettura a posizioni di memoria virtuali arbitrarie. Questo attacco sfrutta appieno il fatto che alcune Branch Target Buffer (BTB) di alcune CPU (almeno Intel Haswell Xeon) utilizza solo una parte dell'intero indirizzo di memoria per memorizzare le informazioni di previsione dei rami. Questo fa parte del fatto che porta all'abilità (attacco) di un processo di influenzare il predittore del target di Branch per un altro processo completamente diverso, bypassando così le protezioni userspace / kernel (e altre).

E ora capisco dove questo indirizzo di memoria parziale BTB si adatta nella risposta che ho indicato nella mia domanda, che ha dichiarato che al fine di proteggere una CPU dagli attacchi Spectre:

"Branch predictor state must take the full address of the branch instruction into account (currently, to save space, only the low-order bits are used)." (@Mark)

Per quanto riguarda la seconda domanda:

  • non sarebbero i controlli (se la risposta alla prima domanda è che sono necessari controlli aggiuntivi) o che l'impatto del "flushing" nelle prestazioni? e se è così, è stato stimato quanto? (quando quelle presunte CPU sarebbero / saranno fatte).

ora che sono a conoscenza degli interni di questi attacchi, e anche degli interni delle ottimizzazioni della CPU (BTB, exec speculativo ...) la domanda mi sembra meno imperativa: i progettisti di CPU cercheranno di mantenere l'equilibrio tra performance e sicurezza ... potrebbe essere solo che la sicurezza non era in questo equilibrio prima che questi attacchi venissero resi pubblici (anche se gli attacchi sul canale laterale molto promettenti erano già noti ... ma questa è un'altra storia).

Ad esempio, come (di nuovo) @ punti cort-ammon:

"For example, I'm certain there's an Intel designer looking right now at whether the next generation CPUs can un-evict cache lines if a speculative read falls though to get them one step closer to "as if.""

Grazie ancora per tutte le risposte.

    
risposta data 02.03.2018 - 21:25
fonte

Leggi altre domande sui tag