
Writeup und Exploit für CVE-2025-22441: Privilegieneskalation von einer installierten App zum SystemUI-Prozess auf Android aufgrund der Übergabe von nicht vertrauenswürdigen ApplicationInfo an LoadedApk
Der Fix für dieses Problem ist als CVE-2025-22441 erschienen: Bulletin Patch Follow-up
ApplicationInfoApplicationInfo ist eine Struktur, die verschiedene Informationen über installierte Apps definiert, insbesondere den Pfad zur APK-Datei, aus der Ressourcen und Code geladen werden
Normalerweise wird sie vom System an Anwendungen übergeben, aber es gibt manchmal Fälle, in denen ein Nicht-System-Aufrufer eine eigene bereitstellen kann. In der Vergangenheit gab es beispielsweise eine Schwachstelle in der Methode bindBackupAgent(), bei der ein Angreifer ein eigenes ApplicationInfo-Objekt mit uid- und sourceDir-Werten als Parameter übergeben konnte, die nicht gegen die tatsächlich im System installierten Apps geprüft wurden, da diese Methode intern von system_server aufgerufen werden sollte, aber für adb shell freigegeben war
Diesmal habe ich mir jedoch das ApplicationInfo-Feld innerhalb von RemoteViews genauer angesehen
RemoteViews ist ein Objekt, das eine Ansicht beschreibt, die aus einem anderen Prozess stammen kann. Dies wird vor allem für Widgets auf dem Startbildschirm verwendet, wobei die App, die das Widget bereitstellt, RemoteViews erstellt und diese dann im Prozess des Startbildschirms "angewendet" werden
Andere Stellen, an denen RemoteViews verwendet werden, sind Benachrichtigungen (angewendet durch den SystemUI-Prozess) und Autofill-Dialoge (bereitgestellt durch den Autofill-Dienst, angewendet durch system_server)
Das Feld RemoteViews.mApplication wird über Parcel serialisiert und kann daher aus entfernten Prozessen stammen. Wann immer RemoteViews angewendet werden, wird es von der folgenden Methode verwendet (Quellcode-Auszug):```java
private Context getContextForResourcesEnsuringCorrectCachedApkPaths(Context context) {
if (mApplication != null) {
if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
&& context.getPackageName().equals(mApplication.packageName)) {
return context;
}
try {
LoadedApk.checkAndUpdateApkPaths(mApplication);
return context.createApplicationContext(mApplication,
Context.CONTEXT_RESTRICTED);
} catch (NameNotFoundException e) {
Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
}
}
return context;
}
Am interessantesten ist hier der Aufruf von `LoadedApk.checkAndUpdateApkPaths()`, da dies eine statische Methode ist und einen globalen Zustand verändert [(Quellcode-Ausschnitt)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=2275-2302;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)```java
public static void checkAndUpdateApkPaths(ApplicationInfo expectedAppInfo) {
// Get the LoadedApk from the cache
ActivityThread activityThread = ActivityThread.currentActivityThread();
if (activityThread == null) {
Log.e(TAG, "Cannot find activity thread");
return;
}
checkAndUpdateApkPaths(activityThread, expectedAppInfo, /* cacheWithCode */ true);
checkAndUpdateApkPaths(activityThread, expectedAppInfo, /* cacheWithCode */ false);
}
private static void checkAndUpdateApkPaths(ActivityThread activityThread,
ApplicationInfo expectedAppInfo, boolean cacheWithCode) {
String expectedCodePath = expectedAppInfo.getCodePath();
LoadedApk loadedApk = activityThread.peekPackageInfo(
expectedAppInfo.packageName, /* includeCode= */ cacheWithCode);
// If there is load apk cached, or if the cache is valid, don't do anything.
if (loadedApk == null || loadedApk.getApplicationInfo() == null
|| loadedApk.getApplicationInfo().getCodePath().equals(expectedCodePath)) {
return;
}
// Duplicate framework logic
List<String> oldPaths = new ArrayList<>();
LoadedApk.makePaths(activityThread, expectedAppInfo, oldPaths);
// Force update the LoadedApk instance, which should update the reference in the cache
loadedApk.updateApplicationInfo(expectedAppInfo, oldPaths);
}
Lass uns besprechen, was in diesen Methoden vor sich geht
Zuerst rufen wir die 3-Parameter-Version von checkAndUpdateApkPaths mit cacheWithCode auf, das sowohl auf true als auch auf false gesetzt ist. Das LoadedApk-Objekt kann in zwei Modi konstruiert werden, entweder mit mIncludeCode auf true oder false, was SDK-seitig einem Context mit gesetztem CONTEXT_INCLUDE_CODE-Flag bzw. ohne dieses entspricht
Diese Methode verwendet zunächst ActivityThread.peekPackageInfo(), das eine bereits gecachte LoadedApk-Instanz zurückgibt. Wenn die App also zuvor kein LoadedApk mit passendem packageName und includeCode konstruiert hat, gibt peekPackageInfo() null zurück und checkAndUpdateApkPaths() tut nichts
Zurück bei RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths(): Dort haben wir context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED). Das CONTEXT_INCLUDE_CODE-Flag wurde nicht angegeben (ebenso wie bei den Kontexten, die als Argument an diese Methode übergeben werden), und daher verwendet diese Methode immer ein LoadedApk mit mIncludeCode=false
Daher ist für RemoteViews das Aktualisieren der Version mit Code unnötig, da RemoteViews immer einen Context ohne Code verwenden. Die Commit-Nachricht sagt lediglich, dass es zwei Stellen gibt, an denen ApplicationInfo gecacht werden kann, und ich denke, dass das Entfernen der Versionsaktualisierung mit Code hier keine Probleme verursachen würde (da checkAndUpdateApkPaths() nur von AppWidgetHostView und RemoteViews verwendet wird). Ein Gegenargument dazu ist jedoch, die Möglichkeit zu vermeiden, dass verschiedene Caches außer Synchronisation geraten, und obwohl das Entfernen des Aufrufs mit cacheWithCode=true die schwerwiegendste Auswirkung dieses Fehlers beseitigt, behebt es das Problem nicht vollständig, da die bloße Änderung von Ressourcen (anstatt von Code) für einen Angreifer immer noch wertvoll sein könnte
LoadedApk.updateApplicationInfo()Der bisher vorgestellte Code wird nur für RemoteViews (Widgets, Benachrichtigungen usw.) verwendet, aber jetzt betreten wir LoadedApk.updateApplicationInfo(), das auch zum Aktualisieren laufender App-Prozesse verwendet wird, nachdem ein neues Split installiert wurde (z. B. bei Verwendung der On-Demand-Bereitstellung von Play Feature Delivery)
Nun wird das nicht vertrauenswürdige ApplicationInfo-Objekt aus RemoteViews an updateApplicationInfo() übergeben. Schauen wir uns an, was diese Methode tut (Quellcode-Auszug)```java
public void updateApplicationInfo(@NonNull ApplicationInfo aInfo,
@Nullable List oldPaths) {
if (!setApplicationInfo(aInfo)) {
return;
}
Schauen wir uns das `setApplicationInfo()` [(Snippet-Quelle)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=392-422;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1) an```java
private boolean setApplicationInfo(ApplicationInfo aInfo) {
if (mApplicationInfo != null && mApplicationInfo.createTimestamp > aInfo.createTimestamp) {
Slog.w(TAG, "New application info for package " + aInfo.packageName
+ " is out of date with TS " + aInfo.createTimestamp + " < the current TS "
+ mApplicationInfo.createTimestamp);
return false;
}
// Snip: assign fields such as mAppDir and mResDir on this object from aInfo
return true;
}
Das Feld createTimestamp wird normalerweise auf SystemClock.uptimeMillis() gesetzt, aber da dieses Objekt vom Angreifer stammt, bedeutet dies, dass der Angreifer einen zukünftigen Wert bereitstellen kann, um weitere updateApplicationInfo()-Aufrufe daran zu hindern, ausgeführt zu werden
Zurück zu updateApplicationInfo() (Quellcode-Auszug)```java
final List newPaths = new ArrayList<>();
makePaths(mActivityThread, aInfo, newPaths);
final List addedPaths = new ArrayList<>(newPaths.size());
// Snip: populate addedPaths with items that are in newPaths and not in oldPaths (passed in argument) synchronized (mLock) { createOrUpdateClassLoaderLocked(addedPaths);
Die Liste der `addedPaths` wird erstellt: Im Falle der Installation eines neuen Splits würde `oldPaths` die Liste der Pfade enthalten, die vor der Installation des neuen Splits verwendet wurden, und `addedPaths` würde die Liste der `.apk`-Dateien enthalten, die zum bestehenden `ClassLoader` hinzugefügt werden müssen. Im Fall von `checkAndUpdateApkPaths()` wird `oldPaths` jedoch aus genau demselben `ApplicationInfo`-Objekt erstellt, und daher wird `addedPaths` leer sein und ein Angreifer kann hier keine neuen Pfade zum bestehenden `ClassLoader` hinzufügen.
`createOrUpdateClassLoaderLocked()` ist eine lange Methode, aber nur wenige Dinge sind hier interessant:
* [Wenn `mIncludeCode` `false` ist, wird nur ein `ClassLoader` erstellt, ohne auf `apk`/`dex`-Dateien zu verweisen](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=968-990;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)
* Das Gefährlichste ist das Erstellen eines neuen `ClassLoader` unter Verwendung von Pfaden, die gerade mit einem angreiferkontrollierten `ApplicationInfo` gesetzt wurden. Dies geschieht jedoch nur, wenn [`mDefaultClassLoader` `null` ist](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1005;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1), was bedeutet, dass dies der erste `createOrUpdateClassLoaderLocked()`-Aufruf auf dieser `LoadedApk`-Instanz ist (das Feld `mDefaultClassLoader` bezieht sich auf die Instanz von `ClassLoader`, die vor der Anwendung von [`AppComponentFactory.instantiateClassLoader()`](https://developer.android.com/reference/android/app/AppComponentFactory#instantiateClassLoader(java.lang.ClassLoader,%20android.content.pm.ApplicationInfo)) verwendet wurde)
* Es gibt auch das [Hinzufügen neuer Pfade zum Suchpfad für native Bibliotheken dieses `ClassLoader`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1050-1058;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1). Um dies auszunutzen, müsste die Opfer-App jedoch `System.loadLibrary()` aufrufen, was normalerweise nicht vorkommt.
* Und es gibt das [Hinzufügen von `apk`/`dex`-Pfaden aus dem `addedPaths`-Argument zum bestehenden `ClassLoader`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1060-1065;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1). Auf dem Codepfad von `RemoteViews` wird `addedPaths` jedoch leer sein.
Zurück zu `updateApplicationInfo()`: Es gibt noch eine relevante Sache, die diese Methode tut: das [Ersetzen von `LoadedApk.mResources` durch eine Instanz, die den neuen `mResDir`-Wert verwendet](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=382;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1). Es sollte beachtet werden, dass dies eine neue Instanz ist, nicht die Aktualisierung bereits vorhandener Instanzen. Neu erstellte `Context`s werden diese neue `Resources`-Instanz zurückgeben/verwenden, aber bereits erstellte `Context`s verwenden weiterhin die alten `Resources`.
# Zusammenfassung der Auswirkungen
Um die obigen Abschnitte zusammenzufassen: Wann immer ein Opferprozess `RemoteViews` anwendet, ermöglicht diese Schwachstelle Folgendes:
* Ersetzen der [`Resources` (Lokalisierungszeichenfolgen, Layouts usw.)](https://developer.android.com/guide/topics/resources/providing-resources) neu erstellter Activities innerhalb dieses Prozesses
* Anhängen an den Suchpfad für native Bibliotheken, der von `System.loadLibrary()` verwendet wird. Die Ausnutzung würde jedoch erfordern, dass das Opfer `System.loadLibrary()` mit dem Namen einer Bibliothek aufruft, die normalerweise nicht vorhanden ist, was unwahrscheinlich ist.
* Laden beliebigen Java-Codes, wenn der Opferprozess `createPackageContext(CONTEXT_INCLUDE_CODE)` verwendet hat, aber [`getClassLoader()` auf diesem `Context`](https://developer.android.com/reference/android/content/Context#getClassLoader()) nicht aufgerufen hat. Da das Laden von Code jedoch der Grund für die Verwendung des `CONTEXT_INCLUDE_CODE`-Flags ist, ist dies auch unwahrscheinlich, dass es natürlich vorkommt.
Obwohl die Chance, in diesem Fall Java-Code zu laden, unwahrscheinlich ist, dass es natürlich vorkommt, konnte ich es auslösen.
# Laden des `WebView`
Auf modernen Android-Versionen ist `WebView` nicht Teil des Systems, sondern wird aus einer normalen `apk` geladen, die in der [Systemkonfiguration](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/res/res/xml/config_webview_packages.xml) definiert ist und [entweder eine System-App ist oder eine Signatur hat, die mit einer im System definierten übereinstimmt](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java;l=688-705;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928).
Das Laden dieser `apk` erfolgt durch das [Erstellen eines neuen `Context` mit den Flags `Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/webkit/WebViewFactory.java;l=521;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928).
Schauen wir uns nun den Aufrufer dieser Methode an [(Quellcode-Ausschnitt)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/webkit/WebViewFactory.java;l=541-563;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928).```java
// Overall snip: try/catch/finally, Trace, logging and timing measurement
webViewContext = getWebViewContextAndSetProvider();
if (android.content.res.Flags.registerResourcePaths()) {
Resources.registerResourcePaths(webViewContext.getPackageName(),
webViewContext.getApplicationInfo());
} else {
// Snip: old resource update path, won't be used on latest Android builds
}
ClassLoader clazzLoader = webViewContext.getClassLoader();
getWebViewContextAndSetProvider() ist eine Methode, die Context erstellt. Resources.registerResourcePaths() ist unsere einzige Chance, eine Verzögerung einzuführen, damit wir checkAndUpdateApkPaths() in einem anderen Thread aufrufen und beliebigen Java-Code laden können, da webViewContext.getClassLoader() das Ende des Zeitfensters ist, in dem es ein LoadedApk mit mIncludeCode als true und mDefaultClassLoader als null gibt.
Resources.registerResourcePaths() erreicht appendLibAssetsLocked(), das über das mResourceImpls-Feld des Singletons iteriert, das alle Ressourcen enthält, die in diesen Prozess geladen wurden, und daher Ressourcen enthalten kann, die mit ApplicationInfo erstellt wurden, das über RemoteViews platziert wurde.
Meine ursprüngliche Idee war, in ApplicationInfo eine große Anzahl von Overlay-Pfaden zu platzieren, die alle denselben hashCode() haben, was die Deduplizierung durch den createNewResourceKeyIfNeeded()-Aufruf verlangsamen würde. Während dies bei Verwendung eines Interpreters (z. B. bei Verwendung eines Debuggers oder innerhalb einer frisch installierten App) eine erhebliche Verzögerung darstellte, war die durch Hash-Kollisionen eingeführte Verzögerung bei optimierter Laufzeit nicht signifikant. Es trat jedoch ein weiterer Verlangsamungsgrund auf: Da diese Overlay-Pfade nicht auf vorhandene Dateien zeigten, wurde für jedes Overlay, das nicht geladen werden konnte, eine Logmeldung mit Stack-Trace ausgegeben, was in der Praxis zu einer Verlangsamung führte und die Ausnutzung dieser Race Condition ermöglichte.
Apps können RemoteViews in Benachrichtigungen an SystemUI übergeben. Meine Exploit-App fordert die Berechtigung POST_NOTIFICATIONS an, was ich für eine angemessene Benutzerinteraktion halte, obwohl es möglich sein könnte, dass einige Benachrichtigungen diese Anforderung umgehen.
Normalerweise verwendet SystemUI kein WebView und kann es tatsächlich nicht einmal verwenden, da SystemUI gerätegeschützten Speicher verwendet (d. h. Speicher, der nicht durch die Sperrbildschirm-Anmeldedaten geschützt ist) und die WebView-Implementierung dies nicht zulässt.
Dieser Exploit erlaubt es mir jedoch, Resources zu modifizieren. Zuerst habe ich also das von SlicePermissionActivity verwendete Layout modifiziert, um ein <WebView />-Element einzufügen, und dann diese Activity gestartet, was die WebView-Initialisierung auf dem SystemUI-Hauptthread auslöst.
Dann muss ich RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths() auf einem anderen Thread aufrufen lassen, um den Pfad zu ersetzen, der zum Laden von Code verwendet wird.
Normalerweise umfasst das Posten einer Benachrichtigung mehrere Threads im SystemUI-Prozess, einschließlich des Hauptthreads (während also WebView initialisiert wird, kann ich keine weitere Benachrichtigung posten). Die Anwendung von RemoteViews erfolgt jedoch auf einem separaten Thread, und ich kann diesen Vorgang aussetzen, indem ich ein ImageView innerhalb der RemoteViews habe und es auffordere, ein Bild von meinem ContentProvider zu laden.
Dieser Exploit lädt WebView, indem er das in SlicePermissionActivity verwendete Layout ersetzt. Wenn das Aktualisieren von Code nicht möglich wäre, wäre ein Angriff auf SlicePermissionActivity dennoch wertvoll, da der Angreifer das Layout ersetzen könnte, um die ursprüngliche Nachricht vollständig zu verbergen und beispielsweise ein Änderungsprotokoll anzuzeigen, den „Erlauben“-Button durch „Verstanden“ zu ersetzen und den „Ablehnen“-Button durch eine leere Zeichenfolge zu ersetzen, wodurch er praktisch unsichtbar wird. Während das Slices-Framework veraltet ist, gibt es immer noch Settings-Slices, die beispielsweise die mobile Dateneinstellung ohne Benutzerinteraktion ändern können.
Eine weitere wichtige Berechtigungsabfrage innerhalb von SystemUI ist die Media-Projection-Bestätigung. Diese war jedoch nicht anfällig, da sie den Anwendungskontext für den Dialog verwendet.
Interessant sind auch Preference-Fragmente, da man einen Intent definieren kann, der von der Preference gestartet wird. Im Fall von SystemUI sind die einzigen Preference-Activities jedoch diejenigen, die mit SystemUI Tuner und Demo Mode zusammenhängen, aber diese laufen in einem anderen Prozess als der, der Benachrichtigungen anzeigt.