Meltdown e Spectre

Se você estava vivendo dentro de uma bolha, de férias ou curtindo o recesso de final de ano e só voltou agora para a Internet, deve ter percebido que está um caos consignado no mundo da computação…

Tudo isso é por conta de duas palavrinhas que estão pipocando em todas as notícias sobre segurança da informação: Meltdown e Spectre.

Meltdown e Spectre

O Meltdown e o Spectre são falhas de arquitetura consideradas gravíssimas que permitem vazar informações sensíveis do kernel e de outros processos do sistema (sem as devidas permissões).

Neste artigo nós vamos sair da superfície e mergulhar não somente nos aspectos técnicos, mas também nas implicações práticas decorrentes dessas duas falhas de segurança. Aperte o cinto e vamo nessa!

Vá Direto ao Assunto…

O que aconteceu?

No início de janeiro, o MITRE publicou três CVEs relativos a falhas em microprocessadores envolvendo o acesso indevido a memória, por meio de execução especulativa e previsão de desvios:

  1. CVE-2017-5715: Injeção de Alvo em Desvios (Spectre)
  2. CVE-2017-5753: Não cumprimento de checagem de limites (Spectre)
  3. CVE-2017-5754: Aproveitamento do Carregamento de Dados em Cache (Meltdown)

Essas falhas foram descobertas e reportadas, de maneira independente, por vários grupos. Dentre eles o Google Project Zero, a empresa Cyberus Technology, a Universidade Tecnológica de Graz, e o pesquisador Paul Kocher em conjunto com outras universidades.

As notícias vieram com tanto termo técnico complexo que ficou difícil entender como essas vulnerabilidades são exploradas… Mas calma que eu vou facilitar para você! Vem comigo revisar a aula de arquitetura de computadores…

Arquitetura de Computadores

Lá na década de 50, um manolo muito cabuloso, chamado John von Neumann, propôs um modelo de arquitetura de computadores que determinava que tanto o programa quanto seus dados deveriam ser armazenados no mesmo local de memória. Esse novo modelo permitiu a existência de “programas que escrevem programas” e revolucionou de inúmeras formas a computação.

O modelo era “sensacionível”, mas ele tinha um porém: a capacidade de trabalho do processador era muito maior que a da memória. Assim, em diversos momentos, o processador ficava ocioso esperando a execução de operações de troca de dados. Esse problema ficou conhecido como Gargalo de von Neumann.

Para mitigar esse gargalo, os processadores utilizam diversas estratégias para melhorar o desempenho de suas arquiteturas. Dentre elas, a primeira que você precisa saber que existe relembrar é o Instruction Level Paralelism (ILP).

ILP é o grau com que as instruções de um programa podem ser avaliadas/executadas em paralelo.

Assim, para alcançar o maior de desempenho, o objetivo básico de toda arquitetura é aproveitar ILP ao máximo. Existem diversas técnicas de ILP, mas as essenciais para você entender o Spectre e o Meltdown são:

  • Execução fora de ordem
  • Execução especulativa
  • Predição de desvios

Vamos nos aprofundar um pouco mais em cada uma delas…

Execução Fora de Ordem

Execução fora de ordem é a capacidade do processador de reordenar as instruções de forma a executá-las gastando o menor número de ciclos possível.

Imagine um processador capaz de executar duas instruções de atribuição ao mesmo tempo:

1
prato, copo = biscoito, suco

Isso é uma característica de processadores superescalares — que possuem mais de um pipeline de execução — que conseguem, em uma análise, identificar e aproveitar oportunidades de paralelismo como essa.

Contudo, nem sempre é possível reordenar as instruções. Existem casos em que as chamadas dependências impedem a execução simultânea das instruções:

1
2
3
4
pao = manteiga 
misteira = pao
copo = suco
prato = biscoito

Sabendo que eu não posso colocar o pão na misteira ao mesmo tempo em que coloco a manteiga no pão (muita bagunça ao mesmo tempo), mesmo tendo a capacidade de fazer duas operações ao mesmo tempo, a ordem dessas instruções nos faria ter que esperar a manteiga ir no pão, para assim poder executar o pão na misteira.

1
2
3
pao = manteiga 
misteira, copo = pao, suco
prato = biscoito

Essa necessidade de espera configura uma dependência de dados. Para resolver o problema, isto é, para que o processador não perca tempo apenas colocando manteiga no pão, ele troca a ordem do pão na misteira com o suco no copo. Assim, mais de uma operação é executada ao mesmo tempo.

1
2
pao, copo = manteiga, suco
misteira, prato = pao, biscoito

Esse rearranjo de instruções para um melhor aproveitamento dos recursos do processador é o que chamamos de execução fora de ordem.

Predição de Desvios

Outro problema de desempenho para os processadores são os malditos condicionais:

1
2
3
4
5
x = 0
if x == 1:
  y = 42
else:
  y = 0

Essencialmente, um if nada mais é do que a definição de dois ou mais caminhos de execução. Para saber qual caminho tomar, o processador precisa primeiro avaliar um conjunto de expressões booleanas. Mas já sabemos que um processador não pode perder tempo, então o que será que ele faz para não ficar parado esperando se deu true ou false?

Sim, é exatamente isso que você pensou… Ele chuta! Ele escolhe “da cabeça dele” um caminho e fé na missão que vai dar certo!

Vamos supor que o processador chutou que ele deveria entrar no if. Se ele tiver acertado, ele terá adiantado a atribuição y = 42 e ganhado tempo com isso. Como o valor correto de x é 0(linha 1) porque a Lei de Murphy sempre impera, o processador desfaz essas instruções adiantadas e retorna para o ponto de continuidade da sua execução normal (y = 0).

Para fazer esses chutes de forma dinâmica, o processador possui mecanismos inteligentes baseados em processos estatísticos e visão do histórico de condicionais anteriores. Por exemplo, como programas normalmente tem diversos condicionais que tendem à repetição (por estarem dentro de laços, etc), a técnica mais simples é repetir o resultado do branch anterior (se foi true, chuta o próximo true). Essa técnica recebe o nome de Modelo de Previsão de 1 bit.

Técnicas mais avançadas utilizam mais bits, correlação entre condicionais de dois níveis, Branch Target Buffer, etc. Não vou entrar em detalhes desses modelos mais complexos porque já deu pra entender o objetivo do previsor, né?

Execução Especulativa

A execução especulativa consiste na execução adiantada de operações que podem ser necessárias adiante. Ela pode funcionar de forma combinada tanto com a predição de desvios quanto com a execução fora de ordem. Lembra do exemplo anterior?

1
2
3
4
5
x = 0
if x == 1:
  y = 42
else:
  y = 0

A atribuição y = 42 é um caso de execução especulativa, pois ela foi feita antes do processador saber, de fato, se deveria ou não entrar nessa condição.

Pronto, já vimos o principal de ILP. Agora vamos falar um pouco de outra estratégia para melhoria de desempenho: a memória cache.

Memória Cache

Lembra lá atrás quando nós falamos que o processador é muito mais rápido que a memória? Pois é! Para reduzir essa diferença, uma melhoria usada nos processadores é o uso de memória cache.

Memória cache é uma memória rápida e com pouca capacidade de armazenamento interposta entre a CPU e a memória principal.

A ideia principal da cache é trazer os dados para próximo do processador antes que ele peça, aproveitando-se de algo chamado de Princípio da Localidade. Esse princípio dita que os programas tendem a reusar dados e instruções utilizados recentemente de forma temporal (ex: repetição em laços) ou de forma espacial (ex: incremento em laços).

Em outras palavras, se um programa acessa a posição i de uma matriz, existem grandes chances de que ele, em breve, acessará a posição i novamente ou a posição i + 1. Assim, para economizar tempo de acesso à memória, quando o processador pede o conteúdo de i na memória principal, o conteúdo de i + 1 também vem junto para a cache.

Tudo no Liquidificador

Para você conseguir jogar seu Minecraft sem o PC travar, os processadores atuais fazem uso de todas essas técnicas (e de muitas outras). Existe ainda um universo de coisas que poderíamos discutir sobre elas, mas agora já temos o necessário para entender o Meltdown e o Spectre!

Meltdown

Dê uma olhada no seguinte trecho de código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
char *memoriaTemporaria = malloc(256 * 4096);
int temposDeAcesso[64];

limpaCacheCompletamente();

unsigned int *enderecoKernel = 0xffffffffffdfec42; // Endereço do Kernel (fictício)
unsigned int geraErro = *enderecoKernel;   // Atribuição com problema
unsigned int varQualquer = *(memoriaTemporaria + 4096 * geraErro);

for (int i = 0; i < 256; i++) {
    int ca1 = clockAtual();
    char c = *(memoriaTemporaria + 4096 * i);
    int ca2 = clockAtual();
    temposDeAcesso[i] = ca2 - ca1;
}

// A função menorTempo retorna o índice da posição com o menor tempo
printf("Conteudo vazado: %d\n", menorTempo(temposAcesso)); 

Esse código tem tudo que a gente precisa para entender o Meltdown. A primeira parte importante do programa é a linha 7, que tenta ler o conteúdo de um endereço do kernel. Obviamente, o programa não conseguirá ler essa linha, porém a execução especulativa faz com que as linhas 7 e 8 sejam executadas ao mesmo tempo. Somente após a detecção do acesso indevido é que a execução especulada da linha 8 será descartada.

O pulo do gato no Meltdown é que, durante a especulação, a execução da linha 8 trouxe o conteúdo de um endereço válido para a cache (originário de memoriaTemporaria), pois a violação em geraErro ainda não havia sido avaliada. Dessa forma, esse conteúdo válido encontra-se na cache. Em seguida, para descobrir o valor que foi carregado para a cache, o programa testa todas as possibilidades de valores, medindo o tempo de acesso em cada um desses endereços de memoriaTemporaria, nas linhas 10 a 15, e armazenando esses tempos na matriz temposDeAcesso.

A diferença no tempo de acesso a conteúdos que se encontram na cache e os demais é bem grande. Como toda a cache foi limpa (ou preenchida com outro conteúdo) no início do programa (linha 4), algum desses valores será, aproximadamente, 10 menor que os outros. E esse é o índice que procuramos, o índice descendente (valeu Felipe Franco).

Resumindo, o programa:

  1. Limpa qualquer conteúdo que esteja na cache inicialmente
  2. Usa a execução especulativa para carregar o conteúdo de um endereço válido de memoriaTemporaria para a cache
  3. Utiliza o conteúdo de um endereço do kernel
  4. Mede o tempo de acesso a todos os endereços possíveis de memoriaTemporaria
  5. Retorna, por meio da função menorTempo(), o valor do índice com o menor tempo de acesso a algum endereço de memoriaTemporaria

Dessa forma, obtém-se o valor contido no endereço do kernel alvejado.

Muito loko, né? Pois é! E o Meltdown é o fácil… Eu usei alguns nomes de funções mais simples para facilitar a leitura, mas as informações detalhadas podem ser vistas no artigo original do Meltdown.

Agora bora pro Spectre!

Spectre

Em alguns trechos, o funcionamento do Spectre é bem similar ao Meltdown. A diferença fundamental é que o Spectre explora também a predição de desvios:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
char *segredo = "MAVINS";

unsigned int tamanhoArray1 = 16;
unsigned char array1[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
unsigned char array2[256 * 512];

unsigned long enderecoProibido = (segredo - (char *) array1);

int resultados[256]; // Valor para receber os resultados das leituras
int tentativa, cacheHits;

preencheArray(array2, 1); // Preenche array2 com valor 1
preencheArray(resultados, 0); 

limpaDaCache(array2); // Remove todas as ocorrências de array2 da cache

for (int i = 0; i < 999; i++) {
    tentativa = i % tamanhoArray1;
    // Treina o previsor por 30 vezes junto com o previsor de desvios
    treinaPrevisorDeDesvios(array1, array2, tamanhoArray1, 30, tentativa); 
    chacheHit = verificaTemposAcesso(resultados);

    if (chacheHits > VALOR_CONFIANCA) 
        break;
}

printf("Caracter lido: '%c'\n", descobreCaracter(resultados));

Para simplificar, esse pseudocódigo extrai apenas o primeiro caractere de segredo, mas isso já é suficiente para entender o ataque.

O primeiro passo do algoritmo é definir as variáveis que serão utilizadas pelo programa:

  • segredo: conteúdo que o atacante quer acessar.
  • array1: vetor de referência que o nosso ataque irá extrapolar.
  • array2: vetor de referência que será carregado para a cache.
  • enderecoProibido: endereço de memória que, teoricamente, não pode ser acessado pelo programa.

Assim, o próximo passo é retirar da cache tudo referente a array2 (linha 15).

Em seguida, há um laço com uma função chamada treinaPrevisorDeDesvios(), que é responsável por adestrar o nosso previsor. Para entender melhor, vamos olhar essa função de perto:

1
2
3
4
5
6
7
8
9
10
11
12
13
void treinaPrevisorDeDesvios(array1, array2, tamanhoArray1, cont, tentativa)
{
    int temp;
    for (int i = 0; i < cont; i++) { // cont = 30
        // ...
        // Valores de treinamento para o previsor
        x = tentativa ^ (x & (enderecoProibido ^ tentativa));

        if (x < tamanhoArray1) {
            temp &= array2[array1[x] * 512];
        }
    }
}

Lembra lá atrás, em Predição de Desvios, quando eu falei que o processador chuta se ele deve ou não entrar em um determinado condicional? E que para melhorar seus próprios chutes ele se baseia em um histórico de resultados? Pois é, o que eu não falei ainda é que o Spectre se aproveita desse histórico para treinar o processador a errar e entrar de maneira indevida (pela execução especulativa) em um condicional que não deveria.

No nosso exemplo, esse condicional está nas linhas 9 e 10. Esse condicional é indevido porque o valor de array1[x], em alguns poucos casos e devido ao treinamento, extrapola as suas 16 possíveis posições válidas. Contudo, a execução especulativa combinada com a previsão de desvios faz com que array2 seja carregada para a cache.

Com o valor carregado na cache, os passos seguintes são similares ao Meltdown: verifica-se os tempos de acesso aos endereços de array2 para encontrar o valor com menor tempo de acesso, que é aquele que se encontra na cache (cache hit).

No código principal, ainda há um laço que itera de 0 a 999 (linha 17). Eu mantive esse trecho no código, que veio direto da PoC do artigo sobre o Spectre, porque durante a execução podem ocorrer mais cache hits do que o esperado. Assim, o código realiza essa execução várias vezes antes de retornar o resultado para o usuário para aumentar a probabilidade de acerto do endereço.

Para finalizar a explicação do Spectre, ainda há uma variante proposta no artigo que explora desvios de formas indiretas. Ao invés de colocar endereços de acesso indevidos, o atacante faz o programa executar gadgets da técnica de Return-Oriented Programming (ROP), fazendo o programa vazar informações sensíveis. Neste artigo, eu não vou entrar em detalhes de como ROP funciona, mas se você quiser, deixe um comentário que eu faço um post só para essa técnica, ok?

Acredito que deu pra ter uma ideia legal de como essas falhas funcionam. Que tal discutirmos um pouco os detalhes filosóficos práticos sobre a existência dessas falhas?

A falha é mesmo grave?

Se depois de tudo que você leu, você ainda me pergunta Sim, muito! Muito mesmo! Relatórios sobre o Meltdown apontam que essa falha atinge praticamente todos os processadores da Intel dos últimos 20 anos. Já outros fabricantes não escaparam do Spectre, que atinge a maioria esmagadora dos processadores, incluindo ARM, AMD, Intel, etc.

Para piorar, tudo indica que essas falhas já haviam sido comunicadas aos fabricantes há seis meses, e que todo esse tempo não foi suficiente para contornar a situação, antes delas caírem nas graças do público.

Quem já se pronunciou?

Uma pá de pessoas e empresas! O problema é tão grave que Mozilla, Apple, Intel, Amazon, Google, etc, não somente se pronunciaram, mas também liberaram gambiarras soluções paliativas para que seus serviços/produtos não fiquem vulneráveis à exploração dessas falhas.

Até o próprio Linus Torvalds fez um singelo comentário sobre a competência dos engenheiros da Intel. Segundo ele, a empresa deveria prestar mais atenção naquilo que produz e decide continuar vendendo.

O que eu faço?

Se você é o CEO da Intel, deveria ter vendido todas as suas ações, enquanto havia tempo.

Se não é, além de sentar e chorar você, mero mortal usuário comum, receberá atualizações de software a torto e a direito. A consequência dessas atualizações é o que muita gente está apontado: queda de 5% a 30% no desempenho dos processadores (faz sentido, né?).

E se eu não atualizar minha máquina?

Sério que você faria isso só para continuar jogando com FPS travado? Não vale o risco. Até agora, só é de conhecimento público a existência e a criticidade das falhas. O que a galera mal intencionada fará com elas ainda é um mistério, mas não sou eu (nem você, eu espero) quem vai pagar pra ver.

Já aconteceu algo parecido antes?

Ano passado (tô falando de 2017), nós tivemos um estardalhaço muito maior por muito menos. Para refrescar sua memória, eu estou falando do Wannacry e afins…

Aí você se pergunta:

Mas o que tem a ver uma coisa com a outra?

Bom, a falha que esses ransomwares exploraram chama-se MS17-010 (KB4012598) e o remédio patch para resolver o problema foi apresentado dois meses antes do Wannacry explodir!

Se a caixola de vocês estiver funcionando igual a minha (modo conspiração: ON), é fácil perceber que — se uma falha para a qual já existia a solução fez o estrago que fez (em agosto, as carteiras de BTC tinham o equivalente a US$ 141.000) — imagina uma outra para a qual só se conhece o problema!

Conclusão

Este artigo fez um apanhado do rebuliço que está a internet com essas duas falhas de segurança tão críticas. O fato é que ninguém esperava essa “novidade” para o início de 2018 (exceto, talvez, o CEO da Intel).

O Meltdown é uma falha que afeta praticamente todos os processadores Intel e que, literalmente, derrete as barreiras de segurança feitas pelo hardware do processador. Ela tem um potencial tão absurdo que aos poucos vêm surgindo demonstrações de possibilidades de recuperação de dados na memória. Veja, por exemplo, a recuperação de uma foto:

Já o Spectre, que afeta praticamente todas as principais arquiteturas de processadores, é uma falha bem mais complexa e bem mais difícil de ser corrigida. Soluções estão aparecendo e irão aparecer para diversas aplicações e sistemas operacionais, porém a maioria servirá para consertar apenas problemas pontuais. É possível que as próximas gerações de processadores sofram mudanças radicais em suas arquiteturas por causa do Spectre.

Bom, acho que já falei demais. Espero que este artigo tenha te ajudado a entender o Meltdown e o Spectre ou, pelo menos, na revisão para a prova de arquitetura de computadores…

Referências

Comentários desabilitados...