Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
ResourcePoison — Writeup и эксплойт для CVE-2025-22441: повышение привилегий от установленного приложения до процесса SystemUI на Android из-за передачи недоверенного ApplicationInfo в LoadedApk | Kitploit
Инструменты/GitHubGitHub/michalbednarski/resourcepoison
Безопасность AndroidПовышение привилегийАнализ уязвимостейЭксплуатацияМобильная безопасностьСтатьи и Исследования
GitHubmichalbednarski/resourcepoison

ResourcePoison

Writeup и эксплойт для CVE-2025-22441: повышение привилегий от установленного приложения до процесса SystemUI на Android из-за передачи недоверенного ApplicationInfo в LoadedApk

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться
Репозиторий
107284911 месяцев назадПроверено Kitploit

Исправление этой проблемы появилось как 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"); } }

root@kitploit:~
return context;

}

root@kitploit:~
Самое интересное здесь — вызов `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()

До сих пор представленный код использовался только для RemoteViews (виджеты, уведомления и т.д.), но теперь мы перейдём к LoadedApk.updateApplicationInfo(), который также используется для обновления запущенных процессов приложения после установки нового сплита (например, при использовании доставки по требованию Play Feature Delivery)

Теперь недоверенный объект ApplicationInfo из RemoteViews будет передан в updateApplicationInfo(), давайте посмотрим, что делает этот метод (исходный фрагмент)```java public void updateApplicationInfo(@NonNull ApplicationInfo aInfo, @Nullable List oldPaths) { if (!setApplicationInfo(aInfo)) { return; }

root@kitploit:~
Давайте посмотрим на `setApplicationInfo()` [(исходный фрагмент)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=392-422;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)```java
private boolean setApplicationInfo(ApplicationInfo aInfo) {
    if (mApplicationInfo != null && mApplicationInfo.createTimestamp > aInfo.createTimestamp) {
        Slog.w(TAG, "New application info for package " + aInfo.packageName
                + " is out of date with TS " + aInfo.createTimestamp + " < the current TS "
                + mApplicationInfo.createTimestamp);
        return false;
    }
    // Snip: assign fields such as mAppDir and mResDir on this object from aInfo
    return true;
}

createTimestamp поле обычно устанавливается в SystemClock.uptimeMillis(), но поскольку этот объект исходит от атакующего, это означает, что атакующий может указать будущее значение, чтобы предотвратить выполнение последующих вызовов updateApplicationInfo()

Возвращаясь к updateApplicationInfo() (исходный код)```java final List newPaths = new ArrayList<>(); makePaths(mActivityThread, aInfo, newPaths); final List addedPaths = new ArrayList<>(newPaths.size());

// Snip: populate addedPaths with items that are in newPaths and not in oldPaths (passed in argument) synchronized (mLock) { createOrUpdateClassLoaderLocked(addedPaths);

root@kitploit:~
Список `addedPaths` строится так: в случае установки нового сплита `oldPaths` будет содержать список путей, которые использовались до установки нового сплита, а `addedPaths` будет содержать список файлов `.apk`, которые необходимо добавить в существующий `ClassLoader`, однако в случае `checkAndUpdateApkPaths()` `oldPaths` будет построен из того же самого объекта `ApplicationInfo`, и поэтому `addedPaths` будет пустым, и атакующий не сможет добавить новые пути в существующий `ClassLoader` здесь

`createOrUpdateClassLoaderLocked()` — длинный метод, но здесь интересны лишь несколько вещей:

* [Если `mIncludeCode` равен `false`, единственное, что делается — создание `ClassLoader` без ссылок на какие-либо файлы `apk`/`dex`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=968-990;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1)
* Самое опасное — создание нового `ClassLoader` с использованием путей, которые только что были заданы через контролируемый атакующим `ApplicationInfo`, однако это произойдёт только если [`mDefaultClassLoader` равен `null`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1005;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1), что означает, что это первый вызов `createOrUpdateClassLoaderLocked()` для данного экземпляра `LoadedApk` (поле `mDefaultClassLoader` ссылается на экземпляр `ClassLoader`, использовавшийся до применения [`AppComponentFactory.instantiateClassLoader()`](https://developer.android.com/reference/android/app/AppComponentFactory#instantiateClassLoader(java.lang.ClassLoader,%20android.content.pm.ApplicationInfo)))
* Также есть [добавление новых путей к пути поиска нативных библиотек этого `ClassLoader`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1050-1058;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1), однако для эксплуатации этого приложению-жертве потребовалось бы выполнить `System.loadLibrary()`, что обычно не встречается
* И есть [добавление путей `apk`/`dex` из аргумента `addedPaths` в существующий `ClassLoader`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=1060-1065;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1), однако на пути кода из `RemoteViews` `addedPaths` будет пустым

Возвращаясь к `updateApplicationInfo()` снова, здесь остаётся ещё одна важная вещь, которую делает этот метод — [замена `LoadedApk.mResources` на экземпляр, использующий новое значение `mResDir`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/app/LoadedApk.java;l=382;drc=5916ee589c4880e2d8a1a9ad6dc852108e4c44c1). Следует отметить, что это новый экземпляр, а не обновление уже существующих экземпляров. Вновь созданные `Context` будут возвращать/использовать этот новый экземпляр `Resources`, однако любые уже созданные `Context` продолжат использовать старый `Resources`

# Сводка по воздействию

Обобщая вышеприведённые разделы, всякий раз, когда процесс-жертва применяет `RemoteViews`, эта уязвимость позволяет:

* Заменить [`Resources` (строки локализации, макеты и т. д.)](https://developer.android.com/guide/topics/resources/providing-resources) вновь создаваемых Activity внутри этого процесса
* Дополнить путь поиска нативных библиотек, используемый `System.loadLibrary()`, однако эксплуатация этого потребовала бы от жертвы вызова `System.loadLibrary()` с передачей имени библиотеки, которая обычно отсутствует, что маловероятно
* Загрузить произвольный Java-код, если процесс-жертва использовал `createPackageContext(CONTEXT_INCLUDE_CODE)`, но не вызывал [`getClassLoader()` для этого `Context`](https://developer.android.com/reference/android/content/Context#getClassLoader()), однако поскольку загрузка кода и является причиной использования флага `CONTEXT_INCLUDE_CODE`, это также вряд ли произойдёт естественным образом

Теперь, хотя вероятность загрузки Java-кода в этом случае вряд ли произойдёт естественным образом, мне удалось её вызвать

# Загрузка `WebView`

В современных версиях Android `WebView` не является частью системы, а загружается из обычного `apk`, который определён в [системной конфигурации](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/res/res/xml/config_webview_packages.xml) и [является либо системным приложением, либо имеет подпись, совпадающую с определённой в системе](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java;l=688-705;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)

Загрузка этого `apk` выполняется путём [создания нового `Context` с передачей флагов `Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/webkit/WebViewFactory.java;l=521;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)

Теперь давайте посмотрим на вызывающий код этого метода [(исходный фрагмент)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/webkit/WebViewFactory.java;l=541-563;drc=f8f9e9ab11e65322aaaeb7373efd77cf1d671928)```java
// Overall snip: try/catch/finally, Trace, logging and timing measurement
webViewContext = getWebViewContextAndSetProvider();
if (android.content.res.Flags.registerResourcePaths()) {
    Resources.registerResourcePaths(webViewContext.getPackageName(),
            webViewContext.getApplicationInfo());
} else {
    // Snip: old resource update path, won't be used on latest Android builds
}
ClassLoader clazzLoader = webViewContext.getClassLoader();

getWebViewContextAndSetProvider() — это метод, который создаёт Context, Resources.registerResourcePaths() — наш единственный шанс внести задержку, чтобы успеть вызвать checkAndUpdateApkPaths() в другом потоке и загрузить произвольный Java-код, поскольку webViewContext.getClassLoader() — это конец окна, где существует LoadedApk с mIncludeCode равным true и mDefaultClassLoader равным null

Resources.registerResourcePaths() дойдёт до appendLibAssetsLocked(), который будет перебирать поле mResourceImpls синглтона, содержащее все ресурсы, загруженные в этот процесс, и поэтому может содержать ресурсы, созданные с помощью ApplicationInfo, внедрённого через RemoteViews

Моя первоначальная идея заключалась в том, чтобы поместить в ApplicationInfo большое количество путей оверлеев, у всех из которых одинаковый hashCode(), что замедлило бы дедупликацию при вызове createNewResourceKeyIfNeeded(). И хотя при использовании интерпретатора (например, при работе с отладчиком или в свежеустановленном приложении) это давало значительную задержку, когда среда выполнения выполняла оптимизации, задержка, вызванная коллизиями хэшей, была незначительной. Однако появилась другая причина замедления: поскольку эти пути оверлеев не указывали на существующие файлы, для каждого оверлея, который не удалось загрузить, выводилось лог-сообщение со стеком вызовов, что на практике приводило к замедлению, делая эксплуатацию этой гонки возможной

Проникновение в SystemUI

Приложения могут передавать RemoteViews в SystemUI через уведомления. Моё эксплойт-приложение запрашивает разрешение POST_NOTIFICATIONS, что, на мой взгляд, является разумным взаимодействием с пользователем, хотя возможно, что некоторые уведомления могут обойти это требование

Обычно SystemUI не использует WebView и фактически даже не может этого делать, поскольку SystemUI использует защищённое устройством хранилище (то есть хранилище, не защищённое учётными данными экрана блокировки), а реализация WebView запрещает это

Однако этот эксплойт позволяет мне изменять Resources, поэтому сначала я изменил макет, используемый SlicePermissionActivity, включив в него элемент <WebView />, а затем запустил эту Activity, что вызывает инициализацию WebView в главном потоке SystemUI

Затем мне нужно вызвать RemoteViews.getContextForResourcesEnsuringCorrectCachedApkPaths() в другом потоке, чтобы заменить путь, используемый для загрузки кода

Обычно публикация уведомления задействует несколько потоков в процессе SystemUI, включая главный поток (поэтому пока инициализируется WebView, я не могу опубликовать другое уведомление). Однако применение RemoteViews происходит в отдельном потоке, и я могу приостановить эту операцию, поместив ImageView внутрь RemoteViews и попросив его загрузить изображение из моего ContentProvider

Атаки только на ресурсы

Этот эксплойт загружает WebView, заменяя макет, используемый в SlicePermissionActivity. Если бы обновление кода было невозможно, атака на SlicePermissionActivity всё равно могла бы быть ценной, поскольку злоумышленник мог бы заменить макет, чтобы полностью скрыть исходное сообщение и, например, показать журнал изменений, заменить кнопку разрешения на «Понятно», а кнопку отказа — на пустую строку, сделав её фактически невидимой. Хотя фреймворк Slices устарел, всё ещё существуют слайсы Настроек, которые могут, например, изменить настройку мобильных данных без взаимодействия с пользователем

Другим важным запросом разрешений в SystemUI является подтверждение Media Projection, однако оно, как оказалось, не уязвимо, поскольку использует контекст приложения для Dialog

Также интересны фрагменты настроек (Preference fragments), поскольку вы можете определить Intent, который будет запущен Preference. Однако в случае SystemUI единственными Activity настроек являются те, что связаны с SystemUI Tuner и Demo Mode, но они работают в другом процессе, нежели тот, что показывает уведомления

Скачать инструмент