
PoC für CVE-2021-39749, der das Starten beliebiger Activities auf Android 12L Beta ermöglicht
Dies ist ein PoC für CVE-2021-39749, der das Starten von Aktivitäten anderer Apps auf Android 12L Beta unabhängig von deren permission- und exported-Einstellungen ermöglicht
In Android 12L erfordert der TaskFragmentOrganizer-Zugriff (absichtlich) nicht länger die MANAGE_ACTIVITY_TASKS-Berechtigung
Die Verwendung der hier bereitgestellten App erfordert das Deaktivieren der Hidden API Checks. Dies kannst du über adb shell settings put global hidden_api_policy 1 tun. Diese stellen keine Sicherheitsgrenze dar und es gibt bekannte app-basierte Umgehungen
Hier sind Commits, die diesen Fehler (und einige verwandte, die im ursprünglichen Bericht erwähnt wurden) beheben:
startActivityInTaskFragment verlässt sich nicht länger auf Binder.getCallingUid()ResolverActivity hat jetzt relinquishTaskIdentity aktiviertSurfaceControl von TaskFragment wird nicht länger bereitgestelltActivityRecord#appToken an TaskFragmentOrganizer gesendet wird, basiert jetzt auf uid statt pidDu kannst android-12.1.0_r4 auschecken und die ersten 3 Commits (oder die ersten 2) zurücknehmen. Die Anwendung kann das Gerät dann weiterhin herunterfahren (durch Starten von ShutdownActivity), aber das Kontrollkästchen „Zoom and set alpha" funktioniert nicht
(Der erste Commit aus dieser Liste führt zu Merge-Konflikten in Tests, wenn du versuchst, ihn zurückzunehmen, aber diese kannst du ignorieren)
Binder.getCallingUid(), das immer die System-uid zurückgibtDie Methode Binder.getCallingUid() gibt die uid des Prozesses zurück, der die aktuell verarbeitete Binder-Transaktion gesendet hat. Diese uid wird in einer thread-lokalen Variable gespeichert. Code, der eine Transaktion verarbeitet, kann Binder.clearCallingIdentity() aufrufen, um diese Variable auf die uid des eigenen Prozesses zu setzen. Dies zeigt Methoden, die später während der Transaktionsverarbeitung aufgerufen werden, an, dass Berechtigungsprüfungen gegen sich selbst (den Code, der die Transaktion verarbeitet) und nicht gegen den Aufrufer der Binder-Transaktion durchgeführt werden sollten
Manchmal gibt es Binder.getCallingUid()-Aufrufe, die immer nach Binder.clearCallingIdentity() aufgerufen werden und daher immer die uid des eigenen Prozesses zurückgeben. Manchmal geschieht dies absichtlich, zum Beispiel in ActivityTaskManagerService#startDreamActivity (obwohl dies ein eher umständlicher Weg ist, um Process.myUid() oder Os.getuid() zu verwenden)
Ich habe für mich selbst ein (auf Soot basierendes) statisches Analysetool geschrieben, das solche Binder.getCallingUid()-Aufrufe (und andere Berechtigungsprüfungen) meldet, die nur nach Binder.clearCallingIdentity() auftreten können. (Ich habe eine eigene Logik für die Verarbeitung der von Soot bereitgestellten Jimple/Shimple-IR, obwohl es möglicherweise einen besseren Weg mit Soot gibt, aber das ist, was ich derzeit habe)
In der Android 12L Beta fand dieses Tool einen solchen Aufruf in ActivityStartController#startActivityInTaskFragment (Hinweis: Der Quellcode war damals nicht verfügbar, da Beta-Versionen nicht Open Source sind, aber Shimple ist im Allgemeinen lesbar, daher habe ich Soot auch als Java-Dekompilierer verwendet)
startActivityInTaskFragment aufAls Teil des statischen Analyseberichts habe ich die Aufrufhierarchie von der onTransact()-Implementierung (wo der Binder-Aufruf beginnt) bis zu startActivityInTaskFragment erhalten:
onTransact im aidl-generierten Code von IWindowOrganizerControllerWindowOrganizerController#applyTransaction (ohne CallerInfo-Argument)WindowOrganizerController#applyTransaction (mit CallerInfo-Argument)WindowOrganizerController#applyHierarchyOpActivityStartController#startActivityInTaskFragmentIch habe festgestellt, dass Binder-Aufrufe an applyTransaction in der TaskFragmentOrganizer-Klasse vorhanden sind, und ich habe beschlossen, sie als bequemeren Wrapper zu verwenden, als alle Binder-Aufrufe direkt durchzuführen (keines davon ist eine öffentliche API, daher musste ich ohnehin Reflection verwenden)
Zunächst ruft Methode „2." enforceTaskPermission auf, das unter Android 12.0 die nur-signature-Berechtigung MANAGE_ACTIVITY_TASKS prüfte, die wir nicht erhalten konnten. Unter Android 12L wurden die Regeln jedoch gelockert, sodass bestimmte Transaktionen ohne Berechtigungen durchgeführt werden können. Es stellte sich heraus, dass keine der für startActivityInTaskFragment benötigten Operationen eine Berechtigung erforderte (wenn die Transaktion einen zugeordneten TaskFragmentOrganizer hatte)
Wir möchten also HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT durchführen. Dazu müssen wir unseren TaskFragment in mLaunchTaskFragments registriert haben, andernfalls wird die Ausnahme "Not allowed to operate with invalid fragment token" gemeldet
Wir können einen solchen TaskFragment über HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT registrieren, das createTaskFragment() aufruft
(Im PoC-Code werden diese Transaktionen in SecondActivity gesendet: HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT wird von initOrganizerAndFragment() gesendet und HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT wird von startActivityInOrganizer gesendet)
Das ermöglicht uns also, startActivityInTaskFragment aufzurufen, und Intents von hier gestarteten Aktivitäten werden als vom System-uid stammend betrachtet. Es stellt sich jedoch heraus, dass uns das allein nichts ermöglicht: vom System gestartete Aktivitäten können keine URI-Grants durchführen, und wenn wir versuchen, eine Aktivität einer anderen App zu starten, werden wir durch die canEmbedActivity-Prüfung gestoppt
canEmbedActivitySchauen wir uns canEmbedActivity noch einmal an: Das Einbetten ist erlaubt, wenn taskFragment.getTask().effectiveUid die uid des Systems ist oder mit der uid der gestarteten App übereinstimmt. Wir müssen uns in einer Task befinden, deren effectiveUid das System ist
Gehen wir auch zurück zu createTaskFragment(): Die Erstellung von TaskFragment war nur erlaubt, wenn rootActivity.getUid() != ownerActivity.getUid(). Das bedeutet, dass unsere Aktivität am unteren Ende des Back-Stacks der Task, in der sie sich befindet, liegen muss
Wir müssen eine neue Task starten (über Intent.FLAG_ACTIVITY_NEW_TASK), die eine Aktivität enthält, die zum System-uid gehört (damit Task#effectiveUid auf AID_SYSTEM gesetzt wird), und dann startet diese Aktivität unsere Aktivität (innerhalb derselben Task) und ruft finish() für sich selbst auf (damit unsere Aktivität zur Wurzel dieser Task wird und wir createTaskFragment() verwenden können)
Eine solche Aktivität ist ChooserActivity. Der Chooser wird normalerweise verwendet, um auszuwählen, welche App der Benutzer verwenden möchte, nachdem er die Option „Teilen" ausgewählt hat. ChooserActivity hat jedoch android:relinquishTaskIdentity="true" in AndroidManifest.xml gesetzt, was bedeutet, dass sie beim Starten einer anderen Aktivität Task#effectiveUid mit der uid der neu gestarteten App überschreibt
(relinquishTaskIdentity funktioniert nur, wenn es von der ersten App in der Task verwendet wird, und nur für System-Apps. Wir können also relinquishTaskIdentity nicht selbst verwenden und eine System-App starten, um Task#effectiveUid unserer Task zu überschreiben)
Eine weitere solche Aktivität (die unsere Aktivität starten und finish() für sich selbst aufrufen kann) ist ResolverActivity. Sie wird verwendet, wenn ein impliziter Intent gestartet wird, der zu mehreren Aktivitäten aufgelöst wird. Der Resolver bietet (anders als der Chooser) die Option, die Auswahl zu speichern, woran du (als Telefonbenutzer) die beiden unterscheiden kannst. ResolverActivity hatte relinquishTaskIdentity nicht gesetzt, aber der Resolver verwendet seinen eigenen Intent, um herauszufinden, welche Optionen verfügbar sind (während der Chooser den in Extras bereitgestellten Intent verwendet). Dies erweist sich als Problem für die Ausnutzung, da die vom Resolver beim Starten der ausgewählten Aktivität verwendeten Intent-Flags dieselben sind wie die zum Starten des Resolvers verwendeten, und:
Intent.FLAG_ACTIVITY_NEW_TASK nicht setzen, wird der Resolver innerhalb unserer Task gestartet, die bereits eine dauerhaft gesetzte effectiveUid hatIntent.FLAG_ACTIVITY_NEW_TASK setzen, startet der Resolver die Auswahl in eine weitere Task, die dann eine effectiveUid hat, die der gestarteten App gehörtDie Lösung für diese Probleme besteht darin, beides zu verwenden:
ChooserActivity: Wir geben ihrem Intent Folgendes mit:
Intent.FLAG_ACTIVITY_NEW_TASK, damit der Chooser in einer neuen Task gestartet wird (die die effectiveUid des Systems hat, aber nur bis zum nächsten Aktivitätsstart)Intent.EXTRA_INTENT gesetzt auf einen Intent, der keiner Aktivität entspricht, und die einzigen im Chooser verbleibenden Optionen stammen aus Intent.EXTRA_INITIAL_INTENTSIntent.EXTRA_INITIAL_INTENTS mit einem Array mit einem Element: dem Intent, den der Chooser starten soll (wenn es nur eine Option gibt, überspringen sowohl Chooser als auch Resolver die Eingabeaufforderung und starten sofort die einzige Option und rufen finish() für sich selbst auf)ResolverActivity von ChooserActivity gestartet:
(Im PoC-Code wird die Vorbereitung dieser Schritte in FirstActivity durchgeführt)
TaskFragmentOrganizer erhielt ein SurfaceControl über den onTaskFragmentAppeared-Callback, und mit diesem SurfaceControl kann man die gestartete Aktivität skalieren und transparent machen, während sie weiterhin Touch-Ereignisse empfängt und nicht als verdeckt betrachtet wird (sodass tap-jacking-geschützte Elemente weiterhin angetippt werden können)
Du kannst dies sehen, indem du das Kontrollkästchen „Zoom and set alpha" in der PoC-App aktivierst
Dies wird durch Commit „3." aus der Fix-Liste oben behoben
Eine weitere Sache ist, dass ActivityRecord#appToken-s von Aktivitäten, die innerhalb von TaskFragment ausgeführt werden, an TaskFragmentOrganizer-Callbacks übergeben werden. Diese Liste wurde gefiltert, um nur Token von Aktivitäten innerhalb desselben Prozesses zu enthalten. Die Prüfung wurde jedoch durchgeführt, indem die pid von TaskFragmentOrganizer mit der pid der Aktivität verglichen wurde, deren appToken wir erhalten konnten. Ich habe es nicht tatsächlich überprüft, aber ich denke, eine Anwendung könnte einen TaskFragmentOrganizer erstellen, den Prozess verlassen, der ursprünglich zum Erstellen verwendet wurde, und dessen pid als pid der Aktivität einer anderen App wiederverwenden lassen, um deren appToken zu erhalten. Hier ist der Commit („4." in der Fix-Liste oben), der die Überprüfung von pid-basiert auf uid-basiert umstellt (es sieht so aus, als ob dieser Commit unabhängig von meinem Bericht erstellt wurde (obwohl danach))
Sobald ein Angreifer das appToken einer Aktivität erhält, kann er onActivityResult()-Aufrufe injizieren (auch wenn die Ziel-App selbst kein startActivityForResult() aufgerufen hat) und möglicherweise savedInstanceState manipulieren (durch Aufruf von activityStopped(), vorausgesetzt, der Angreifer kann das Rennen mit der Zielanwendung gewinnen, die diese Methode aufruft, und ein zusätzlicher Aufruf führt nicht zu einem Zustandsverlust aufgrund eines Absturzes)
Ich habe nicht überprüft, ob dies in diesem Fall möglich ist. Zuvor konnte ich jedoch mit CVE-2020-0001 (Ja, ich habe eine ausgefallene Nummer bekommen) savedInstanceState-Manipulation und onActivityResult()-Injektion verwenden, um die Systemeinstellungen-App dazu zu bringen, meinen AccessibilityService ohne Benutzerinteraktion zu aktivieren, aber das ist eine Geschichte für ein anderes Mal
relinquishTaskIdentity nicht gesetzt, daher ist Task#effectiveUid jetzt auf das System gesetzt und bleibt so, unabhängig von den nächsten in dieser Task gestarteten AktivitätenIntent.FLAG_ACTIVITY_NEW_TASK, daher wird die nächste Aktivität innerhalb derselben Task gestartet<intent-filter> übereinstimmt, sodass der Resolver sofort mit dem Starten unserer Aktivität fortfährtResolverActivity startet unsere Aktivität
effectiveUid AID_SYSTEM ist, sodass canEmbedActivity() alles erlaubtfinish() für sich selbst aufgerufen, daher sind wir die Wurzelaktivität in der Task und dürfen createTaskFragment() verwenden