
إثبات المفهوم (PoC) لـ CVE-2021-39749، والذي يسمح بتشغيل أي نشاط (Activity) على Android 12L Beta
هذا هو PoC لـ CVE-2021-39749، والذي يسمح بتشغيل أنشطة تطبيقات أخرى على Android 12L Beta بغض النظر عن إعدادات permission و exported الخاصة بها
في Android 12L، الوصول إلى TaskFragmentOrganizer (عن قصد) لم يعد يتطلب صلاحية MANAGE_ACTIVITY_TASKS
يتطلب استخدام التطبيق المقدم هنا تعطيل فحوصات واجهات برمجة التطبيقات المخفية، يمكنك القيام بذلك عبر adb shell settings put global hidden_api_policy 1. هذه ليست حدودًا أمنية وهناك تجاوزات معروفة قائمة على التطبيقات
فيما يلي الالتزامات (commits) التي تصلح هذا الخطأ (وبعض الأخطاء ذات الصلة المذكورة في التقرير الأصلي):
startActivityInTaskFragment لم يعد يعتمد على Binder.getCallingUid()ResolverActivity الآن لديها relinquishTaskIdentity مفعّلةSurfaceControl الخاص بـ TaskFragment لم يعد متاحًاActivityRecord#appToken إلى TaskFragmentOrganizer أصبح الآن يعتمد على uid بدلاً من pidيمكنك التحقق من android-12.1.0_r4، وعكس أول 3 التزامات (أو أول 2، سيظل التطبيق قادرًا على إيقاف تشغيل الجهاز (عن طريق تشغيل ShutdownActivity)، لكن خانة الاختيار "Zoom and set alpha" لن تعمل)
(سيكون للالتزام الأول من تلك القائمة تعارضات دمج في الاختبارات إذا حاولت عكسه، ولكن يمكنك تجاهلها)
Binder.getCallingUid() الذي يعيد دائمًا uid النظامطريقة Binder.getCallingUid() تعيد uid العملية التي أرسلت معاملة Binder التي تتم معالجتها حاليًا. يتم تخزين هذا uid في متغير محلي للخيط. يمكن للكود الذي يعالج المعاملة استدعاء Binder.clearCallingIdentity() لتعيين هذا المتغير إلى uid العملية الخاصة به للإشارة إلى الطرق التي سيتم استدعاؤها لاحقًا أثناء معالجة المعاملة بأنه يجب إجراء فحوصات الصلاحيات ضد نفسه (الكود الذي يعالج المعاملة) وليس ضد مستدعي معاملة Binder
في بعض الأحيان توجد استدعاءات Binder.getCallingUid() التي يتم استدعاؤها دائمًا بعد Binder.clearCallingIdentity()، وبالتالي تعيد دائمًا uid العملية الخاصة. في بعض الأحيان يحدث هذا عن قصد، على سبيل المثال في ActivityTaskManagerService#startDreamActivity (على الرغم من أن هذه طريقة ملتوية إلى حد ما للقيام بـ Process.myUid() أو Os.getuid())
لقد كتبت لنفسي أداة تحليل ثابتة (قائمة على Soot) تبلغ عن استدعاءات Binder.getCallingUid() هذه (وفحوصات صلاحيات أخرى) التي يمكن أن تحدث فقط بعد Binder.clearCallingIdentity(). (لدي منطق مخصص للتعامل مع Jimple/Shimple IR المقدم من Soot، على الرغم من أنه قد تكون هناك طريقة أفضل للقيام بذلك مع Soot، لكن هذا ما لدي الآن)
في Android 12L Beta وجدت تلك الأداة واحدًا في ActivityStartController#startActivityInTaskFragment (ملاحظة: الكود المصدري لم يكن متاحًا آنذاك لأن إصدارات Beta ليست مفتوحة المصدر، لكن Shimple قابل للقراءة بشكل عام لذلك كنت أستخدم Soot أيضًا كمفكك شفرة Java)
startActivityInTaskFragmentكجزء من تقرير التحليل الثابت حصلت على تسلسل الاستدعاءات من تنفيذ onTransact() (حيث تبدأ استدعاءات Binder) إلى startActivityInTaskFragment:
onTransact في الكود المولد بواسطة aidl لـ IWindowOrganizerControllerWindowOrganizerController#applyTransaction (بدون وسيط CallerInfo)WindowOrganizerController#applyTransaction (مع وسيط CallerInfo)WindowOrganizerController#applyHierarchyOpActivityStartController#startActivityInTaskFragmentلقد وجدت أن استدعاءات Binder إلى applyTransaction موجودة في فئة TaskFragmentOrganizer وقررت استخدامها كغلاف أكثر ملاءمة من القيام بجميع استدعاءات Binder مباشرة (لا يعتبر أي منهما واجهة برمجة تطبيقات عامة لذا كان علي استخدام الانعكاس على أي حال)
أولاً وقبل كل شيء، الطريقة "2." تستدعي enforceTaskPermission، والتي في Android 12.0 كانت تتحقق من صلاحية MANAGE_ACTIVITY_TASKS المقتصرة على signature والتي لم نتمكن من الحصول عليها، ومع ذلك في Android 12L تم تخفيف القواعد بحيث يمكن إجراء معاملات معينة دون صلاحيات. اتضح أن أياً من العمليات المطلوبة للقيام بـ startActivityInTaskFragment لم تتطلب صلاحية (إذا كانت المعاملة مرتبطة بـ TaskFragmentOrganizer)
لذلك نريد تنفيذ HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT. للقيام بذلك يجب أن يكون لدينا TaskFragment مسجل في mLaunchTaskFragments وإلا سيتم الإبلاغ عن استثناء "Not allowed to operate with invalid fragment token"
يمكننا تسجيل مثل هذا TaskFragment من خلال HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT، والذي يستدعي createTaskFragment()
(في كود PoC يتم إرسال هذه المعاملات في SecondActivity: يتم إرسال HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT بواسطة initOrganizerAndFragment() ويتم إرسال HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT بواسطة startActivityInOrganizer)
لذلك يسمح لنا ذلك باستدعاء startActivityInTaskFragment وتعتبر نوايا (Intents) الأنشطة التي تبدأ هنا قادمة من uid النظام، ولكن اتضح أن ذلك في حد ذاته لا يسمح لنا بفعل أي شيء: الأنشطة التي يبدأها النظام لا يمكنها القيام بمنح URI وإذا حاولنا تشغيل نشاط تطبيق آخر فسيتم إيقافنا بواسطة فحص canEmbedActivity
canEmbedActivityلنلقِ نظرة على canEmbedActivity مرة أخرى: يُسمح بالتضمين إذا كان taskFragment.getTask().effectiveUid هو uid النظام أو يطابق uid التطبيق الذي تم تشغيله. سنحتاج إلى أن نكون في مهمة يكون effectiveUid الخاص بها هو uid النظام
أيضًا نعود خطوة إلى الوراء إلى createTaskFragment(): كان إنشاء TaskFragment مسموحًا فقط إذا كان rootActivity.getUid() != ownerActivity.getUid(). هذا يعني أن نشاطنا سيحتاج إلى أن يكون في أسفل مكدس الرجوع للمهمة التي يوجد فيها
سنحتاج إلى تشغيل Task جديدة (من خلال Intent.FLAG_ACTIVITY_NEW_TASK) والتي ستحتوي على نشاط ينتمي إلى uid النظام (بحيث يتم تعيين Task#effectiveUid إلى AID_SYSTEM) ثم يقوم ذلك النشاط بتشغيل نشاطنا (داخل نفس المهمة) و finish() نفسه (بحيث يصبح نشاطنا جذر تلك المهمة مما يسمح لنا باستخدام createTaskFragment())
أحد هذه الأنشطة هو ChooserActivity. يُستخدم Chooser عادةً لاختيار التطبيق الذي يريد مستخدم التطبيق استخدامه بعد تحديد خيار "مشاركة". ومع ذلك فإن ChooserActivity لديها android:relinquishTaskIdentity="true" مضبوطة في AndroidManifest.xml، مما يعني أنه عندما تشغل نشاطًا آخر فإنها ستقوم باستبدال Task#effectiveUid بـ uid التطبيق الذي تم تشغيله حديثًا
(relinquishTaskIdentity تعمل فقط عند استخدامها من قبل أول تطبيق في المهمة وفقط لتطبيقات النظام، لذلك لا يمكننا استخدام relinquishTaskIdentity بأنفسنا وتشغيل تطبيق نظام لاستبدال Task#effectiveUid لمهمتنا)
نشاط آخر من هذا القبيل (يمكنه تشغيل نشاطنا و finish() نفسه) هو ResolverActivity. يتم استخدامه عند تشغيل Intent ضمني يحل إلى أنشطة متعددة. يقدم Resolver (على عكس Chooser) خيار تذكر الاختيار، وهذا هو كيف يمكنك (كمستخدم للهاتف) التمييز بين الاثنين. لم يكن لدى ResolverActivity relinquishTaskIdentity مضبوطة، ومع ذلك يستخدم Resolver نية خاصة به لمعرفة الخيارات المتاحة (بينما يأخذ Chooser النية المقدمة في Extras). يتبين أن هذا يمثل مشكلة للاستغلال لأن علامات النية التي يستخدمها Resolver عند تشغيل النشاط المحدد ستكون نفسها المستخدمة لتشغيل Resolver و:
Intent.FLAG_ACTIVITY_NEW_TASK، سيتم تشغيل Resolver داخل مهمتنا التي لديها بالفعل effectiveUid مضبوط بشكل دائمIntent.FLAG_ACTIVITY_NEW_TASK، سيقوم Resolver بتشغيل التحديد في مهمة أخرى، والتي سيكون لها بعد ذلك effectiveUid مضبوطًا على ما ينتمي إلى التطبيق الذي تم تشغيلهالحل لهذه المشاكل هو استخدام كليهما:
ChooserActivity: نقدم لنيته:
Intent.FLAG_ACTIVITY_NEW_TASK، بحيث يتم تشغيل Chooser في مهمة جديدة (سيكون لها effectiveUid للنظام ولكن فقط حتى تشغيل النشاط التالي)Intent.EXTRA_INTENT مضبوطًا على Intent لا يطابق أي أنشطة والخيارات الوحيدة المتبقية في Chooser ستأتي من Intent.EXTRA_INITIAL_INTENTSIntent.EXTRA_INITIAL_INTENTS يحتوي على مصفوفة بعنصر واحد: النية التي نريد أن يشغلها Chooser (عندما يكون هناك خيار واحد فقط، يتخطى كل من Chooser و Resolver المطالبة ويشغلان على الفور الخيار الوحيد و finish() نفسه)ResolverActivity بواسطة ChooserActivity:
relinquishTaskIdentity مضبوطة، لذا الآن مضبوط على النظام وسيبقى كذلك بغض النظر عن الأنشطة التالية التي يتم تشغيلها في هذه المهمة(في تطبيق PoC يتم إعداد هذه الخطوات في FirstActivity)
تلقى TaskFragmentOrganizer SurfaceControl من خلال استدعاء onTaskFragmentAppeared وباستخدام ذلك SurfaceControl يمكن للمرء تغيير حجم النشاط الذي تم تشغيله وجعله شفافًا، بينما سيستمر في استقبال أحداث اللمس ولن يعتبر محجوبًا (لذا يمكن النقر على العناصر المحمية من النقر الخادع)
يمكنك رؤية ذلك عن طريق تحديد خانة الاختيار "Zoom and set alpha" في تطبيق PoC
تم إصلاح هذا بواسطة الالتزام "3." من قائمة الإصلاحات في الأعلى
شيء آخر هو أن ActivityRecord#appToken-s للأنشطة التي تعمل داخل TaskFragment يتم تمريرها إلى استدعاءات TaskFragmentOrganizer. تمت تصفية هذه القائمة لتشمل فقط رموز الأنشطة داخل نفس العملية، ومع ذلك تم إجراء الفحص بمقارنة pid الخاص بـ TaskFragmentOrganizer مع pid الخاص بالنشاط الذي يمكننا الحصول على appToken الخاص به. لم أتحقق فعليًا لكنني أعتقد أن التطبيق يمكنه إنشاء TaskFragmentOrganizer، والخروج من العملية المستخدمة لإنشائه في البداية وإعادة استخدام pid الخاص بها كـ pid لنشاط تطبيق آخر من أجل الحصول على appToken الخاص به. إليك الالتزام ("4." في قائمة الإصلاحات أعلاه) الذي يحول التحقق من القائم على pid إلى القائم على uid (يبدو أن هذا الالتزام تم بشكل مستقل عن تقريري على الرغم من ذلك (على الرغم من أنه بعده))
بمجرد حصول المهاجم على appToken لنشاط ما، يمكنه حقن استدعاءات onActivityResult() (حتى لو لم يستدعِ التطبيق المستهدف startActivityForResult() بنفسه) وربما العبث بـ savedInstanceState (عن طريق استدعاء activityStopped()، بافتراض أن المهاجم يمكنه الفوز بسباق مع التطبيق المستهدف الذي يستدعي تلك الطريقة وأن الاستدعاء الإضافي لن يتسبب في فقدان الحالة بسبب تعطل)
لم أتحقق مما إذا كان يمكن القيام بذلك في هذه الحالة، ومع ذلك سابقًا، مع CVE-2020-0001 (نعم، حصلت على رقم رائع)، تمكنت من استخدام العبث بـ savedInstanceState وحقن onActivityResult() لخداع تطبيق إعدادات النظام لتمكين AccessibilityService الخاص بي دون تفاعل المستخدم، لكن تلك قصة لوقت آخر
Task#effectiveUidIntent.FLAG_ACTIVITY_NEW_TASK، لذا يتم تشغيل النشاط التالي داخل نفس المهمة<intent-filter> الذي أعلناه بأنفسنا في تطبيقنا، لذا يتابع Resolver فورًا تشغيل نشاطناResolverActivity بتشغيل نشاطنا
effectiveUid الخاص بها هو AID_SYSTEM، لذا يسمح canEmbedActivity() بأي شيءcreateTaskFragment()