
Writeup ed exploit per CVE-2025-22441: escalatione dei privilegi da app installata al processo SystemUI su Android dovuta al passaggio di ApplicationInfo non attendibile a LoadedApk
La correzione per questo problema è apparsa come CVE-2025-22441: bollettino patch follow up
ApplicationInfo in giroApplicationInfo è una struttura che definisce varie informazioni sull'app installata, in particolare il percorso del file apk da cui vengono caricati risorse e codice
Di solito viene passato dal sistema alle applicazioni, tuttavia a volte ci sono casi in cui un chiamante non di sistema potrebbe fornirne uno proprio. Ad esempio in passato c'è stata una vulnerabilità nel metodo bindBackupAgent(), dove un attaccante poteva passare come parametro un proprio oggetto ApplicationInfo con valori uid e sourceDir che non venivano verificati rispetto alle app realmente installate nel sistema, perché quel metodo era pensato per essere chiamato internamente da system_server, ma era esposto a adb shell
Questa volta, però, ho esaminato attentamente il campo ApplicationInfo all'interno di RemoteViews
RemoteViews è un oggetto che descrive una vista che può provenire da un altro processo. Questo è soprattutto usato per i widget della schermata home, dove l'app che fornisce il widget costruisce RemoteViews e poi viene "applicato" all'interno del processo della schermata home
Altri punti in cui vengono usati RemoteViews sono le notifiche (applicate dal processo SystemUI) e i dialoghi di autofill (forniti dal servizio di autofill, applicati da system_server)
Il campo RemoteViews.mApplication viene serializzato tramite Parcel e quindi può provenire da processi remoti e ogni volta che RemoteViews vengono applicati viene usato dal seguente metodo (snippet sorgente):```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;
}
La parte più interessante qui è la chiamata a `LoadedApk.checkAndUpdateApkPaths()`, poiché si tratta di un metodo statico che modificherà alcuni stati globali [(snippet source)](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);
}
Parliamo di cosa succede in questi metodi
Prima chiamiamo checkAndUpdateApkPaths con 3 parametri, con cacheWithCode impostato sia su true che su false. L'oggetto LoadedApk può essere costruito in due modalità, con mIncludeCode impostato su true o false, che a livello di SDK corrisponde a un Context con il flag CONTEXT_INCLUDE_CODE impostato o meno
Questo metodo userà prima ActivityThread.peekPackageInfo(), che restituirà l'istanza LoadedApk già memorizzata nella cache, quindi se l'app non ha precedentemente costruito un LoadedApk con packageName e includeCode corrispondenti, peekPackageInfo() restituirà null e checkAndUpdateApkPaths() non farà nulla
Tornando a RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths(), abbiamo context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED) lì, il flag CONTEXT_INCLUDE_CODE non è stato specificato (lo stesso vale per i contesti passati come argomento a quel metodo) e quindi quel metodo userà sempre LoadedApk con mIncludeCode=false
Pertanto, per RemoteViews l'aggiornamento della versione con il codice non è necessario, poiché RemoteViews usa sempre Context senza codice. Il messaggio del commit dice solo che ci sono due punti in cui ApplicationInfo può essere memorizzato nella cache e penso che rimuovere l'aggiornamento della versione con il codice non causerebbe problemi qui (poiché checkAndUpdateApkPaths() è usato solo da AppWidgetHostView e RemoteViews). Tuttavia, la controargomentazione è evitare la possibilità di avere varie cache fuori sincronia, e sebbene rimuovere la chiamata con cacheWithCode=true elimini l'impatto più grave di questo bug, non risolve completamente il problema, poiché la modifica delle sole risorse (invece del codice) potrebbe comunque essere preziosa per un attaccante