1 pontos por GN⁺ 2024-03-27 | 1 comentários | Compartilhar no WhatsApp
  • CVE-2024-1086 no nf_tables do kernel Linux cria um double free de sk_buff devido a uma falha de validação de entrada no verdict do Netfilter e, nas condições certas, pode levar a escalonamento local de privilégios
  • O fluxo do ataque faz com que um skb liberado durante o processamento de NF_DROP continue sendo tratado como se fosse NF_ACCEPT, de modo que o mesmo objeto acaba sendo liberado novamente em um caminho posterior
  • O PoC foi validado com mitigação do KernelCTF, Debian, Ubuntu e kernels vanilla, e pelo menos o intervalo v5.14.21~v6.6.14 pode ser afetado dependendo do kconfig; a correção foi distribuída nos branches stable em fevereiro de 2024
  • O ponto central é o Dirty Pagedirectory, que duplica a alocação de páginas PTE e PMD na mesma página física para acessar endereços físicos arbitrários apenas com leitura/escrita no espaço de usuário, em uma abordagem KSMA somente de dados
  • A técnica ainda combina varredura de KASLR físico, desvio de modprobe_path, execução sem arquivos e encadeamento de descritores de arquivo para escape de namespaces, formando um caso prático de LPE que mira ao mesmo tempo o gerenciamento de memória do kernel e o subsistema de rede

Condições da vulnerabilidade e escopo de impacto

  • O bug em nf_tables foi registrado como CVE-2024-1086 e tem como núcleo uma falha de validação de entrada que permitia erro positivo de drop no processamento de verdicts do Netfilter no kernel Linux
  • O exploit exige as seguintes condições
    • nf_tables precisa estar ativado
    • namespace de usuário não privilegiado precisa estar ativado
    • Em distribuições principais como Debian e Ubuntu, onde essa configuração costuma vir habilitada por padrão, isso faz parte do pressuposto do ataque
  • Com base nos testes, os branches stable linux-5.15.y, linux-6.1.y, linux-6.6.y estão no escopo afetado, e linux-6.7.1 também pode estar
  • A correção do bug foi distribuída nos branches stable em fevereiro de 2024
  • O código-fonte do PoC está publicado em CVE-2024-1086 PoC repository

Fluxo de código que gera o double free

  • Um verdict do Netfilter é o valor que decide se um pacote será descartado, aceito, enviado para fila etc.
  • O fluxo vulnerável começa porque nft_verdict_init() não restringia de forma suficiente o valor de verdict fornecido pelo usuário, permitindo configurar um valor que parecia NF_DROP, mas cujo drop error era positivo
  • nf_hook_slow() olha para os bits inferiores do verdict e, ao interpretá-lo como NF_DROP, libera primeiro o skb com kfree_skb_reason()
  • Depois, se o resultado de NF_DROP_GETERR() retornar um valor correspondente a NF_ACCEPT, o lado chamador continua o processamento como se o pacote tivesse sido aceito
  • Como resultado, o skb já liberado é liberado novamente em um caminho posterior, criando uma primitive de double free

Objetos corrompidos e forma de processamento de pacotes

  • O double free afeta struct sk_buff de skbuff_head_cache e o objeto sk_buff->head
  • sk_buff->head contém o conteúdo real do pacote e, dependendo do tamanho do pacote IPv4, pode ser alocado desde kmalloc-256 até páginas order 4 do buddy allocator
  • O PoC foi projetado para usar pacotes IP grandes e assim seguir pelo caminho do buddy allocator, e não do slab allocator
  • A fila de fragmentação do IPv4 é usada para atrasar a segunda liberação do skb ou induzi-la no momento desejado
  • Como o uso de campos corrompidos do skb no caminho do pacote pode causar kernel panic, o exploit evita as stacks TCP/UDP e usa um caminho específico de erro de fragmentos IP

Escopo dos testes e taxa de sucesso

  • Foram testados vários kernels e configurações em ambientes vanilla, KernelCTF, Debian e Ubuntu
  • Os casos de sucesso incluem os seguintes ambientes
    • Linux v5.14.21, v5.15.148, v5.16.20, v5.17.15, v5.18.19, v5.19.17, v6.0.19
    • Linux v6.1.55 com KernelCTF Mitigation v3
    • Linux v6.1.69 no Debian Bookworm 6.1.0-17
    • Linux v6.1.72 no KernelCTF LTS
    • Ubuntu Jammy v6.2.0-37
    • Linux v6.2.16, v6.3.13
  • Os casos de falha incluem v5.4.270, v5.10.209, v6.4.16, Ubuntu Jammy v6.5.0-15, v6.5.13, v6.6.14 e v6.7.1
  • Parte das falhas após v6.4.0 está ligada à detecção de bad_page() causada por CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y
  • No ambiente v6.4.16, a taxa de sucesso foi de 99,4% e, em alguns casos, caiu até 93,0%, com tamanho de amostra n=1000 em ambos

Forma de correção

  • A correção inicialmente proposta poderia introduzir uma breaking change no meio da stack do Netfilter
  • A correção do maintainer do Netfilter restringe com mais rigor os verdicts vindos da entrada do usuário já na camada de API
  • O patch rejeita parâmetros de verdict DROP/QUEUE vindos do userland
  • Segundo a descrição do CVE, nft_verdict_init() permitia valores positivos no drop error dentro do hook verdict, e nf_hook_slow() podia gerar double free quando NF_DROP e NF_ACCEPT se sobrepunham
  • O patch de correção pode ser visto em [PATCH nf] netfilter: nf_tables: reject QUEUE/DROP verdict parameters.

Dirty Pagedirectory e acesso arbitrário à memória física

  • A técnica central do PoC é o Dirty Pagedirectory, uma variação da técnica existente Dirty Pagetable
  • A ideia principal é duplicar a alocação de páginas PTE e PMD na mesma página física
  • Ao gravar em uma região de endereço virtual valores que parecem entradas de PTE, outra região virtual passa a interpretá-los como entradas de tabela de páginas e mapeia a página física escolhida
  • Dessa forma, a técnica funciona como um kernel-space mirroring attack, definindo endereços físicos arbitrários e flags de permissão por meio apenas de leitura/escrita em endereços do espaço de usuário
  • Ela é usada para contornar mitigação como virtual KASLR, KPTI, SMAP, SMEP e CONFIG_STATIC_USERMODEHELPER

Manipulação do page allocator

  • A alocação de páginas no kernel envolve slab allocator, buddy allocator e PCP allocator
  • O head do skb usa páginas order 4 no buddy allocator quando o tamanho é grande, mas páginas PTE/PMD são páginas order 0, então elas não se encaixam diretamente
  • Para resolver isso, são usados dois métodos de conversão de páginas
    • PCP list draining: coloca páginas order 4 na freelist do buddy e esvazia a freelist PCP order 0, forçando o buddy allocator a reabastecê-la com páginas order 0
    • race condition: usa uma disputa durante a segunda liberação para inserir a página order 4 na freelist order 0
  • O método de PCP list draining é mais simples, estável e rápido
  • O método baseado em race condition foi usado no exploit inicial do KernelCTF, mas passou a ser considerado obsoleto por depender de ambientes com grande latência de serial TTY, como VMs QEMU

Desvio das mitigação do KernelCTF

  • No ambiente com mitigação do KernelCTF, a proteção que mais exigiu desvio foi a checagem de corrupção da freelist de sk_buff
  • Como skbuff_head_cache->offset == 0x70, o ponteiro next da freelist se sobrepõe a skb->len
  • Após a primeira liberação do skb, skb->len é parcialmente sobrescrito por parte do ponteiro da freelist e, se esse valor mudar durante o parsing posterior do pacote, a checagem de corrupção pode disparar
  • O desvio consiste em liberar um skb normal adicional sobre o skb corrompido para sobrescrever a cabeça da freelist e evitar a detecção
  • Desenvolvedores do KernelCTF observaram que verificar também o ponteiro next da cabeça da freelist no momento da liberação poderia mitigar esse desvio

TLB flush e busca do KASLR físico

  • Quando o Dirty Pagedirectory altera tabelas de páginas de forma inesperada, o cache de TLB da CPU pode manter traduções antigas
  • O TLB é limpo fazendo fork() no espaço de usuário, deixando o filho executar munmap() e depois dormir
  • Esse método foi confirmado como funcional em 100% dos casos em CPUs AMD e em VMs QEMU
  • O KASLR físico é buscado aproveitando o fato de que o endereço base físico do kernel é alinhado conforme CONFIG_PHYSICAL_START ou CONFIG_PHYSICAL_ALIGN
  • Assumindo 8GiB de memória física e alinhamento de 16MiB, há 512 candidatos; o script get-sig é usado para gerar a assinatura do base do kernel e identificá-lo

modprobe_path e obtenção de shell root

  • Depois de conseguir leitura/escrita arbitrária na memória física, o PoC varre cerca de 80MiB após o base do kernel para localizar modprobe_path
  • Em configurações comuns, ele procura o padrão "/sbin/modprobe" com preenchimento nulo e valida a variável real verificando se ela se reflete em /proc/sys/kernel/modprobe
  • Quando CONFIG_STATIC_USERMODEHELPER está ativado, o alvo passa a ser a string "/sbin/usermode-helper"
  • Para obter um shell root, o exploit sobrescreve modprobe_path ou a string do static usermode helper com um caminho memfd no formato /proc/<pid>/fd/<fd>
  • O script de escalonamento de privilégios conecta os descritores de arquivo do exploit ao stdin/stdout do shell, permitindo funcionamento tanto em terminal local quanto em reverse shell

Execução sem arquivos e composição do PoC

  • O PoC suporta execução sem arquivos sem gravar nada no disco
  • Se o alvo tiver Perl, é possível usar memfd_create() para carregar o binário do exploit na memória e executá-lo via /proc/$$/fd/<fd>
  • As dependências de compilação são libnftnl-dev e libmnl-dev
  • O build estático para KernelCTF usou musl-gcc, escolhido para evitar problemas de linkagem estática com glibc e problemas com opcode AVX512 no QEMU
  • O código do exploit está dividido em vários arquivos e, por ter como objetivo um binário autônomo, em caso de erro prefere crash/exit a retornar códigos de erro

Estabilidade e limitações

  • Como o estado das pagetables do processo do exploit pode ficar instável, após sucesso ou falha os processos filhos são deixados em sleep em vez de serem encerrados, reduzindo a instabilidade do kernel
  • Atividade de rede pode introduzir ruído na freelist de skb e afetar a estabilidade
  • Em ambientes com SSH ou reverse shell, a saída para stdout é reduzida perto do momento do double free para minimizar alocação/liberação de skb causada pela rede
  • Em alguns testes de hardware, o sistema travou após alguns segundos, e como frames de Wi‑Fi também usam skb, a atividade de Wi‑Fi pode ter influenciado
  • Desativar o adaptador Wi‑Fi na BIOS fez o exploit funcionar normalmente nesse ambiente

Conclusões obtidas durante a pesquisa

  • O PoC foi refinado com foco em ampla compatibilidade, alta estabilidade e execução discreta
  • Além de dois meses de desenvolvimento, foram usados mais dois meses para melhorar estabilidade e compatibilidade
  • O exploit em si não depende fortemente do comportamento do slab allocator, e se concentra em recursos amplamente habilitados, como o subsistema IPv4 e a memória virtual
  • Embora o bug inicial exija namespace de usuário não privilegiado e nftables, técnicas como Dirty Pagedirectory e PCP draining podem ser aproveitadas em outros exploits reais
  • Este trabalho fica como um caso de exploração prática que investigou em conjunto o subsistema de rede e o subsistema de gerenciamento de memória do kernel Linux

1 comentários

 
GN⁺ 2024-03-27
Comentários do Hacker News
  • Hoje foi publicado um exploit de prova de conceito para CVE-2024-1086, que funciona em Debian, Ubuntu e outros
    As versões afetadas são o kernel Linux v5.14 a v6.6, e o suporte a v6.4~v6.6 depende da configuração de kernel CONFIG_INIT_ON_ALLOC_DEFAULT_ON
    Esse bug foi corrigido em fevereiro de 2024, então é preciso atualizar as máquinas Linux

    • O código-fonte é bom e o trabalho é excelente. Fico curioso para ver qual será a repercussão maior
      https://github.com/Notselwyn/CVE-2024-1086/blob/main/src/mai...
    • Se, na etapa pós-exploit, foi obtida uma primitiva semelhante a KSMA, fico curioso por que ainda usar o método modprobe_path e até incluir força bruta de pid por causa da abordagem sem arquivo
      Por exemplo, gostaria de perguntar por que não escolheram uma abordagem de aplicar patch no .text do kernel com um shellcode curto para virar root e escapar do namespace
    • Fico curioso sobre quais seriam os possíveis vetores de ataque e impactos dessa vulnerabilidade
    • No texto, a versão afetada pelo exploit aparece como do kernel Linux v5.14 até v6.4, mas na página linkada aparece como de v5.14 até v6.6
      Porém, está escrito que os branches corrigidos v5.15.149>, v6.1.76>, v6.6.15> ficam excluídos
  • Há esta frase no patch: “This reverts commit e0abdadcc6e1. [...] Its not clear to me why this commit was made.”
    Fico curioso se alguém investigou o histórico de contexto desse commit
    [1] https://lore.kernel.org/all/20240120215012.129529-1-fw@strle...

    • O commit original já tem mais de 10 anos, então é bem provável que tenha se perdido no tempo
      Procurei na antiga lista netdev, mas não encontrei nada; pode ter sido um patch enviado diretamente ao committer, Pablo Neira Ayuso
      O autor original era Patrick McHardy, que hoje se tornou uma figura evitada, e, a menos que Pablo se lembre ou encontre nos e-mails, o caso de uso exato parece difícil de descobrir com uma investigação básica
    • O commit relacionado é este: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin...
      netfilter: nf_tables: accept QUEUE/DROP verdict parameters, que permite ao espaço de usuário especificar um número de fila ou código errno nos veredictos QUEUE e DROP
  • Este artigo também é muito impressionante do ponto de vista de redação sobre segurança
    Ao escrever um blog de segurança, a dúvida é sempre “quanto conhecimento de contexto e prévio devo presumir?”, e é difícil encontrar um equilíbrio que seja acessível e ainda viável de escrever
    Achei excelente o fato de definir primeiro o público-alvo e fornecer explicação de contexto suficiente; por isso, marquei como favorito para usar como material de orientação para pesquisadores iniciantes que encontro todo ano

  • Este exploit depende do acesso a namespaces de usuário sem privilégios: sysctl kernel.unprivileged_userns_clone = 1
    Esse é o padrão nos kernels Debian/Ubuntu e Arch Linux, e é melhor desativá-lo se você não precisa, por exemplo, executar comandos Docker sem sudo

    • Essa configuração não é usada apenas por Docker ou Pacman
      O sandbox do Chrome usado por apps Electron ou pelo 1Password também está relacionado, e o binário auxiliar do sandbox também pode funcionar como um programa setuid
      Também não seria surpreendente se o Proton começasse a usar namespaces de usuário no futuro, então talvez seja melhor não desativar isso em um Linux desktop comum
      Em servidores ou em Linux especialmente reforçado, desativar costuma não ser uma má ideia
    • Essa configuração é boa no sentido de permitir que pessoas executem coisas como contêineres sem privilégios de root, mas é realmente lamentável que ela tenha um histórico de acabar levando a vulnerabilidades de elevação de privilégio para root
    • O problema real não é essa configuração em si, mas o fato de que namespaces originalmente são um meio de reduzir privilégios
      O problema é que, para contêineres “simplesmente funcionarem”, são necessários inúmeros hacks na parte de rede, e o principal argumento de venda do Docker é quase fazer esses hacks perigosos por você
      No fim, qualquer solução de contêiner acaba aceitando esses hacks e, mesmo que a funcionalidade normal de namespaces reduza o acesso, o kernel acaba abrindo a porta por causa dos hacks de rede
    • Na 6.1.65 essa opção não existe; fico curioso se o nome mudou
  • Não entendo por que namespaces de usuário sem privilégios vêm ativados por padrão
    Mesmo que sejam executados dentro de um namespace “sem privilégios”, fico me perguntando qual é a razão para dar ao usuário, por padrão, a capacidade de executar coisas como iptables ou mount

    • Namespaces de usuário sem privilégios permitem que programas sem privilégios montem um sandbox
      Por exemplo, o Chrome usa namespaces para implementar o sandbox de processos, mas instala um binário setuid-root para funcionar mesmo sem namespaces sem privilégios
      Como um binário setuid-root por si só é um risco de segurança, no longo prazo é desejável que o Chrome não precise instalar esse tipo de binário
      Só que, para isso, namespaces de usuário sem privilégios precisam estar amplamente disponíveis, e bugs como este acabam atrasando esse futuro
      Além disso, programas que montam sandboxes baseados em namespaces, como o Chrome, frequentemente usam seccomp junto para impedir que o código dentro do sandbox use funcionalidades incomuns do kernel, como namespaces
      Em desktops de usuário único, o benefício de aplicar fortemente a separação entre usuário e root não é tão grande, e a maior parte do que é interessante também está acessível pela conta do usuário
      Por outro lado, sandboxes como o do Chrome são essenciais para a segurança no desktop; portanto, em desktops de usuário único, tende-se a considerar que ativar namespaces de usuário sem privilégios aumenta a segurança geral
      Sistemas multiusuário, naturalmente, são outra história
  • Se esses bugs simplesmente não continuassem aparecendo, namespaces de usuário sem privilégios seriam um excelente recurso de segurança
    Por exemplo, seria bom se executar o Flatpak não exigisse um binário setuid do host para isolar apps

    • O problema é a filosofia de “simplesmente funciona”
      A maioria dos padrões não é segura e exige conhecimento técnico e trabalho de hardening
  • Commit que introduziu o problema: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin...
    A estrutura parece estranha: há um return no caminho padrão dentro de um switch/case aninhado, mas depois se espera um fall-through

    • Fico me perguntando se é a mesma pessoa
      https://lwn.net/Articles/882397/
    • Convertendo o link para o formato do GitHub, fica assim: https://github.com/torvalds/linux/commit/f342de4e2f33e0e3916...
      https://bugs.launchpad.net/bugs/cve/2024-1086
      É uma vulnerabilidade de use-after-free no componente netfilter: nf_tables do kernel Linux que permite escalação local de privilégios
      A função nft_verdict_init() permite valores positivos como erro de drop dentro de um veredicto de hook e, como resultado, nf_hook_slow() processa NF_DROP com um erro de drop que parece NF_ACCEPT, o que pode criar uma vulnerabilidade de double-free
      Recomenda-se atualizar para uma versão posterior a f342de4e2f33e0e39165d8639387aa6c19dff660
  • Fico curioso sobre como esse tipo de exploit é possível mesmo com mitigações modernas como ASLR
    Em uma disciplina da faculdade, recebemos vários binários para executar em uma versão específica do Ubuntu e tínhamos de encontrar bugs como use-after-free ou buffer overflow e explorá-los; foi realmente difícil
    Encontrar a falha já era difícil, e escrever o shellcode exato que fizesse algo útil com ela era ainda mais difícil
    Nas etapas mais difíceis, mitigações como ASLR e stack canary também estavam ativadas e, mesmo em um ambiente controlado para estudantes, parecia quase impossível
    No fim, é preciso 1) descobrir uma falha explorável e 2) encontrar o payload binário exato que faça algo útil sem simplesmente derrubar o programa; no mundo real isso deve ser ainda mais difícil, então fica a dúvida de como é possível
    [0] https://web.stanford.edu/class/archive/cs/cs107/cs107.1194/a...

    • Quem estava fazendo a disciplina era um iniciante com pouca experiência e precisava encontrar e explorar vários bugs em um curso de 10 a 15 semanas
      Paralelamente a outras disciplinas, o tempo disponível para cada bug provavelmente era de algumas horas a alguns dias
      A Zerodium paga US$ 50 mil por uma escalação local de privilégios Linux comum, o que, em termos de custo de desenvolvedores de exploits experientes, compra aproximadamente 200 horas-pessoa
      Ou seja, especialistas gastam de 10 a 100 vezes mais tempo que estudantes iniciantes
      Pense na diferença entre alguém que vai a uma marcenaria pela primeira vez e um marceneiro, entre um iniciante em cerâmica e um artesão, entre um calouro em pintura e um artista profissional
      Além disso, há 10 a 100 vezes mais tempo investido
      [1] https://zerodium.com/program.html
    • Mitigações modernas tornaram exploits muito mais difíceis, mas pesquisadores continuam encontrando maneiras de contorná-las
      Existem técnicas “clássicas” para burlar a maioria das proteções modernas e, quando não há, às vezes surgem novos ataques ou métodos de bypass
      Por exemplo, para bypass de proteções de heap, dá para ver o how2heap[0]; também houve exemplos de exploits que contornam KASLR[1], e este exploit parece usar a técnica dirty pagetable[2]
      A dinâmica é a de um jogo de gato e rato contínuo: mitigações são adicionadas e pesquisadores encontram formas de contorná-las
      [0] https://github.com/shellphish/how2heap
      [1] https://www.willsroot.io/2022/12/entrybleed.html
      [2] https://pwning.tech/nftables/#452-the-technique
    • Resumindo: há gente muito boa, há gente com sorte e há gente que é boa e também tem sorte
      Para encontrar esse tipo de coisa, basta uma pessoa do último grupo
      Hoje em dia é realmente difícil e, mesmo desativando as mitigações, encontrar vulnerabilidades e escrever exploits ainda não é nada fácil
      Porém, muitas pessoas que encontram esse tipo de vulnerabilidade trabalham em equipes, paralelizam fuzzing e conseguem combinar ou encadear conhecimento e outros exploits
      A especialização e o talento de alguns pesquisadores são impressionantes, e nessa área anos ou décadas de experiência têm um valor enorme
    • Em uma disciplina parecida, também fiz uma tarefa em que recebíamos binários e precisávamos encontrar bugs, e também foi difícil
      Mas, pensando que os principais bugs encontrados hoje são descobertos por equipes bem financiadas ou atores estatais, fica mais compreensível que seja possível investir muita mão de obra e recursos para contornar as mitigações existentes
    • O post do blog linkado no repositório tem uma seção separada sobre KASLR
  • Segundo a Ubuntu, todas as versões LTS são afetadas e a falha foi corrigida nos kernels atualmente corrigidos: https://ubuntu.com/security/CVE-2024-1086
    No Focal, foi corrigida em 5.4.0-174.193; no Jammy, em 5.15.0-101.111; no Mantic, em 6.5.0-26.26
    Se você usa suporte estendido, Xenial e Bionic também estão incluídos

  • Testei em um sistema Debian vulnerável e, embora não tenha ocorrido elevação de privilégios, na segunda execução o sistema inteiro travou
    A primeira execução simplesmente falhou, então ainda assim vale muito a pena dedicar um tempo para aplicar o patch

  • As configurações atuais do kernel podem ser verificadas em arquivos como /boot/config ou /proc/config.gz