
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