Vulnerabilidade no kernel Linux eBPF descoberta e como corrigi-la
(bughunters.google.com)- O Google descobriu a CVE-2023-2163 no verificador do eBPF com o Buzzer e confirmou que essa vulnerabilidade pode ser explorada para elevação local de privilégios e escape de contêiner
- O eBPF expande recursos do kernel em tempo de execução, mas como executa bytecode arbitrário com altos privilégios, a verificação do verifier antes do carregamento é um pilar central da segurança
- O problema ocorre porque o path pruning do verifier conclui incorretamente que caminhos de execução diferentes são equivalentes a um caminho já considerado seguro
- O exploit aproveita a diferença em que o verifier vê um registrador como 0, mas em tempo de execução ele tem outro valor, contaminando o ponteiro da pilha eBPF e levando a leitura/escrita arbitrária e bypass de KASLR
- O patch passa a marcar como precise até mesmo registradores imprecise que afetam registradores precise, e a mesma estratégia de fuzzing de aritmética de ponteiros não encontrou novos problemas depois disso
O eBPF verifier amplia a superfície de ataque do kernel
- O eBPF é uma tecnologia que permite expandir recursos do kernel Linux em tempo de execução sem módulos de kernel complexos
- Programas eBPF são escritos em bytecode customizado e passam por verificação de segurança antes de serem executados quando um evento específico ocorre
- Um exemplo típico é um programa eBPF executado ao chamar um determinado syscall
- Como a estrutura permite executar código arbitrário em um nível alto de privilégio, a superfície de ataque do kernel aumenta bastante
- Antes de serem carregados, os programas precisam passar pelo verifier, que verifica se as premissas de segurança do eBPF são atendidas
- Quando uma vulnerabilidade no verifier é explorada, o resultado normalmente é elevação local de privilégios ou escape de contêiner em ambientes de contêiner
Buzzer e fuzzing de aritmética de ponteiros
- O Google criou o Buzzer para auditar automaticamente o código do eBPF verifier
- O Buzzer é um fuzzer que gera em massa programas eBPF sintaticamente válidos e pode definir estratégias para provocar bugs lógicos
- Na descoberta da CVE-2023-2163 foi usada a estratégia de aritmética de ponteiros
- Gera um cabeçalho que inicializa registradores com valores arbitrários
- Gera sequências arbitrárias de instruções aritméticas e de salto
- Escolhe um registrador arbitrário e executa uma operação de soma com um ponteiro para elemento de map eBPF
- Grava um valor mágico nesse elemento
- Se o valor gravado não for observado no espaço do usuário, pode ter ocorrido uma escrita fora dos limites
- A CVE-2023-2163, encontrada pelo Buzzer, estava na lógica de path pruning do eBPF e levou a um exploit utilizável tanto para escape de contêiner quanto para elevação local de privilégios
Path pruning e bug de rastreamento precise
- O eBPF verifier simula os possíveis caminhos de execução para confirmar se o programa pode rodar com segurança
- Quando o valor de um registrador não é certo em um desvio condicional, o verifier tenta seguir a execução de todos os estados possíveis
- Quanto mais saltos condicionais existem, mais o número de caminhos cresce exponencialmente, o que também prejudica o desempenho ao carregar o programa no kernel
- Para reduzir isso, os desenvolvedores do eBPF introduziram o path pruning
- Se o verifier puder garantir que um estado equivalente a um estado específico já alcança com segurança a instrução
exit, ele não explora mais aquele caminho
- Se o verifier puder garantir que um estado equivalente a um estado específico já alcança com segurança a instrução
- Para tornar o pruning mais eficiente, o conceito de precise tracking é usado em conjunto
- Um registrador é marcado como precise quando participa de operações de aritmética de ponteiros ou é passado como constante para uma função helper
- O verifier precisa explorar todos os estados relacionados a esse registrador
- Na CVE-2023-2163, o r9, que afetava a precisão do r6, não foi marcado corretamente
- O verifier assumiu que o r9 não contribuía para a preciseness de r6
- Depois de decidir que em um caminho anterior era possível alcançar o
exitcom segurança usando r6, tratou os estados restantes como equivalentes e fez pruning - Em tempo de execução, o caminho 1:2:4:6 foi executado e, no passo 6, o r6 que o verifier considerava 0 podia na verdade ter outro valor e ser usado em uma operação de aritmética de ponteiros
Leitura/escrita arbitrária e bypass de KASLR
- O código do exploit foi publicado no repositório de pesquisa de segurança do Google
- No desenvolvimento do exploit, os trabalhos de @chompie e @_manfp foram fundamentais
- O fluxo geral é obter primeiro leitura/escrita arbitrária e depois localizar as credentials do processo para alterar o uid e o ponteiro
fs_struct, elevando os privilégios - Na primeira etapa, o valor do registrador contaminado é transformado em 1
- Na análise manual, o valor de r6 em tempo de execução era
0x400, mas o verifier o via como 0 - A instrução
r6 >>= 10foi usada para obter o valor desejado 1
- Na análise manual, o valor de r6 em tempo de execução era
- Em seguida, a função helper
bpf_skb_load_bytes_relativeé usada para contaminar o ponteiro da pilha eBPF- O verifier entende que
lené 8, mas em tempo de execução, por causa do valor de r6,lense torna 9 - Como resultado, são gravados 9 bytes em vez de 8, contaminando o primeiro byte do valor no offset de pilha
-32
- O verifier entende que
- Ao manipular o ponteiro de pilha contaminado, é possível vazar o ponteiro do map eBPF
- No espaço do usuário, os valores de R2 e R3 são lidos e verifica-se se R2 se torna
0xBACA - Se essa condição for atendida, R3 se torna o valor vazado do ponteiro do map
- O vazamento do ponteiro do map eBPF leva a um estado de bypass de KASLR
- No espaço do usuário, os valores de R2 e R3 são lidos e verifica-se se R2 se torna
- Com a mesma estratégia, se em vez de um único byte toda uma região contínua da pilha for sobrescrita com o ponteiro desejado, torna-se possível fazer leitura/escrita arbitrária
- Do ponto de vista do verifier, trata-se de manipulação da pilha BPF, mas na prática é possível ler e escrever memória do kernel
Do vazamento do map até um shell root
- Depois do vazamento do ponteiro do map, o exploit não difere muito do exploit do Chompie e reutiliza parte do código
- O procedimento aproximado é o seguinte
- Procura repetidamente a string
init_pid_nsemkstrtab - Encontra o símbolo ksymtab que referencia essa string e obtém o endereço da estrutura init_pid_ns
- Percorre a radix tree para encontrar uma entrada cujo campo
commcorresponda ao nome do executável do exploit em execução- Se estiver rodando dentro de um contêiner, o PID não é uma heurística confiável, então não se usa apenas o PID
- Altera o uid para 0 e modifica o ponteiro
fs_struct - Se
fs_structfor alterado para o mesmo valor do ponteiro referenciado pelo PID 1, será possível observar o sistema de arquivos do host ao executar dentro de um contêiner - Executa
system("/bin/bash")para obter um shell root
- Procura repetidamente a string
- O código publicado no GitHub funciona como escape de contêiner apenas em versões específicas do Linux
- No Ubuntu e em algumas distribuições, ele funciona apenas como elevação local de privilégios porque os offsets da estrutura de dados sobrescrita são diferentes
- Para funcionar em qualquer distribuição Linux, o código precisa ser ajustado
Patch e validação posterior
- A análise da causa raiz e o patch da CVE-2023-2163 podem ser vistos na kernel mailing list
- A correção marca como precise os registradores imprecise de operações que afetam registradores precise
- Não está claro se essa correção afeta o desempenho do eBPF verifier
- A mesma estratégia de fuzzing de aritmética de ponteiros continuou sendo executada, mas não encontrou problemas adicionais
- O Buzzer segue em desenvolvimento, e a comunidade open source pode contribuir pelo repositório no GitHub
1 comentários
Opiniões no Hacker News
Nas plataformas em que o eBPF é mais comumente usado, em geral o impacto de bugs no verificador não é tão grande, porque código sem privilégios não pode carregar programas eBPF para começo de conversa.
No fim, esse tipo de bug é uma vulnerabilidade de root → ring0; isso não quer dizer que não seja importante, mas, em cargas de trabalho do lado do servidor, costuma ser um compromisso aceitável.
Em especial, o histórico do eBPF em elevação local de privilégios no kernel é bastante bom em comparação com o kernel como um todo, e o maior valor do verificador nos ambientes eBPF atuais é dificultar que um programa eBPF incorreto derrube o kernel acidentalmente.
Isso não se aplica, a ponto de ser quase risível, a módulos de kernel carregáveis comuns.
Nesse caso, na maioria das plataformas, não é necessário ter privilégios.
E, se houver namespaces de usuário sem privilégios, você pode se tornar “root” por conta própria, então root → ring0 também passa a ser um problema menos restrito.
Esse é um padrão que temos visto continuamente em PoCs de bugs de eBPF desde que as distribuições ativaram isso e depois, em sua maioria, voltaram a desativar.
À medida que ferramentas como o Cilium crescem, caminhos de ataque que entram em ambientes de contêiner com cap_bpf se tornam cada vez mais realistas.
“Uno no es ninguno” literalmente seria algo como “um não é nenhum”, ou seja, mais próximo de “One is not none”.
Em espanhol, a dupla negação muitas vezes não funciona, de fato, como dupla negação.
Por exemplo, “não há nada aqui” se diz “no hay nada aquí”, mas, traduzido palavra por palavra, parece algo como “não é que não haja nada aqui”.
A Royal Spanish Academy também explica assim:
https://www.rae.es/espanol-al-dia/doble-negacion-no-vino-nad...
A chamada “dupla negação” surge por causa da concordância negativa, que em espanhol e em outras línguas românicas deve ocorrer obrigatoriamente em certas situações; como resultado, o advérbio no e outros elementos de sentido negativo aparecem juntos na mesma frase.
O fato de essas duas “negações” aparecerem ao mesmo tempo não anula o sentido negativo da frase.
Quando tentei usar eBPF algum tempo atrás, ele não era expressivo o suficiente para a tarefa de que eu precisava.
Fico me perguntando se realmente se justifica aumentar a complexidade no espaço do kernel para ganhar uma flexibilidade limitada.
Para filtragem de pacotes eu entendo, mas usá-lo para outros fins, como sandboxing, me parece menos convincente.
A escolha do kernel não é entre eBPF ou nada, mas entre eBPF ou outra coisa parecida.
Você pode não usar muito diretamente, mas há pessoas que passam o dia inteiro usando.
Acho que engenheiros da FAANG já disseram que rodam dezenas, talvez centenas, desses programas o tempo todo em todos os servidores, sem contar usos pontuais.
A FAANG também emprega desenvolvedores de kernel dedicados, então de certa forma financia a complexidade que ela mesma usa.
Eu também já resolvi problemas com eBPF.
Eram problemas que, sem eBPF, seriam praticamente insolúveis para alguém que não fosse especialista em kernel; não são necessários com frequência, mas, quando são, não há substituto.
Em alguns casos, até para um especialista em kernel a escolha é entre usar eBPF ou manter patches customizados no kernel para sempre.
Sei que não é seguro, mas quem lida com isso sabe que grandes poderes trazem grandes responsabilidades.
Parece correto traduzir “Uno no es ninguno” como “One is not none”.
https://bughunters.google.com/blog/6303226026131456/a-deep-d...
Mas ele foi traduzido como “One is none”.
É a famosa dupla negação que dá trabalho para falantes estrangeiros, eu incluído.
https://spanish.stackexchange.com/questions/26777/how-does-d...
Literalmente é isso, mas, em espanhol, por estranho que pareça, a dupla negação normalmente é usada apenas como negação.
Talvez algo como “one ain't nothin'” transmita melhor a ideia.
No nosso país existe a expressão “ouriço dentro da calça”.
Por mais útil que seja, isso não parece ter sido escrito de forma segura e cuidadosa.