O que é um retpoline e como ele funciona?

244

Para mitigar a divulgação do kernel ou da memória entre processos (o ataque Spectre ), o kernel 1 do Linux será compilado com uma nova opção , -mindirect-branch=thunk-externapresentada gccpara executar chamadas indiretas por meio do chamado retpoline .

Esse parece ser um termo recém-inventado, pois uma pesquisa no Google mostra apenas um uso muito recente (geralmente tudo em 2018).

O que é um retpoline e como ele evita os recentes ataques de divulgação de informações do kernel?


1 No entanto, não é específico do Linux - construções semelhantes ou idênticas parecem ser usadas como parte das estratégias de mitigação em outros sistemas operacionais.

BeeOnRope
fonte
6
Um artigo de suporte interessante do Google.
sgbj
2
oh, então é pronunciado / ˌtræmpəˈlin / (americano) ou / ˈtræmpəˌliːn / (britânico) #
Walter Tross
2
Você pode mencionar que este é o kernel do Linux , embora gccaponte dessa maneira! Eu não reconheci lkml.org/lkml/2018/1/3/780 como no site Linux Kernel Mailing List, nem mesmo uma vez que olhei lá (e recebi uma captura instantânea, pois estava offline).
PJTraill
@PJTraill - adicionou uma tag do kernel do Linux
RichVel 10/01
@PJTraill - bom ponto, atualizei o texto da pergunta. Observe que eu o vi pela primeira vez no kernel do Linux por causa de seu processo de desenvolvimento relativamente aberto, mas sem dúvida as técnicas iguais ou similares estão sendo usadas como atenuações em todo o espectro de sistemas operacionais de código aberto e fechado. Portanto, não vejo isso como específico do Linux, mas o link certamente é.
BeeOnRope 12/01

Respostas:

158

O artigo mencionado por sgbj nos comentários escritos por Paul Turner, do Google, explica o seguinte com muito mais detalhes, mas vou tentar:

Tanto quanto eu posso juntar isso a partir das informações limitadas no momento, um retpoline é um trampolim de retorno que usa um loop infinito que nunca é executado para impedir que a CPU especule sobre o alvo de um salto indireto.

A abordagem básica pode ser vista na ramificação do kernel de Andi Kleen, que trata desse problema:

Ele introduz a nova __x86.indirect_thunkchamada que carrega o destino da chamada cujo endereço de memória (que eu chamarei ADDR) está armazenado no topo da pilha e executa o salto usando uma RETinstrução. O thunk em si é chamado usando a macro NOSPEC_JMP / CALL , que foi usada para substituir muitas (se não todas) chamadas e saltos indiretos. A macro simplesmente coloca o destino da chamada na pilha e define o endereço de retorno corretamente, se necessário (observe o fluxo de controle não linear):

.macro NOSPEC_CALL target
    jmp     1221f            /* jumps to the end of the macro */
1222:
    push    \target          /* pushes ADDR to the stack */
    jmp __x86.indirect_thunk /* executes the indirect jump */
1221:
    call    1222b            /* pushes the return address to the stack */
.endm

A colocação de callno final é necessária para que, quando a chamada indireta for concluída, o fluxo de controle continue por trás do uso da NOSPEC_CALLmacro, para que possa ser usado no lugar de uma consulta regular.call

O thunk em si tem a seguinte aparência:

    call retpoline_call_target
2:
    lfence /* stop speculation */
    jmp 2b
retpoline_call_target:
    lea 8(%rsp), %rsp 
    ret

O fluxo de controle pode ficar um pouco confuso aqui, então deixe-me esclarecer:

  • call empurra o ponteiro de instrução atual (etiqueta 2) para a pilha.
  • leaadiciona 8 ao ponteiro da pilha , descartando efetivamente a palavra-chave empurrada mais recentemente, que é o último endereço de retorno (no rótulo 2). Depois disso, a parte superior da pilha aponta para o endereço de retorno real ADDR novamente.
  • retpula *ADDRe redefine o ponteiro da pilha para o início da pilha de chamadas.

No final, todo esse comportamento é praticamente equivalente a pular diretamente para *ADDR. O único benefício que obtemos é que o preditor de ramificação usado para instruções de retorno (Buffer de Pilha de Retorno, RSB), ao executar a callinstrução, assume que a retinstrução correspondente passará para o rótulo 2.

A parte depois que o rótulo 2 nunca é executado, é simplesmente um loop infinito que, em teoria, preencheria o pipeline de JMPinstruções com instruções. Ao usar LFENCE, PAUSEou mais geralmente, uma instrução que faz com que o pipeline de instruções fique parado impede a CPU de desperdiçar energia e tempo nessa execução especulativa. Isso ocorre porque, caso a chamada para retpoline_call_target retorne normalmente, essa LFENCEseria a próxima instrução a ser executada. Isso também é o que o preditor de ramificação irá prever com base no endereço de retorno original (o rótulo 2)

Para citar o manual de arquitetura da Intel:

As instruções após um LFENCE podem ser buscadas na memória antes do LFENCE, mas elas não serão executadas até que o LFENCE seja concluído.

Observe, no entanto, que a especificação nunca menciona que LFENCE e PAUSE causam a interrupção do pipeline, por isso estou lendo um pouco entre as linhas aqui.

Agora, de volta à sua pergunta original: A divulgação de informações da memória do kernel é possível devido à combinação de duas idéias:

  • Embora a execução especulativa deva ser livre de efeitos colaterais quando a especulação estiver errada, a execução especulativa ainda afeta a hierarquia do cache . Isso significa que, quando uma carga de memória é executada especulativamente, ela ainda pode ter causado a remoção de uma linha de cache. Essa alteração na hierarquia de cache pode ser identificada medindo cuidadosamente o tempo de acesso à memória que é mapeado no mesmo conjunto de cache.
    Você pode até vazar alguns bits de memória arbitrária quando o endereço de origem da memória lida foi ele próprio lido na memória do kernel.

  • O preditor de ramificação indireta das CPUs Intel usa apenas os 12 bits mais baixos da instrução fonte, portanto, é fácil envenenar todos os 2 ^ 12 históricos de previsão possíveis com endereços de memória controlados pelo usuário. Estes podem então, quando o salto indireto é previsto dentro do kernel, ser executado especulativamente com privilégios de kernel. Usando o canal lateral de tempo de cache, você pode vazar memória arbitrária do kernel.

ATUALIZAÇÃO: Na lista de discussão do kernel , há uma discussão em andamento que me leva a crer que os retpolines não atenuam completamente os problemas de previsão de ramificação, como quando o RSB (Return Stack Buffer) fica vazio, as arquiteturas Intel mais recentes (Skylake +) retornam ao vulnerável Target Target Buffer (BTB):

Retpoline como estratégia de mitigação troca ramificações indiretas por retornos, para evitar o uso de previsões provenientes do BTB, pois elas podem ser envenenadas por um invasor. O problema com o Skylake + é que um fluxo insuficiente de RSB volta a usar uma previsão de BTB, o que permite ao invasor assumir o controle da especulação.

Tobias Ribizel
fonte
Não acho que a instrução LFENCE seja importante, a implementação do Google usa uma instrução PAUSE. support.google.com/faqs/answer/7625886 Observe que a documentação que você citou diz "não executará" não executará "não será executada especulativamente".
Ross cume
1
Na página de perguntas frequentes do Google: "As instruções de pausa em nossos loops especulativos acima não são necessárias para correção. Mas isso significa que a execução especulativa não produtiva ocupa menos unidades funcionais no processador". Portanto, não apoia sua conclusão de que LFENCE é a chave aqui.
Ross cume
@RossRidge Concordo parcialmente, para mim isso parece duas implementações possíveis de um loop infinito que sugerem que a CPU não execute especulativamente o código após a PAUSE / LFENCE. No entanto, se o LFENCE foi executado especulativamente e não foi revertido porque a especulação estava correta, isso contradizia a alegação de que ele só será executado quando as cargas de memória tiverem terminado. (Caso contrário, todo o conjunto de instruções que foram executadas especulativamente teria que ser revertida e executado novamente para cumprir as especificações)
Tobias Ribizel
1
Isso tem a vantagem de push/ retque não desequilibra a pilha preditora de endereço de retorno. Há uma imprevisibilidade (ir para lfenceantes de o endereço de retorno real ser usado), mas usar uma callmodificação + rspequilibrou isso ret.
Peter Cordes
1
oops, vantagem sobre push / ret(no meu último comentário). re: sua edição: o fluxo insuficiente de RSB deve ser impossível porque o retpoline inclui a call. Se a preempção do kernel fizesse uma mudança de contexto lá, retomaríamos a execução com o RSB preparado do callpara o planejador. Mas talvez um manipulador de interrupção possa terminar com rets suficientes para esvaziar o RSB.
Peter Cordes
46

Um retpoline foi projetado para proteger contra a exploração de injeção de destino do ramo ( CVE-2017-5715 ). Este é um ataque no qual uma instrução de ramificação indireta no kernel é usada para forçar a execução especulativa de um pedaço arbitrário de código. O código escolhido é um "gadget" que é de alguma forma útil para o invasor. Por exemplo, o código pode ser escolhido para vazar os dados do kernel através de como isso afeta o cache. O retpoline impede essa exploração simplesmente substituindo todas as instruções de ramificação indireta por uma instrução de retorno.

Eu acho que o principal do retpoline é apenas a parte "ret", que substitui o ramo indireto por uma instrução de retorno, para que a CPU use o preditor de pilha de retorno em vez do preditor de ramo explorável. Se uma simples instrução push e return fosse usada, o código que seria executado especulativamente seria o código para o qual a função retornará de qualquer maneira, não algum dispositivo útil para o invasor. O principal benefício da parte do trampolim parece ser a manutenção da pilha de retorno. Assim, quando a função realmente retorna ao chamador, isso é previsto corretamente.

A idéia básica por trás da injeção de alvo de ramificação é simples. Aproveita o fato de a CPU não registrar o endereço completo da origem e destino das ramificações em seus buffers de destino da ramificação. Portanto, o invasor pode preencher o buffer usando saltos em seu próprio espaço de endereço, o que resultará em acertos de previsão quando um salto indireto específico for executado no espaço de endereço do kernel.

Observe que o retpoline não impede a divulgação direta de informações do kernel, apenas impede que instruções de ramificação indireta sejam usadas para executar especulativamente um gadget que divulgaria informações. Se o invasor puder encontrar outros meios para executar especulativamente o gadget, o retpoline não impedirá o ataque.

O artigo Specter Attacks: Explorating Speculative Execution de Paul Kocher, Daniel Genkin, Daniel Gruss, Werner Haas, Mike Hamburg, Moritz Lipp, Stefan Mangard, Thomas Prescher, Michael Schwarz e Yuval Yarom dão a seguinte visão geral de como as ramificações indiretas podem ser exploradas :

Exploração de ramos indiretos. Com base na programação orientada a retorno (ROP), nesse método o invasor escolhe um gadgetdo espaço de endereço da vítima e influencia a vítima a executar o gadget especulativamente. Ao contrário do ROP, o invasor não depende de uma vulnerabilidade no código da vítima. Em vez disso, o atacante treina o Buffer de destino da ramificação (BTB) para predizer incorretamente uma ramificação de uma instrução de ramificação indireta para o endereço do gadget, resultando em uma execução especulativa do gadget. Enquanto as instruções especulativamente executadas são abandonadas, seus efeitos no cache não são revertidos. Esses efeitos podem ser usados ​​pelo gadget para vazar informações confidenciais. Mostramos como, com uma seleção cuidadosa de um gadget, esse método pode ser usado para ler a memória arbitrária da vítima.

Para controlar mal o BTB, o invasor encontra o endereço virtual do dispositivo no espaço de endereço da vítima e executa ramificações indiretas nesse endereço. Esse treinamento é realizado no espaço de endereço do atacante e não importa o que reside no endereço do gadget no espaço de endereço do atacante; tudo o que é necessário é que a ramificação usada para ramificações de treinamento use o mesmo endereço virtual de destino. (De fato, desde que o invasor lide com exceções, o ataque poderá funcionar mesmo se não houver código mapeado no endereço virtual do gadget no espaço de endereço do atacante.) Também não há necessidade de uma correspondência completa do endereço de origem. da filial usada para treinamento e o endereço da filial segmentada. Assim, o atacante tem flexibilidade significativa na configuração do treinamento.

Uma entrada de blog intitulada Lendo memória privilegiada com um canal lateral da equipe do Project Zero no Google fornece outro exemplo de como a injeção de destino de ramificação pode ser usada para criar uma exploração em funcionamento.

Ross Ridge
fonte
9

Esta pergunta foi feita há um tempo e merece uma resposta mais recente.

Sumário Executivo :

As sequências de "retpolina" são uma construção de software que permite que ramos indiretos sejam isolados da execução especulativa. Isso pode ser aplicado para proteger os binários confidenciais (como implementações de sistema operacional ou hipervisor) contra ataques de injeção do destino da ramificação contra suas ramificações indiretas.

A palavra " ret poline " é um portmanteau das palavras "return" e "trampoline", bem como a melhoria " rel poline " foi cunhada a partir de "call relativo" e "trampolim". É uma construção de trampolim construída usando operações de retorno que também garantem figurativamente que qualquer execução especulativa associada “salte” infinitamente.

Para mitigar a divulgação do kernel ou da memória entre processos (o ataque Spectre), o kernel Linux [1] será compilado com uma nova opção, -mindirect-branch=thunk-externintroduzida no gcc para executar chamadas indiretas por meio do chamado retpoline.

[1] No entanto, não é específico do Linux - construções semelhantes ou idênticas parecem ser usadas como parte das estratégias de mitigação em outros sistemas operacionais.

O uso desta opção de compilador protege apenas contra o Spectre V2 nos processadores afetados que possuem a atualização de microcódigo necessária para CVE-2017-5715. Ele ' funcionará ' em qualquer código (não apenas em um kernel), mas apenas o código contendo "segredos" vale a pena atacar.

Esse parece ser um termo recém-inventado, pois uma pesquisa no Google mostra apenas um uso muito recente (geralmente tudo em 2018).

O compilador LLVM tem um -mretpolinecomutador desde antes de 4 de janeiro de 2018 . Essa data é quando a vulnerabilidade foi relatada publicamente pela primeira vez . O GCC disponibilizou seus patches em 7 de janeiro de 2018.

A data do CVE sugere que a vulnerabilidade foi ' descoberta ' em 2017, mas afeta alguns dos processadores fabricados nas últimas duas décadas (portanto, provavelmente foi descoberta há muito tempo).

O que é um retpoline e como ele evita os recentes ataques de divulgação de informações do kernel?

Primeiro, algumas definições:

  • Trampolim - Às vezes chamados de vetores de salto indiretos, os trampolins são locais de memória contendo endereços que apontam para interromper rotinas de serviço, rotinas de E / S, etc. Tradicionalmente, o GCC oferece suporte a funções aninhadas criando um trampolim executável em tempo de execução quando o endereço de uma função aninhada é obtido. Esse é um pequeno pedaço de código que normalmente reside na pilha, no quadro da pilha da função que o contém. O trampolim carrega o registro de cadeia estática e pula para o endereço real da função aninhada.

  • Thunk - Uma thunk é uma sub-rotina usada para injetar um cálculo adicional em outra sub-rotina. Os thunks são usados ​​principalmente para atrasar um cálculo até que seu resultado seja necessário ou para inserir operações no início ou no final da outra sub-rotina

  • Memoização - Uma função memorizada "lembra" os resultados correspondentes a algum conjunto de entradas específicas. As chamadas subseqüentes com entradas lembradas retornam o resultado lembrado em vez de recalculá-lo, eliminando, assim, o custo primário de uma chamada com determinados parâmetros de todas, exceto a primeira chamada feita à função com esses parâmetros.

Muito grosso modo, um retpolim é um trampolim com um retorno como um thunk , para " estragar " a memorização no preditor indireto de ramificação.

Fonte : O retpoline inclui uma instrução PAUSE para Intel, mas uma instrução LFENCE é necessária para a AMD, pois nesse processador a instrução PAUSE não é uma instrução de serialização; portanto, o loop de pausa / jmp usará excesso de energia, pois é especulado durante a espera de retorno imprevisível para o alvo correto.

A Arstechnica tem uma explicação simples do problema:

"Cada processador possui um comportamento arquitetural (o comportamento documentado que descreve como as instruções funcionam e dos quais os programadores dependem para escrever seus programas) e um comportamento microarquitetural (o modo como uma implementação real da arquitetura se comporta). Eles podem divergir de maneiras sutis. Por exemplo, arquiteturalmente, um programa que carrega um valor de um endereço específico na memória aguardará até que o endereço seja conhecido antes de tentar executar a carga.Microarquiteturalmente, no entanto, o processador pode tentar adivinhar especulativamente o endereço para que ele possa iniciar carregando o valor da memória (que é lenta) mesmo antes de ter certeza absoluta de qual endereço ele deve usar.

Se o processador considerar errado, ele ignorará o valor calculado e executará a carga novamente, desta vez com o endereço correto. O comportamento definido arquitetonicamente é preservado. Mas esse palpite incorreto perturbará outras partes do processador - em particular o conteúdo do cache. Esses distúrbios microarquiteturais podem ser detectados e medidos pelo tempo que leva para acessar dados que deveriam (ou não) estar no cache, permitindo que um programa malicioso faça inferências sobre os valores armazenados na memória ".

Do artigo da Intel: " Retpoline: uma mitigação de injeção de alvo de ramificação " ( .PDF ):

"Uma sequência retpoline impede a execução especulativa do processador de usar o" preditor indireto de ramificação "(uma maneira de prever o fluxo do programa) para especular para um endereço controlado por uma exploração (elemento satisfatório 4 dos cinco elementos da injeção do alvo de ramificação (variante 2 do espectro) ) explore a composição listada acima). "

Observe, o elemento 4 é: "A exploração deve influenciar com êxito essa ramificação indireta para prever de maneira especulativa e executar um gadget. Esse gadget, escolhido pela exploração, vaza os dados secretos por um canal lateral, geralmente por tempo de cache".

Roubar
fonte