
تحليل ثغرة CVE في نواة أندرويد ودليل إثبات المفهوم (PoC) لثغرة خلط الأنواع (type confusion) في مخصّص ذاكرة ION الخاص بشركة MediaTek، مع تغطية مقارنة السبب الجذري (root-cause diffing)، وآلية الاستغلال بدون صلاحيات، وتقييم قابلية الاستغلال.
الملخص. CVE-2023-20768 هي ثغرة خلط أنواع (CWE-843) في مُخصِّص ION من MediaTek. تحققتُ مما إذا كانت قابلة للاستغلال فعليًا على جهاز Samsung SM-M325F (Galaxy M32، Helio G80) يعمل بالبرمجيات الثابتة لشهر يوليو 2022. الكود القابل للاستغلال موجود، وأثبتُ أنه يُنفَّذ على هذا الجهاز عند تفعيله بواسطة عملية غير مميزة. لم أتمكن من تحويله إلى سلاح استغلال. مسار ioctl مقيد بالتحقق من المدخلات، والمسار الذي يحتوي على القراءة الفعلية خارج النطاق لا يعالج إلا مخازن ION الحقيقية فقط، لأن فحصًا ثانيًا على مؤشر dma_buf_ops يرفض أي شيء يمكنني تزويره. يغطي هذا التقرير كيف توصلتُ إلى هذا الاستنتاج، بما في ذلك استنتاج وسيط واحد اتضح لاحقًا أنه خاطئ.
الثغرة معلنة وتم إصلاحها. جرى جميع الاختبارات على جهازي الخاص، بصلاحيات الجذر عبر Magisk.
الخلل في كود ION الخاص بـ MediaTek، وليس في نواة Linux الأصلية (upstream) ولا في أي شيء كتبته Samsung. توزع MediaTek نسختها المعدلة (fork) من مُخصِّص ION الخاص بنظام Android ضمن حزمة BSP التي تصل إلى كل شركة مصنعة تعتمد على رقائقها. يستخدم M32 معالج Helio G80، لذا فهو يحصل على هذا الكود. نفس الهاتف بمعالج Exynos SoC لن يتأثر إطلاقًا.
سحبتُ صورتي vmlinux لعام 2022 (غير المُصلَّحة) و2023 (المُصلَّحة) وأجريت مقارنة (diff) بينهما في IDA. تغيرت دالتان في كود ION:
| الدالة | 2022 | 2023 |
|---|---|---|
ion_drv_file_to_buffer | strstr(name, "dmabuf") | is_dma_buf_file() |
_ion_ioctl | strcmp(name, "ion") | is_dma_buf_file() |
الدالة is_dma_buf_file غير موجودة في صورة 2022، وتظهر في صورة 2023. إذن كانت الدالتان تقرران ما إذا كان struct file يمثل dma_buf من خلال النظر إلى الاسم فقط، وقد استُبدل هذا الفحص في التصحيح بفحص نوع فعلي. الخلط بين الكائنات هنا يعني أن النواة تقرأ كائنًا ليس dma_buf كما لو كان واحدًا.
من بين الدالتين، الدالة _ion_ioctl هي التي يمكن لعملية غير مميزة الوصول إليها:
open("/dev/ion")
-> ion_ioctl (.unlocked_ioctl)
-> ION_IOC_CUSTOM (0xC0104906)
-> ion_custom_ioctl
-> _ion_ioctl
-> case 0: ION_SYS_CACHE_SYNC
-> find_vma(user_VA) (call site at _ion_ioctl+0x9f0)
-> strcmp(vma->vm_file...name, "ion")
الطلب هو بنية ion_custom_data { u32 cmd = 0; u64 arg; } تشير إلى بنية ion_sys_data بحجم 120 بايت:
يقوم spoof.c ببناء هذا الطلب.
لم أستطع مجرد تتبعها. فالجهاز يحظر kprobe_events وset_ftrace_filter وfunction_graph عبر سياسة SELinux من Samsung وتقوية النواة، كما أن دوال printk الخاصة بـ ION مقيدة بأعلام التصحيح (debug-gated)، لذا تبقى dmesg صامتة.
لذا استخدمتُ رموز الإرجاع كمؤشر (oracle) بدلاً من ذلك. أربعة طلبات، ونمط النتائج يُخبرك أين اتجه التنفيذ:
عودة C بالنجاح تعني أن جملة switch توجّه التنفيذ فعلًا بناءً على sys_cmd. واختلاف A عن B يعني أن العنوان الافتراضي VA قيد المعالجة، وهو ما يُدخل التنفيذ داخل find_vma. هذا هو المسار القابل للاستغلال، وتم الوصول إليه بدون صلاحيات الجذر.
الوصول إلى الفحص ليس مثل تجاوزه. اختبرتُ ما يقبله strcmp فعلًا:
ION_IOC_SHARE ثم mmap يجتاز الفحص ويعيد 0.memfd:ion يفشل ويعيد -EFAULT.ion يفشل أيضًا ويعيد -EFAULT.إذن الحقل الذي تتم مقارنته عند vm_file+0x60 ليس اسم الملف. إنه حقل داخلي في dma_buf، وشبه المؤكد أنه dma_buf->exp_name، والذي يضبطه ION على "ion". الفحص غير سليم من حيث التصميم، لكن لا يوجد شيء يمكنني إنشاؤه من مساحة المستخدم لضبط هذا الحقل.
ثم أجريتُ اختبار fuzzing على المسار: sync_type من 0 إلى 7، وأحجام {0, 1, 0x1000, 0x100000, 0xffffffff}، وعناوين VA {مخزن ion حقيقي، memfd، 0}، أي 120 حالة، بالإضافة إلى فحص بمقبض مُحرَّر لاستكشاف استخدام بعد التحرير (use-after-free). لم يحدث أي انهيار، وبقي الجهاز يعمل. الأحجام المتجاوزة للحد الأقصى تخرج مبكرًا قبل find_vma وتعيد 0. أنواع المزامنة من 3 إلى 5 تدخل مسار m4u وتعيد -EPERM. أي قيمة أعلى من 5 تعيد -EINVAL. المقبض المُحرَّر يعيد -EINVAL، لذا يتحقق ION منه ولا يوجد UAF هناك.
القراءة الفعلية خارج النطاق موجودة في ion_drv_file_to_buffer. فهي تنفذ ldr [private_data+0x28]، أي تقرأ private_data لكائن ليس dma_buf كما لو كان dma_buf. توجد مقارنة ops == &ion_dma_buf_ops (الجدول عند العنوان 0xFFFFFF800A097F18)، لكنها تحدث بعد تلك القراءة، لذا لا تمنعها. في المراحل اللاحقة، تقرأ __do_dump_share_fd الحقول عند +0x28 و+0x48 و+0x50 و+0xb8 و+0xe4 من المخزن المُرجَع وتطبعها، والتعليمة ldr x8, [buf+0x28]; ldr [x8+0x30] هي إلغاء مرجعية مؤشر بري (wild pointer deref) لكائن مختلَط.
المشغِّل هو ion_dump_all_share_fds، الذي يستخدم iterate_fd لاجتياز واصفات الملفات لكل عمليات عملاء ION. كان استنتاجي الأول أن هذا يعمل فقط عند قراءة عقدة debugfs الخاصة بـ ION، وهذه النواة لديها CONFIG_DEBUG_FS غير مفعّل. تحققتُ من ذلك بثلاث طرق: عبر /proc/config.gz، وغياب debugfs من /proc/filesystems، وعودة mount -t debugfs برمز ENODEV. فاستبعدتُ هذا المسار باعتباره غير قابل للوصول بنيويًا.
وكان ذلك خاطئًا. الدالة dump_header، وهي تفريغ الذاكرة الخاص بقاتل OOM، تحتوي على استدعاء مباشر bl ion_mm_heap_memory_detail عند العنوان 0xffffff8008204b9c، ويتم استدعاء dump_header من out_of_memory وoom_kill_process. لا حاجة إلى debugfs.
يؤكد ذلك memcg_oom.c. فهو ينشئ cgroup تحت /dev/memcg، ويحدّ كلا من memory.limit_in_bytes وmemory.memsw.limit_in_bytes بـ 8MB (تحديد الأول فقط يسمح للعملية الابنة بالهروب إلى مساحة التبديل zram)، ثم ينشئ عملية ابنة (fork) تخصص الذاكرة حتى تموت. ثم تُظهر dmesg:
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
متبوعة بمخرجات كاملة من ion_mm_heap_memory_detail و__do_dump_share_fd تُظهر مخازن gralloc dma_buf الحقيقية. إذن، الدالة القابلة للاستغلال تُنفَّذ خلال حدث يمكن لأي عملية غير مميزة التسبب به. يوجد أيضًا مشغِّل ثالث، وهو ShowStatus من hang_detect_dump_thread في مراقب MediaTek (watchdog).
احتفظتُ بـ 32 memfd مفتوحًا باسم memfd:dmabuf وفعّلتُ نفس OOM عبر memcg. لو أن أحد memfds الخاصة بي مُرِّر إلى ion_drv_file_to_buffer، لاجتاز strstr الفحص، وكانت private_data ستصبح NULL، وستطبع النواة الرسالة [ION]ion_drv_file_to_buffer warnning, dmabuf is NULL بمستوى KERN_ERR. هذا السطر لم يظهر أبدًا. فقط عملاء الرسوميات وgralloc تم تفريغهم. إما أن جدول واصفات الملفات (fd) لعميل /dev/ion عادي ليس ما يجتازه iterate_fd هنا، أو أن memfd يفشل بصمت قبل الطبع.
في كلتا الحالتين، لا يتعامل تفريغ OOM إلا مع مخازن dma_buf الشرعية للنظام، والتي تمر عبر الفحص دون أخطاء. كما أن memfd هو النوع الوحيد من واصفات الملفات الذي يمكنني التحكم في اسمه بما يكفي ليحتوي على "dmabuf"، وهو لا يمكن أن يسبب خطأ.
الثغرة موجودة فعلًا. الكود القابل للاستغلال قابل للوصول ويُتنفَّذ فعلًا على هذا الإصدار، ويمكن تفعيله بدون صلاحيات الجذر. لكنه غير قابل للتحويل إلى سلاح استغلال من مساحة المستخدم هنا. مسار ioctl مقيد بالتحقق من المقبض، وفحص للحجم، ودالة access_ok. ومسار التفريغ لا يرى سوى مخازن ION الحقيقية، والتزوير محظور بواسطة ops == &ion_dma_buf_ops. المضي قدمًا يتطلب كائنًا غير تابع لـ ION قابلًا للتحكم بحيث يكون exp_name الخاص به هو "ion"، أو بدائية استغلال مختلفة مثل UAF في مخزن ION، أو سباق TOCTOU.
spoof.c — برهان إثبات المفهوم (PoC) لـ ioctl مزامنة الكاش (cache-sync) والمؤشر التفاضلي (differential oracle)memcg_oom.c — مشغِّل OOM عبر memcg لمسار التفريغtrigger.c، oom_trigger.c — محاولات تفعيل سابقةboot_images/ — صور النواة المستخرجة (2022 و2023) وقاعدة بيانات IDA لإصدار 2022| الإزاحة | الحقل |
|---|
+0x00 | sys_cmd = 0 |
+0x08 | مقبض ion (خصِّص واحدًا أولاً، heap_id_mask = 0x1 يعمل) |
+0x10 | عنوان افتراضي للمستخدم |
+0x18 | النصف المنخفض = الحجم، والنصف المرتفع = sync_type ضمن {0,1,2} |
| الحالة | الطلب | النتيجة |
|---|
| A | sys_cmd=0، عنوان افتراضي مزوَّر | -EFAULT |
| B | sys_cmd=0، VA = 0 | 0 |
| C | sys_cmd=4 | 0 |
| D | sys_cmd=99 | -EFAULT |