
PoC pour CVE-2021-39749, permettant de lancer une Activity arbitraire sur Android 12L Beta
Ceci est un PoC pour CVE-2021-39749, qui permet de démarrer des activités d'autres applications sur Android 12L Beta, indépendamment de leurs paramètres permission et exported
Dans Android 12L, l'accès à TaskFragmentOrganizer (intentionnellement) ne nécessite plus la permission MANAGE_ACTIVITY_TASKS
L'utilisation de l'application fournie ici nécessite la désactivation des vérifications des API cachées, vous pouvez le faire via adb shell settings put global hidden_api_policy 1. Celles-ci ne constituent pas une frontière de sécurité et il existe des contournements connus basés sur des applications
Voici les commits corrigeant ce bug (et quelques autres liés mentionnés dans le rapport original) :
startActivityInTaskFragment ne repose plus sur Binder.getCallingUid()ResolverActivity a maintenant relinquishTaskIdentity activéSurfaceControl de TaskFragment n'est plus fourniActivityRecord#appToken à TaskFragmentOrganizer est désormais basée sur l'uid au lieu du pidVous pouvez vérifier android-12.1.0_r4, annuler les 3 premiers commits (ou les 2 premiers, l'application pourra toujours éteindre l'appareil (en démarrant ShutdownActivity), mais la case « Zoom and set alpha » ne fonctionnera pas)
(Le premier commit de cette liste aura des conflits de fusion dans les tests si vous essayez de l'annuler, mais vous pouvez les ignorer)
Binder.getCallingUid() qui retourne toujours l'uid systèmeLa méthode Binder.getCallingUid() retourne l'uid du processus qui a envoyé la transaction Binder actuellement traitée. Cet uid est stocké dans une variable locale au thread. Le code gérant la transaction peut appeler Binder.clearCallingIdentity() pour définir cette variable sur l'uid de son propre processus afin d'indiquer aux méthodes appelées plus tard pendant le traitement de la transaction que les vérifications de permission doivent être effectuées contre lui-même (le code gérant la transaction) et non contre l'appelant de la transaction Binder
Parfois, il y a des Binder.getCallingUid() qui sont toujours appelés après Binder.clearCallingIdentity(), retournant donc toujours l'uid du propre processus. Parfois cela se produit intentionnellement, par exemple dans ActivityTaskManagerService#startDreamActivity (bien que ce soit une façon plutôt alambiquée de faire Process.myUid() ou Os.getuid())
J'ai écrit pour moi-même un outil d'analyse statique (basé sur Soot) qui signale ces appels Binder.getCallingUid() (et d'autres vérifications de permission) qui ne peuvent se produire qu'après Binder.clearCallingIdentity(). (J'ai une logique personnalisée pour gérer l'IR Jimple/Shimple RI fournie par Soot, bien qu'il puisse y avoir une meilleure façon de le faire avec Soot, mais c'est ce que j'ai actuellement)
Dans Android 12L Beta, cet outil en a trouvé un dans ActivityStartController#startActivityInTaskFragment (Remarque : le code source n'était pas disponible à ce moment-là car les versions Beta ne sont pas open source, mais Shimple est généralement lisible, donc j'utilisais Soot aussi comme décompilateur Java)
startActivityInTaskFragmentDans le cadre du rapport d'analyse statique, j'ai obtenu la hiérarchie d'appels depuis l'implémentation de onTransact() (où l'appel Binder commence) jusqu'à startActivityInTaskFragment :
onTransact dans le code généré par aidl de IWindowOrganizerControllerWindowOrganizerController#applyTransaction (sans l'argument CallerInfo)WindowOrganizerController#applyTransaction (avec l'argument CallerInfo)WindowOrganizerController#applyHierarchyOpActivityStartController#startActivityInTaskFragmentJ'ai découvert que les appels Binder à applyTransaction sont présents dans la classe TaskFragmentOrganizer et j'ai décidé de l'utiliser comme wrapper plus pratique que de faire tous les appels Binder directement (ni l'un ni l'autre n'est une API publique, donc j'ai dû utiliser la réflexion de toute façon)
Tout d'abord, la méthode « 2. » appelle enforceTaskPermission, qui sur Android 12.0 vérifiait la permission MANAGE_ACTIVITY_TASKS réservée à la signature, que nous ne pouvions pas obtenir. Cependant, sur Android 12L, les règles ont été assouplies afin que certaines transactions puissent être effectuées sans permissions. Il s'est avéré qu'aucune des opérations nécessaires pour effectuer startActivityInTaskFragment ne requérait de permission (si la transaction avait un TaskFragmentOrganizer associé)
Nous voulons donc effectuer HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT. Pour ce faire, nous devons avoir notre TaskFragment enregistré dans mLaunchTaskFragments, sinon l'exception "Not allowed to operate with invalid fragment token" sera signalée
Nous pouvons enregistrer un tel TaskFragment via HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, qui appelle createTaskFragment()
(Dans le code du PoC, ces transactions sont envoyées dans SecondActivity : HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT est envoyé par initOrganizerAndFragment() et HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT est envoyé par startActivityInOrganizer)
Cela nous permet donc d'appeler startActivityInTaskFragment et les Intents des Activités démarrées ici sont considérés comme provenant de l'uid système, mais il s'avère qu'en soi cela ne nous permet pas de faire quoi que ce soit : les activités démarrées par le système ne peuvent pas faire de grants URI et si nous essayons de lancer une activité d'une autre application, nous serons arrêtés par la vérification canEmbedActivity
canEmbedActivityReprenons canEmbedActivity : l'intégration est autorisée si taskFragment.getTask().effectiveUid est l'uid du système ou correspond à l'uid de l'application lancée. Nous devrons être dans une tâche dont effectiveUid est le système
Revenons aussi à createTaskFragment() : la création de TaskFragment n'était autorisée que si rootActivity.getUid() != ownerActivity.getUid(). Cela signifie que notre activité devra être en bas de la pile arrière de la Task dans laquelle elle se trouve
Nous devrons lancer une nouvelle Task (via Intent.FLAG_ACTIVITY_NEW_TASK) qui aura une Activité appartenant à l'uid système (afin que Task#effectiveUid soit défini sur AID_SYSTEM), puis cette Activité démarrera notre Activité (dans la même Task) et finish() elle-même (afin que notre Activité devienne la racine de cette tâche, nous permettant d'utiliser createTaskFragment())
Une de ces Activités est ChooserActivity. Le Chooser est généralement utilisé pour choisir à quelle application l'utilisateur veut recourir après avoir sélectionné l'option « partager ». ChooserActivity a cependant android:relinquishTaskIdentity="true" défini dans AndroidManifest.xml, ce qui signifie que lorsqu'elle lance une autre Activité, elle écrase Task#effectiveUid avec l'uid de l'application nouvellement lancée
(relinquishTaskIdentity ne fonctionne que lorsqu'il est utilisé par la première application dans la Task et uniquement pour les applications système, donc nous ne pouvons pas utiliser relinquishTaskIdentity nous-mêmes et lancer une application système pour écraser Task#effectiveUid de notre Task)
Une autre Activité de ce type (qui peut démarrer notre Activité et finish() elle-même) est ResolverActivity. Elle est utilisée lors du démarrage d'un Intent implicite qui se résout en plusieurs Activités. Le Resolver (contrairement au Chooser) offre une option pour mémoriser le choix, ce qui est la façon dont vous (en tant qu'utilisateur du téléphone) pouvez distinguer les deux. ResolverActivity n'avait pas relinquishTaskIdentity défini, cependant le Resolver utilise son propre Intent pour trouver quelles options sont disponibles (tandis que le Chooser prend l'Intent fourni dans les Extras). Cela s'avère être un problème pour l'exploitation car les flags d'Intent utilisés par le Resolver lors du lancement de l'Activité sélectionnée seront les mêmes que ceux utilisés pour lancer le Resolver et :
Intent.FLAG_ACTIVITY_NEW_TASK, le Resolver sera lancé dans notre Task qui a déjà effectiveUid défini en permanenceIntent.FLAG_ACTIVITY_NEW_TASK, le Resolver lancera la sélection dans une autre Task, qui aura alors effectiveUid défini sur celui appartenant à l'application lancéeLa solution à ces problèmes est d'utiliser les deux :
ChooserActivity : Nous fournissons à son Intent :
Intent.FLAG_ACTIVITY_NEW_TASK, afin que le Chooser soit lancé dans une nouvelle Task (qui aura effectiveUid du système mais seulement jusqu'au prochain lancement d'Activité)Intent.EXTRA_INTENT défini sur un Intent qui ne correspond à aucune Activité et les seules options restantes dans le Chooser proviendront de Intent.EXTRA_INITIAL_INTENTSIntent.EXTRA_INITIAL_INTENTS contenant un tableau avec un seul élément : l'Intent que nous voulons que le Chooser lance (lorsqu'il n'y a qu'une seule option, le Chooser et le Resolver sautent l'invite et lancent immédiatement la seule option et finish() eux-mêmes)ResolverActivity est lancée par ChooserActivity :
(Dans l'application du PoC, la préparation de ces étapes est effectuée dans FirstActivity)
TaskFragmentOrganizer a reçu un SurfaceControl via le callback onTaskFragmentAppeared et en utilisant ce SurfaceControl, on peut mettre à l'échelle l'Activité lancée et la rendre transparente, tout en continuant à recevoir les événements tactiles et sans être considérée comme obscurcie (donc les éléments protégés contre le tap-jacking peuvent toujours être touchés)
Vous pouvez le voir en cochant la case « Zoom and set alpha » dans l'application du PoC
Cela est corrigé par le commit « 3. » de la liste des correctifs en haut
Une autre chose est que les ActivityRecord#appToken-s des Activités s'exécutant dans TaskFragment sont passés aux callbacks de TaskFragmentOrganizer. Cette liste était filtrée pour n'inclure que les jetons des Activités dans le même processus, cependant la vérification était effectuée en comparant le pid de TaskFragmentOrganizer avec le pid de l'Activité dont nous pouvions obtenir le appToken. Je n'ai pas réellement vérifié, mais je pense qu'une application pourrait créer un TaskFragmentOrganizer, quitter le processus utilisé pour le créer initialement et avoir son pid réutilisé comme pid d'une Activité d'une autre application afin d'obtenir son appToken. Voici le commit (« 4. » dans la liste des correctifs ci-dessus) qui change la vérification de basée sur le pid à basée sur l'uid (il semble que ce commit ait été fait indépendamment de mon rapport cependant (bien qu'après celui-ci))
Une fois que l'attaquant obtient le appToken d'une Activité, il peut injecter des appels onActivityResult() (même si l'application cible n'a pas appelé startActivityForResult() elle-même) et éventuellement falsifier savedInstanceState (en appelant activityStopped(), en supposant que l'attaquant puisse gagner la course contre l'application cible appelant cette méthode et que l'appel supplémentaire ne cause pas la perte d'état due à un crash)
Je n'ai pas vérifié si cela peut être fait dans ce cas, cependant précédemment, avec CVE-2020-0001 (Ouais, j'ai un numéro chic), j'ai pu utiliser la falsification de savedInstanceState et l'injection de onActivityResult() pour tromper l'application des paramètres système afin d'activer mon AccessibilityService sans interaction de l'utilisateur, mais c'est une histoire pour une autre fois
relinquishTaskIdentity défini, donc maintenant Task#effectiveUid est défini sur le système et le restera indépendamment des prochaines Activités lancées dans cette TaskIntent.FLAG_ACTIVITY_NEW_TASK, donc la prochaine Activité est lancée dans la même Task<intent-filter> que nous avons déclaré nous-mêmes dans notre application, donc le Resolver procède immédiatement au lancement de notre ActivitéResolverActivity lance notre Activité
effectiveUid est AID_SYSTEM, donc canEmbedActivity() autorise toutfinish() eux-mêmes, donc nous sommes l'Activité racine dans la Task et nous sommes autorisés à utiliser createTaskFragment()