2 pontos por GN⁺ 2024-10-27 | 1 comentários | Compartilhar no WhatsApp
  • O mseal do Linux 6.10 é um syscall de mitigação de exploits que sela áreas de memória virtual em execução, dificultando que um invasor que já obteve permissão de execução de código altere depois as permissões ou o layout das VMAs
  • mseal(start, len, flags) adiciona VM_SEALED a um intervalo de VMA alinhado a páginas, e o kernel passa a rejeitar mudanças em caminhos como mprotect, munmap, mmap(MAP_FIXED), mremap e alguns caminhos destrutivos de madvise
  • Enquanto memfd_create e memfd_secret estão mais próximos de arquivos anônimos baseados em RAM e controle de acesso a memória secreta, o mseal foca em impedir mudanças de permissão, unmapping e remapping após execução de código por um invasor remoto
  • Os principais alvos de defesa são ataques que contornam NX com mprotect(PROT_EXEC) para executar shellcode e exploits somente de dados que criam buracos na memória com munmap/remapeamento e depois os preenchem com dados controlados pelo atacante
  • Há possibilidade de integração com a glibc a partir da 2.41, mas stack e heap não são alvos de selagem automática por causa da expansão e contração em tempo de execução, então os desenvolvedores precisam definir com cuidado quando e onde aplicar isso

Proteção oferecida pelo mseal

  • Memory sealing é um recurso que torna um intervalo de VMA em execução quase imutável diante de modificações perigosas posteriores
  • Mesmo que um invasor obtenha um primitivo de execução de código, em uma VMA selada fica difícil alterar permissões ou manipular o layout da memória virtual a seu favor
  • A equipe de segurança do Chrome introduziu esse syscall para dar suporte à estratégia de CFI do V8 no ChromeOS baseado em Linux
  • Depois de várias discussões e reescritas, ele entrou no kernel Linux e pode ampliar seu uso além dos navegadores por meio da integração com a glibc

Diferença em relação à família memfd

  • memfd_create e memfd_secret estão mais próximos da família de file sealing
    • Permitem criar arquivos anônimos baseados em RAM
    • memfd_secret faz com que apenas processos com o descritor de arquivo possam acessar aquela região de memória
    • Podem ser usados em mapeamentos em espaço de usuário no estilo “secure enclave” para proteger dados sensíveis em memória
  • O mseal é um syscall voltado para mitigação de exploits contra invasores remotos que buscam execução de código, mais do que contra invasores locais interessados em vazamento de informações sensíveis

Funcionamento interno no kernel

  • A assinatura do syscall é int mseal(unsigned long start, size_t len, unsigned long flags)
    • start e len representam o início e o tamanho de uma VMA válida a ser selada
    • len precisa estar corretamente alinhado a páginas
    • No momento, flags não é usado e deve ser 0
  • Na implementação do Linux 6.12, ele chama do_mseal
    • current->mm aponta para mm_struct, que representa todo o espaço de endereços de memória virtual do processo chamador
    • O mmap de mm_struct contém a lista de vm_area_struct, que representam regiões contínuas de memória criadas com mmap
    • Regiões como stack ou VDSO também podem ser representadas como uma única VMA
  • do_mseal calcula o endereço final do intervalo e então bloqueia a região de memória com mmap_write_lock_killable
  • check_mm_seal percorre cada VMA do intervalo alvo e primeiro valida os limites
    • Se houver memória não alocada no meio do intervalo, retorna -ENOMEM
    • Essa primeira checagem evita que apenas parte das VMAs seja selada em caso de erro
  • apply_mm_seal percorre o mesmo intervalo novamente e adiciona a flag VM_SEALED às VMAs alvo por meio de mseal_fixup

Operações de memória proibidas após a selagem

  • O patchset do kernel adiciona verificações de VM_SEALED em mm/madvise.c, mm/mmap.c, mm/mprotect.c, mm/mremap.c e mm/mseal.c
  • mprotect e pkey_mprotect chamam internamente can_modify_vma em mprotect_fixup, e retornam -EPERM se a VMA estiver selada
  • Em uma VMA selada, as seguintes operações não são permitidas
    • Mudança de bits de permissão via mprotect ou pkey_mprotect
    • Unmapping via munmap
    • Substituição de um mapeamento selado por um mapeamento variável e não selado via mmap(MAP_FIXED)
    • Expansão ou redução de tamanho via mremap
    • Movimentação para um novo destino via mremap(MREMAP_MAYMOVE | MREMAP_FIXED)
    • madvise com algumas flags destrutivas
  • Na movimentação com mremap, a checagem de selagem se aplica tanto à VMA de origem quanto à de destino
  • Sem MREMAP_DONTUNMAP, a VMA de origem é desmontada, e isso também precisa passar pela checagem de selagem de munmap
  • No Linux 6.10 ou superior, já é possível usar mseal com chamada direta de syscall, e o wrapper de exemplo usa o número de syscall 462 com flags=0

Bloqueio de execução de shellcode por reforço de NX

  • Mesmo com técnicas de reutilização de código como ROP, um invasor ainda pode preferir executar shellcode
    • Espalha shellcode em uma stack ou heap não executável
    • Executa uma cadeia ROP inicial por meio de uma vulnerabilidade
    • Desativa o bit NX da região com shellcode usando mprotect(PROT_EXEC)
    • Salta para essa região e executa o shellcode
  • O ataque ao daemon SMB do MikroTik RouterOS usando CVE-2018-7445 é um exemplo desse fluxo
    • Shellcode baseado em socket é colocado em um heap não executável
    • Uma cadeia ROP criada por stack overflow altera as permissões da memória do heap
    • Depois disso, o shellcode é executado
  • Se a VMA correspondente for selada com mseal, a checagem can_modify_vma bloqueia a mudança de permissão durante a chamada de mprotect
  • No código de exemplo, se a página de shellcode na stack não for selada, ela pode ser executada após mprotect(PROT_READ|PROT_WRITE|PROT_EXEC)
  • Se a mesma página for selada com mseal, mprotect não consegue alterar as permissões de fato, e a execução do shellcode resulta em falha de segmentação

Limitações na aplicação em stack e heap

  • Quando mseal entrar na glibc a partir da 2.41, o loader dinâmico deverá aplicar selagem a um conjunto previamente definido de VMAs
  • No plano atual, stack e heap não serão selados automaticamente
  • Stack e heap podem crescer em tempo de execução, e a selagem automática pode quebrar o funcionamento da aplicação
    • O alocador de heap pode chamar o syscall brk para recuperar espaço
    • Nesse caminho, a redução pode ocorrer passando por arch_unmap e do_vmi_unmap
    • Com a selagem ativa, esse unmapping não é permitido, o que pode quebrar a alocação dinâmica de memória
  • Os desenvolvedores precisam avaliar, no contexto da aplicação, em que momento e em quais regiões a selagem pode ser aplicada com segurança
  • O macro de exemplo sela repetidamente as páginas de frames de stack selecionados que podem conter dados não confiáveis
  • Chamar mseal novamente em uma VMA já selada vira um no-op e não gera erro
  • Recursos como crescimento automático da stack ou stack splitting exigem atenção extra
  • Se o invasor insistir em shellcode, ainda pode criar uma nova região executável com mmap e copiar o payload de uma região legível, mas o processo fica mais trabalhoso

Mitigação de exploits somente de dados baseados em unmapping

  • Bloquear mprotect também impede que regiões seladas se tornem graváveis, o que ajuda a proteger variáveis de dados que, se modificadas, poderiam fortalecer primitivos de exploit
  • Mantenedores do Chrome consideraram a técnica de passar ponteiros corrompidos para syscalls de unmapping/remapeamento para abrir buracos na memória e depois preenchê-los com dados controlados pelo atacante
  • Como essa abordagem não altera diretamente transferências de fluxo de controle, como endereços de retorno da stack ou ponteiros de função, ela pode contornar garantias de CFI tanto em forward-edge quanto em backward-edge
  • Em navegadores com compiladores JIT, essa técnica pode ser especialmente útil
    • O Turbofan do V8 pode criar regiões que alternam entre RW e RX
    • O invasor pode induzir a geração de código executável com JavaScript hot-path durante a compilação JIT
    • Depois de preencher a região desmontada com dados que sobrescrevem informações importantes, pode produzir uma mudança que leve à execução de código
  • Trata-se de um exploit somente de dados que influencia o fluxo de controle manipulando dados específicos na memória, sem exigir sequestro direto do fluxo de controle nem vazamento prévio de ponteiros
  • O mseal pode bloquear esse cenário de hole-punching ao impedir unmapping e remapping

Caso House of Muney

  • House of Muney usa uma técnica semelhante baseada em unmapping em exploração de heap em espaço de usuário
  • A Qualys usou essa técnica em um exploit real para um bug antigo do Qmail
  • A técnica depende do fato de que chunks grandes de alocação, quando ultrapassam M_MAP_THRESHOLD, fazem com que malloc e free chamem diretamente mmap e munmap
  • Como chunks grandes não têm cache intermediário de freelist, o procedimento de exploração fica mais simples
  • Se o metadado de tamanho no topo do chunk alocado for adulterado para outro tamanho de página e depois for feito free, pode ocorrer munmap sobre uma região de memória adjacente ao chunk
  • O exemplo de Dulin mira as regiões .gnu.hash e .dynsym com munmap arbitrário
    • Depois as preenche novamente com um chunk mmap maior
    • Isso permite sobrescrever uma entrada de PLT ainda não resolvida
    • E revive um ataque no estilo overwrite de GOT
  • O PoC resumido aloca dois chunks grandes, manipula o campo size de top[-1], faz free(top) para desmontar até a região adjacente e então preenche novamente com dados X usando uma alocação maior
  • A técnica pode funcionar mesmo com CFI e não exige vazamento prévio de ASLR
  • Espera-se que a selagem do conjunto de VMAs planejada na integração do mseal com a glibc mitigue automaticamente esse ataque ao proteger código binário mapeado e bibliotecas dinâmicas contra truques de unmapping/remapping
  • Desenvolvedores que quiserem reforço adicional podem selar seletivamente alocações mmap que não serão expandidas nem desmontadas durante a vida útil do programa

Uso futuro

  • O mseal é uma funcionalidade de mitigação relativamente nova no kernel Linux, e pode haver mais casos de uso que ainda não foram explorados
  • À medida que a integração com a glibc for concluída e amadurecer, podem surgir melhorias para adequá-lo melhor às exigências do syscall
  • O parâmetro flags, atualmente sem uso, também pode ganhar uma função mais concreta no futuro
  • Ao aplicar isso em software real, é preciso avaliar tanto o ciclo de vida da memória selada quanto a necessidade de alterações nela

1 comentários

 
GN⁺ 2024-10-27
Opiniões no Hacker News
  • Seria bom se alguém de dentro resumisse quais foram as objeções e preocupações nas discussões acaloradas da lista de e-mails do kernel
    Eu costumo evitar a própria lista de e-mails porque às vezes ela é pesada demais, e já tenho dores de cabeça suficientes
    O mecanismo em si parece razoável, mas é surpreendente que esse recurso ainda não existisse no kernel

    • Não sei se houve mais além da thread linkada, mas no geral foi a franqueza ao estilo Linus
      Entre as flags propostas havia uma que permitiria ignorar o seal, e Linus reagiu mais ou menos como: “quer dizer que em um lugar você impede munmap, mas em outro quer ignorar o seal?”
      Depois ele foi ainda mais enfático, dizendo que “uma vez selado, está selado” e que não dá para ter “alguns lugares respeitando o seal e outros lugares arbitrários não”
      Mais tarde, chegou a dizer que seals não podem ser ignorados, não são negociáveis, e que colocaria na ignore list quem continuasse propondo uma flag para ignorá-los
    • https://lwn.net/ml/linux-kernel/7071.1697661373@cvs.openbsd....
      Em um e-mail de Theo de Raadt para Jeff Xu, respondendo à afirmação de que, embora mimmutable() e mseal() tenham abordagens diferentes, ambos têm o objetivo de tornar aplicações Linux mais seguras ao selar a memória contra invasores, ele disse: “parece que você está criando o mseal só para o Chrome
      Ele considerou que isso não se encaixaria bem para aplicações em geral, citando como motivos o excesso de complexidade e, pela experiência com mimmutable(), o fato de que isso acaba ficando a cargo de execve(), da inicialização da libc e do ld.so, em vez de ser tratado diretamente pelas aplicações
    • Matthew Wilcox também respondeu de forma dura a Jeff Xu, em tom de “obrigado por mostrar que você não sabe o que precisa ser bloqueado”
      Ele concordou com Theo e Linus, considerando que a ideia básica de mimmutable() é boa, mas que a forma como ela foi dividida e implementada é terrível
    • Este artigo pode ajudar: https://lwn.net/Articles/948129/
  • Fico curioso sobre como se usa essa syscall
    O Chrome quer isso, mas como um atacante poderia remapear com outras flags, páginas seladas não podem ser desmapeadas
    Então, para páginas alocadas em runtime, parece que elas praticamente não podem ser usadas a menos que você pretenda mantê-las durante toda a vida do processo
    Nesse caso, não seria difícil aplicar isso a alvos muito atraentes, como a memória do sandbox de JS?
    Fico me perguntando se esse tipo de coisa é resolvido executando o trabalho em um processo separado, selando a memória e depois matando o processo quando terminar
    Não conheço bem o gerenciamento de memória e processos do Chrome, então não tenho certeza de por que isso não seria um problema, e também me pergunto se é por isso que esse recurso é frequentemente descrito como pouco útil para a maioria dos programas

    • Aqui, múltiplos processos podem ser uma opção
      Pelo que sei, o Chrome usa isso amplamente, e talvez esse seja o caminho, já que processos separados de qualquer forma são necessários por motivos como isolamento via namespaces
  • Discussão após o mseal(), 20 de outubro de 2023: https://lwn.net/Articles/948129/
    O mseal() se aproxima, 19 de janeiro de 2024: https://lwn.net/Articles/958438/
    Selagem de memória para a GNU C Library, 12 de junho de 2024: https://lwn.net/Articles/978010/

  • É uma pena que o sistema operacional precise implementar chamadas desse tipo, mesmo com a arquitetura x86_64 moderna tendo muitos recursos que ajudam na programação e na computação seguras
    Acho que a atitude de tentar remendar sistemas antigos, construídos sobre legados e mentalidades ultrapassados e sobre paradigmas que não se ajustam ao mundo e ao conhecimento atuais, atrasa o avanço da computação e literalmente coloca bilhões de pessoas em risco
    Claro, não quero dizer que essa mudança não seja um passo na direção certa, mas, se abandonarmos o antigo ideal de que o sistema operacional simplesmente precisa funcionar e considerarmos os sistemas e conhecimentos atuais e o que as pessoas querem deles, é possível imaginar sistemas livres do ônus e do risco impostos hoje a desenvolvedores e usuários
    É verdade que existem bugs de arquitetura, mas o software nem sequer aproveita corretamente os recursos atuais; portanto, usar bugs de arquitetura como contra-argumento foge do ponto central
    Se for construído sobre uma base instável, sempre haverá uma forma mais barata de invasão

    • Fiquei curioso: seria bom saber quais são os recursos de segurança não usados ou pouco usados no x86_64 que poderiam ser utilizados sem que o sistema operacional tivesse uma forma de usá-los ou ativá-los
      Também gostaria de entender por que você acha que mseal não se justifica e o que seria melhor em seu lugar
  • Será que dá para sobrescrever ou desativar a chamada de sistema mseal com o truque de LD_PRELOAD?

    • Diferentemente dos métodos tradicionais de proteção de memória do Linux, mseal é uma chamada de sistema voltada mais para mitigar ataques de execução remota de código do que para impedir um atacante local de extrair segredos sensíveis da memória
      Se um atacante remoto consegue alterar o ambiente local, então já se deve considerar que ele invadiu o sistema
    • Provavelmente não vai funcionar com LD_PRELOAD
      Para surtir efeito, teria que ser uma função importada, mas chamadas de sistema brutas não podem ser interceptadas dessa forma
      Discussão sobre interceptação de chamadas de sistema no Linux: https://stackoverflow.com/questions/69859/how-could-i-interc...
      Criar você mesmo um kernel com patch que finja que mseal funciona pode ser a maneira mais fácil de “desativar” o recurso
      Porém, como programas que usam mseal podem verificar se ele realmente está funcionando, um kernel comprometido também precisaria de um modo de desativar mseal às escondidas para impedir as verificações do app após a aplicação
    • Se houver controle no início do processo, há várias maneiras de sobrescrever
      Por exemplo, é possível rastrear o executável com ptrace para monitorar chamadas de sistema e fazer com que mseal(2) seja pulada
      Essa chamada de sistema não foi pensada para um modelo de ameaça em que “o atacante já tinha acesso antes da inicialização do processo”
    • O wrapper da chamada mseal pode ser sobrescrito, mas a chamada de sistema em si não
      Pelo que encontrei, todos os métodos de sobrescrita de chamadas de sistema via preload trocam o wrapper, e parece que não dá para sobrescrever quando a chamada de sistema é feita diretamente
      Tecnicamente, talvez seja possível sobrescrever a própria função syscall
    • Segundo https://lwn.net/Articles/978010/, um glibc tunable deverá ser incluído
  • Meta: o protótipo de mseal() mostrado no texto precisa ser editado
    O primeiro argumento aparece como unsigned start addr, mas provavelmente o correto é unsigned long start_addr

    • Agora parece estar certo: int mseal(unsigned long start, size_t len, unsigned long flags)
  • Em “Memory Sealing ‘Mseal’ System Call Merged for Linux 6.10” (2024) https://news.ycombinator.com/item?id=40474510#40474551, já houve uma discussão sobre como o CPython deveria dar suporte à chamada de sistema mseal()

  • É um recurso que já existia há muito tempo no OpenBSD https://man.openbsd.org/mimmutable.2
    Fico me perguntando por que uma funcionalidade tão óbvia só está chegando agora ao Linux

    • O mimmutable do OpenBSD foi introduzido no OpenBSD 7.3, lançado em 10 de abril de 2023, então não é algo que exista “há muito tempo”
      Por outro lado, Linux e FreeBSD já tinham memfd_create há muito tempo, enquanto o OpenBSD não tem arquivos anônimos e depende de shm_open