
Linux LPE - هروب موثوق من السجن/الحاوية
إثبات مفهوم موثوق للهروب من حاوية / بيئة معزولة (jail) بدون صلاحيات، لنظامي CentOS / RHEL 10.
يعتمد على ثغرة تجزئة IPv6 التي أُصلحت الآن في __ip6_append_data() (أُغلقت في المصدر العلوي (upstream) عبر 38becddc، بدون CVE)، وهي تجاوز خطي داخل slab يصل إلى skb_shared_info في ذيل كائن head الخاص بالحزمة نفسها. يوثّق ملف README هذا سلسلة الاستغلال فقط، ولا يغطي المشغّل (trigger).
6.12.0-242.el10).هذا إثبات مفهوم، وليس سلاحًا جاهزًا للاستخدام عن قصد. الهيكل الداعم للموثوقية متروك عمدًا: لا تهيئة PCP لكل معالج، ولا تحصين SLUB عبر الخزائن (cross-cache)، ويوضع رأس skb في خزانة مشتركة ليست هادئة بما يكفي. ما يُطرح هنا يعمل بتكرار كافٍ لإثبات السلسلة، ولا أكثر من ذلك بكثير. النطاق مقصور على CentOS / RHEL 10 فقط.
CONFIG_INIT_ON_ALLOC_DEFAULT_ON، وهو الوضع الافتراضي في RHEL / CentOS. يزرع إثبات المفهوم هذا مؤشرًا قديمًا (stale pointer) في بايتات slab غير مهيأة؛ ومع init_on_alloc=1 يُصفَّر هذا الموضع، ولا تؤدي هذه التقنية تحديدًا سوى إلى انهيار النواة (إلغاء مرجع NULL). هذا قيد في التقنية لا في الثغرة: سيأتي استغلال كامل يغطي تلك النواة أيضًا بتقنية مختلفة بمجرد نشر الإصلاح ورقم CVE./sys/kernel/btf/vmlinux وقابلية قراءته للجميع (موجود افتراضيًا في RHEL / CentOS).| التوزيعة | النواة | النتيجة |
|---|---|---|
| CentOS Stream 10 | 6.12.0-242.el10 | root، هروب من الحاوية |
| RHEL 10 | 6.12.0-228.el10 | هروب من سياق httpd_t |
تجاوز داخل slab إلى UAF ذاتي (self-UAF). تمنح الثغرة التحكم في البايت الوحيد nr_frags داخل skb_shared_info. مسار التحرير (skb_release_data()) يجتاز frags[0 .. nr_frags) ويستدعي put_page() لكل عنصر، ولا يُهيأ frags[] أبدًا. نزرع مسبقًا struct page * لصفحة مخزن أنبوب (pipe buffer) في ذلك الموضع من slab عبر إعادة استخدام خزانة بشكل مُتحكم به، ثم نضبط nr_frags على 1، فيُسقط التفكيك (teardown) مرجعًا من مراجع صفحة ما زلنا نملكها. يتحول التجاوز الخطي إلى استخدام بعد التحرير (use-after-free) للصفحة. لا يلمس التجاوز frags[0] أبدًا، لذا لا يحتاج إلى تزامن زمني مع الرش (spray)، وهذا هو معظم ما يجعله متسامحًا.
من UAF للصفحة إلى جدول صفحات قذر (Dirty-Pagetable). تُستعاد صفحة الأنبوب المحررة كجدول صفحات من المستوى الأخير عبر إحداث خطأ صفحة (page fault) على تعيين مجهول جديد. وهكذا تصبح صفحة فيزيائية واحدة في آنٍ واحد جدول صفحات طرفي (leaf) نشطًا والصفحة التي ما زال الأنبوب يقرأها ويكتبها. كتابة ثمانية بايتات إلى الأنبوب تُثبّت إدخال PTE مزورًا؛ وقراءة الأنبوب تعيد قراءة الجدول. هذا قراءة/كتابة فيزيائية عشوائية محدودة، في حدود نحو 460 نافذة PTE لكل جدول.
كسر KASLR. عبر القراءة الفيزيائية نمسح جدول صفحات الانطلاق (trampoline) الثابت الخاص بـ SMP في الذاكرة المنخفضة، الذي لا يعيد KASLR تحديد موقعه أبدًا؛ فمدخله في النصف الخاص بالنواة يشير إلى ، أي القاعدة الفيزيائية للنواة. ومن هناك، يشكّل (الذي يمكن تمييزه بمدخله المرجع ذاتيًا رقم 511) مترجمًا شاملًا من العناوين الافتراضية إلى الفيزيائية، فنستعيد القاعدة الافتراضية للنواة ونربط مساحة عناويننا بالذاكرة الفيزيائية.
level4_kernel_pgtinit_top_pgtمن قراءة/كتابة محدودة إلى غير محدودة. نُزور إدخال PTE واحدًا في الجدول الطرفي يشير إلى العنوان الفيزيائي للجدول نفسه. عندها تُنشئ النافذة الافتراضية المطابقة اسمًا مستعارًا لجدول الصفحات ذاته، فتصبح إدخالات PTE مجرد ذاكرة عادية: قراءة/كتابة غير محدودة للنواة بوصول عشوائي، ودون أنبوب في الحلقة. يُعالَج تماسك TLB في الحلقة 3 بفرض تحديث شامل لـ mm عبر استدعاء mprotect() بحجم مفرط.
حل الإزاحات وسرقة بيانات الاعتماد. تُقرأ إزاحات أعضاء البنى في وقت التشغيل من /sys/kernel/btf/vmlinux، وليست مضمّنة مسبقًا. نحدد موقع task_struct الخاص بنا عبر اجتياز struct page في vmemmap وعبر mm_struct.owner، ثم نُصفّر معرّفات بيانات الاعتماد ونملأ جميع مجموعات القدرات (capabilities) في cred وreal_cred. تأتي عناوين الرموز من محلل داخلي يفسّر جداول kallsyms الخاصة بالنواة نفسها عبر قراءتنا العشوائية؛ وفقط إذا فشل ذلك، نتراجع إلى توجيه cred.user_ns نحو init_user_ns (وبذلك تتحقق CAP_SYSLOG في مساحة الأسماء الأصلية) وقراءة العناوين الفعلية من /proc/kallsyms.
تعطيل SELinux دون تبديل وضع enforcing. نستبدل مقدمة الدالة avc_denied() بكعب برمجي (stub) xor eax, eax ; ret (متجاوزين endbr64 تحت IBT). عندها يعيد كل رفض نتيجة "ممنوح" على مستوى العملية بالكامل، بينما ما يزال getenforce يبلغ Enforcing. وهذا أيضًا شرط مسبق للهروب: فإعادة تنفيذ معالج تفريغ النواة (core-dump) عبر النطاقات المختلفة كانت سترفض لولا ذلك.
الهروب عبر core_pattern. نستبدل المتغير العام core_pattern بمعالج مسبوق بـ | يشير إلى ثنائينا عبر /proc/<PID>/root (جذر chroot لحاوية المهمة المنهارة)، ثم نتسبب في انهيار عملية فرعية. تشغّل النواة المعالج كـ usermodehelper بصلاحيات root في مساحات الأسماء الأصلية، ونمرّر شلًا تفاعليًا بصلاحيات root عبر مقبس Unix مربوط داخل الحاوية. يولد المعالج داخل مساحات الأسماء الأصلية (init namespaces)، لذا لا حاجة إلى أي تعديل جراحي لمساحات الأسماء في المكان نفسه، ولا إلى أي من مخاطر مساحة اسم PID المرتبطة به.