
Writeup и эксплойт для CVE-2025-22441: повышение привилегий от установленного приложения до процесса SystemUI на Android из-за передачи недоверенного ApplicationInfo в LoadedApk
Исправление этой проблемы появилось как CVE-2025-22441: бюллетень патч дополнение
ApplicationInfo между процессамиApplicationInfo — это структура, содержащая различную информацию об установленном приложении, в первую очередь путь к apk-файлу, из которого загружаются ресурсы и код
Обычно она передаётся от системы к приложениям, однако иногда возникают случаи, когда вызывающий код, не являющийся системным, может предоставить собственную структуру. Например, в прошлом это была уязвимость в методе bindBackupAgent(), где атакующий мог передать в параметре собственный объект ApplicationInfo со значениями uid и sourceDir, которые не проверялись на соответствие реально установленным в системе приложениям, поскольку этот метод предназначался для внутреннего вызова из system_server, но был доступен через adb shell
В этот раз, однако, я внимательно изучил поле ApplicationInfo внутри RemoteViews
RemoteViews — это объект, описывающий представление, которое может прийти из другого процесса. Чаще всего он используется для виджетов главного экрана, где приложение, предоставляющее виджет, создаёт RemoteViews, а затем он "применяется" в процессе главного экрана
Другие места, где используются RemoteViews, — это уведомления (применяются процессом SystemUI) и диалоги автозаполнения (предоставляются сервисом автозаполнения, применяются в system_server)
Поле RemoteViews.mApplication сериализуется через Parcel и поэтому может поступать из удалённых процессов, и всякий раз, когда RemoteViews применяются, оно используется следующим методом (фрагмент исходного кода):```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;
}
Самое интересное здесь — вызов `LoadedApk.checkAndUpdateApkPaths()`, так как это статический метод, который изменяет некоторое глобальное состояние [(исходный фрагмент)](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);
}
Давайте обсудим, что происходит в этих методах
Сначала мы вызываем трёхпараметрический checkAndUpdateApkPaths с cacheWithCode, установленным как в true, так и в false. Объект LoadedApk может быть создан в двух режимах: либо с mIncludeCode, равным true, либо false, что на уровне SDK соответствует Context с установленным или не установленным флагом CONTEXT_INCLUDE_CODE
Этот метод сначала вызовет ActivityThread.peekPackageInfo(), который вернёт уже закешированный экземпляр LoadedApk, поэтому если приложение ранее не создавало LoadedApk с соответствующими packageName и includeCode, peekPackageInfo() вернёт null, и checkAndUpdateApkPaths() ничего не сделает
Возвращаясь к RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths(), там у нас есть context.createApplicationContext(mApplication, Context.CONTEXT_RESTRICTED), флаг CONTEXT_INCLUDE_CODE не был указан (то же самое касается контекстов, передаваемых в аргументах этому методу), и поэтому этот метод всегда будет использовать LoadedApk с mIncludeCode=false
Следовательно, для RemoteViews обновление версии с кодом не требуется, так как RemoteViews всегда используют Context без кода. В сообщении коммита просто сказано, что есть два места, где ApplicationInfo может быть закеширован, и я думаю, что удаление обновления версии с кодом не вызовет здесь проблем (так как checkAndUpdateApkPaths() используется только AppWidgetHostView и RemoteViews). Однако контраргументом этому является избежание возможности рассинхронизации различных кешей, и хотя удаление вызова с cacheWithCode=true устраняет наиболее серьёзное влияние этой ошибки, оно не полностью исправляет проблему, так как модификация только ресурсов (вместо кода) всё ещё может быть ценной для злоумышленника
LoadedApk.updateApplicationInfo()