Skip to main content

Ataque SPECTRE e a vulnerabilidade de processadores modernos, explicados em português

Em linhas gerais digamos que o velho papai noel achou que todos os profissionais de infosec do mundo foram maus meninos em 2017...

Disclaimer: este artigo não é daqueles "aprenda a ser ráquer", não tenho este interesse e muito menos o conhecimento. Apenas tenho uma curiosidade sobre como as coisas funcionam. Em especial, eu ainda não entendi exatamente como o ataque é de fato realizado, já que o artigo cita referências e procedimentos nos quais não me aprofundei.

Esta publicação foi baseada predominantemente no artigo científico publicado sobre o ataque Spectre, que pode ser obtido neste website.

Antes de tudo havia a Lei de Moore. E em determinado momento, com as dificuldades de aumento da densidade de transistores nos micro-processadores, houve uma corrida para a realização de otimizações para aumento do desempenho e consequentemente velocidade de processamento das unidades de processamento computacionais (CPU). Se você não é da área de computação (antes de tudo: coragem para seguir a leitura), em resumo a Lei de Moore "rege" a velocidade com a qual as CPU evoluem em termos de capacidade de processamento.

Introdução

Processadores modernos implementam uma otimização chamada de Execução Especulativa e Predição de Ramificação Condicional, que de maneira simplificada pode ser explicado da seguinte maneira: quando o código atinge um teste condicional, que pode se ramificar em dois caminhos distintos de execução, e este teste depende de um valor que não está disponível imediatamente para o processador, ele (o processador), ao invés de pausar a execução do programa enquanto o valor necessário é buscado na memória (o que pode levar centenas de ciclos de relógio do processador), presume um resultado do teste condicional, executa a ramificação resultante desta presunção e aguarda o famigerado valor ser obtido da memória. Eventualmente, quando o valor é obtido, o processador verifica se a presunção que ele fez estava correta e, caso tenha sido correta, o resultado do processamento da ramificação presumida é utilizado, caso contrário é descartado e executa-se o outro ramo. Desta forma é natural perceber-se que no pior caso (em que a presunção foi incorreta) o desempenho é praticamente igual ao sem a otimização, e no melhor caso (em que a presunção foi correta) o desempenho é bem superior ao sem a otimização.

O ataque Spectre envolve induzir a vítima a executar especulativamente trechos de código que não deveriam ser executados e que por consequência vazam informações através de um ataque de canal lateral.

Em linhas gerais, o ataque Spectre viola o isolamento de memória combinando a execução especulativa com a exfiltração via canais micro-arquiteturais encobertos. Mais especificamente, para realizar um ataque Spectre, o atacante começa por encontrar um trecho de código dentro do espaço de memória do processo alvo que quando é executado age como um canal encoberto e revela o conteúdo desta memória.

Sobretudo, a preocupação advinda desta vulnerabilidade é muito grande pois a sua circunvenção prescinde de correção na arquitetura física dos processadores e substituição do parque tecnológico atualmente existente, o que pode levar anos, ou até décadas para acontecer.

Explorando a execução especulativa.

Para explorar a execução especulativa, um atacante deve fazer com que o preditor de ramificação do processador seja enganado e por consequência presuma erroneamente a direção que o ramo irá tomar e então execute código malicioso durante esta presunção incorreta. Veja abaixo um exemplo de código explorável:

SE (x < tamanho_da_array)
  y = array2[array1[x] * 256];

Neste exemplo considere que o valor de x é determinado pelo atacante. O bloco condicional SE é compilado para uma instrução de ramificação cujo propósito é verificar se o acesso à matriz array1 é válido, garantindo que os índices estarão dentro de um valor legal.

Para realizar a violação, o atacante treina o processador com valores legais de x até que ele comece a presumir que a condicional x < tamanho_da_array vai ser sempre verdadeiro e passe a executar a declaração seguinte (y = array2[array1[x] * 256]) especulativamente porém utilizando valores de x maliciosos. A leitura da matriz array2 carrega valores no cache do processador que são dependentes do valor de array1[x], sendo que x é controlado pelo atacante.

A chave para este ataque é que mesmo depois que o processador descobre que especulou incorretamente e o resultado da declaração maliciosa é descartado, o cache não é descartado e a mudança neste cache pode ser detectada pelo atacante para obter um byte de memória da vítima. Repetindo o procedimento com valores diferentes de x permite a leitura de regiões inteiras da memória da vítima.

A realização do ataque envolve, portanto, uma série de premissas, que nem consigo descrever precisamente, mas são as seguintes:

Etapa de configuração:
É preciso encontrar um processo vítima que contenha um trecho de código que implemente uma leitura que possa ser explorada -- já que a leitura deve ser executada no espaço de memória do processo vítima;
É preciso escrever uma rotina que implemente uma ramificação que possa ser explorada;
É preciso treinar o preditor para que ele execute ramificações incorretas;
É preciso realizar operações que "esvaziem" o cache do processador dos valores do teste condicional (para que ele tenha de ser buscado na memória e enquanto isso resulte na execução especulativa)
É preciso preparar o canal lateral através do qual o byte da vítima será inferido

Etapa de execução
O código especulativo com o valor de x indevido é executado -- que pode ler um valor na memória de um processo da vítima (fora dos limites do processo escrito e compilado do atacante)

Etapa de obtenção do byte do processo da vítima
É preciso utilizar um método chamado flush+reload ou evict+reload medindo o tempo necessário para a leitura de um valor do cache que está sendo monitorado pelo canal lateral. Isto foi meio física quântica pra mim e não entendi exatamente como funciona.

Por hoje é só, mas já dá pra entender por alto o impacto que isto pode causar, já que nenhuma destas operações deixa rastros em logs, ou viola qualquer barreira existente de proteção de memória (seg faults, buffer overflow, etc).

Aceito comentários, fiz este post com intuito de aprender mais!

Comments

Popular posts from this blog

CoAP server mruby bindings for ESP32