
Writeup e exploit para CVE-2025-22441: Escalação de privilégio de aplicativo instalado para o processo SystemUI no Android devido à passagem de ApplicationInfo não confiável para LoadedApk
A correção para este problema apareceu como CVE-2025-22441: boletim patch acompanhamento
ApplicationInfo por aíApplicationInfo é uma estrutura que define várias informações sobre o aplicativo instalado, principalmente o caminho para o arquivo apk do qual recursos e código são carregados
Normalmente ele é passado do sistema para os aplicativos, no entanto, às vezes há casos em que um chamador não pertencente ao sistema pode fornecer um próprio. Por exemplo, no passado houve vulnerabilidade no método bindBackupAgent(), onde o atacante podia passar como parâmetro um objeto ApplicationInfo próprio com valores de uid e sourceDir que não eram verificados em relação aos aplicativos realmente instalados no sistema, porque esse método deveria ser chamado internamente pelo system_server, mas estava exposto ao adb shell
Desta vez, porém, examinei de perto o campo ApplicationInfo dentro de RemoteViews
RemoteViews é um objeto que descreve uma visualização que pode vir de outro processo. Isso é usado principalmente para widgets da tela inicial, onde o aplicativo que fornece o widget constrói RemoteViews e então ele é "aplicado" dentro do processo da tela inicial
Outros lugares onde RemoteViews são usados incluem notificações (aplicadas pelo processo SystemUI) e diálogos de preenchimento automático (fornecidos pelo serviço de preenchimento automático, aplicados pelo system_server)
O campo RemoteViews.mApplication é serializado através de Parcel e, portanto, pode vir de processos remotos e, sempre que RemoteViews são aplicados, ele é usado pelo seguinte método (fonte do trecho):```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;
}
O mais interessante aqui é a chamada `LoadedApk.checkAndUpdateApkPaths()`, pois este é um método estático e modificará algum estado global [(fonte do trecho)](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);
}
Vamos discutir o que está acontecendo nesses métodos
Primeiro, estamos chamando checkAndUpdateApkPaths com 3 parâmetros, com cacheWithCode definido tanto como true quanto como false. O objeto LoadedApk pode ser construído em dois modos, ou com mIncludeCode sendo true ou false, o que, em termos de SDK, corresponde a um Context com a flag CONTEXT_INCLUDE_CODE definida ou não
Esse método primeiro usará ActivityThread.peekPackageInfo(), que retornará a instância de LoadedApk já armazenada em cache; portanto, se o aplicativo não tiver construído anteriormente um LoadedApk com packageName e includeCode correspondentes, peekPackageInfo() retornará null e checkAndUpdateApkPaths() não fará nada
Voltando a RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths(), temos context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED) ali; a flag CONTEXT_INCLUDE_CODE não foi especificada (o mesmo ocorre com os contextos passados como argumento para esse método) e, portanto, esse método sempre usará LoadedApk com mIncludeCode=false
Portanto, para RemoteViews, atualizar a versão com código é desnecessário, pois RemoteViews sempre usam Context sem código. A mensagem do commit apenas diz que há dois lugares onde ApplicationInfo pode ser armazenado em cache e acredito que remover a atualização da versão com código não causaria problemas aqui (já que checkAndUpdateApkPaths() é usado apenas por AppWidgetHostView e RemoteViews). O contra-argumento para isso, no entanto, é evitar a possibilidade de dessincronizar vários caches e, embora remover a chamada com cacheWithCode=true elimine o impacto mais severo desse bug, isso não corrige completamente o problema, pois a modificação apenas de recursos (em vez de código) ainda pode ser valiosa para um atacante
LoadedApk.updateApplicationInfo()