
Exploit proof-of-concept che aggira ASLR sulle CPU Intel abusando del branch target buffer e dell'esecuzione speculativa per far trapelare indirizzi randomizzati tramite canale laterale.
Address Space Layout Randomization è una mitigazione usata per rendere più difficile lo sfruttamento degli attacchi di corruzione della memoria. In uno scenario di vulnerabilità di buffer overflow, ad esempio, un attaccante che cerca di realizzare un exploit con Return Oriented Programming deve conoscere gli indirizzi dei gadget nella catena. Se il segmento del codice del binario sfruttato viene randomizzato, allora è molto più difficile per un attaccante scegliere l'indirizzo corretto per l'exploit, rendendo lo sfruttamento non fattibile.
Il seguente esempio mostra come un indirizzo viene randomizzato:
#include <stdio.h>
void DoNothing();
void (*codePtr)() = DoNothing;
void DoNothing(){}
int main(int argc,char **argv){
printf("Destination %p\n",codePtr);
DoNothing();
}
Ogni esecuzione il valore viene randomizzato:
Destination 0x563714256149
Destination 0x556d8e2f1149
Destination 0x5618c8bdd149
Destination 0x55ee623b0149
Gli ultimi 12 bit 149 sono sempre gli stessi, ma la posizione della funzione può essere approssimativamente ovunque tra 0x550000000000 e 0x570000000000, il che significa che 29 bit sono randomizzati, occupando un possibile spazio di indirizzi di 0x200 0000 0000, ovvero 2,2 TB.
Il processamento di ogni istruzione è un compito complesso. Alcune fasi del processamento di una singola istruzione sono:
Per aumentare il throughput delle istruzioni nella CPU, ogni compito dell'istruzione viene eseguito da un'unità specifica del processore. Con tutte le unità che lavorano in parallelo, la CPU può eseguire a velocità di clock molto più elevate; questa è l'idea di una pipeline.
Esecuzione delle istruzioni A, B e C nei cicli 1-5. Nel ciclo 3, ad esempio, le unità di lettura, decodifica ed esecuzione sono attive simultaneamente
Tuttavia, le istruzioni non sono completamente indipendenti tra loro. Ad esempio, la seguente sequenza:
A. add ax,[bx]
B. jz $+1
C. mov dl,[rsi]
D. nop
In questo caso, l'istruzione A nel caso migliore terminerà solo al ciclo 3 in fase di esecuzione. Tuttavia, l'unità di fetch deve decidere qual è la prossima istruzione da recuperare dalla memoria, e se l'istruzione C (mov dl,[rsi]) debba essere saltata.
In questo scenario, la CPU ha l'opzione di attendere che l'istruzione A termini, cosa che avverrà solo al terzo ciclo di clock, per poi recuperare l'istruzione corretta dalla memoria, se l'operazione di add restituisce 0, per esempio:
Questo comporta un ritardo nella pipeline perché la CPU deve attendere che l'istruzione venga eseguita. In questo esempio il ritardo è di un singolo ciclo di clock, ma l'istruzione add ax,[bx] richiede un'operazione di memoria che, come visto prima, può richiedere fino a centinaia di cicli per essere completata, causando così un costo prestazionale significativo sul processore.
Un'opzione più veloce sarebbe cercare di "indovinare" il percorso di esecuzione corretto. La CPU può speculare se il branch viene effettuato o meno. Dopo quel punto, l'esecuzione continua dal percorso speculato e i valori vengono committati solo se il percorso si rivela corretto al termine dell'istruzione A. Se il percorso si rivela sbagliato, i risultati vengono scartati e lo stato viene ripristinato a prima del punto di speculazione.
L'unico problema nel ripristinare il percorso intrapreso è che lo stato microarchitetturale della CPU non può essere ripristinato. Quindi se la CPU specula eseguendo l'istruzione C (mov dl,[rsi]), i dati puntati da rsi verranno spostati nella cache. Questo effetto può essere misurato successivamente usando un attacco side-channel (canale laterale).

Predittore condizionale a 2 bit. https://en.wikipedia.org/wiki/Branch_predictor
Non solo le istruzioni condizionali devono essere predette, ma anche i branch indiretti. La CPU deve avere un meccanismo per indovinare le destinazioni di un'istruzione come call [rdi].
La vulnerabilità Spectre v2 mostra che è possibile sfruttare il predittore indiretto per ottenere esecuzione transiente in altri processi:
Estratto da https://spectreattack.com/spectre.pdf
Posizionando un'istruzione call nel contesto A allo stesso indirizzo virtuale di un'altra call nel contesto B, l'attaccante può addestrare la CPU a eseguire codice in una posizione scelta dall'attaccante nel contesto B, in un attacco di riuso del codice (code reuse), simile al Return Oriented Programming (ROP).
La vittima designata deve avere un pezzo di codice noto come "spectre gadget" in grado di far trapelare un segreto usando un attacco side-channel. Per un attacco Spectre riuscito, l'attaccante deve anche conoscere la posizione dello spectre gadget. Pertanto, negli attacchi user-user, proteggere la vittima con ASLR era una mitigazione per questo tipo di attacchi. Tuttavia, esistono anche tecniche per estrarre l'ASLR usando attacchi microarchitetturali come Jump Over ASLR. Questa tecnica ha però alcune limitazioni sulla quantità di bit leakati, poiché si basa sulle collisioni nel predittore diretto per bypassare l'ASLR.
I meccanismi interni di questo predittore sono mostrati di seguito:

Estratto da https://spectreattack.com/spectre.pdf
Alcuni di questi componenti sono:
Il layout classico dell'attacco Spectre v2 si presenta così:

getenv + secret[0]*4096 può far trapelare il valore del segreto alla posizione 0 usando la libc come memoria condivisa.Exec ASLR (noto anche come Reverse Branch Target Buffer Poisoning) è una nuova tecnica per bypassare l'ASLR usando la vulnerabilità Spectre-BTI. Questo attacco abusa del fatto che non solo l'attaccante può inquinare il BTB in uno scenario Spectre-BTI classico, ma anche le vittime possono innescare una predizione errata del branch nel processo dell'attaccante, portando l'attaccante a un salto speculativo verso un indirizzo protetto da ASLR. Poi, usando un side-channel che rivela quale indirizzo viene eseguito, un attaccante può recuperare l'indirizzo di destinazione completo, bypassando l'ASLR per il processo preso di mira.
Il layout dell'attacco Exec ASLR si presenta così:

In questo tipo di attacco non c'è bisogno di trovare uno spectre gadget né di avere una memoria condivisa per il side-channel: l'unico requisito è un branch indiretto da sfruttare; tutti i gadget necessari di Spectre v2 sono posizionati all'interno del processo dell'attaccante. L'unico nuovo requisito per questo attacco è poter mappare l'indirizzo di destinazione della vittima nel proprio processo, quindi questo attacco non può funzionare contro KASLR, per esempio. Non è nemmeno un attacco di brute force: con un singolo tentativo è possibile testare più indirizzi contemporaneamente, tuttavia c'è un limite a quanti "leak gadget" la memoria può contenere allo stesso tempo. Questo riduce significativamente il tempo per eseguire l'attacco rispetto a Jump Over ASLR, passando da ~100 indirizzi al secondo a poche centinaia di miliardi di indirizzi al secondo.
Il leak gadget usa probeArray per informare l'attaccante su dove viene eseguito il gadget stesso. Riceve probeArray e l'indice di RIP da far trapelare come argomento e esegue una sorta di programmazione senza branch per decidere se accedere a probeArray[0] o probeArray[4096].
lea rax,[rip - 7] ;load current address
shr rax,cl ;selects the bit using cl arg
and rax,1
shl rax,12 ;loads probearray
mov dl,[rsi+rax] ;or probearray+4096
Come visto in precedenza, il BHB viene usato per selezionare una voce del BTB. Per trovare una collisione nel BTB e sfruttare questa vulnerabilità, un attaccante deve conoscere gli ultimi N branch eseguiti (29 se < Skylake). Nei nostri test, abbiamo usato un ciclo for per impostare lo stato del BHB a un valore noto prima della chiamata indiretta. Ecco un esempio vulnerabile di codice vittima:
#include <stdio.h>
void DoNothing();
void (*codePtr)() = DoNothing;
void DoNothing(){
return;
}
int main(){
printf("Destination = %p\n",codePtr);
while(1){
for(int i=0;i<200;i++){}
codePtr();
}
}
Per garantire che lo stato del BHB sia lo stesso in entrambi i contesti, l'attaccante copia i byte corrispondenti al ciclo for e alla call della vittima sotto forma di shellcode. Lo shellcode viene copiato in tutte le 256 posizioni possibili che possono corrispondere all'allineamento dei 20 LSB richiesto affinché lo stato del BHB sia lo stesso.
Victim Code
0x5594c566a152 <+28>: mov eax,0xc8
0x5594c566a157 <+33>: dec eax
0x5594c566a159 <+35>: jne 0x1157 <main+33>
0x5594c566a15b <+37>: nop
0x5594c566a15c <+38>: nop
0x5594c566a15d <+39>: nop
0x5594c566a15e <+40>: lea rdi,[rip+0x2ecb]
0x5594c566a165 <+47>: call QWORD PTR [rdi]
--> Executes to
0x5594c5669135: ret
Attacker Code
… //eax=200 rsi=probeArray, cl=0
0x6a157 dec eax
0x6a159 jne 0x455555500157
…
0x6a165 jmp QWORD PTR [rdi]
--> Misspredicts to
0x5594c566a135: lea rax,[rip - 7]
0x5594c566a13c: shr rax,cl
0x5594c566a13f: and rax,1
0x5594c566a133: shl rax,12
0x5594c566a137: mov dl,[rsi+rax]
C'è un problema quando si cerca di posizionare un gadget in tutte le posizioni possibili contemporaneamente. Nei nostri test, l'ASLR colloca la destinazione da qualche parte tra 0x550000000000 e 0x570000000000. Ciò significa che ci sono 2,2 TB di possibili indirizzi virtuali da mappare, ovvero 537 milioni di leak gadget. Ma il sistema ha solo 8 GB di RAM. Oltre al fatto che sarebbe possibile mappare 2 TB di RAM usando il COW, non abbiamo avuto molto successo con questo approccio. Suppongo che crei troppa pressione sul Translation Lookaside Buffer (TLB), rendendo la speculazione verso un indirizzo non tradotto troppo lenta. Nei test abbiamo creato una pagina in memoria da 1 GB e l'abbiamo riempita con i gadget che leakano. Poi abbiamo spostato la pagina attraverso l'intervallo di 2 TB usando la syscall remap. Un altro problema osservato è stato il fatto che l'indirizzo speculato probabilmente non era presente nel TLB, poiché non era mai stato effettivamente eseguito. Ma il manuale Intel afferma:
Pertanto, per aumentare le probabilità di un pagewalk "indotto dalla speculazione", abbiamo cercato di rendere il più difficile possibile la risoluzione dell'indirizzo corretto. Questo viene fatto usando una catena di puntatori (pointer chain) per l'indirizzo di destinazione. L'idea è che il frontend della CPU speculi sulla destinazione del branch e l'unità di riordino esegua il leak gadget prima di completare la lettura della catena di puntatori.
Improved Caller - Frontend fetched instructions
mov rcx,%1 ;mask arg for gadget
lea rsi,[%2] ;probe array ptr arg
lea rdx,[%0]
mov rdx,[rdx] ;pointer chain
mov rdx,[rdx]
mov rdx,[rdx]
...
mov rdx,[rdx]
mov rdx,[rdx]
check [rdx] ;mispredicts to gadget
lea rax,[rip - 7];speculative execution
shr rax,cl
and rax,1
shl rax,12
mov dl,[rsi+rax]
Improved Caller - Reorder unit scheduled instructions
mov rcx,%1 ;mask arg for gadget
lea rsi,[%2] ;probe array ptr arg
lea rdx,[%0]
mov rdx,[rdx] ;pointer chain
mov rdx,[rdx]
; <pagewalk occurs some where here>
... ;Out of order + speculation
lea rax,[rip - 7]
shr rax,cl
and rax,1
shl rax,12
mov dl,[rsi+rax]
...
mov rdx,[rdx]
mov rdx,[rdx]
check [rdx] ;the execution path reverted
Questa tecnica di esecuzione fuori ordine (Out of Order) + speculativa ha mostrato un miglioramento nei tassi di misprediction desiderati nell'attacco.
Per eseguire un attacco Spectre V2, l'attaccante deve eseguire codice nello stesso core della vittima, così da condividere la stessa Branch Prediction Unit (BPU). Nelle CPU <= Skylake abbiamo osservato che è possibile ottenere la co-residenza sul core usando l'hyperthreading. Le CPU Icelake e Cascade Lake implementano una mitigazione chiamata Single Thread Indirect Branch Predictor, che separa la BPU tra i thread. Pertanto è necessario eseguire vittima e attaccante sullo stesso thread e usare usleep per alternare tra processo vittima e processo attaccante; questo renderebbe l'attaccante più lento su queste CPU, se non fosse per il fatto che le cache (e probabilmente anche il TLB) sono molto buone in quelle generazioni, permettendo di mettere in cache molti più gadget contemporaneamente.
Questa tecnica è stata testata su tutte le CPU Intel disponibili su Google Cloud, sia sulle generazioni N1 che N2:
Oltre ad alcune differenze negli exploit per Cascade e Ice Lake, tutti i test sono in grado di recuperare gli indirizzi con una precisione >99% in meno di 10 secondi.
Le mitigazioni sono le stesse di Spectre V2 per mitigare gli attacchi user-user.
La Indirect Branch Prediction Barrier (IBPB) consente il flush della BPU e può essere usata nei context switch. In Linux, l'IBPB può essere usata tramite la syscall prctl con l'opzione PR_SET_SPECULATION_CTRL.
Non ho idea di quale sia la mitigazione equivalente per Windows, per favore fatemelo sapere.
https://cos.ufrj.br/uploadfile/publicacao/3061.pdf
https://www.youtube.com/watch?v=Qj4z-KvnkxU
https://docs.google.com/presentation/d/10t-oo-c26x9ydx1_FYgmhy204rxfmQ92eboPlCnA2y4/edit?usp=sharing
https://googleprojectzero.blogspot.com/2018/01/reading-privileged-memory-with-side.html https://eprint.iacr.org/2013/448.pdf https://spectreattack.com/spectre.pdf https://www.cs.ucr.edu/~nael/pubs/micro16.pdf http://download.vusec.net/papers/bhi-spectre-bhb_sec22.pdf https://www.kernel.org/doc/html/latest/userspace-api/spec_ctrl.html Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3. Santa Clara, USA: Intel Corporation, 2016, iSBN 325384-060US
| Operazione \ ciclo di clock | 1 | 2 | 3 | 4 | 5 |
|---|
| Fetch | A | B | C | ||
| Decodifica | A | B | C | ||
| Esecuzione | A | B | C |
| Operazione \ ciclo di clock | 1 | 2 | 3 | 4 | 5 | 6 |
|---|
| Fetch | A | B | D | |||
| Decodifica | A | B | D | |||
| Esecuzione | A | B | D |
| Operazione \ ciclo di clock | 1 | 2 | 3 | 4 | 5 | 6 |
|---|
| Fetch | A | B | (S) C | |||
| Decodifica | A | B | (S) C | |||
| Esecuzione | A | B | (S) C |