
إثبات مفهوم غير مميز ورفع صلاحيات محلي لمعمارية x86_64 لثغرة استخدام بعد التحرير (use-after-free) في نواة لينكس Binder (CVE-2026-64468)، مع مختبر KASAN وفرق تفاضلي بين النسخة الضعيفة والمصححة.
binder_free_transaction() بنواة لينكس Binder
يحتوي هذا المستودع، لثغرة استخدام بعد التحرير في Binder بنواة لينكس التي تم إصلاحها
بواسطة الالتزام العلوي
f223d27a546c1e1f48d38fd67760e78f068fe8c4:
binder_chain_64468.c — إثبات مفهوم غير مميَّز يصل إلى
الثغرة ويجعل النواة تُثبتها، مع مختبر KASAN و
مقارنة تفاضلية بين النواة المعرّضة والمُصححة (lab/, run.sh, verify.sh).exploit.c — تصعيد صلاحيات محلي مستقل لمعمارية x86_64.
يُترجم باستخدام gcc -O2 -pthread -o exploit exploit.c، ويُشغَّل كمستخدم عادي،
وينتهي بصدفة صلاحيات جذر.demo/ — مختبر يُقلع ببيئة مستخدم Debian 13 حقيقية على
نواة غير مُصححة، بحيث يمكن ترجمة الاستغلال بواسطة gcc الخاص بالهدف نفسه و
تشغيله على الجهاز الذي يستولي عليه بعد ذلك.يعمل كل شيء كمستخدم عادي (uid/gid 1000، بدون صلاحيات، بدون مساحات أسماء) ضد نواة علوية أصلية بدون أي تصحيحات من أي نوع.
تحذير
تتعمد هذه الشيفرة التنافس على أعمار كائنات النواة ثم تختطف تدفق التحكم في النواة. فقدان السباق يُفسد حالة كومة النواة ويمكن أن يُسبب تعليقًا أو انهيارًا للجهاز. شغّلها فقط في جهاز افتراضي معزول وقابل للتخلص تملكه. لا تشغّلها على مضيف.
| الحالة | الادعاء |
|---|---|
| مؤكَّد | الثغرة حقيقية، ويمكن الوصول إليها من عملية غير مميَّزة، والإصلاح العلوي يزيلها. |
| مُثبَت | إلغاء الإشارة المعرّض للثغرة إلى binder_proc المحتضر يُتحقق منه طبيعيًا وبشكل متكرر على النواة غير المُصححة، ولا يحدث أبدًا على النواة المُصححة. |
| مُثبَت | استخدام التحرير الكامل، كما أبلغ عنه KASAN، على النواة غير المُصححة. |
| مُثبَت | استعادة binder_proc المُحرَّر ببايتات يتحكم بها المهاجم، واختطاف تدفق التحكم في النواة، وتصعيد الصلاحيات إلى uid 0 من مستخدم غير مميَّز، على معمارية x86_64. |
| غير مُدَّعى | أي نتيجة على جهاز بائع أو جهاز Android محدد. فقط النواة العلوية المذكورة أدناه تم اختبارها، على معمارية x86_64. |
| غير مُدَّعى | أن الاستغلال المُرفق يعمل دون تعديل ضد نواة توزيعة. انظر أي الأنظمة المتأثرة: يتطلب إزاحات خاصة بكل نواة، وعلى كل توزيعة عامة تم مسحها، لا يمكن لمستخدم غير مميَّز الوصول إلى جهاز binder من الأساس. |
يقرأ binder_free_transaction() العملية الهدف من المعاملة
تحت t->lock، ثم يحرر هذا القفل، وبعد ذلك يكتسب القفل الداخلي للهدف:```c
spin_lock(&t->lock);
target_proc = t->to_proc;
spin_unlock(&t->lock);
if (target_proc) {
binder_inner_proc_lock(target_proc); /* use after free */
لا شيء يُبقي `target_proc` حيّاً عبر تلك الفجوة. العملية التي يتم تفكيكها
بالتوازي يمكن أن تصل إلى `binder_proc_dec_tmpref() -> kfree()` في ما بين ذلك، لذا
يتم قفل الذاكرة المحرَّرة. الإصلاح في المنبع يثبّت `t->to_thread` بينما
`t->lock` ما زال محتفظاً به، مما يُبقي العملية المالكة حيّة حتى يتم استخدام
القفل الداخلي وتحريره.
تم الإبلاغ عن الثغرة بواسطة **Alice Ryhl** وتم إصلاحها بواسطة **Carlos
Llamas**، وكلاهما من Google. يحمل
[التقرير الأصلي](https://lore.kernel.org/all/[email protected]/)
أثر KASAN المرجعي.
### الوصول إلى المسار القابل للاستغلال
المستدعي الوحيد الذي يمكنه الوصول إلى `to_proc` *الأجنبي* هو
`binder_send_failed_reply()`، وهو لا ينتقل إلى `t->from_parent` إلا عندما يكون
`t->from` هو `NULL`:```c
target_thread = binder_get_txn_from_and_acq_inner(t);
if (target_thread) { ...; binder_free_transaction(t); return; }
next = t->from_parent;
binder_free_transaction(t);
t = next;
from_parent يُعيَّن في مكان واحد بالضبط، ويكون هذا التعيين محميًا قبل بضعة أسطر بواسطة فحص مكدس المعاملات السيئ الخاص بـ binder: لا يُسمح للخيط بإرسال معاملة متزامنة إلا إذا كان أعلى مكدسه معاملةً يكون يستقبلها. لذلك، بالنسبة لكل حلقة في تلك السلسلة، يكون مُرسِل الطفل ومُستقبِل الوالد نفس الخيط.
يترتب على ذلك نتيجة حادة. binder_thread_release() يجتاز مكدس الخيط المُحتضَر مع الاحتفاظ بـ proc->inner_lock، ويكتب كلاً من```
iteration j : [holds child->lock ] child->from = NULL ; unlock child->lock
spin_lock(parent->lock)
iteration j+1 : [holds parent->lock] parent->to_proc = NULL
في **التكرارات المتجاورة لمسار واحد**، كل منها تحت
`t->lock` الخاص بها. لا يكتشف المُتجوّل أن `child->from == NULL` إلا بعد أن يحرّر المسار
قفل `child->lock`، ويحتاج إلى `parent->lock` للقطة الخاصة به. لذا فإن الفرصة
الكاملة هي الفجوة بين `spin_unlock(&child->lock)` لذلك المسار و
`spin_lock(&parent->lock)` الخاص به — بضع تعليمات. المسار المُحرِّر يحمل قفل دوران
ولا يمكن استباقه هناك؛ فقط المقاطعة يمكنها تأخيره.
لهذا السبب تكون السباق ضيقة ولماذا يكون كل من إثبات المفهوم
والاستغلال احتماليين.
### ما يبنيه إثبات المفهوم
`binder_chain_64468.c` يبني أقصر سلسلة تصل إلى
الوصول الضعيف، بحيث يكون عمل المُتجوّل داخل تلك الفجوة صغيرًا قدر ما يسمح به
binder — عمليتا اكتساب لـ `t->lock` و `kfree()` واحدة، دون أي
`inner_proc_lock` خارجي، ولا `wake_up` ولا تسليم رد:```
B thread i --e2 (sync, code 0x4442414b)--> P thread Y_i
P thread Y_i --e1 (sync, code 0x54414c4c)--> B thread i (nested target)
B thread i stack: [ e2 outgoing , e1 incoming (top) ]
P thread Y_i stack: [ e2 incoming , e1 outgoing (top) ]
P ثم يُسقط واصف binder الخاص به، لذا يقوم binder_deferred_release() بتحرير
Y_1..Y_K وأخيرًا يحرر binder_proc، بينما كل خيط B
يصدر بشكل متزامن BINDER_THREAD_EXIT:```
binder_thread_release(B_i) nulls e1->to_proc (own proc) and e2->from
binder_send_failed_reply(e1) e1->from == NULL once Y_i was released
-> binder_free_transaction(e1) target_proc already NULL, kfree(e1)
-> binder_free_transaction(e2) target_proc == P <-- vulnerable access
العملية الثالثة هي مدير سياق binder، وتُستخدم فقط لتوزيع
المقابض التي تحتاجها العمليتان الأخريان. `exploit.c` يعيد استخدام هذا البناء بالضبط.
### شرطان مستقلان
تقرير KASAN يتطلب كليهما:
* **الوصول الضعيف** — يجب أن يأخذ المتجول `parent->lock` داخل
النافذة الضيقة أعلاه، بحيث يلتقط لقطة لـ `to_proc` الذي لا يزال حيًا؛ و
* **هبوط التحرير داخل النافذة** — يجب أن يفقد المتجول بعد ذلك وحدة المعالجة المركزية
بين إسقاط `t->lock` وأخذ القفل الداخلي للضحية، ويبقى بعيدًا عنها
حتى يكتمل التحرير المؤجل ويُحرَّر `binder_proc`.
الشرط الأول له مؤشر خاص به لا يحتاج إلى KASAN: binder يطبع```
binder: binder_free_proc: Unexpected outstanding_txns -1
عندما يحدث ذلك، لأن المتجول وbinder_thread_release() كلاهما
يخفضان نفس العداد لمعاملة واحدة. لاحظ أن هذا ليس فرقًا
قابلًا للاستغلال/الإصلاح بحد ذاته — الإصلاح يوقف التحرير، وليس الخفض الثاني —
لذلك يظهر على كلا النواتين. يُستخدم هنا فقط لإظهار مسار الكود القابل للاستغلال وهو قيد التنفيذ.
يتوقف إثبات المفهوم عند خفض 4 بايت من الذاكرة المحررة. تحويل ذلك إلى uid 0 يتطلب أربعة أشياء، ولا يأتي أي منها من الثغرة نفسها: الثغرة لا تسرّب شيئًا.
struct binder_proc يبلغ 648 بايت ويُخصص باستخدام GFP_KERNEL
kzalloc عادي — وليس __GFP_ACCOUNT. لذلك يقع في kmalloc-1k،
جنبًا إلى جنب مع كل تخصيص غير محسوب آخر بنفس الحجم، ولا يكون
معزولًا خلف kmalloc-cg-*. هذه الحقيقة الواحدة هي ما يجعل الكائن
قابلًا للاسترداد أصلًا.
لحظة kfree() غير قابلة للملاحظة من مساحة المستخدم، وكذلك لحظة
لمس المتجول للكائن مرة أخرى، لذلك لا يوجد شيء للتوقيت بناءً عليه.
لذلك يعمل الرش كمضخة: يخصص ويحرر كائنات kmalloc-1k
بشكل مستمر، من وحدة المعالجة المركزية التي نفّذت التحرير المؤجل، طالما
أن المتجولين يعملون.
تُستخدم رسائل System V لهذا الغرض. alloc_msg() هو kmalloc
غير محسوب عادي لرأس 48 بايت بالإضافة إلى الحمولة، لذا فإن رسالة بحجم 976 بايت هي تخصيص بحجم 1024 بايت؛
الحصة لكل قائمة انتظار وليست لكل uid؛ وmsgrcv() يحرر
بشكل متزامن. الإنتاجية المُقاسة: ~198,000 تخصيصًا في الثانية، صفر
فشل.