payload A do flatmap-stream
Em novembro, o pacote npm event-stream foi explorado por meio de uma dependência maliciosa, flatmap-stream. Todo o ocorrido foi documentado aqui e o objetivo desta publicação é usá-lo como estudo de caso para a engenharia reversa de JavaScript. Os três payloads associados ao flatmap-stream são simples o bastante para serem fáceis de explicar e complexos o suficiente para serem interessantes. Embora não seja essencial conhecer o contexto desse incidente para entender esta publicação, farei algumas suposições que talvez não sejam óbvias para quem não estiver minimamente familiarizado com os detalhes.
A engenharia reversa da maioria dos códigos JavaScript é mais simples do que a de executáveis binários que você pode rodar no sistema operacional do seu computador — afinal, o código-fonte está bem diante de você —, mas códigos JavaScript criados para serem difíceis de entender costumam passar por algumas etapas de ofuscação para ocultar sua finalidade. Parte dessa ofuscação vem do que se chama de “minificação”, que é o processo de reduzir ao máximo o número total de bytes do código-fonte para economizar espaço. Isso envolve encurtar variáveis para identificadores de um único caractere e converter expressões como true em algo mais curto, porém equivalente, como !0. A minificação é praticamente exclusiva do ecossistema JavaScript devido às suas origens nos navegadores e, ocasionalmente, aparece em pacotes node por causa da reutilização de ferramentas, não tendo a finalidade de servir como medida de segurança. Para reverter técnicas comuns de minificação e ofuscação em casos básicos, confira a ferramenta unminify da Shape. Etapas específicas de ofuscação podem ser realizadas por ferramentas criadas para esse fim ou manualmente pelo desenvolvedor.
O primeiro passo é obter o código-fonte isolado para análise. O pacote flatmap-stream foi criado especificamente para parecer inofensivo, exceto por uma carga maliciosa incluída em apenas uma versão do pacote, a versão 0.1.1. Você pode ver rapidamente as alterações no código-fonte comparando a versão 0.1.2 e a versão 0.1.1 ou até mesmo alternando entre as URLs em duas abas. No restante da publicação, vamos nos referir ao código-fonte anexado como carga A. Abaixo está o código-fonte formatado da carga A.

Antes de mais nada: NUNCA EXECUTE CÓDIGO MALICIOSO (exceto em ambientes isolados). Desenvolvi minhas próprias ferramentas para me ajudar a refatorar código dinamicamente usando o conjunto Shift de analisadores sintáticos e transformadores de JavaScript mas você pode usar uma IDE como o Visual Studio Code para acompanhar esta publicação.
Ao fazer engenharia reversa de JavaScript, é importante reduzir ao mínimo o esforço mental. Isso significa eliminar quaisquer expressões ou instruções que não agreguem valor imediato e também desfazer o princípio DRY de qualquer código que tenha sido otimizado automática ou manualmente. Como estamos analisando o JavaScript estaticamente e acompanhando a execução mentalmente, quanto mais profunda for sua pilha mental, maior será a probabilidade de você se perder.
Uma das coisas mais simples que você pode fazer é desfazer a minificação das variáveis às quais são atribuídas propriedades globais como require e process, como nas linhas 3 e 4.
Você pode fazer isso com qualquer IDE que ofereça recursos de refatoração (geralmente pressionando “F2” sobre um identificador que deseja renomear). Depois disso, vemos a definição de uma função, e, que parece simplesmente decodificar uma string hexadecimal.

A primeira linha de código interessante parece importar um arquivo cujo caminho é obtido a partir do resultado da função e ao decodificar a string "2e2f746573742f64617461"
É extremamente comum que códigos JavaScript deliberadamente ofuscados ocultem todos os valores de string literais, para que ninguém que dê uma olhada rápida seja alertado por strings ou propriedades particularmente suspeitas claramente visíveis. A maioria dos desenvolvedores reconhece que esse é um obstáculo muito fácil de superar, por isso é comum encontrar codificações que podem ser desfeitas de forma trivial — e aqui não é diferente. A função e simplesmente inverte strings hexadecimais, e você pode fazer isso manualmente usando uma ferramenta on-line ou sua própria função utilitária. Mesmo que você tenha certeza de que entende o que a função e está fazendo, ainda é recomendável não executá-la (mesmo que você a extraia) com dados de entrada encontrados em um arquivo malicioso, pois não há garantia de que o invasor não tenha descoberto uma vulnerabilidade de segurança acionada por esses dados.
Após inverter essa string, vemos que o script está incluindo um arquivo de dados, './test/data', localizado no pacote npm distribuído.

Depois de renomear n para data e desofuscar as chamadas de e(n[2]) a e(n[9]), começamos a ter uma visão mais clara do que estamos analisando aqui.

Também é fácil entender por que essas strings foram ocultadas: encontrar qualquer referência à descriptografia em uma simples biblioteca de flatMap seria um indício claro de que há algo muito errado.
A partir daqui, vemos que o script está importando a biblioteca “crypto” do node.js e, depois de consultar as APIs, descobrimos que o segundo argumento de createDecipher, o neste caso, é a senha usada para descriptografar. Agora podemos renomear esse argumento e os valores de retorno seguintes com nomes adequados com base na API. Sempre que encontramos uma nova peça do quebra-cabeça, é importante registrá-la por meio de uma refatoração ou de um comentário, mesmo que seja apenas a renomeação de uma variável que pareça trivial. Ao passar horas analisando código alheio, é muito comum perder o ponto em que você estava, se distrair ou precisar voltar atrás por causa de alguma refatoração equivocada. Usar o git para salvar pontos de verificação durante uma refatoração também é útil, mas deixo essa decisão para você. Agora o código está como mostrado a seguir, com a função e removida porque não é mais usada, assim como a instrução if (!o) {..., pois ela não agrega valor à análise.

Você também notará que renomeei f para newModuleInstance. Em um código tão curto, isso não é essencial, mas, em um código que pode ter centenas de linhas, é importante que tudo seja o mais claro possível.
Agora, o payload A está em grande parte desofuscado, e podemos analisá-lo passo a passo para entender o que ele faz.
A linha 3 importa nossos dados externos.
A linha 4 obtém uma senha do ambiente. process.env permite acessar variáveis de dentro de um script Node, e npm_package_description é uma variável que o npm, o gerenciador de pacotes do Node, define quando você executa scripts especificados em um arquivo package.json.
A linha 5 cria uma instância de decifração usando o valor de npm_package_description como senha. Isso significa que a carga útil criptografada só pode ser descriptografada quando esse script é executado via npm e para um projeto específico que tenha, em seu package.json, um campo de descrição específico. Isso vai ser difícil.

As linhas 6 e 7 descriptografam o primeiro elemento do nosso arquivo externo e o armazenam na variável “decrypted“
As linhas 8 a 11 criam um novo módulo e, em seguida, fornecem os dados descriptografados ao método não documentado _compile. Esse módulo então exporta o segundo elemento do nosso arquivo de dados externo. module.exports é o mecanismo do Node para expor dados de um módulo a outro; portanto, newModuleInstance.exports(data[1]) está expondo uma segunda carga útil criptografada encontrada em nosso arquivo de dados externo.

Neste ponto, temos dados criptografados que só podem ser descriptografados com uma senha encontrada em algum package.json e cujos dados descriptografados são passados para o método _compile. Agora nos deparamos com um problema: como descriptografar dados quando a senha é desconhecida? Essa não é uma questão trivial; se fosse fácil quebrar a criptografia aes256 por força bruta, teríamos problemas maiores do que um pacote npm sendo tomado. Felizmente, não estamos lidando com um conjunto completamente desconhecido de possíveis senhas, mas apenas com qualquer string que tenha sido inserida em um package.json em algum lugar. Os arquivos package.json surgiram como o formato de arquivo para os metadados de pacotes npm, então podemos começar pelo registro oficial do npm. Felizmente, existe um pacote npm que nos fornece um fluxo com todos os metadados dos pacotes.

Não há garantia de que nosso arquivo-alvo esteja localizado em um pacote npm. Muitos projetos que não usam npm utilizam o package.json para armazenar configurações de ferramentas baseadas em Node, e as descrições do package.json podem mudar de uma versão para outra, mas esse é um bom ponto de partida. É possível descriptografar esse payload com várias chaves, gerando dados ininteligíveis, portanto precisamos de alguma forma de validar o payload descriptografado durante esse processo de força bruta. Como estamos lidando com algo que é passado para Module.prototype._compile que, por sua vez, repassa para vm.runInThisContext podemos presumir, com razoável segurança, que a saída é JavaScript e usar qualquer um dos diversos parsers de JavaScript para validar os dados. Se nossa senha falhar ou se funcionar, mas o parser gerar um erro, precisaremos passar para o próximo package.json. Por conveniência, a Shape Security desenvolveu seu próprio conjunto de parsers de JavaScript para uso em ambientes JavaScript e Java. O script de força bruta utilizado está aqui:

Após executar isso por 92,1 segundos e processar 740543 pacotes, chegamos à nossa senha — “Uma carteira de Bitcoin segura” — que decodifica com sucesso o payload incluído abaixo:

Foi um golpe de sorte. O que poderia ter se tornado um problema monstruoso de força bruta acabou exigindo menos de um milhão de iterações. O pacote afetado que continha a chave em questão era o aplicativo cliente da carteira de bitcoins Copay. Os próximos dois payloads exploram mais a fundo o próprio aplicativo e, considerando que o aplicativo-alvo é voltado ao armazenamento de bitcoins, você provavelmente já imagina aonde isso vai dar.
Se você acha interessantes assuntos como este e quer ler uma análise dos outros dois payloads ou de ataques futuros, não deixe de “curtir” esta publicação ou fale comigo no Twitter em @jsoverson.
Sobre o autor
Publicações relacionadas no blog

Entrega e proteção de agentes de IA em fábricas de IA: F5 para a arquitetura de referência NVIDIA OpenShell para agentes de IA seguros
As soluções da F5 ampliam a Arquitetura de Referência NVIDIA OpenShell para agentes de IA seguros.
Como medir – e minimizar – o custo total de propriedade do seu Kubernetes
O Kubernetes pode ser baixado e executado gratuitamente, mas sua manutenção está longe de não ter custos. Assim como outras grandes plataformas de código aberto que são “gratuitas” apenas em um sentido simplista, o Kubernetes quase certamente exigirá investimentos para ser implantado e operado.
As-Code: a pilha de TI contínua
Infraestrutura, configuração, pipeline, operações. De repente, tudo é “como código”. Lori MacVittie busca esclarecer a terminologia e identificar os diferentes componentes de uma stack contínua de TI.
O estado da entrega de aplicações em 2017: automação, APIs e DevOps
Explore o relatório State of Application Delivery de 2017. Leia o blog retrospectivo da F5 sobre o início da ascensão das redes declarativas e dos pipelines de DevOps.
DevOps Summit: onde a rede foi convidada para a festa
No DevOps Summit NY, falou-se muito não apenas sobre DevOps, mas também sobre contêineres, IoT e microsserviços. As sessões se concentraram não só na mudança cultural necessária para crescer em escala com uma abordagem DevOps, mas também fizeram questão de incluir...
F5 e Nutanix convergem em infraestrutura no .NEXT
Aplicações empresariais como Microsoft SharePoint e Exchange, juntamente com a VDI, continuam sendo a espinha dorsal da sua empresa. A produtividade, embora não seja tão atraente quanto o lucro, é um fator essencial para o sucesso das operações empresariais atuais. Deixar de...