V8: um ano com Spectre

Em 3 de janeiro de 2018, o Google Project Zero e outros revelaram os três primeiros de uma nova classe de vulnerabilidades que afetam processadores de execução especulativos. Eles foram chamados Spectre (1 e 2) e Meltdown . Usando mecanismos especulativos de execução da CPU, um invasor pode ignorar temporariamente as verificações de segurança explícitas e implícitas que impedem os programas de ler dados inacessíveis na memória. Embora a execução especulativa tenha sido projetada como parte da microarquitetura, invisível no nível da arquitetura, programas cuidadosamente projetados podem ler informações inacessíveis em um bloco especulativo e revelá-las através de canais laterais, como o tempo de execução de um fragmento de programa.

Quando foi demonstrado que os ataques do Spectre são possíveis usando JavaScript, a equipe do V8 participou da solução do problema. Formamos uma equipe de resposta a emergências e trabalhamos em estreita colaboração com outras equipes do Google, nossos outros parceiros de navegador e parceiros de hardware. Juntamente com eles, realizamos proativamente pesquisas ofensivas (construindo módulos de ataque para provar o conceito) e defensivas (atenuando possíveis ataques).

O ataque Spectre consiste em duas partes:

  • Vazamento de dados inacessíveis para o estado latente da CPU . Todos os ataques Spectre conhecidos usam especulação para transferir bits de dados inacessíveis para caches de CPU.
  • Recuperando um estado oculto para restaurar dados inacessíveis. Para isso, um invasor precisa de um relógio com precisão suficiente. (Surpreendentemente baixa precisão, especialmente com métodos como limiar de borda - comparando com um limiar ao longo de um contorno selecionado).

Teoricamente, seria suficiente bloquear qualquer um dos dois componentes do ataque. Como não sabemos como bloquear completamente nenhum deles, desenvolvemos e implantamos atenuações que reduzem significativamente a quantidade de informações vazando nos caches da CPU e atenuações que dificultam a restauração do estado oculto.

Temporizadores de alta precisão


Pequenas mudanças de estado que permanecem após a execução especulativa produzem diferenças temporais correspondentemente pequenas, quase impossivelmente pequenas - da ordem de um bilionésimo de segundo. Para detectar diretamente essas diferenças individuais, o invasor precisa de um cronômetro de alta precisão. Os processadores oferecem esses timers, mas a plataforma da web não os define. O cronômetro mais preciso da plataforma web performance.now() tinha uma resolução de vários microssegundos, inicialmente considerada inadequada para esse fim. No entanto, há dois anos, um grupo de pesquisa especializado em ataques de microarquitetura publicou um artigo sobre temporizadores em uma plataforma web. Eles concluíram que a memória compartilhada mutável simultânea e vários métodos de recuperação de resolução permitem a criação de temporizadores de resolução ainda mais alta, até o nanossegundo. Esses cronômetros são precisos o suficiente para detectar ocorrências e falhas individuais do cache L1. É ele quem geralmente é usado para capturar informações nos ataques de espectros.

Proteção do temporizador


Para interromper a capacidade de detectar pequenas diferenças no tempo, os desenvolvedores de navegadores escolheram uma abordagem multilateral. Em todos os navegadores, a resolução de performance.now() foi reduzida (no Chrome de 5 microssegundos para 100) e o jitter aleatório foi introduzido para impedir a restauração da resolução. Após consultas entre os desenvolvedores de todos os navegadores, decidimos juntos dar um passo sem precedentes: desabilitar imediata e retroativamente a API SharedArrayBuffer em todos os navegadores para impedir a criação de um cronômetro de nanossegundos.

Ganho


No início de nossa pesquisa ofensiva, ficou claro que apenas as atenuações temporizadas não são suficientes. Um dos motivos é que um invasor pode simplesmente executar seu código repetidamente para que a diferença de tempo acumulada seja muito mais do que uma ocorrência de acerto ou falha de cache. Conseguimos criar "gadgets" confiáveis ​​que usam muitas linhas de cache por vez, até toda a capacidade do cache, o que fornece uma diferença de tempo de até 600 microssegundos. Mais tarde, descobrimos métodos de amplificação arbitrários que não são limitados pela capacidade do cache. Tais métodos de amplificação são baseados em tentativas repetidas de ler dados secretos.

Proteção JIT


Para ler dados inacessíveis usando o Spectre, um invasor força a CPU a executar especulativamente o código que lê dados normalmente inacessíveis e os coloca no cache. A proteção pode ser considerada de dois lados:

  1. Impedir a execução de código especulativo.
  2. Prevenção de ler dados inacessíveis do pipeline especulativo.

Experimentamos a primeira opção inserindo instruções recomendadas para evitar especulações, como Intel LFENCE de cada ramo condicional crítico e usando retpolinas para ramos indiretos. Infelizmente, essas mitigações pesadas reduzem significativamente a produtividade (desaceleração de 2-3x no benchmark da Octane). Em vez disso, adotamos a segunda abordagem inserindo sequências de mitigação que impedem a leitura de dados confidenciais devido a especulações impróprias. Deixe-me ilustrar a técnica com o seguinte trecho de código:

 if (condition) { return a[i]; } 

Por simplicidade, assumimos que a condição 0 ou 1 . O código acima é vulnerável se a CPU ler especulativamente de a[i] quando i estiver fora da faixa, obtendo acesso a dados normalmente inacessíveis. Uma observação importante é que, neste caso, a especulação tenta ler a[i] quando a condição é 0 . Nossa mitigação reescreve esse programa para que ele se comporte exatamente da mesma forma que o programa original, mas não permite que nenhum dado carregado especulativamente vaze.

Reservamos um registro de CPU, que chamamos de "veneno", para rastrear se o código está sendo executado em um ramo mal interpretado. O registro de envenenamento é suportado em todas as ramificações e chamadas do código gerado, portanto, qualquer ramificação interpretada incorretamente faz com que o registro de veneno se torne 0 . Em seguida, medimos todos os acessos à memória para que eles mascarem incondicionalmente o resultado de todos os downloads com o valor atual do registro de envenenamento. Isso não impede que o processador preveja (ou interprete mal) as ramificações, mas destrói as informações (potencialmente fora dos limites) dos valores carregados devido a ramificações interpretadas incorretamente. O código da ferramenta é mostrado abaixo ( a é uma matriz de números).

 let poison = 1; // … if (condition) { poison *= condition; return a[i] * poison; } 

Código adicional não afeta o comportamento normal (definido pela arquitetura) do programa. Afeta apenas o estado micro-arquitetural ao trabalhar em uma CPU com execução especulativa. Se você instrumentar um programa no nível do código-fonte, as otimizações avançadas nos compiladores modernos poderão remover essa instrumentação. Na V8, impedimos que o compilador remova atenuações inserindo-as em um estágio muito tardio da compilação.

Também usamos essa técnica de envenenamento para evitar vazamentos de ramificações indiretas no loop de bytecode do intérprete e na sequência de chamadas de função JavaScript. No intérprete, definimos o veneno como 0 se o manipulador de bytecodes (ou seja, uma sequência de código de máquina que interpreta um bytecode) não corresponder ao bytecode atual. Para chamadas JavaScript, passamos a função de destino como um parâmetro (no registro) e configuramos o veneno como 0 no início de cada função, se a função de destino de entrada não corresponder à função atual. Com esse amolecimento, vemos uma desaceleração inferior a 20% no benchmark da Octane.

A atenuação do WebAssembly é mais simples, pois a principal verificação de segurança é garantir que o acesso à memória esteja dentro dos limites. Para plataformas de 32 bits, além das verificações de limites usuais, enchemos toda a memória até a próxima potência de dois e mascaramos incondicionalmente quaisquer bits superiores do índice de memória do usuário. As plataformas de 64 bits não precisam dessa mitigação, pois a implementação usa proteção de memória virtual para verificações de borda. Experimentamos compilar instruções de opção / caso em código de pesquisa binário em vez de usar uma ramificação indireta potencialmente vulnerável, mas é muito caro para algumas cargas de trabalho. As chamadas indiretas são protegidas por retpolins.

Proteção de software - Não confiável


Felizmente ou infelizmente, nossa pesquisa ofensiva progrediu muito mais rápido que na defensiva, e rapidamente achamos impossível mitigar programaticamente todos os possíveis vazamentos durante os ataques de Espectros. Existem várias razões para isso. Primeiro, os esforços de engenharia para combater o Spectre são desproporcionais ao nível de ameaça. Na V8, enfrentamos muitos outros riscos de segurança muito piores, desde ler diretamente fora das fronteiras devido a erros comuns (que são mais rápidos e fáceis que o Spectre), escrever fora das fronteiras (isso é impossível com Specter e pior) e o potencial remoto execução de código (impossível com Spectre e muito, muito pior). Em segundo lugar, as medidas de mitigação cada vez mais sofisticadas que desenvolvemos e implementamos carregavam complexidade significativa, que é uma obrigação técnica e pode realmente aumentar a superfície de ataque e a sobrecarga de desempenho. Em terceiro lugar, testar e manter a atenuação de vazamentos de microarquitetura é ainda mais difícil do que projetar os próprios dispositivos para um ataque, já que é difícil ter certeza de que as atenuações continuam a funcionar da maneira como foram projetadas. Pelo menos uma vez, mitigações importantes foram efetivamente desfeitas pelas otimizações posteriores do compilador. Quarto, descobrimos que a mitigação eficaz de algumas opções do Spectre, especialmente a opção 4, simplesmente não é possível no software, mesmo após os esforços heróicos de nossos parceiros da Apple para lidar com o problema em seu compilador JIT.

Isolamento do site


Nossa pesquisa levou à conclusão: em princípio, o código não confiável pode ler todo o espaço de endereço de um processo usando o Spectre e os canais laterais. As mitigações de software reduzem a eficácia de muitos dispositivos em potencial, mas não são eficazes ou abrangentes. A única medida eficaz é mover dados confidenciais para fora do espaço de endereço do processo. Felizmente, o Chrome tenta há muitos anos separar sites em diferentes processos para reduzir a superfície de ataque devido a vulnerabilidades comuns. Esses investimentos valeram a pena e, em maio de 2018, chegamos ao estágio de prontidão e expandimos o isolamento de sites no número máximo de plataformas. Portanto, o modelo de segurança do Chrome não assume mais a privacidade do idioma durante o processo de renderização.

Specter percorreu um longo caminho e enfatizou os méritos da colaboração de desenvolvedores na indústria e na academia. Até agora, os chapéus brancos estão à frente dos pretos. Ainda não sabemos sobre um único ataque real, com exceção de experimentadores curiosos e pesquisadores profissionais desenvolvendo gadgets para provar o conceito. Novas variantes dessas vulnerabilidades continuam aparecendo e isso continuará por algum tempo. Continuamos monitorando essas ameaças e as levando a sério.

Como muitos programadores, também pensamos que linguagens seguras fornecem a borda certa para abstração, impedindo que programas bem digitados leiam memória arbitrária. É triste que isso tenha sido um erro - essa garantia não corresponde ao equipamento atual. Obviamente, ainda acreditamos que linguagens seguras têm mais vantagens de engenharia e o futuro está com elas, mas ... nos equipamentos de hoje eles vazam um pouco.

Os leitores interessados ​​podem se aprofundar no tópico e obter informações mais detalhadas em nosso artigo científico .

Source: https://habr.com/ru/post/pt449546/


All Articles