
تشغيل وتحليل ثغرة نواة أندرويد 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) ويسبّب خطأً!!
أنشأنا event_poll يحتوي على red_black_tree، كل عقدة هي ep_item ولها حقل عبارة عن قائمة من epoll_entry، وكل epoll_entry يحتوي على مؤشرين إلى بنية binder_thread (wait, whead).
من خلال استدعاء ioctl() قمنا بتحرير binder_thread من الذاكرة، ثم أثناء الخروج يتم الوصول إلى هذه البنية عبر مؤشر كان لا يزال متاحًا!
التحليل الديناميكي هو اختبار وتقييم البرنامج عن طريق تنفيذ البيانات في الوقت الفعلي ; للعثور على الأخطاء في البرنامج أثناء تشغيله.
الخطوات:
ابنِ نواة Android بدون KASan
شغّل المحاكي بالنواة المبنية حديثًا
ابدأ تشغيل المحاكي
ابنِ مشغّل الثغرة وادفعه إلى الجهاز الافتراضي
ضع نقطة توقف في GDB
قم بتحميل سكربت python المخصص (dynamic-analysis.py في المستودع) : لتتبع استدعاءات الدوال وتفريغ جزء بنية binder_thread قبل وبعد تحريرها. كذلك تفريغ نفس بنية binder_thread قبل وبعد تنفيذ عملية إلغاء الربط (unlink).
في هذا الملف نقوم أولاً بحذف جميع نقاط التوقف ثم نضع نقطتي توقف (BP); الرمز الأول هو “binder_free_thread” (سيتتبع دالة binder_free_thread) قبل تحرير binder_thread، سيتم استدعاء دالة الإيقاف؛ لذلك سيتم عرض المعاملات والرمز باستخدام (gb.write(....) ) ثم سيتم استدعاء طريقة الاستدعاء (التي قمنا بتعيينها في set_dump_binder_thread )؛ في هذه الدالة سيتم تعيين binder_thread_address في متغيرنا العام، ويقوم gdb.execute بإرسال أي مخرجات ينتجها الأمر إلى المخرجات القياسية لـ GDB.
الرمز الثاني هو “remove_wait_queue” (سيتتبع دالة remove_wait_queue) المعاملات التي نريد ملاحظتها هي "wq_head" و "wq_entry" وعند الخروج سيتم تعيين نقطة توقف عند wait.c:52. دالة الاستدعاء الخاصة بها هي dump_binder_thread . ستعرض نقاط التوقف هذه ما يحدث قبل وبعد عملية إلغاء الربط.
شغّل adb shell وقم بتشغيل مشغّل PoC
النتيجة:
binder_free_thread(thread=0xffff88800c18f200)(enter) 0xffff88800c18f200: 0xffff88806793c000 0x0000000000000001 0xffff88800c18f210: 0x0000000000000000 0x0000000000000000 0xffff88800c18f220: 0xffff88800c18f220 0xffff88800c18f220 0xffff88800c18f230: 0x0000002000001b35 0x0000000000000001 0xffff88800c18f240: 0x0000000000000000 0xffff88800c18f248 0xffff88800c18f250: 0xffff88800c18f248 0x0000000000000000 0xffff88800c18f260: 0x0000000000000000 0x0000000000000000 0xffff88800c18f270: 0x0000000000000003 0x0000000000007201 0xffff88800c18f280: 0x0000000000000000 0x0000000000000000 0xffff88800c18f290: 0x0000000000000003 0x0000000000007201 0xffff88800c18f2a0: 0x0000000000000000 0xffff88805c05cae0 0xffff88800c18f2b0: 0xffff88805c05cae0 0x0000000000000000 0xffff88800c18f2c0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2d0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2e0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2f0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f300: 0x0000000000000000 0x0000000000000000 0xffff88800c18f310: 0x0000000000000000 0x0000000000000000 0xffff88800c18f320: 0x0000000000000000 0x0000000000000000 0xffff88800c18f330: 0x0000000000000000 0x0000000000000000 0xffff88800c18f340: 0x0000000000000000 0x0000000000000000 0xffff88800c18f350: 0x0000000000000000 0x0000000000000000 0xffff88800c18f360: 0x0000000000000000 0x0000000000000000 0xffff88800c18f370: 0x0000000000000000 0x0000000000000000 0xffff88800c18f380: 0x0000000000000000 0x0000000000000001 0xffff88800c18f390: 0xffff88806d4bb200
في كود python الخاص بنا كانت لدينا الأكواد التالية:``` gdb.write ( "{function}({param})(enter)\n".format( function=self.function_name, param=params ) )
وهي دالة binder_free_thread لذا فإن المعامل لهذه الدالة هو مؤشر إلى binder_thread:```
static void binder_free_thread(struct binder_thread *thread)
{
[...]
kfree(thread);
}
في النتيجة حصلنا على(binder_free_thread(thread=0xffff88800c18f200)(enter)).
الأسطر التالية تُظهر نتيجة التنفيذ، باستخدام الأمر أدناه نحصل على إزاحة binder_thread.wait:``` p offsetof(struct binder_thread, wait)
النتيجة هي 0xa0 وإذا وضعنا wait.head بدلاً من wait في الأمر ، ستكون النتيجة 0xa8 وتحتوي على `0xffff88805c05cae0````
0xffff88805c05cae0 is pointer to eppoll_entry->wait.entry which is of type struct list_head
remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter) 0xffff88800c18f200: 0xffff88800c18f600 0x0000000000000001 0xffff88800c18f210: 0x0000000000000000 0x0000000000000000 0xffff88800c18f220: 0xffff88800c18f220 0xffff88800c18f220 0xffff88800c18f230: 0x0000002000001b35 0x0000000000000001 0xffff88800c18f240: 0x0000000000000000 0xffff88800c18f248 0xffff88800c18f250: 0xffff88800c18f248 0x0000000000000000 0xffff88800c18f260: 0x0000000000000000 0x0000000000000000 0xffff88800c18f270: 0x0000000000000003 0x0000000000007201 0xffff88800c18f280: 0x0000000000000000 0x0000000000000000 0xffff88800c18f290: 0x0000000000000003 0x0000000000007201 0xffff88800c18f2a0: 0x0000000000000000 0xffff88805c05cae0 0xffff88800c18f2b0: 0xffff88805c05cae0 0x0000000000000000 0xffff88800c18f2c0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2d0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2e0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2f0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f300: 0x0000000000000000 0x0000000000000000 0xffff88800c18f310: 0x0000000000000000 0x0000000000000000 0xffff88800c18f320: 0x0000000000000000 0x0000000000000000 0xffff88800c18f330: 0x0000000000000000 0x0000000000000000 0xffff88800c18f340: 0x0000000000000000 0x0000000000000000 0xffff88800c18f350: 0x0000000000000000 0x0000000000000000 0xffff88800c18f360: 0x0000000000000000 0x0000000000000000 0xffff88800c18f370: 0x0000000000000000 0x0000000000000000 0xffff88800c18f380: 0x0000000000000000 0x0000000000000001 0xffff88800c18f390: 0xffff88806d4bb200
في remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter) ، wq_head هو عنوان binder_thread.wait و wq_entry هو بيانات wait.head .
بعد ذلك ستتم عملية إلغاء الربط.
Breakpoint 3 at 0xffffffff802aa5be: file /home/ashfaq/workshop/android-4.14-dev/goldfish/kernel/sched/wait.c, line 53. remove_wait_queue_wait.c:52(exit) 0xffff88800c18f200: 0xffff88800c18f600 0x0000000000000001 0xffff88800c18f210: 0x0000000000000000 0x0000000000000000 0xffff88800c18f220: 0xffff88800c18f220 0xffff88800c18f220 0xffff88800c18f230: 0x0000002000001b35 0x0000000000000001 0xffff88800c18f240: 0x0000000000000000 0xffff88800c18f248 0xffff88800c18f250: 0xffff88800c18f248 0x0000000000000000 0xffff88800c18f260: 0x0000000000000000 0x0000000000000000 0xffff88800c18f270: 0x0000000000000003 0x0000000000007201 0xffff88800c18f280: 0x0000000000000000 0x0000000000000000 0xffff88800c18f290: 0x0000000000000003 0x0000000000007201 0xffff88800c18f2a0: 0x0000000000000000 0xffff88800c18f2a8 0xffff88800c18f2b0: 0xffff88800c18f2a8 0x0000000000000000 0xffff88800c18f2c0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2d0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2e0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f2f0: 0x0000000000000000 0x0000000000000000 0xffff88800c18f300: 0x0000000000000000 0x0000000000000000 0xffff88800c18f310: 0x0000000000000000 0x0000000000000000 0xffff88800c18f320: 0x0000000000000000 0x0000000000000000 0xffff88800c18f330: 0x0000000000000000 0x0000000000000000 0xffff88800c18f340: 0x0000000000000000 0x0000000000000000 0xffff88800c18f350: 0x0000000000000000 0x0000000000000000 0xffff88800c18f360: 0x0000000000000000 0x0000000000000000 0xffff88800c18f370: 0x0000000000000000 0x0000000000000000 0xffff88800c18f380: 0x0000000000000000 0x0000000000000001 0xffff88800c18f390: 0xffff88806d4bb200
الجزء المميز هو بسبب هذا .النتيجة بعد عملية إلغاء الربط ويمكننا أن نرى أنه من أجل إلغاء الربط يتم كتابة العنوان (0xffff88800c18f2a0 + 0x8) إلى التالي والسابق.
هذا يعني:
(مؤشر إلى binder_thread->wait.head) = binder_thread->wait.head.next = `binder_thread->wait.head.prev
في هذا الجزء سنعرض كيف يمكننا استخدام هذا الخطأ للحصول على صلاحيات الجذر.
تذكّر مما سبق، كان لدينا بنية binder_thread، تم تحريرها ثم استخدامها مرة أخرى عبر مؤشر. هذا هو كود بنية binder_thread:``` struct binder_thread { struct binder_proc proc; struct rb_node rb_node; struct list_head waiting_thread_node; int pid; int looper; / only modified by this thread*/ bool looper_need_return; /* can be written by other thread*/ struct binder_transaction *transaction_stack; struct list_head todo; bool process_todo; struct binder_error return_error; struct binder_error reply_error; wait_queue_head_t wait; struct binder_stats stats; atomic_t tmp_ref; bool is_dead; struct task_struct *task; };
أحد الحقول هو مؤشر **task_struct**، وهذه البنية تحتوي على حقل يسمى **addr_limit**.
عندما نريد الوصول إلى عنوان في عملية ما، يتم التحقق مما إذا كان العنوان في **مساحة المستخدم** أم لا؛ إذا كان في **مساحة النواة** فيجب حظر هذا الوصول. يتم هذا التحقق بمقارنة عنواننا مع **addr_limit**، وإذا كان عنواننا أقل من **addr_limit** فلدينا وصول صالح.
**addr_limit** في الواقع يفصل مساحة المستخدم عن مساحة النواة، لذلك من خلال تغيير هذا الحقل في **task_struct** نحصل على وصول كامل إلى مساحة النواة ويمكننا فعل أي شيء!
هنا لدينا خطوتان للاستغلال:
1. العثور على عنوان task_struct في مساحة النواة
2. تغيير addr_limit في task_struct
#### العثور على عنوان task_struct
في النواة، يمكننا تنفيذ الإدخال/الإخراج المتجه، وهذا يعني أنه يمكننا كتابة أو قراءة أكثر من جزء من البيانات إلى أو من واصف ملف (ملف، مقبس، إلخ).
يتم تنفيذ الإدخال/الإخراج المتجه باستخدام أساليب **writev** و **readv** و **recvmsg** وبنية **iovec**.```
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ \
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */ \
};
باستخدام الإدخال/الإخراج المتجه (vectored I/O)، نقوم بعمليات إدخال/إخراج على مصفوفة من المخازن المؤقتة (iovec). كل iovec يحتوي على مؤشر إلى مخزن مؤقت (iov_base) وحجم المخزن (iov_len).
على سبيل المثال، إذا أردنا كتابة مصفوفة من المخازن المؤقتة إلى ملف (fd)، فإننا نستدعي
writev(fd, iovecStack, count)
تقوم هذه الطريقة (مثل readv و recvmsg) أولاً بنسخ مصفوفة iovec (iovecStack) إلى نطاق النواة، ثم تقرأ من هذه المخازن المؤقتة وتكتب إلى fd.
يمكننا كتابة وقراءة المخازن المؤقتة باستخدام pipe , pipe هو struct يمنحنا مؤشري ملفات، أحدهما للقراءة والآخر للكتابة. Pipe له طول بالبايتات، عندما يكتب أحد العمليات إلى pipe أكثر من طوله، يتم حظر pipe في تلك العملية وينتظر حتى تقوم عملية أخرى بالقراءة من ذلك pipe (باستخدام مؤشر ملف القراءة الخاص بذلك pipe).
تحاول النواة تخصيص ذاكرة (من أجل struct) وفقًا لحجمها. على سبيل المثال عندما حررنا struct binder_thread من الذاكرة (انظر التحليل الثابت)، وبعد ذلك إذا كان لدينا struct بحجم مشابه لـ binder_thread، فهناك فرصة جيدة لتخصيصه في نفس مكان binder_thread المُحرَّر.
أولاً ننشئ iovecStack (مصفوفة من structs الخاصة بـ iovec) بحجم مشابه لـ struct الخاص بـ binder_thread.
ثم نحرر binder_thread من الذاكرة (انظر جزء ‘free’ في التحليل الثابت)
ثم نستدعي writev() على iovecStack هذا. الجزء الأول من هذه الطريقة ينسخ iovecStack في نطاق النواة،
من المرجح أن يتم تخصيص iovecStack في نفس مكان binder_thread المُحرَّر.
إذا كان لدينا ما يكفي من iovec في iovecStack، فستبدو الذاكرة في موقع binder_thread كما يلي:

ترى أن iovecStack[10].iov_base و iovecStack[10].iov_len و iovecStack[11].iov_base سيكونون في نفس مكان wait.lock و wait.head.next و wait.head.prev الحقول. الخاصة بـ binder_thread.
في جزء ‘الاستخدام’ من التحليل الثابت رأينا أن الانهيار حدث بسبب الوصول إلى جزء wait الخاص بـ binder_thread أثناء عملية إلغاء الربط (إزالة عنصر من قائمة مرتبطة).
أثناء عملية إلغاء الربط، يتم إلغاء ربط wait.head وستشير حقول next و prev الخاصة به إلى حقل wait الخاص بـ binder_thread (إلغاء الربط).

قبل حدوث الجزء الثاني من writev (الكتابة من iovecs إلى ملف)، إذا قمنا بتشغيل عملية إلغاء الربط، فسيتم استبدال iovecStack[10].iov_len و iovecStack[11].iov_base بعنوان kernel. ثم بعد تشغيل بقية writev، عندما يريد معالجة iovecStack[11]، فإنه يقرأ من iovecStack[11].iov_base (=عنوان wait في binder_thread) مقدارًا من البيانات بطول iovecStack[11].iov_len.
إذا كان iovecStack[11].iov_len كافيًا، فإننا نقرأ من حقل wait إلى حقل task_struct الخاص بـ binder_thread وبذلك نحصل على مؤشر task_struct.
إذن للحصول على مؤشر task_struct نقوم بما يلي:
انظر exploit.cpp في المستودع.
الآن لدينا عنوان task_struct في نطاق النواة (task_ptr).
هنا نستخدم socket_pair بدلاً من pipe. ونستخدم recvmsg() للقراءة من socket والكتابة إلى iovecs.
الخطوات:
بعد الكتابة، تبدأ **recvmsg**() في القراءة من المقبس والكتابة في **iovecStack**. بسبب البيانات العشوائية، تكون قد كتبت حتى **iovecStack**[10].
لذا تبدأ بكتابة **finalSocketData** في **iovecStack**[12]، وتحصل على العنوان من **iovecStack[12].iov_base**، وهو عنوان **wait** في **binder_thread** بسبب عملية unlink، ويتم تعيين **iovecStack[12].iov_len** إلى 4 بايتات، فتكتب:
* 0x1 في iovecStack[10].iov_len
* 0x41414141 في iovecStack[11].iov_base
* 0x8 + 0x8 + 0x8 + 0x8 في iovecStack[11].iov_len
* **pointer_to_addr_limit** في iovecStack[12].iov_base
الآن تمت كتابة 4 بايتات في **iovecStack[11]**، لذلك تنتقل **recvmsg**() إلى **iovecStack[12]** لكتابة باقي **finalSocketData**:
تكتب **0xFFFFFFFFFFFFFFFE** في العنوان الموجود في **iovecStack[12].iov_base** والذي تم تعيينه إلى **pointer_to_addr_limit**، وهذا يعني أن recvmsg() يغيّر addr_limit إلى 0xFFFFFFFFFFFFFFFE (وليس 0xFFFFFFFFFFFFFFFF بسبب بعض المشاكل مع arm64).
الآن تتوسع مساحة المستخدم لتشمل تقريبًا كل مساحة النواة! (ويمكنها فعل أي شيء!!)
# التصحيح
binder_poll() يمرر قائمة الانتظار thread->wait التي يمكن النوم عليها بانتظار العمل. عندما يخرج مؤشر ترابط يستخدم epoll بشكل صريح باستخدام BINDER_THREAD_EXIT، يتم تحرير قائمة الانتظار، لكنها <span style="text-decoration:underline;">لا تتم إزالتها أبدًا من بنية بيانات epoll المقابلة</span>. عندما تخرج العملية لاحقًا، يحاول رمز تنظيف epoll الوصول إلى قائمة الانتظار، مما يؤدي إلى use-after-free.
امنع ذلك باستخدام POLLFREE عندما يخرج مؤشر الترابط.
كان لدينا هذا الكود:```
static int binder_thread_release(struct binder_proc *proc, struct binder_thread *thread)
{
.
.
.
int active_transactions = 0;
.
.
.
binder_thread_dec_tmpref(thread);
return active_transactions;
}
تمت إضافة هذه الأسطر إلى الكود المصدري:
static int binder_thread_release(struct binder_proc *proc,
binder_thread *thread)
{
.
.
.
/*
* If this thread used poll, make sure we remove the waitqueue
* from any epoll data structures holding it with POLLFREE.
* waitqueue_active() is safe to use here because we're holding
* the inner lock.
*/
if ((thread->looper & BINDER_LOOPER_STATE_POLL) && waitqueue_active(&thread->wait)) {
wake_up_poll(&thread->wait, EPOLLHUP | POLLFREE);
}
binder_inner_proc_unlock(thread->proc);
/*
* This is needed to avoid races between wake_up_poll() above and
* and ep_remove_waitqueue() called for other reasons (eg the epoll file
* descriptor being closed); ep_remove_waitqueue() holds an RCU read
* lock, so we can be sure it's done after calling synchronize_rcu().
*/
if (thread->looper & BINDER_LOOPER_STATE_POLL)
synchronize_rcu();
.
.
.
}
انظر الكود الكامل هنا
نظام التشغيل هو طبقة من البرمجيات مسؤولة عن جعل جميع العتاد (hardware) يعمل بكفاءة أكبر، وبناء بنية تحتية يمكن للتطبيقات التي تستخدمها أن تعمل فوقها؛ نواته هي النواة (kernel).
الفكرة وراء الاستغلال بسيطة: البرمجيات تحتوي على أخطاء، والأخطاء تجعل البرمجيات تتصرف بشكل غير صحيح أو تؤدي مهمةً صُممت لأدائها بشكل سليم بطريقة خاطئة؛ استغلال الخطأ يعني تحويل هذا السلوك غير الصحيح إلى ميزة لصالح المهاجمين.
الأخطاء القابلة للاستغلال تُعرف باسم الثغرات (vulnerabilities).
المستخدم أو العملية المتميّزة (privileged) هو من يملك وصولاً كاملاً إلى الجهاز.
معظم معماريات مجموعة التعليمات (instruction set architectures) توفر نمطين على الأقل للتنفيذ:
المتميّز (privileged) : جميع تعليمات مستوى الآلة متاحة.
غير المتميّز (unprivileged) : فقط مجموعة فرعية من التعليمات متاحة.
ثغرات الاستخدام بعد التحرير (Use-After-Free) هي نوع من عيوب تلف الذاكرة التي يمكن أن يستغلها المخترقون لتنفيذ تعليمات برمجية عشوائية.
يشير الاستخدام بعد التحرير تحديدًا إلى محاولة الوصول إلى الذاكرة بعد تحريرها، وهو ما يمكن أن يتسبب في تعطّل البرنامج، أو في حالة وجود خلل من نوع الاستخدام بعد التحرير، قد يؤدي إلى تنفيذ تعليمات برمجية عشوائية أو حتى تمكين قدرات تنفيذ كود كامل عن بُعد.
هي ثغرة استخدام بعد التحرير (use-after-free) في Binder داخل نواة أندرويد. هذا الخطأ هو ثغرة تصعيد صلاحيات محلية تسمح باختراق كامل للجهاز المصاب. إذا تم دمجها مع استغلال لمحرك عرض المتصفح، فقد يؤدي هذا الخطأ إلى اختراق كامل للجهاز عبر موقع ويب ضار. ويمكن الوصول إليها من داخل بيئة Chrome المعزولة (sandbox).
ملاحظة: تعمل على Pixel 1 و2، ولكنها لا تعمل على Pixel 3 و3a. انظر هذا الهجوم.
النشرات الأمنية لأندرويد هي قائمة تنشرها جوجل (شهريًا). تحتوي هذه القائمة على الثغرات الأمنية التي تم إصلاحها والتي تؤثر على إطار عمل أندرويد، ونواة لينكس، وغيرها.
Syzkaller هو أداة اختبار عشوائي (fuzzer) للنواة. الاختبار العشوائي (Fuzzing) هو أسلوب اختبار ينتج فيه برنامج آلي مدخلات شبه عشوائية لبرنامج مستهدف للتحقق مما إذا تم إثارة أي خطأ. يعد الاختبار العشوائي مفيدًا بشكل خاص في العثور على أخطاء تلف الذاكرة في برامج C أو C++.
Project Zero هو فريق من محللي الأمن توظفه جوجل، ومهمته العثور على ثغرات اليوم الصفري (zero-day). ثغرة اليوم الصفري هي ثغرة غير معروفة لمن يجب عليهم إصلاحها.
GDB هو اختصار لـ GNU Project Debugger، وهو مصحح الأخطاء الأكثر شيوعًا في أنظمة UNIX لتصحيح أخطاء برامج C وC++. يتيح لك GDB تشغيل البرنامج حتى نقطة معينة، ثم إيقافه وطباعة قيم متغيرات معينة عند تلك النقطة، أو التنقل في البرنامج سطرًا سطرًا وطباعة قيم كل متغير بعد تنفيذ كل سطر.
يمكنك توصيل محاكي أندرويد الخاص بك مع gdbserver لأغراض التصحيح.
Kernel Address SANitizer (KASAN) هي أداة كشف أخطاء ذاكرة ديناميكية مصممة للعثور على أخطاء تجاوز الحدود (out-of-bounds) والاستخدام بعد التحرير (use-after-free). تستخدم KASAN أدوات القياس في وقت الترجمة (compile-time instrumentation) لإدراج فحوصات صلاحية قبل كل وصول إلى الذاكرة، ولذلك تتطلب إصدار مترجم (compiler) يدعم ذلك. يمكن فحص وصول النواة إلى الذاكرة مقابل خريطة الظل (shadow map) للتأكد من صلاحيتها.
QEMU (Quick Emulator) هو محاكٍ مجاني ومفتوح المصدر يقوم بالمحاكاة الافتراضية للعتاد (hardware virtualization). محاكي أندرويد مشتق من محاكي QEMU؛ فهو يضيف دعمًا لإقلاع أجهزة أندرويد، ويحاكي عتاد أندرويد النموذجي (OpenGL وGPS وGSM وأجهزة الاستشعار) وواجهة مستخدم رسومية. يعمل محاكي أندرويد على توسيع QEMU بطرق متعددة.
في التحليل الثابت، نستخدم الكود المصدري للبرنامج للعثور على خطأ أو أي مشكلة. لا نقوم بتشغيل البرنامج.
في التحليل الديناميكي، نقوم بتحليل سلوك البرنامج أثناء تشغيله، على سبيل المثال من خلال توفير مدخلات خاصة.
Android NDK هو مجموعة أدوات تتيح لك تشغيل الكود الأصلي (native code) مثل C وC++ على جهاز أندرويد.
نواة Android goldfish تُستخدم لتشغيل كود النواة في محاكي أندرويد. يمكن استنساخها وتعديلها ثم بناؤها لاستخدامها في محاكٍ.
ADB هو أداة سطر أوامر. تساعد في التواصل مع جهاز أندرويد قيد التشغيل والحصول على شل (shell) منه. ويمكن استخدامها لأغراض التصحيح.
https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html
https://nvd.nist.gov/vuln/detail/CVE-2019-2215
https://www.tutorialspoint.com/gnu_debugger
https://www.kernel.org/doc/html/latest/dev-tools/kasan.html
https://android.googlesource.com/kernel/goldfish/
https://man7.org/linux/man-pages/man7/epoll.7.html
https://www.scaler.com/topics/c/debugging-c-program/
...