
Writeup y exploit para CVE-2025-22441: Escalada de privilegios desde una app instalada al proceso SystemUI en Android debido al paso de ApplicationInfo no confiable a LoadedApk
La corrección para este problema ha aparecido como CVE-2025-22441: boletín parche seguimiento
ApplicationInfoApplicationInfo es una estructura que define diversa información sobre una aplicación instalada, sobre todo la ruta al archivo apk desde el que se cargan los recursos y el código
Normalmente se pasa del sistema a las aplicaciones, sin embargo a veces hay casos en los que un llamador que no es del sistema podría proporcionar uno propio. Por ejemplo, en el pasado hubo una vulnerabilidad en el método bindBackupAgent(), donde un atacante podía pasar como parámetro su propio objeto ApplicationInfo con valores de uid y sourceDir que no se comprobaban contra las aplicaciones realmente instaladas en el sistema, porque ese método estaba pensado para ser llamado internamente por system_server, pero estaba expuesto a adb shell
Esta vez, sin embargo, he examinado de cerca el campo ApplicationInfo dentro de RemoteViews
RemoteViews es un objeto que describe una vista que puede provenir de otro proceso. Esto se usa sobre todo para los widgets de la pantalla de inicio, donde la aplicación que proporciona el widget construye RemoteViews y luego se "aplica" dentro del proceso de la pantalla de inicio
Otros lugares donde se usan RemoteViews son las notificaciones (aplicadas por el proceso SystemUI) y los diálogos de autocompletado (proporcionados por el servicio de autocompletado, aplicados por system_server)
El campo RemoteViews.mApplication se serializa a través de Parcel y, por tanto, puede provenir de procesos remotos y, siempre que se aplican RemoteViews, lo usa el siguiente método (fuente del fragmento):```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;
}
Lo más interesante aquí es la llamada a `LoadedApk.checkAndUpdateApkPaths()`, ya que es un método estático y modificará algún estado global [(fuente del fragmento)](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);
}
Discutamos qué está ocurriendo en estos métodos
Primero estamos llamando a checkAndUpdateApkPaths de 3 parámetros con cacheWithCode establecido tanto en true como en false. El objeto LoadedApk puede construirse en dos modos, ya sea con mIncludeCode siendo true o false, lo que a nivel de SDK se corresponde con un Context con el flag CONTEXT_INCLUDE_CODE establecido o no
Ese método primero usará ActivityThread.peekPackageInfo(), que devolverá la instancia de LoadedApk ya almacenada en caché, por lo tanto, si la aplicación no construyó previamente un LoadedApk con el packageName e includeCode coincidentes, peekPackageInfo() devolverá null y checkAndUpdateApkPaths() no hará nada
Volviendo a RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths(), tenemos context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED) allí, el flag CONTEXT_INCLUDE_CODE no fue especificado (lo mismo ocurre con los contextos pasados como argumento a ese método) y por lo tanto ese método siempre usará LoadedApk con mIncludeCode=false
Por lo tanto, para RemoteViews actualizar la versión con código es innecesario, ya que RemoteViews siempre usan Context sin código. El mensaje del commit solo dice que hay dos lugares donde ApplicationInfo puede estar en caché y creo que eliminar la actualización de la versión con código no causaría problemas aquí (ya que checkAndUpdateApkPaths() solo es usado por AppWidgetHostView y RemoteViews). El contraargumento a esto, sin embargo, es evitar la posibilidad de que varias cachés se desincronicen, y aunque eliminar la llamada con cacheWithCode=true elimina el impacto más severo de este error, no soluciona completamente el problema, ya que la modificación de solo recursos (en lugar de código) aún podría ser valiosa para un atacante