Em novembro, o pacote npm event-stream foi explorado por meio de uma dependência maliciosa, flatmap-stream. Todo o calvário foi descrito aqui e o foco deste post é usá-lo como um estudo de caso para engenharia reversa de JavaScript. As 3 cargas úteis associadas ao flatmap-stream são simples o suficiente para serem fáceis de escrever e complexas o suficiente para serem interessantes. Embora não seja essencial entender a história por trás desse incidente para entender esta postagem, farei suposições que podem não ser óbvias se você não estiver familiarizado com os detalhes.
A engenharia reversa da maioria dos JavaScript é mais direta do que executáveis binários que você pode executar no seu sistema operacional de desktop – afinal, o código-fonte está bem na sua frente – mas o código JavaScript que é projetado para ser difícil de entender geralmente passa por algumas passagens de ofuscação para obscurecer sua intenção. Parte dessa ofuscação vem do que é chamado de “minificação”, que é o processo de reduzir a contagem geral de bytes da sua fonte o máximo possível para fins de economia de espaço. Isso envolve encurtar variáveis para identificadores de caracteres únicos e traduzir expressões como true para algo mais curto, mas equivalente, como !0. A minimização é exclusiva do ecossistema do JavaScript por causa de suas origens no navegador da web e é vista ocasionalmente em pacotes do Node devido à reutilização de ferramentas e não tem a intenção de ser uma medida de segurança. Para reversão básica de técnicas comuns de minimização e ofuscação, confira a ferramenta unminify do Shape. Os passes de ofuscação dedicados podem vir de ferramentas projetadas para ofuscar ou são executados manualmente pelo desenvolvedor
O primeiro passo é obter a fonte isolada para análise. O pacote flatmap-stream foi criado especificamente para parecer inocente, 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 na 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 do post, nos referiremos à fonte anexada como carga útil A. Abaixo está a fonte formatada da carga útil A.
Primeiras coisas primeiro: NUNCA EXECUTE CÓDIGO MALICIOSO (exceto em ambientes isolados). Eu escrevi minhas próprias ferramentas para me ajudar a refatorar código dinamicamente usando o conjunto de analisadores Shift e transformadores JavaScript , mas você pode usar um IDE como o Visual Studio Code para acompanhar esta postagem.
Ao fazer engenharia reversa do JavaScript, é importante manter o malabarismo mental no mínimo. Isso significa livrar-se de quaisquer expressões ou declarações que não agregam valor imediato e também reverter a secura de qualquer código que tenha sido otimizado automática ou manualmente. Como estamos analisando estaticamente o JavaScript e rastreando a execução em nossas cabeças, quanto mais profunda sua pilha mental se torna, maior a probabilidade de você se perder.
Uma das coisas mais simples que você pode fazer é desminificar variáveis que estão recebendo 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 você deseja renomear). Depois disso, vemos uma definição de função, e, que parece simplesmente decodificar uma string hexadecimal.
A primeira linha de código interessante parece importar um arquivo que vem do resultado da função e decodificando a string "2e2f746573742f64617461"
É extremamente comum que o JavaScript ofusque deliberadamente qualquer valor literal de string para que qualquer pessoa que dê uma olhada rápida não seja alertada por strings ou propriedades particularmente ameaçadoras à vista. A maioria dos desenvolvedores reconhece que esse é um obstáculo muito baixo, então você frequentemente encontrará codificações trivialmente irreversíveis, e isso não é diferente aqui. A função e simplesmente inverte strings hexadecimais e você pode fazer isso manualmente por meio de uma ferramenta online ou com sua própria função conveniente. Mesmo que você tenha certeza de que entendeu o que a função e está fazendo, ainda é uma boa ideia não executá-la (mesmo que você a extraia) com uma entrada encontrada em um arquivo malicioso, porque você não tem garantias de que o invasor não encontrou uma vulnerabilidade de segurança que é acionada pelos dados.
Depois de reverter essa sequência, vemos que o script está incluindo um arquivo de dados, './test/data', que está localizado no pacote npm distribuído.
Depois de renomear n para dados e desofuscar chamadas de e(n[2]) para e(n[9]), começamos a ter uma ideia melhor do que estamos lidando aqui.
Também é fácil ver por que essas sequências de caracteres foram ocultadas. Encontrar qualquer referência à descriptografia em uma biblioteca de flatmap simples seria um sinal claro de que algo está 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 para createDecipher, aqui, é a senha usada para descriptografar. Agora podemos renomear esse argumento e os seguintes valores de retorno para nomes sensatos com base na API. Toda vez que encontramos uma nova peça do quebra-cabeça, é importante imortalizá-la por meio de um refator ou um comentário, mesmo que seja uma variável renomeada que pareça trivial. É muito comum que, ao mergulhar em código estrangeiro por horas, você se perca, se distraia ou precise voltar atrás por causa de alguma refatoração errônea. Usar o git para salvar pontos de verificação durante uma refatoração também é valioso, mas deixarei essa decisão para você. O código agora se parece com o seguinte, com a função e excluída porque ela não é mais usada junto com a instrução if (!o) {... porque ela não agrega valor à análise.
Você também notará que renomeei f para newModuleInstance. Com um código tão curto, não é crítico, mas com um código que pode ter centenas de linhas, é importante que tudo seja o mais claro possível.
Agora a carga útil A está amplamente desofuscada e podemos analisá-la para entender o que ela faz.
A linha 3 importa nossos dados externos.
A linha 4 pega uma senha do ambiente. process.env permite que você acesse variáveis de dentro de um script de nó e npm_package_description é uma variável que o npm, o gerenciador de pacotes do nó, define quando você executa scripts definidos em um arquivo package.json.
A linha 5 cria uma instância decipher com o valor de npm_package_description como senha. Isso significa que a carga criptografada só pode ser descriptografada quando esse script é executado via npm e está sendo executado para um projeto específico que tem, 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 em nosso arquivo externo e o armazenam na variável “decrypted“
As linhas 8 a 11 criam um novo módulo e então alimentam os dados descriptografados no método não documentado _compile. Este módulo então exporta o segundo elemento do nosso arquivo de dados externo. module.exports é o mecanismo do nó de expor dados de um módulo para outro, então newModuleInstance.exports(data[1]) está expondo uma segunda carga criptografada encontrada em nosso arquivo de dados externo.
Neste ponto, temos dados criptografados que só podem ser descriptografados com uma senha encontrada em um package.json em algum lugar e cujos dados descriptografados são inseridos no método _compile. Agora ficamos com um problema: como descriptografar dados cuja senha é desconhecida? Esta é uma pergunta nada trivial: se fosse fácil forçar a criptografia aes256, teríamos mais problemas do que um pacote npm sendo assumido. Felizmente, não estamos lidando com um conjunto completamente desconhecido de senhas possíveis, apenas com qualquer string que tenha sido inserida em um package.json em algum lugar . Os arquivos package.json se originaram como o formato de arquivo para metadados de pacotes npm, então podemos começar pelo registro oficial do npm. Felizmente, há um pacote npm que nos fornece um fluxo de todos os metadados do pacote .
Não há garantia de que nosso arquivo de destino esteja localizado em um pacote npm, muitos projetos não npm usam package.json para armazenar configurações para ferramentas baseadas em nó, e as descrições do package.json podem mudar de versão para versão, mas é um bom lugar para começar. É possível descriptografar essa carga com várias chaves, resultando em algo incompreensível, então precisamos de alguma forma de validar nossa carga descriptografada durante esse processo de força bruta. Como estamos lidando com algo que é alimentado para Module.prototype._compile , que alimenta vm.runInThisContext, podemos razoavelmente assumir que a saída é JavaScript e podemos usar qualquer número de analisadores JavaScript para validar os dados. Se nossa senha falhar ou se for bem-sucedida, mas nosso analisador gerar um erro, precisamos passar para o próximo package.json. Convenientemente, a Shape Security criou seu próprio conjunto de analisadores JavaScript para uso em ambientes JavaScript e Java. O script de força bruta usado está aqui:
Após executar isso por 92,1 segundos e processar 740543 pacotes, obtemos nossa senha – “A Secure Bitcoin Wallet” – que decodifica com sucesso a carga útil incluída abaixo:
Isso foi sorte. O que poderia ter sido um problema monstruoso de força bruta acabou precisando de menos de um milhão de iterações. O pacote afetado com a chave em questão acabou sendo o aplicativo cliente da carteira bitcoin Copay. As próximas duas cargas úteis se aprofundam no aplicativo em si e, dado que o aplicativo de destino é centrado no armazenamento de bitcoins, você provavelmente pode imaginar onde isso pode estar indo.
Se você achar tópicos como esse interessantes e quiser ler uma análise sobre as outras duas cargas úteis ou ataques futuros, não deixe de "curtir" esta publicação ou me avise no Twitter em @jsoverson .