Xz/liblzma: explicação da ofuscação na etapa Bash
(gynvael.coldwind.pl)- O backdoor do xz/liblzma não envolve apenas o payload binário final; a etapa Bash executada durante o build também fica escondida por várias camadas de extração e decodificação, o que eleva bastante a dificuldade de análise
- As versões afetadas são xz/liblzma 5.6.0 e 5.6.1, e o script ofuscado junto com o payload binário está contido em dois arquivos que parecem ser arquivos de teste
- O Stage 0 começa em
m4/build-to-host.m4, recupera um fluxo xz que parece corrompido e extrai comxz -do script Stage 1 para executá-lo - O Stage 1 recorta dados de
good-large_compressed.lzma, aplica substituições e então executa o script Bash Stage 2; na 5.6.1 foi adicionado código que verifica 5 vezes se a execução está ocorrendo em Linux - O Stage 2 usa file carving, cifra de substituição e uma decodificação variante de RC4 baseada em AWK para inserir um arquivo
.ono processo de build e linkedição; a 5.6.1 também inclui uma estrutura extensível para executar scripts adicionais no futuro
Escopo da etapa Bash do backdoor xz/liblzma
- Andres Freund anunciou na mailing list oss-security a descoberta do backdoor no xz/liblzma, que afeta o servidor OpenSSH
- O foco da análise não é o backdoor binário em si, mas as etapas iniciais em Bash executadas antes dele e o método de ofuscação usado
- O fluxo em Bash vai do Stage 0 ao Stage 2, e também revela parcialmente um possível Stage 3 que parece não estar totalmente implementado
- As etapas ofuscadas/criptografadas e o backdoor binário posterior estão escondidos em dois arquivos de teste
tests/files/bad-3-corrupt_lzma2.xztests/files/good-large_compressed.lzma
- As versões afetadas são 5.6.0 e 5.6.1, com pequenas diferenças entre elas
Stage 0: recuperação de um fluxo xz que parece um arquivo de teste corrompido
- O ponto de partida é um trecho de código em
m4/build-to-host.m4, que aparentemente é executado em algum momento do processo de build - O pipeline principal conecta
sed,trexz -dpara extrair o script Stage 1 - Depois de ler os bytes de
tests/files/bad-3-corrupt_lzma2.xz, ele substitui alguns bytes comtr "\t \-_" " \t_\-"0x09tab vira0x20espaço0x20espaço vira0x09tab0x2dhífen vira0x5funderscore0x5funderscore vira0x2dhífen
- Essa substituição serve para transformar
bad-3-corrupt_lzma2.xznovamente em um fluxo xz válido - No fim, o fluxo recuperado é descompactado com
xz, o erro é ignorado e o script Stage 1 obtido é executado imediatamente- O fluxo parece truncado, mas isso não causa problema porque a saída relevante já foi emitida
Stage 1: extraindo o Stage 2 de good-large_compressed.lzma
- O Stage 1 é o pequeno arquivo Bash que começa com
####Hello####mencionado no e-mail de Andres Freund - A primeira diferença entre as duas versões está em bytes aleatórios dentro do comentário da segunda linha
- 5.6.0:
86 F9 5A F7 2E 68 6A BC - 5.6.1:
E5 55 89 B7 24 04 D8 17
- 5.6.0:
- Na 5.6.1 foi adicionada uma verificação de Linux no formato
[ ! $(uname) = "Linux" ] && exit 0, repetida 5 vezes - A extração do Stage 2 começa em uma longa linha
export i=..., cujo valor depois é usado quase como uma função, chamada viaeval $i - O fluxo de extração é um pipeline que encadeia continuamente stdout e stdin
- Primeiro,
good-large_compressed.lzmaé descompactado comxz -dc eval $irepete chamadas aheadem um padrão que descarta 1024 bytes e produz 2048 bytes- No final do arquivo, a 5.6.0 produz 724 bytes e a 5.6.1 produz 939 bytes
- Primeiro,
- Essa cadeia de
headremove regiões vazias de baixa entropia e concatena apenas as áreas que contêm dados reais - Em seguida,
tail -c +31233descarta a parte inicial- Nessa parte inicial está escondido o backdoor binário que será extraído na etapa seguinte
- Na 5.6.0, o efeito é descartar os primeiros 31264 bytes; na 5.6.1, os primeiros 31232 bytes
- Depois,
tré usado como uma cifra de substituição simples, com chaves de mapeamento diferentes para 5.6.0 e 5.6.1- 5.6.0:
tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" - 5.6.1:
tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
- 5.6.0:
- Os dados substituídos são então descompactados novamente com
xz -F raw --lzma1 -dc, e o Stage 2 resultante é executado na hora
Stage 2: modificação do processo de build e mecanismo de expansão
- O Stage 2 é o arquivo
infected.txtanexado por Andres Freund no e-mail original, e é o script Bash em que ocorre a modificação real do processo de compilação - Do ponto de vista de ofuscação, o Stage 2 contém três trechos interessantes, dos quais dois aparecem apenas na 5.6.1
- Os dois trechos da 5.6.1 são quase idênticos entre si e procuram, em
tests/files/, arquivos com assinaturas específicas- O Fragment 1 procura
~!:_ We|_!{ - - O Fragment 3 procura
jV!.^%e%.R.1Z
- O Fragment 1 procura
- Quando a assinatura é encontrada, ele extrai o nome do arquivo e o offset, e recorta o intervalo de bytes entre a primeira e a segunda assinatura
- Os dados recortados passam por uma decodificação por substituição com a chave
trusada na 5.6.0, depois são descompactados comxz -F raw --lzma2 -dce executados comeval - Nos arquivos TAR investigados das versões 5.6.0 e 5.6.1, não havia arquivos com essas assinaturas
- Essa estrutura parece um sistema de expansão e patch, permitindo executar scripts futuros apenas adicionando novos arquivos de teste, sem precisar modificar novamente os arquivos de teste que já transportam o payload existente
Stage 2: extração do arquivo .o do backdoor binário
- Outro ponto central do Stage 2 é o código que extrai um arquivo
.oe o injeta no processo de compilação e linkedição - O artefato produzido pela extração é
liblzma_la-crc64-fast.o, que corresponde ao backdoor binário final - Como nas etapas anteriores, a extração conecta várias ferramentas padrão em um pipeline
good-large_compressed.lzmaé descompactado comxz -dc- A mesma cadeia de
headusada viaeval $ino Stage 1 é reutilizada sed "s/\(.\)/\1\n/g"adiciona uma quebra de linha após cada byte, convertendo a entrada em uma linha por byte
- O script AWK seguinte executa uma decodificação parecida com RC4 sobre o fluxo de entrada
- O array de chaves é gerado com
((i * 7) + 5) % 256 - A 5.6.0 pula os primeiros 4096 bytes do fluxo PRNG de saída
- A 5.6.1 pula os primeiros 8192 bytes
- O RC4 original usa XOR, mas este código usa soma no formato
(v + k) % 256
- O array de chaves é gerado com
- Os dados decodificados são então descompactados com
xz -dc --single-stream - Depois disso,
headrecorta apenas o intervalo de bytes necessário e salva o resultado comoliblzma_la-crc64-fast.o- O tamanho do payload comprimido, porém parcialmente transformado, é de 88792 bytes na 5.6.0 e 88664 bytes na 5.6.1
- O intervalo final extraído tem cerca de 86 KB
A natureza da ofuscação feita apenas com ferramentas padrão
- Toda a etapa em Bash é composta apenas por ferramentas padrão de linha de comando
sedtrxzheadtailawkgrepcutexpr
- O payload fica escondido em arquivos binários que parecem arquivos de teste legítimos, e apenas as áreas necessárias são retiradas por file carving
- A ofuscação combina uma cifra de substituição simples com uma decodificação variante de RC4 baseada em AWK
- A execução se desenrola em 3 etapas, e a 5.6.1 inclui uma estrutura que permite no futuro executar scripts por meio da adição de arquivos de teste separados
- A estrutura toda mostra um esforço grande para parecer inofensiva e permanecer bem escondida; se este caso foi descoberto por acaso, fica a dúvida de quantos casos semelhantes ainda não foram encontrados
1 comentários
Comentários do Hacker News
Graças à explicação simplificada e à comparação com a imagem de ruído, deu para entender o que as pessoas querem dizer com sofisticação
Também vi no reddit que a abordagem de “sandboxing” foi quebrada por um único ponto, e dá para ver um ponto bem à esquerda na linha logo após o
#includehttps://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2...
https://old.reddit.com/r/linux/comments/1brhlur/xz_utils_bac...
+,+.,+, e aquele único ponto passa despercebidoSó um erro simples que faça um teste sempre falhar ou sempre passar já é dor de cabeça; dá para ver por que isso vira um bom alvo para comportamento malicioso
a) Na prática, quase ninguém compila esse pacote com cmake
b) Se você compilar com cmake e
-DENABLE_SANDBOX=landlock, o build simplesmente falha: https://i.imgur.com/7xbeWFx.pngEsse ponto não desativa o sandboxing; ele apenas impede que seja compilado com cmake. Se alguém de fato tivesse tentado compilar com cmake, teria visto o erro e percebido que havia algo errado, então não faz sentido considerar isso uma tentativa maliciosa de reduzir a segurança
“Se isso foi descoberto por acaso, quanto ainda resta que não foi descoberto?” é a pergunta mais importante
É improvável que Andres Freund tenha encontrado por acaso o único projeto open source popular com um backdoor implantado. Não seria surpreendente se já houvesse uma dúzia dessas coisas em circulação
O próximo atacante será muito menos descuidado em deixar rastros como aumento no tempo de execução
A ideia desconfortável aqui é que os testes unitários abriram o caminho para o ataque. Sem testes, teria sido muito mais difícil esconder isso
bad-3-corrupt_lzma2.xztem três streams, e o primeiro e o terceiro são streams xz válidos. Está escrito que o stream do meio tem cabeçalho de stream, cabeçalho de bloco, índice e rodapé de stream corretos, apenas os dados LZMA2 estão corrompidos, e que ele deve ser descompactado ao usar--single-streamAs strings
####Hello####e####World####fazem com que um resultado normal plausível apareça quando as instruções do README são de fato seguidas$ cat tests/files/bad-3-corrupt_lzma2.xz | xz -d --single-stream####Hello####Essas strings são comentários de shell, então não atrapalham a execução do payload
Por fim, mais tarde elas servem como marcadores para que uma regex encontre o arquivo sem referenciar diretamente o nome do arquivo nem usar as strings Hello/World reais
$ gl_am_configmake=\grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null``$ echo $gl_am_configmake./tests/files/bad-3-corrupt_lzma2.xzEssa verificação precisaria ser aplicada transitivamente, de modo que, quando uma distribuição Linux tentasse atualizar para uma nova versão da liblzma, o build também falhasse ou gerasse aviso por causa de uma nova dependência binária
Não sei quão comum é essa prática em builds de distribuições Linux. Se for comum, exigiria uma quantidade enorme de trabalho para organizar, e nem sei se isso é possível para começo de conversa. Com bazel parece possível, mas não sei quanto aos outros sistemas de build
Fico curioso se alguém já procurou no GitHub por um truque parecido com head | tail. É difícil acreditar que isso tenha sido inventado especificamente para este caso
Um arquivo
.shenorme mostra a licença, recebe a aceitação e depois dácatem si mesmo, passando por um pipe dehead/tailaté ocpiopara extrair os ativos reaisNão há uma resposta melhor, mas esse monte de bash obscuro em si já não parece suspeito?
Como em outras áreas do mundo do desenvolvimento, não dava para escrever de forma menos opaca, para deixar mais claro o que está acontecendo?
Eu sei que um mantenedor pode inserir código malicioso sem uma revisão tão rigorosa quanto a de um contribuidor externo, mas deveria haver um caminho melhor do que um monte de código “conciso”, algo que na prática parece uma ofuscação não intencional
O problema central é que, nos anos 80 e 90, havia inúmeros sistemas do tipo Unix, cada um com seus próprios defeitos e recursos ausentes, e os autores de software queriam minimizar as dependências de build
Por isso, muitas comunidades padronizaram a automação de build com scripts shell que funcionassem em qualquer lugar. Mas scripts shell eram dolorosos de escrever, e as pessoas passaram a gerar scripts shell com ferramentas como o pré-processador de macros M4
O resultado é que muitos projetos acabaram com enormes blocos opacos de scripts shell, para o caso de alguém querer executar o código no AIX ou em algum Unix antigo quebrado
Para se livrar desse matagal de shell quase impenetrável, seria preciso reduzir muito o número de plataformas suportadas, padronizar ferramentas de build mais limpas e construir mais da infraestrutura central em linguagens que não precisem de shell para builds portáveis
Mas isso é um trabalho gigantesco, e uma boa parte das bibliotecas C centrais é mantida por um ou dois voluntários não remunerados. Abandonar o suporte ao “Obscurnix-1997” também costuma ser uma decisão bastante controversa
Por isso, boa parte da infraestrutura central ainda está cercada por um pântano de scripts shell gerados por máquina de origem pouco clara
Um número considerável de ferramentas é construído dessa forma
O que parece suspeito são as chamadas repetidas a tr. Quando vejo isso, penso que alguém está tentando ser esperto — e aqui “esperteza” tem sentido negativo. Se eu fosse mantenedor, teria pedido uma explicação do que aquele código faz ao recebê-lo. Quase sempre há uma solução melhor do que esse tipo de encadeamento
O problema real é que não havia outro mantenedor para revisar esse código quando ele entrou. Um componente importante da stack dependia de uma pessoa só e, neste caso, essa pessoa era mal-intencionada
execem alguma coisa? Fico imaginando se o bash poderia ter algum tipo de “modo seguro”Dito isso, não consigo imaginar como o script configure do xv poderia executar bash nesse “modo seguro” hipotético, então retiro o que disse
https://github.com/tukaani-project/.github/issues/2
Todo o ecossistema C, incluindo ferramentas de build e utilitários Unix antigos, é uma bagunça de segurança esperando para ser explorada — e acabará sendo
Basta ver como é fácil quebrar tudo com um único ponto. Já passou da hora de as pessoas perceberem que não dá para apostar a segurança do mundo em C
Espero que usem Ada ou Rust com toolchains modernas
Não faço ideia de como isso passou por code review e foi mergeado. Se eu não estiver deixando algo passar, parece absurdamente descuidado
Além disso, estava fortemente ofuscado dentro de arquivos binários marcados como arquivos de teste, ou seja, arquivos de teste compactados com xz “good”/“bad”. Se você não soubesse o que procurar, não havia como perceber
Usar uma distribuição LTS pode oferecer alguma proteção. O Slackware parece usar lzma, ou seja,
tar.xz, nos pacotes, mas a última versão estável, exceto a-current, não tinha esse problemaSe quiser dar mais um passo na direção do software livre, o Hyperbola GNU também não tinha esse problema
Além disso, o Slackware
-currenttambém não linka osshdao xz e não usa systemd