
تشغيل وتحليل ثغرة نواة أندرويد CVE-2019-2215
في نوفمبر 2017، تم اكتشاف خلل use-after-free في نواة لينكس بواسطة نظام syzkaller . في فبراير 2018، تم إصلاح هذا الخلل في بعض أنوية لينكس وإصدارات أندرويد.
لم يتم تضمين هذا الإصلاح أبداً في النشرات الأمنية الشهرية لأندرويد، لذلك لم يتم إصلاحه في العديد من الأجهزة الصادرة حديثاً مثل Pixel وPixel2.
في سبتمبر 2019، تم إبلاغ أندرويد بالآثار الأمنية لهذا الخلل من قبل Project Zero. ثم خصصت أندرويد لهذه الثغرة CVE-2019-2215 لجعلها أكثر رسمية وشهرة.
CVE-2019-2215 هي ثغرة use-after-free في binder.c تسمح بتصعيد الصلاحيات (الحصول على صلاحية الجذر) من تطبيق أندرويد. لا حاجة لتفاعل المستخدم، لاستغلال هذه الثغرة. يتطلب الأمر فقط تثبيت تطبيق محلي خبيث.
هنا سنقدم هذه الثغرة الأمنية في نواة أندرويد بمزيد من التفاصيل وسنستخدم هذه الثغرة للحصول على صلاحية الجذر (تصعيد الصلاحيات) لجهاز أندرويد بأكمله.
سنستخدم إثبات المفهوم (PoC) التالي:
https://github.com/cloudfuzz/android-kernel-exploitation
سنعرض لكم أولاً طريقة لتحفيز هذه الثغرة على محاكي أندرويد وإحداث تعطل للنواة. ثم لمعرفة مدى خطورتها، سنواصل استخدام الـ PoC للحصول على صلاحية الجذر في جهاز أندرويد المحاكى. بعدها سنحلل كود النواة لمعرفة السبب (التحليل الساكن والتحليل الديناميكي).
بعد التحليل، سنرى كيف حصلنا على صلاحية الجذر. وفي النهاية سنرى كيف يتم تخفيف هذه الثغرة باستخدام التصحيحات.
لإحداث تعطل في النواة عن طريق تحفيز هذه الثغرة، نتبع الخطوات التالية:
شاهد الفيديو التالي:
[فيديو]
في هذا القسم (والقسم التالي) سنفهم سبب حدوث التعطل باستخدام التحليل الساكن والديناميكي.
هنا سنقوم بتحليل كود النواة (التحليل الساكن) لفهم المشكلة. في crash_report.txt يوجد تقرير من KASan يوضح أن هذه ثغرة use-after-free. هذا يعني أن كائناً تم تخصيصه في الكومة (وكان لدينا مرجع إليه)، ثم قمنا بتحرير الكائن من الكومة، وبعد ذلك قمنا باستدعائه خطأً عبر مرجع. في هذا التقرير تتم طباعة تتبع المكدس لهذه المراحل الثلاث.
إذا كنت تتذكر مما سبق، فقد استخدمنا 'trigger.cpp' في الـ PoC.
إليك الكود الرئيسي لـ trigger.cpp:``` int main() { //1 int fd, epfd; //2 struct epoll_event event = {.events = EPOLLIN}; //3 fd = open("/dev/binder", O_RDONLY); //4 epfd = epoll_create(1000); //5 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); //6 ioctl(fd, BINDER_THREAD_EXIT, NULL); }
دعونا نرى ما يفعله 'trigger.cpp'.
في أندرويد (مثل أنظمة التشغيل الأخرى المبنية على يونكس) لدينا بعض العمليات (processes). كل برنامج نشغّله يُنشئ عملية واحدة (أو أكثر)، وهذه العمليات يديرها نظام التشغيل (OS). قد يبدّل نظام التشغيل بينها (تعدد المهام) أو ينهي عملية، إلخ. لأسباب أمنية، تكون العمليات معزولة عن بعضها البعض افتراضيًا.
في بعض الحالات، قد تحتاج عملية إلى تبادل بيانات مع عملية أخرى. يُسمى هذا التواصل بين العمليات (IPC). هناك عدة طرق للعمليات للتواصل في لينكس. أدخلت أندرويد آلية IPC محددة تسمى **'Binder'**. Binder هو مشغّل نواة (kernel driver) لتسهيل التواصل بين العمليات.
في أندرويد، يمكن تنفيذ IPC عن طريق استدعاء بعض دوال النواة مباشرة (معظمها في drivers/binder.c) أو باستخدام تطبيقات عالية المستوى (مثلًا في جافا).

لاستخدام **binder**، يجب علينا فتح وحدة binder الخاصة بالنواة. يتم ذلك باستخدام السطر 3 من trigger.cpp. بعدها يكون لدينا مؤشر واصف ملف (file descriptor). باستخدام واصف الملف هذا يمكن تحديد مُبادر النواة ومستقبلي الـ IPC.
جميع التفاعلات مع المشغّل ستتم عبر مجموعة صغيرة من أوامر **'ioctl**' (BINDER_THREAD_EXIT, BINDER_WRITE_READ, ...).
المزيد عن binder: [link1](https://www.nds.ruhr-uni-bochum.de/media/attachments/files/2012/03/binder.pdf), [link2](http://rts.lab.asu.edu/web_438/project_final/CSE_598_Android_Architecture_Binder.pdf)
في لينكس، لدينا مفهوم يسمى '**استطلاع الأحداث (event polling)**'. تُستخدم واجهة برمجة التطبيقات '**epoll**' عندما نريد مراقبة عدة واصفات ملفات (واصفات الملفات هي ما نملكه عند فتح مشغّل أو العمل مع الإدخال/الإخراج، إلخ).
**epoll** هو بنية نواة (kernel struct) لها حقلان مهمان.
* interest list = قائمة واصفات الملفات التي نريد مراقبتها.
* ready list = قائمة واصفات الملفات الجاهزة للإدخال/الإخراج (I/O).
لاستخدام استطلاع الأحداث، نقوم أولاً بإنشاء epoll (السطر 4)، ثم نضيف أو نحذف (EPOLL_CTL_ADD) حدثًا (&event) مرتبطًا بواصف ملف (**fd**) إلى الـ epoll الذي أنشأناه (**epfd**) عن طريق استدعاء دالة النواة **epoll_ctl**.
=>
`epoll_event event` هو حدث يتم تشغيله عندما يكون الملف المرتبط (fd) متاحًا لعملية القراءة.
الآن نفهم ما يفعله **trigger.cpp** (لا حاجة للتعمق!). إنه يفتح وحدة **binder**، وينشئ **epoll** للاستماع إليها عندما تكون جاهزة. ثم في السطر 6، نخرج من binder الذي بدأناه في السطر 3.
#### التخصيص:
عند استدعاء open()، فإننا في الواقع نستدعي **open_binder()** (تنفيذ open() في binder.c)، وفي open_binder()، سيتم إنشاء بنية **'binder_proc'** جديدة و:
` fd->pricate_data = binder_proc`
عند استدعاء **epoll_create()**، سيتم إنشاء بنية epoll جديدة وإضافتها إلى بنية قائمة انتظار.

عند استدعاء **epoll_ctrl(epdf, ADD, fd, event)**، يتم إنشاء **ep_item** جديد، وربط **fd** (واصف الملف المراد الاستماع إليه) بـ **ep_item**، وإدراجه في event_poll's** red black tree** (بنية بيانات في ep لحفظ ep_items). كما يستدعي **ep_item_poll()**، وهذه الدالة تتعامل مع ربط دالة الاستدعاء (callback) بـ ep_item.
يقوم بإنشاء بنية **binder_thread** جديدة (**يحدث التخصيص هنا**)، ويربطها بـ **binder_proc** (الذي تم إنشاؤه أعلاه)، ثم يتم إنشاء بنية **epoll_entry**، ولها قائمتان: **epoll_entry->wait** و **epoll_entry->whead**، وكلا القائمتين لديهما مؤشر إلى **binder_thread** الذي تم إنشاؤه سابقًا.
ثم يتم ربط **epoll_entry** بـ ep_item (**ep_item->pwqlist** هي قائمة تحتوي على هذا epoll_entry).

#### التحرير:
عند استدعاء **ioctl(fd, ...)**، يتم الوصول إلى **binder_proc** عبر fd->private_data، ثم يتم **تحرير** بنية **binder_thread** من الذاكرة.
#### الاستخدام:
عندما تنتهي عمليتنا الحالية، سيتم استدعاء **epoll_ctl(epfd, DEL, fd, event)**.
يستدعي **ep_remove(event_poll, ep_item)**. تحصل هذه الدالة على **epoll_entry** من **ep_item->pwqlist**، ثم تحصل على قائمة الانتظار الخاصة بـ **ep_item (ep_tem->wait)**، وهي قائمة مرتبطة، وتريد إزالة أحد عناصر قائمة الانتظار هذه.
يستخدم الكود التالي (كود زائف):```
entry = wait->entry;
entry.next.prev = entry.prev;
entry.prev.next = entry.next;
هنا wait->entry هو مؤشر إلى binder_thread الذي تمت إزالته من الذاكرة! إذن فهو استخدام بعد التحرير (use after free) ويسبّب خطأً!!