
إثبات مفهوم لاستغلال CVE-2026-31431، وهو تصعيد صلاحيات محلي في نواة لينكس عبر splice() وخيارات مأخذ مُصممة بعناية، مما يتيح كتابة عشوائية في ذاكرة النواة لاستبدال ملفات setuid الثنائية.
copyFail.py هو استغلال إثبات مفهوم (PoC) يوضح ثغرة تصعيد صلاحيات محلي في نواة لينكس. من خلال إساءة استخدام استدعاء النظام splice() مع خيارات مقبس معدّة بعناية، يكتسب الاستغلال القدرة على إتلاف ذاكرة النواة بشكل عشوائي. يستخدم هذه القدرة لاستبدال أي ملف ثنائي setuid على القرص بملف ELF مخصص، ثم يشغّل الملف المعدّل للحصول على صلاحيات مرتفعة.
نوع الهجوم: تصعيد صلاحيات محلي (LPE) التأثير: وصول كامل لقشرة الجذر (عند استهداف ملفات ثنائية setuid-root) التعقيد: منخفض (سكربت واحد، بدون تبعيات خارجية)
الاستخدام:
./copyFail_cleaned.py [target_binary] # الافتراضي: /usr/bin/su
كان الإصدار الأصلي من PoC يحدد /usr/bin/su بشكل ثابت، لكن الثغرة الأساسية في النواة تنطبق على أي ملف — يمكن للمهاجم استبدال أي ملف ثنائي setuid يختاره. النسخة المنظفة تقبل وسيط سطر أوامر اختياري لتحديد الهدف، مما يجعل هذا السلوك صريحًا.
توفر نواة لينكس استدعاء النظام splice() الذي ينسخ البيانات بين واصفي ملفات بالكامل داخل النواة — دون نسخ البيانات إلى مساحة المستخدم أولاً. يُسمى هذا "إدخال/إخراج صفر النسخ" ويُستخدم للأداء في التطبيقات عالية الإنتاجية مثل خوادم الويب والوكلاء العكسيين.
تنشأ الثغرة لأن النواة لا تتحقق بشكل صحيح من الحالة الداخلية لبعض هياكل المقابس عندما يُستخدم splice() على مقابس PACKET التي تم تكوينها بخيارات مقبس غير مدعومة على مستوى البروتوكول. من خلال صياغة هذه الخيارات بعناية، يمكن للمهاجم إتلاف مؤشرات النواة، ثم استخدام splice() لإعادة توجيه عمليات كتابة ذاكرة النواة إلى مواقع عشوائية.
بعبارات بسيطة: تمتلك النواة اختصارًا لنقل البيانات داخليًا، وهذا الاستغلال يخدع ذلك الاختصار لكتابة البيانات في المكان الخاطئ — تحديدًا، في النسخة الموجودة في الذاكرة من ملف نظام ثنائي.
يتبع الاستغلال تسلسلًا واضحًا من الخطوات:
ينشئ الاستغلال مقبس PACKET خام (AF_PACKET / SOCK_RAW) ويربطه بعناوين مشوّهة عمدًا. ثم يستدعي setsockopt() باستخدام SOL_PNIO (مستوى البروتوكول 279)، وهو ثابت على مستوى Solaris لا تتعرف عليه لينكس.
لماذا هذا مهم: عندما تواجه النواة مستوى بروتوكول غير مدعوم في setsockopt()، تنتقل إلى معالج عام لا يتحقق من البيانات بشكل صحيح. هذا يترك ذاكرة النواة في حالة غير متناسقة وقابلة للاستغلال.
بعد إنشاء اتصال مقبس عبر accept()، يرسل الاستغلال رسائل تحكم (ancillary) معدّة باستخدام sendmsg(). تحتوي هذه الرسائل على رؤوس أطوال غير متطابقة عمدًا — بعضها يدّعي أنه أقصر مما هو عليه، والبعض الآخر يدّعي أنه أطول.
لماذا هذا مهم: يستخدم محلل رسائل التحكم في النواة هذه الأطوال لحساب المؤشرات. الأطوال غير المتطابقة تجعل المحلل يقرأ أو يكتب خارج الحدود المقصودة، مما يتلف هياكل النواة المجاورة — تحديدًا، المؤشرات التي سيتبعها splice() لاحقًا.
ينشئ الاستغلال أنبوبًا (pipe) ويستدعي splice() مرتين:
/usr/bin/su إلى الأنبوب.لماذا هذا مهم: يتبع استدعاء splice() مؤشرات النواة المتلفة المزروعة في الخطوة 2، مما يجعله يكتب البيانات إلى عنوان ذاكرة نواة يختاره المهاجم بدلاً من مخزن المقبس المؤقت. هذا يمنح الاستغلال بدائية كتابة عشوائية في ذاكرة النواة.
الحمولة في الاستغلال هي ملف ELF قابل للتنفيذ بحجم 160 بايت (انظر أدناه). كل استدعاء لدالة الاستغلال يكتب 4 بايتات بالضبط. لذا تعمل الحلقة الرئيسية 40 مرة (160 / 4 = 40)، في كل مرة:
بعد كل التكرارات الأربعين، تم استبدال أول 160 بايت من /usr/bin/su.
أخيرًا، يشغّل الاستغلال os.system("su"). تحمّل النواة ملف /usr/bin/su المعدّل من ذاكرة التخزين المؤقت للصفحات (التي تحتوي الآن على شيفرة القشرة الخاصة بالمهاجم بدلاً من ملف su الحقيقي)، وتنفذ شيفرة القشرة.
تتفكك الحمولة المدمجة إلى ملف ELF قابل للتنفيذ بحجم 160 بايت لمعمارية x86-64 يحتوي على شيفرة القشرة التالية:
; محاولة استدعاء النظام 105 (execveat) — قد لا يكون متاحًا على النوى الأقدم
xor eax, eax
xor edi, edi
mov al, 0x69 ; استدعاء النظام 105
syscall
; البديل: استدعاء النظام 59 (execve) — المسار الموثوق
lea rdi, [rip+0xf] ; RDI = مؤشر إلى "/bin/sh"
xor esi, esi ; RSI = NULL (envp)
push 0x3b ; رقم استدعاء النظام 59
pop eax
cdq ; RDX = NULL (argv)
syscall
; الخروج بشكل نظيف
xor edi, edi
push 0x3c ; استدعاء النظام 60 (exit)
pop eax
syscall
; قسم البيانات: "/bin/sh\0\0\0"
ما يفعله: يشغّل /bin/sh بدون وسائط، ويرث صلاحيات العملية التي شغّلت su. إذا كان الاستغلال يعمل كجذر (أو بصلاحيات تمنح su وصول الجذر)، فإن القشرة الناتجة تكون قشرة جذر.
ما لا يفعله:
هذا تصعيد صلاحيات مباشر لمرة واحدة، بما يتوافق مع PoC بحثي.
copyFail.py
├── hex_to_bytes() — مفكك سلسلة سداسية عشرية
├── exploit_splice() — الاستغلال الأساسي (إعداد المقبس + الإتلاف + splice)
│ ├── المرحلة 1: إنشاء مقبس PACKET، ربط، setsockopt (SOL_PNIO)
│ ├── المرحلة 2: اتصال accept()
│ ├── المرحلة 3: sendmsg() برسائل تحكم معدّة
│ ├── المرحلة 4: pipe() + splice() لإتلاف ذاكرة النواة
│ └── المرحلة 5: محاولة recv() (تثبيت الإتلاف)
└── الحلقة الرئيسية:
├── فتح /usr/bin/su (للقراءة فقط)
├── تفكيك الحمولة المدمجة (zlib → ELF بحجم 160 بايت)
├── حلقة: حقن 4 بايتات لكل تكرار (40 تكرارًا إجمالاً)
└── تنفيذ su المعدّل → قشرة جذر
يفتح الاستغلال /usr/bin/su مع O_RDONLY (للقراءة فقط). لا يحتاج صلاحية الكتابة لأن الإتلاف يحدث عبر ذاكرة التخزين المؤقت للصفحات في النواة — النسخة الموجودة في الذاكرة من الملف التي تستخدمها النواة قبل كتابة التغييرات إلى القرص. تتجاوز الكتابة القائمة على splice() صلاحيات الملفات العادية تمامًا، وتذهب مباشرة إلى ذاكرة النواة.
/usr/bin/su معدّل (قارن التجزئة مع مدير الحزم)setsockopt() غير متوقعة بمستويات بروتوكول غير معروفةsplice() على واصفات ملفات غير عاديةsysctl لتقييد إنشاء مقابس AF_PACKET للمستخدمين غير المميزينcopyFail.py (كما تم توزيعه)copyFail_cleaned.py (نفس الدليل)net/packet/، fs/splice.c، net/core/sock.cهذا التقرير لأغراض البحث الأمني والتحليل الدفاعي فقط.
| الثابت | القيمة | الغرض في الاستغلال |
|---|
AF_PACKET | 17 | عائلة مقابس الحزم الخام |
SOCK_RAW | 3 | نوع المقبس الخام |
SOL_PNIO | 279 | مستوى بروتوكول غير مدعوم (Solaris) |
MSG_DONTWAIT | 0x400 | علامة sendmsg غير الحاجبة |