1 pontos por GN⁺ 2025-03-24 | 1 comentários | Compartilhar no WhatsApp
  • O backdoor do xz, descoberto em março de 2024, poderia levar à execução remota de código em ambientes sshd afetados, e o caso do NixOS mostra que builds reproduzíveis podem expor mecanicamente adulterações na cadeia de suprimentos
  • O componente malicioso não estava em todo o repositório Git do xz, mas escondido nos tarballs de release fornecidos pelo mantenedor das versões 5.6.0 e 5.6.1, gerando durante o build um script de shell e um arquivo-objeto a partir de falsos arquivos de teste .xz
  • O ataque usava o ifunc do glibc para executar código ao carregar liblzma e mirava ambientes das famílias Debian e Fedora em que o sshd depende transitivamente de liblzma via libsystemd
  • O NixOS normalmente prefere arquivos de código-fonte gerados automaticamente pelo GitHub, mas como o xz está na etapa de bootstrap do nixpkgs, era preciso confiar no tarball do mantenedor para evitar um ciclo de dependência com autoconf
  • A defesa proposta é reconstruir o xz a partir do código-fonte do GitHub depois do bootstrap e comparar o resultado com o artefato do tarball existente; ao ativar a condição, surgem a diferença de tamanho em liblzma.so e a adição do símbolo _get_cpuid

Como o backdoor do xz foi escondido

  • xz é um software de compressão e descompressão frequentemente usado em caminhos centrais de distribuições Linux ao extrair tarballs de código-fonte
  • Em março de 2024, foi descoberto um backdoor no xz, e o mantenedor malicioso Jia Tan ganhou confiança ao longo de cerca de 3 anos, obteve permissão de push no repositório e então inseriu gradualmente código ofuscado entre contribuições legítimas
  • Andres Freund, ao investigar uma degradação de desempenho de 500ms no ssh em várias máquinas Debian unstable, rastreou o problema até liblzma, identificou o backdoor e o documentou
  • O objetivo do backdoor era sequestrar o ssh para que, ao fazer login com uma chave RSA específica, o invasor pudesse executar comandos arbitrários na máquina da vítima

O tarball malicioso e os objetos gerados durante o build

  • O componente malicioso não foi inserido diretamente em todo o código do repositório Git do xz, mas incluído nos tarballs de release do xz 5.6.0 e 5.6.1 que Jia Tan compilou, assinou e distribuiu
  • Alguns arquivos de teste .xz dentro do repositório Git escondiam código de máquina, e os tarballs de release incluíam alterações disfarçadas para extrair esse conteúdo
  • Em especial, a mudança em m4/build-to-host.m4 parecia inofensiva à primeira vista e até tinha comentários, mas na prática escondia uma cadeia de comandos que decodificava e desofuscava vários falsos arquivos de teste .xz
  • O resultado desse processo eram duas coisas
    • um script de shell executado durante o build do xz
    • um arquivo-objeto binário malicioso
  • O script de shell executado durante o build tinha dois papéis
    • verificar as condições de execução do backdoor, como distribuição Linux específica, recursos do glibc e presença do ssh
    • modificar o objeto legítimo liblzma_la-crc64_fast.o para que ele usasse o símbolo _get_cpuid do arquivo-objeto do backdoor
  • A análise do script do xz de Russ Cox detalha como os recursos maliciosos são gerados durante o build

Hijacking com ifunc até chegar ao sshd

  • Em programas com linkedição dinâmica, no momento da execução o carregador dinâmico carrega bibliotecas compartilhadas na memória e preenche os endereços de símbolos na Global Offset Table (GOT)
  • O ifunc do glibc é um recurso para escolher, no carregamento dinâmico, uma entre várias implementações de uma mesma função, e o backdoor do xz o usava como caminho para execução de código malicioso
  • Em algumas distribuições como Debian e Fedora, o ssh é linkado com libsystemd para suporte a notificações do systemd, e libsystemd por sua vez é linkado com liblzma
    • nesse ambiente, o sshd tem dependência transitiva de liblzma
  • Ao executar o sshd, o carregador dinâmico carrega libsystemd e liblzma, e, se o backdoor estiver instalado, código malicioso é executado durante o carregamento de liblzma
  • O backdoor faz o resolvedor de ifunc chamar o símbolo malicioso _get_cpuid e manipula a GOT, ainda não marcada como somente leitura, para trocar o endereço de RSA_public_decrypt por uma função maliciosa
  • A análise detalhada da Securelist e este artigo de pesquisa que resume o vetor de ataque e as mitigaçãos podem ser consultados como referência

Fontes confiáveis e a restrição de bootstrap do NixOS

  • Esse ataque pôde funcionar porque muitas distribuições compilavam o xz a partir de tarballs fornecidos pelo mantenedor, e não do código-fonte original do forge Git
  • O fluxo com tarball existe por razões históricas e práticas
    • é o método das primeiras distribuições Linux, anterior ao git
    • preserva, em um arquivo autocontido, o estado do código no momento da release
    • pode incluir artefatos intermediários como manpages e scripts configure, reduzindo o trabalho de build
    • ganha eficiência de armazenamento por meio da compressão
  • Do ponto de vista de segurança, quando tecnicamente possível, o ideal é compilar a partir de código-fonte autenticado pela entidade mais confiável disponível
    • se o projeto é desenvolvido no GitHub, pode-se usar os arquivos gerados automaticamente pelo GitHub a cada release
    • a mesma lógica vale para plataformas terceiras confiáveis como Codeberg, SourceHut e GitLab
  • O NixOS usa um modelo funcional de gerenciamento de pacotes, em que as receitas de build são definidas em expressões Nix
  • Entre mantenedores do NixOS há a preferência por usar fetchFromGitHub com arquivos de código-fonte gerados automaticamente pelo GitHub quando possível, mas o pacote xz estava buscando, com fetchurl, um tarball do mantenedor enviado manualmente
  • O motivo é que o xz faz parte da etapa de bootstrap do nixpkgs
    • bootstrap é o processo que permite reconstruir todo o conjunto de pacotes do nixpkgs a partir de um pequeno conjunto de binários-semente
    • stdenv é o ambiente básico de compilação usado por outros pacotes
    • stdenv depende de xz em tempo de execução, e pacotes como coreutils precisam de xz para extrair arquivos-fonte .tar.xz
  • Para compilar o xz a partir do código do GitHub, é preciso autoconf para gerar o script configure, mas o próprio autoconf também depende de xz
    • o tarball fornecido pelo mantenedor já inclui o script configure gerado, quebrando esse ciclo de dependência
    • por isso, no ponto do grafo do nixpkgs em que o xz é construído, era difícil usar o arquivo de código-fonte do GitHub e era necessário confiar no tarball do mantenedor

Comparar a convergência dos artefatos em vez do código-fonte

  • Comparar se o tarball do mantenedor e o tarball de código-fonte do GitHub são idênticos parece natural, mas na prática isso não se encaixa bem
  • Daniel Stenberg explica que o fato de o tarball de release ser diferente do código-fonte é uma funcionalidade
    • ele pode incluir artefatos intermediários como manpages e scripts configure
    • isso é especialmente útil quando a distribuição quer evitar dependências de autoconf
  • Do ponto de vista de segurança da cadeia de suprimentos, essa flexibilidade se transforma na obrigação de confiar que o mantenedor foi honesto
  • Builds reproduzíveis são a propriedade de produzir artefatos bit a bit idênticos ao compilar duas vezes nas mesmas condições
  • O grupo reproducible-builds busca tornar reproduzível o maior número possível de pacotes, e Reproducible Builds: Increasing the Integrity of Software Supply Chains trata isso como uma propriedade que aumenta a confiança na distribuição de artefatos binários
  • Uma abordagem para tornar a própria geração do tarball reproduzível foi adotada pelo projeto PostgreSQL
    • isso permite que usuários verifiquem de forma independente se o tarball do mantenedor foi gerado honestamente a partir do código-fonte original
    • ainda assim, como precisa ser implementado pelo mantenedor do projeto upstream, a adoção pode ser lenta na comunidade FOSS

Procedimento de detecção proposto no NixOS

  • Se o xz for reproduzível bit a bit e o tarball do mantenedor não contiver mudanças que afetem o processo de build, então o build a partir do tarball do GitHub e o build a partir do tarball do mantenedor deveriam gerar o mesmo resultado
  • O método proposto é compilar o xz mais uma vez depois do bootstrap, mas desta vez usando fetchFromGitHub com o arquivo de código-fonte do GitHub, e comparar o resultado com o artefato de xz produzido na etapa existente de bootstrap
  • Um exemplo de implementação coloca after-bootstrap.nix em pkgs/tools/compression/xz e compila o xz 5.6.1 a partir do código-fonte do GitHub
    • o tarball do mantenedor já contém o script configure, mas no build a partir do código do GitHub é preciso executar ./autogen.sh usando autoconf, libtool, automake etc.
  • Como etapa de comparação, adiciona-se compareArtifacts para comparar liblzma.so ao final do build
    • no exemplo, foi comparado apenas liblzma.so, o arquivo modificado pelo backdoor, mas isso pode ser generalizado para todos os binários e bibliotecas gerados
  • Em uma revisão normal da branch master, o build xz-after-bootstrap foi concluído com sucesso e os dois resultados eram idênticos
  • Na revisão com o backdoor c53bbe3, a princípio tudo parecia igual
    • no NixOS, o backdoor não era ativado, e um dos motivos é que o script de instalação verificava se a variável RPM_ARCH estava definida
    • ao configurar env.RPM_ARCH = true; para ativar a condição, a diferença binária em liblzma.so apareceu
  • Ao salvar e comparar os dois liblzma.so, o artefato com backdoor tinha 258KB, enquanto o artefato compilado depois do bootstrap a partir do código do GitHub tinha 210KB, uma diferença de cerca de 48KB
  • Ao comparar os símbolos com nm, apareceram _cpuid, _get_cpuid e __tls_get_addr@GLIBC_2.3, e _get_cpuid é um símbolo documentado em vários relatórios técnicos sobre o backdoor do xz

Aplicação prática e limitações

  • A proteção proposta consiste em aplicá-la a todos os pacotes construídos na etapa de bootstrap que não usem um arquivo de código-fonte confiável
  • Ao adicionar pacotes *-after-bootstrap como bloqueadores de canal, uma falha de build pode se tornar um alerta importante que exige intervenção de mantenedores
  • Como prova de conceito para o caso do xz, foi aberto um pull request no repositório nixpkgs
  • Esse método exige a suposição de que o pacote alvo seja reproduzível bit a bit
    • de 2017 a 2023, foram amostradas 17 revisões de nixpkgs-unstable, recompilando com nix-build --check as derivações de saída não fixa de stdenv
    • em todas as revisões, xz era reproduzível bit a bit
    • em 12 das 17 revisões havia 1 ou 2 pacotes que compilavam, mas não eram reproduzíveis; por exemplo, gcc foi consistentemente não reproduzível de 2017 a 2021, e bash até 2019
  • Não é viável aplicar esse método a todos os pacotes de stdenv, mas no longo prazo ele pode ser ativado seletivamente para pacotes com boa reprodutibilidade
  • O problema trusting trust de Ken Thompson continua sendo um limite teórico dessa abordagem
    • é preciso assumir que um xz não confiável da etapa de bootstrap não conseguiria contaminar indiretamente o build de xz-after-bootstrap a ponto de fazer os artefatos parecerem idênticos
    • esse tipo de ataque pode ser extremamente complexo, mas o método parte dessa suposição
  • Esse método de detecção só funciona quando a alteração no tarball afeta o artefato final
    • como os executáveis do NixOS não continham o backdoor, este caso específico não teria sido detectado se a condição não tivesse sido ativada separadamente

1 comentários

 
GN⁺ 2025-03-24
Opiniões no Hacker News
  • Vale destacar que o NixOS e os builds reproduzíveis não detectaram o backdoor do xz. Na prática, o NixOS também distribuiu aos usuários builds maliciosos do xz; o código malicioso só não funcionou porque não mirava o NixOS
    Um desenvolvedor do NixOS também disse: “quando o backdoor veio a público, fiquei surpreso ao saber que uma versão maliciosa do xz havia sido distribuída aos nossos usuários”. Como sempre, teoria e realidade são coisas diferentes, e o que tornou o xz possível não foi tanto uma vulnerabilidade técnica, mas sim vulnerabilidades humanas do mundo real. A comunidade tem dificuldade em admitir que nem sempre é possível corrigir esse tipo de problema apenas com software melhor

    • A configuração declarativa do Nix é bastante útil para aumentar a resistência a ataques de várias formas, mas ainda há muito potencial não explorado. Pessoalmente, eu priorizaria implementar contêineres descartáveis de granularidade fina, e o Guix já tem esse recurso
      Se todos os processos puderem ser executados com permissões limitadas, sem acesso nem a ~/ exceto aos diretórios necessários para a tarefa, seria possível impedir, por exemplo, que um pacote pip malicioso roubasse chaves SSH. Ainda assim, acho que o motivo de o backdoor do xz não ter funcionado no NixOS foi a peculiar estrutura de sistema de arquivos não FHS do NixOS
    • Mesmo assim, o artigo foi bom. O NixOS propõe uma solução técnica para impedir artefatos de build separados do código-fonte, ou seja, artefatos não reproduzíveis, e o backdoor do xz estava escondido justamente dentro de um artefato de build
    • Sim. O título parecia sugerir que o método do Nix talvez tivesse detectado o backdoor, mas, na prática, o texto parece mais uma proposta de como mudar o Nix para conseguir detectar esse tipo de backdoor
    • Gostei do fato de ter enfatizado que builds reproduzíveis não chegaram a capturar de fato o backdoor do xz antes de outros caminhos de descoberta
      Hoje em dia, a realidade concreta é frequentemente desdenhada quando não combina com os sentimentos ou valores pessoais de alguém sobre a realidade objetiva. Tenho meus valores pessoais, mas não acho que a realidade concreta seja menos importante do que meus sentimentos em relação a ela. Como eu gostava de dizer, a diferença entre teoria e prática é que, em teoria, as duas são iguais, mas na prática não são. Espero que a constatação de que os builds reproduzíveis do NixOS poderiam ter detectado o ataque ao xz, mas não detectaram, leve a avanços na análise desses builds para encontrar outros ataques mais rapidamente no futuro
    • O motivo é meio engraçado. O bootstrap do NixOS baixa código-fonte, e esse código vem em um tarball compactado com xz
  • Tenho a impressão de que o autor ficou com uma visão estreita demais por causa da forma como isso aconteceu por acaso desta vez. O caso Jia Tan é uma amostra única, então é míope achar que só aquele método é possível
    Dá para imaginar muitos cenários em que a defesa proposta aqui não funcionaria. Mesmo como usuário do Nix, acho pouco provável que o NixOS tivesse detectado isso. Até porque, na prática, não detectou. Eu acabei de dizer que da próxima vez pode acontecer de outro jeito, mas também é tolice confiar no Nix sem evidências

  • Aqui o NixOS não é muito relevante. O backdoor do xz mirava especificamente Red Hat e Debian
    Pela mesma lógica, seria igualmente relevante dizer que o backdoor do xz não afetou o Windows. Ironicamente, esse backdoor acabou sendo descoberto por um funcionário da Microsoft, um ponto que costuma ser ignorado

    • Um NixOS ou Guix um pouco melhorado teria detectado automaticamente esse backdoor no momento em que ele entrou no repositório. Por isso é relevante
  • O texto diz que as distribuições deveriam obter o código-fonte diretamente de um sistema de controle de versão, por exemplo do Github, em vez de usar os tarballs tradicionais de instalação
    Mas não entendo bem o que isso resolve. Um mantenedor mal-intencionado não poderia simplesmente adicionar um bloco binário diretamente ao repositório de código-fonte? O autor parece confiar no Github como se ele verificasse o código, mas é claro que o Github não faz essa verificação

    • O que essa abordagem resolve é o problema frequente de “o que foi revisado” ser diferente do “código-fonte usado para compilar o software”
      Builds reproduzíveis verificados poderiam ter ajudado a evitar casos como o comprometimento do xz utils e a adulteração do SolarWinds Orion, e isso vale bastante o esforço
    • Essa crítica não é muito adequada. Ao compilar a partir do código-fonte, você e todos os demais podem inspecionar o código; ao compilar a partir do tarball fornecido, o processo do autoconf altera arquivos, então ele fica essencialmente diferente do código-fonte original
      Se você baixa um binário de um release do Github e o executa, só resta confiar totalmente no mantenedor. O Nix só faz isso para pacotes de código fechado
  • O argumento se apoia no fato de que o mantenedor do XZ escondeu código malicioso em um tarball que não havia sido registrado no Git
    O autor mostra que o Nix pode ser configurado para gerar um tarball a partir do Git e colocá-lo no build binário. Mas não entendo por que isso seria um recurso que exige Nix ou NixOS. Qualquer sistema de build, incluindo os usados em RPMs ou Debs, pode ser configurado para gerar um tarball como etapa intermediária. De fato, o Debian trabalha há muito tempo com builds reproduzíveis como uma meta importante. https://wiki.debian.org/ReproducibleBuilds

    • O texto, na verdade, cita o projeto reproducible-builds na seção “Leveraging bitwise reproducibility”. O ponto do artigo é duplo
      Primeiro, o processo de build do NixOS precisava do xz cedo demais no bootstrap para conseguir fazer um build completo a partir do código-fonte do xz. Segundo, ele propõe ajustes no nixpkgs para detectar automaticamente se dependências do nixpkgs necessárias no início do bootstrap foram comprometidas. Outros ecossistemas, claro, também podem tentar builds completos a partir do código-fonte e encontrar divergências. O ponto central do texto é que o nixpkgs atualmente não consegue fazer isso
  • Se for para focar em um caso que o NixOS poderia ter evitado, é melhor olhar para o incidente da CrowdStrike. Só o fato de poder inicializar com a configuração de ontem quando a de hoje não funciona já teria mitigado a maior parte dos danos

    • Mas isso é um problema do Windows, que não tem flexibilidade de boot. O Ubuntu também consegue fazer isso sobre ZFS
  • Se você usa um framework em que confia, ele é seguro até o momento em que esse framework for atacado. O backdoor do xz pode até ter sido descoberto, mas não foi projetado com o objetivo de funcionar no ecossistema Nix.
    Algum dia, se um desenvolvedor central do Nix for um espião ou algo parecido acontecer, também surgirão ataques voltados ao ecossistema Nix. Eu preferiria que não respondessem como se o Nix fosse intrinsecamente seguro. Se o Nix for atacado com sucesso em um ou dois anos, vai dar vontade de ir atrás e fazer reconhecerem que estavam errados

    • O critério não é, nem nunca foi nem será, segurança absoluta. Esse é um critério impossível, que nada consegue satisfazer. Ainda assim, é objetivamente verdadeiro que o software de hoje é, em geral, mais seguro do que o de 30 anos atrás.
      A afirmação mais forte é: “o Nix é mais difícil e mais caro de atacar do que sistemas de build tradicionais”. Portanto, se você encontrar uma forma barata de atacar o Nix, aí sim venha falar. Até lá, no nível técnico, dizer que o Nix é mais difícil de atacar do que sistemas alternativos é no mínimo plausível e, na prática, muito provavelmente verdadeiro
    • O backdoor não tinha o Nix como alvo, mas, para não ser exposto, ele precisava não levantar suspeitas também enquanto fosse compilado no Nix
  • O NixOS vai um pouco além, mas a maioria das outras distribuições também compila tudo a partir do código-fonte, verifica criptograficamente que o código-fonte usado não foi adulterado e tem dependências com versões especificadas entre pacotes. O Debian também tem builds reprodutíveis.
    O problema é que o sistema de build não removia os arquivos-objeto pré-compilados antes de compilar a partir do código-fonte. Mesmo corrigindo isso, se ninguém inspecionar o código-fonte, é possível adicionar quantos backdoors quiser; nem o NixOS nem outras distribuições conseguem impedir isso

  • É uma excelente análise técnica, mas o título está errado e induz ao erro. Mesmo que se possa dizer que “tecnicamente está correto”, na melhor das hipóteses ele está mais próximo de significar que um backdoor foi inserido.
    Isso mostra bem a necessidade de ferramentas de gerenciamento de build que vão além das camadas de filesystem em união. Por exemplo, testes devem ser rastreados e impedidos de contaminar os artefatos de build. É preciso criar um grafo de rastreamento causal de como arquivos afetam arquivos durante o processo de build, tornar esse grafo explícito e então aplicá-lo ou relatar diferenças em relação ao grafo de rastreamento anterior

  • Certo. Esconder o backdoor certamente teria ficado mais difícil. Mas está longe de ser impossível.
    Se quiser, sempre dá para esconder um backdoor dentro do código-fonte. Apenas exige mais esforço para fazê-lo parecer um bug plausível, e a chance de ser detectado também fica maior