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

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

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

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

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

Категории

Все категории
Loading categories
ReparcelBug2 — Writeup и эксплойт для повышения привилегий установленного приложения до системных на Android 12 Beta через CVE-2021-0928, несоответствие сериализации `writeToParcel`/`createFromParcel` в `OutputConfiguration` | Kitploit
Инструменты/GitHubGitHub/michalbednarski/reparcelbug2
Безопасность AndroidПовышение привилегийАнализ уязвимостейЭксплуатацияМобильная безопасностьАнализ Бинарных Файлов
GitHubmichalbednarski/reparcelbug2

ReparcelBug2

Writeup и эксплойт для повышения привилегий установленного приложения до системных на Android 12 Beta через CVE-2021-0928, несоответствие сериализации `writeToParcel`/`createFromParcel` в `OutputConfiguration`

Популярное

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

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

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

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

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

CVE-2021-0928, несоответствие сериализации writeToParcel/createFromParcel в android.hardware.camera2.params.OutputConfiguration

Это эксплойт, использующий данную уязвимость для повышения привилегий из установленного Android-приложения в приложение «Настройки» Android (или любое другое установленное приложение, которому можно отправить сообщение в <receiver>, объявленный в AndroidManifest.xml; повышение привилегий путём отправки в <activity> также было возможно, хотя здесь не представлено)

Изначально я обнаружил проблему в Android 12 Developer Preview 3

Версия эксплойта, представленная в этом репозитории, работает на Android 12 Beta 2 и 3

Уязвимость была исправлена в первом официальном выпуске Android 12

Описание ниже изначально было написано для Google для рассмотрения этого отчёта как полной цепочки эксплойта

На момент написания Android 12 не был доступен в AOSP (релизы Android Developer Preview/Beta не являются открытым исходным кодом)

Скриншот уведомления Android из приложения «Настройки»: Hello from uid=1000(system) gid=1000(system) groups=1000(system),1007(log),1065(reserved_disk),1077(external_storage),3001(net_bt_admin),3002(net_bt),3003(inet),3007(net_bw_acct),9997(everybody) context=u:r:system_app:s0

Введение в Parcel

Большая часть IPC в Android осуществляется через класс Parcel

Базовое использование Parcel выглядит следующим образом:```java Parcel p = Parcel.obtain(); p.writeInt(1); p.writeString("Hello");

root@kitploit:~
Затем `Parcel` отправляется в другой процесс [через `Binder`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Альтернативно для тестирования можно вызвать [`p.setDataPosition(0)`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)), чтобы перемотать parcel в начальную позицию и начать чтение:```java
int a = p.readInt(); // a = 1
String b = p.readString(); // b = "Hello"

Следует отметить, что Parcel внутренне хранит позицию, с которой выполняются чтения. Ответственность пользователя класса Parcel заключается в том, чтобы методы read* соответствовали ранее использованным методам write*, иначе последующие чтения будут выполняться с неправильных позиций в буфере.

Parcel также предоставляет возможность записи пользовательских объектов, предпочтительный способ сделать это — реализация интерфейса Parcelable.

Вот пример реализации интерфейса Parcelable (нерелевантный код удалён, класс WindowContainerTransaction используется в эксплойте как часть цепочки гаджетов, однако с ним нет ничего плохого)```java package android.window; public final class WindowContainerTransaction implements Parcelable { private final ArrayMap<IBinder, Change> mChanges = new ArrayMap<>(); private final ArrayList mHierarchyOps = new ArrayList<>();

root@kitploit:~
private WindowContainerTransaction(Parcel in) {
    in.readMap(mChanges, null /* loader */);
    in.readList(mHierarchyOps, null /* loader */);
}

@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
    dest.writeMap(mChanges);
    dest.writeList(mHierarchyOps);
}

@NonNull
public static final Creator<WindowContainerTransaction> CREATOR =
        new Creator<WindowContainerTransaction>() {
            @Override
            public WindowContainerTransaction createFromParcel(Parcel in) {
                return new WindowContainerTransaction(in);
            }
        };

}

root@kitploit:~
As can be seen above, `writeToParcel()` method is used during writing. Then while reading `CREATOR.createFromParcel()` factory method is called. It is responsibility of `Parcelable` implementation to ensure that `createFromParcel` reads same amount of data as was written by `writeToParcel`, otherwise all subsequent reads from that `Parcel` will read data from wrong offset

Such class can be written to/read from `Parcel` through:

* Directly calling `obj.writeToParcel(parcel, 0)` / `obj = WindowContainerTransaction.CREATOR.createFromParcel()`, this is often used when type of class is known, for example when `Parcelable` has field with different `Parcelable` or in code generated by AIDL when defined RPC method has `Parcelable` as argument
* Through `Parcel.writeParcelable`/`readParcelable`. [`writeParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=1909;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3) first writes name of class and then calls `writeToParcel` method from `Parcelable` interface. [`readParcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3282;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3) reads written class name, finds class with that name in provided `ClassLoader` or `BOOTCLASSPATH` if null was provided. Once class is found it's static field `CREATOR` is used to obtain [`Parcelable.Creator`](https://developer.android.com/reference/android/os/Parcelable.Creator) instance which is factory used to read that class. It is important to note that when `readParcelable` method is used it can read any `Parcelable` available in class path as name of object to be created is read from same `Parcel`
* `readParcelable` is used by many other `Parcel` methods, for example `readList` seen in example above reads elements through `readValue`, which is most generic method of transferring objects in `Parcel` and one of ways it uses is through `readParcelable`. Also in above example due to Java's Type Erasure `ArrayList<HierarchyOp> mHierarchyOps` field can actually contain any objects supported by Parcel, not only those compatible with type specified in generic type declaration

# Несоответствия `writeToParcel`/`createFromParcel`

Как отмечено выше, именно реализация интерфейса `Parcelable` обязана гарантировать, что `createFromParcel` считывает из `Parcel` тот же объём данных, который ранее был записан соответствующим `writeToParcel`. Всякий раз, когда в `BOOTCLASSPATH` существует `Parcelable`, способный нарушить этот контракт, это создаёт уязвимость, поскольку позволяет реализовать следующий сценарий:

1. Вредоносное приложение отправляет в `system_server` `Bundle` ИЛИ `Parcelable`, содержащий повреждённый экземпляр `Parcelable`, вместе со специально сконструированными данными, которые будут фактически прочитаны на шаге 3, но переданы без изменений на шаге 2
2. `system_server` проверяет, что `Bundle` безопасен, и пересылает его ИЛИ `system_server` передаёт предоставленный `Parcelable` в метод AIDL, который также получает критически важные данные в следующем параметре (если бы данные, полученные в этом параметре, можно было изменить, это вызвало бы проблему безопасности)
3. Другое приложение получает данные от `system_server` и доверяет им, однако из-за некорректной сериализации данные, которые оно фактически видит, отличаются от данных, которые `system_server` намеревался отправить

Я использовал "ИЛИ" в приведённых шагах, поскольку эти шаги описывают как [старый вариант эксплуатации, который приводит к запуску произвольного Activity и который я опубликовал в 2017 году](https://github.com/michalbednarski/ReparcelBug) (слева от "ИЛИ"), так и новый вариант, который я опишу здесь в следующем разделе

# Как `BroadcastReceiver` выполняется в приложении

С точки зрения разработчика приложения, использующего API, доступные в Android SDK, [`BroadcastReceiver`](https://developer.android.com/guide/components/broadcasts) работает следующим образом: одно приложение вызывает [`sendBroadcast`](https://developer.android.com/reference/android/content/Context#sendBroadcast(android.content.Intent)) (хотя часто приложения хотят получать Broadcasts от системы, а не от другого приложения), а затем отправленный `Intent` сопоставляется с `<receiver>`, определённым в `AndroidManifest.xml`; когда это происходит, система запускает процесс принимающего приложения, создаёт экземпляр подкласса `BroadcastReceiver`, как определено в атрибуте `<receiver android:name>`, и затем вызывает метод [`onReceive`](https://developer.android.com/reference/android/content/BroadcastReceiver#onReceive(android.content.Context,%20android.content.Intent))

Давайте рассмотрим взаимодействие с `system_server`, происходящее в процессе, принимающем broadcast:

* Когда процесс приложения первоначально запускается, он [вызывает `IActivityManager.attachApplication()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7340;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c), тем самым передавая дескриптор [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl), который используется системой для указания процессу приложения, что делать
* Когда система хочет выполнить зарегистрированный в манифесте `BroadcastReceiver` в процессе приложения, она вызывает метод [`scheduleReceiver`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=950;drc=f53e23b917aa0f6a6310e46a233a29b6d6226b2c), используя `IApplicationThread`, описанный в предыдущем пункте. Этот метод имеет несколько аргументов, но здесь наиболее важными являются первые два:
  1. `Intent intent`, который ранее был передан системе при вызове `sendBroadcast()`
  2. `ActivityInfo info`, который содержит информацию о компоненте, который должен быть выполнен. Значение этого параметра система берёт из Package Manager Service. Наиболее важно то, что данные, передаваемые в этом параметре, включают путь к файлу, из которого будет загружен Java-класс, обрабатывающий полученный broadcast

На этом этапе вы, вероятно, можете догадаться, в чём заключается этот новый путь эксплуатации: вызвать `sendBroadcast()`, передав `Intent`, который приведёт к тому, что когда система попытается вызвать `scheduleReceiver`, приложение, в котором вызывается `scheduleReceiver`, увидит подменённый `ActivityInfo`

Следует отметить, что этот новый путь эксплуатации стал возможен в Android 12, поскольку ранее не было способа поместить произвольные `Parcelable` в `Intent` ([extras Intent](https://developer.android.com/reference/android/content/Intent#putExtra(java.lang.String,%20android.os.Parcelable)) не считаются, так как они помещаются в `Bundle`, длина которого целиком записывается в Parcel и который [читается как единый блок](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1671-1681;drc=5d123b67756dffcfdebdb936ab2de2b29c799321), поэтому extras не могут вызвать неверную интерпретацию содержащего их объекта `Intent`)

# Инициирование несоответствия `writeToParcel`/`createFromParcel`

В большинстве случаев несоответствия `writeToParcel`/`createFromParcel` возникают, когда в одном из этих методов одно из полей забыто или записано дважды; в таком случае отправка такого объекта всегда вызовет несоответствие. (Чаще всего это происходит, когда объект, будучи `Parcelable`, на самом деле не используется между процессами, иначе это было бы быстро замечено при обычном использовании)

Однако в этот раз дело обстояло иначе, и инициирование несоответствия не является очевидным

Давайте рассмотрим уязвимый класс ([оригинал был здесь](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/hardware/camera2/params/OutputConfiguration.java;drc=46c390a1c695e2dc458cb889e40559f259f60aed), строки, помеченные `// New in Android 12`, были добавлены вручную, так как их не было в AOSP на момент написания) ([Вот коммит, изначально внёсший уязвимость](https://android.googlesource.com/platform/frameworks/base/+/a69b1bc58b0838e06deefb190e226774a34671e6%5E%21/#F10), однако он был опубликован после выхода Android 12)```java
package android.hardware.camera2.params;

public final class OutputConfiguration implements Parcelable {
    private OutputConfiguration(@NonNull Parcel source) {
        int rotation = source.readInt();
        int surfaceSetId = source.readInt();
        int surfaceType = source.readInt();
        int width = source.readInt();
        int height = source.readInt();
        boolean isDeferred = source.readInt() == 1;
        boolean isShared = source.readInt() == 1;
        ArrayList<Surface> surfaces = new ArrayList<Surface>();
        source.readTypedList(surfaces, Surface.CREATOR);
        String physicalCameraId = source.readString();
        boolean isMultiResolution = source.readInt() == 1; // New in Android 12
        ArrayList<Integer> sensorPixelModesUsed = new ArrayList<Integer>(); // New in Android 12
        source.readList(sensorPixelModesUsed, Integer.class.getClassLoader()); // New in Android 12

		// SNIP: copy values from variables set above to fields of this class
    }

    public static final @android.annotation.NonNull Parcelable.Creator<OutputConfiguration> CREATOR =
            new Parcelable.Creator<OutputConfiguration>() {
        @Override
        public OutputConfiguration createFromParcel(Parcel source) {
            try {
                OutputConfiguration outputConfiguration = new OutputConfiguration(source);
                return outputConfiguration;
            } catch (Exception e) {
                Log.e(TAG, "Exception creating OutputConfiguration from parcel", e);
                return null;
            }
        }

        @Override
        public OutputConfiguration[] newArray(int size) {
            return new OutputConfiguration[size];
        }
    };

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        if (dest == null) {
            throw new IllegalArgumentException("dest must not be null");
        }
        dest.writeInt(mRotation);
        dest.writeInt(mSurfaceGroupId);
        dest.writeInt(mSurfaceType);
        dest.writeInt(mConfiguredSize.getWidth());
        dest.writeInt(mConfiguredSize.getHeight());
        dest.writeInt(mIsDeferredConfig ? 1 : 0);
        dest.writeInt(mIsShared ? 1 : 0);
        dest.writeTypedList(mSurfaces);
        dest.writeString(mPhysicalCameraId);
        dest.writeInt(mIsMultiResolution ? 1 : 0); // New in Android 12
        dest.writeList(mSensorPixelModesUsed); // New in Android 12
    }

    private ArrayList<Surface> mSurfaces;
    private final int mRotation;
    private final int mSurfaceGroupId;
    private final int mSurfaceType;
    private final Size mConfiguredSize;
    private final int mConfiguredFormat;
    private final int mConfiguredDataspace;
    private final int mConfiguredGenerationId;
    private final boolean mIsDeferredConfig;
    private boolean mIsShared;
    private String mPhysicalCameraId;
    private boolean mIsMultiResolution; // New in Android 12
    private ArrayList<Integer> mSensorPixelModesUsed; // New in Android 12
}

Итак, что здесь не так и почему это изменение вносит уязвимость? Как я уже говорил при обсуждении примера Parcelable WindowContainerTransaction, readList на самом деле может заполнить список любым объектом, поддерживаемым Parcel, а не только теми, что соответствуют объявлению дженерика (ArrayList<Integer>). Однако, поскольку мы используем этот класс лишь как часть цепочки сериализации и на самом деле не используем это поле ни для чего, кроме чтения и записи в Parcel (а попытки использовать элементы из ArrayList, содержащего элементы, не соответствующие его объявлению дженерика, в любом случае привели бы лишь к ClassCastException), это само по себе не является проблемой.

В этом классе также есть try-catch внутри createFromParcel, что означает, что если во время чтения возникнет Exception, чтение OutputConfiguration будет остановлено, и чтение объекта, содержащего OutputConfiguration, продолжится. Когда это происходит, весь OutputConfiguration записывается в Parcel, но читается только до точки, в которой произошло Exception. Это создает несоответствие, так как непрочитанные данные, записанные внутри OutputConfiguration.writeToParcel, на самом деле будут прочитаны объектом, который вызывал OutputConfiguration.CREATOR.createFromParcel.

Теперь комбинация этих двух факторов (возможность вкладывать произвольные объекты, поддерживаемые Parcel, и оборачивание этого в try-catch без повторного выброса) дает возможность сконструировать Parcelable, который может быть записан system_server и позже прочитан способом, контролируемым приложением, которое изначально создало Parcelable, пересылаемый system_server.

Итак, чтобы сконструировать такой Parcelable, нам нужно найти что-то, что можно поместить в mSensorPixelModesUsed, что будет успешно прочитано в system_server (так как этот объект принимается от приложения атакующего через Parcel), успешно записано system_server, а затем не сможет быть распаковано и вызовет Exception в приложении-жертве.

Один из способов сделать это — использовать класс, который присутствует в system_server, но отсутствует в приложениях, так что попытка его десериализации приведет к ClassNotFoundException. Однако я не могу выбрать Parcelable из system_server, так как readParcelable без явно указанного ClassLoader будет искать только в BOOTCLASSPATH, который не содержит специфичных для system_server классов. Решение этой проблемы — использовать один из классов Serializable, так как ObjectInputStream выберет ClassLoader из первого метода в стеке вызовов, не относящегося к BOOTCLASSPATH.

Я выбрал PackageManagerException, однако прежде чем использовать его, нужно сделать еще одну вещь. В конструкторе OutputConfiguration, когда вызывается readList, аргумент loader явно устанавливается в Integer.class.getClassLoader(). Это [значение loaderпередается вreadValue()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3641;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3), затем [в readSerializable()](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3236;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3), и внутри [readSerializable(), если параметр loaderresolveClassObjectInputStreamc != nullClass.forNamePackageManagerExceptionParcelablereadListClassLoaderWindowContainerTransaction`.

Итак, на данный момент у нас есть следующий объект:

  • OutputConfiguration
    • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
      • mHierarchyOps.get(0) = PackageManagerException

Теперь такой объект может быть успешно десериализован внутри system_server: когда WindowContainerTransaction вызывает readList, он попытается найти класс PackageManagerException, используя ClassLoader системного сервера (не BootClassLoader), так как может найти его в стеке вызовов. Этот загрузчик классов оказывается в стеке вызовов, потому что, хотя все следующие методы не были из classpath системного сервера: Binder#execTransact(), IActivityManager$Stub#onTransact(), сгенерированный AIDL, и методы из всех используемых классов Parcelable, в стеке вызовов был метод, объявленный внутри системного сервера: переопределенный onTransact в ActivityManagerService. Поэтому может прочитать и позже записать такой объект в , и когда целевое приложение попытается его прочитать, класс будет недоступен, и поэтому будет выброшен , , а затем .

Итак, мы вызвали это несоответствие. Ну, на самом деле еще не совсем, потому что исключение было перехвачено, когда после OutputConfiguration.writeToParcel не осталось непрочитанных данных, но мы можем легко добавить еще один элемент в List mSensorPixelModesUsed, и этот элемент будет записан через Parcel.writeValue и останется непрочитанным после чтения OutputConfiguration.

Помещение этого в Intent

Как отмечено выше, я хочу вызвать несоответствие из объекта Intent, так как он будет передан system_server в метод AIDL, у которого Intent находится в первом параметре, а информация о выполнении — во втором параметре, так что сериализация/десериализация Intent, переданного в первом параметре, приведет к изменению значения во втором параметре.

В Intent.readFromParcel() все значения читаются через специализированные типизированные методы, поэтому там мы не можем указать пользовательский класс Parcelable.

Однако внутри Intent есть вложенный ClipData, и начиная с Android 12 в ClipData$Item есть новое поле ActivityInfo mActivityInfo (отсутствовало в AOSP на момент первоначального написания, вот коммит, вводящий это поле, это поле читается через in.readTypedObject(ActivityInfo.CREATOR) внутри конструктора ClipData(Parcel in)).

Затем внутри конструктора ActivityInfo(Parcel source) снова нет способа поместить пользовательский Parcelable, но так как ActivityInfo наследуется от ComponentInfo, у него есть поле applicationInfo.

Наконец, в ApplicationInfo есть поле SparseArray<int[]> splitDependencies, которое читается через readSparseArray, которое, в свою очередь, использует readValue для чтения элементов SparseArray.

На этом этапе мы могли бы поместить OutputConfiguration в splitDependencies, однако чтение splitDependencies сопровождается несколькими вызовами readString8(), и было бы неплохо иметь полный контроль над непрочитанными данными после возникновения несоответствия, чтобы мы могли напрямую поместить туда пустые строки и не беспокоиться о различной интерпретации непрочитанных данных.

Чтобы сделать это, сначала нам нужно поместить некоторый контейнер необработанных данных в OutputConfiguration.mSensorPixelModesUsed, который будет записан через writeValue. Я выбрал Bundle. Таким образом, в непрочитанных данных у нас останется:

  1. Тег VAL_BUNDLE из writeValue
  2. Длина необработанных данных (эта ссылка также применима к остальным элементам в этом списке)
  3. BUNDLE_MAGIC
  4. Необработанные данные, переданные дословно через Parcel.appendFrom

Итак, у нас есть три элемента Parcel.writeInt, которые останутся непрочитанными; мы можем избавиться от них, обернув OutputConfiguration в какой-нибудь Parcelable, который при чтении считывает произвольное значение Parcelable, за которым следуют три целых числа. Я нашел это в ZenPolicy CREATOR.

Подводя итог, мы получили следующую иерархию объектов (которая присутствует в system_server и которую он пытается передать в scheduleReceiver):

  • Intent
    • mClipData = ClipData
      • mItems.get(0).mActivityInfo = ActivityInfo
        • applicationInfo = ApplicationInfo
          • splitDependencies.get(0) = ZenPolicy
            • mVisualEffects.get(0) = OutputConfiguration
              • mSensorPixelModesUsed.get(0) = WindowContainerTransaction
                • mHierarchyOps.get(0) = PackageManagerException
              • mSensorPixelModesUsed.get(1) = Bundle

Это записывается system_server. Затем принимающее приложение читает все вплоть до (и включая данные readSerializable для) PackageManagerException нормально, однако после чтения данных Serializable для PackageManagerException выбрасывается исключение, и чтение всего, что находится ниже OutputConfiguration, отменяется, оставляя Bundle непрочитанным. Чтение переходит к ZenPolicy, который потребляет три целых числа, предшествующие необработанным данным внутри Bundle. Затем чтение ApplicationInfo продолжается с чтения данных, которые ранее были необработанными данными, переданными дословно в Bundle. Чтение этих необработанных данных продолжится с оставшимися объектами в этом стеке (ApplicationInfo, ActivityInfo, и ), а затем эти необработанные данные будут использованы для чтения следующего параметра метода .

Что затем происходит внутри handleReceiver

Как я только что сказал, теперь оставшиеся параметры scheduleReceiver читаются из буфера, контролируемого атакующим.

Давайте посмотрим, что происходит после вызова этого метода.

Сначала scheduleReceiver упаковывает значения из всех аргументов и использует sendMessage() для передачи выполнения в главный поток.

Затем в главном потоке вызывается handleReceiver.

handleReceiver вызывает getPackageInfoNoCheck, передавая ему ApplicationInfo, который он получил как часть ActivityInfo, переданного в аргумент scheduleReceiver.

getPackageInfo проверяет, присутствует ли пакет с данным именем в кэше, и если нет, создает новый экземпляр LoadedApk, передавая ему объект ApplicationInfo, полученный ранее (поскольку атакующий хочет вызвать создание нового LoadedApk, используется packageName пакета, который ранее не встречался в этом процессе).

Затем используется метод ContextImpl.getClassLoader(), который при первом запуске делегирует mPackageInfo.getClassLoader(), где mPackageInfo — это LoadedApk, созданный в предыдущем абзаце.

Затем есть createOrUpdateClassLoaderLocked, который вызывает makePaths для заполнения zipPaths путями, которые будут использоваться в ClassLoader, затем они объединяются и присваиваются переменной zip, и это передается в createClassLoader.

makePaths заполняет zipPaths, используя информацию из ApplicationInfo, что наиболее важно, включает sourceDir. Приложение атакующего делает так, чтобы внедренный ApplicationInfo имел sourceDir, установленный на путь к собственному apk, поэтому класс приемника на самом деле будет загружен из apk атакующего. Это напрямую приводит к выполнению кода, контролируемого атакующим, в приложении, принимающем широковещательное сообщение.

Примечание о проверках скрытых API

Была еще одна вещь, которую нужно было обойти: проверки скрытых API. Они никогда не предназначались для использования в качестве границы безопасности (поскольку приложение всегда может использовать NDK и вызывать базовый syscall напрямую), но в этом случае они были обойдены путем создания специально сконструированного ClipData с помощью ручной записи данных в Parcel, а затем использования readParcelable. Такой ClipData затем можно было нормально прикрепить к Intent и передать в sendBroadcast(), так что сама отправка широковещательного сообщения выполнялась только с использованием публичных API.

Исправления

Вышеописанное исследование было изначально отправлено в Google, и похоже, что они им воспользовались, так как есть несколько исправлений, являющихся его результатом (я думаю, у меня нет доказательств прямой причинно-следственной связи).

Выпущено с Android 12:

  • Проглатывание исключений из OutputConfiguration и связанных классов было удалено
  • OutputConfiguration#mSensorPixelModesUsed больше не записывается через writeValue
  • ClipData#mActivityInfo больше не записывается в Parcel, если это явно не запрошено при записи (так что Intent больше не может содержать произвольные Parcelable из BOOTCLASSPATH, что устраняет эту технику эксплуатации).

Присутствует только в ветке master на момент написания, не в выпущенных версиях, вероятно, появится в Android 13 (не в 12L):

  • Появились новые методы чтения List в Parcel, которые проверяют тип элементов, и нетипизированные версии помечены как устаревшие
  • Появился новый метод Parcel#enforceNoDataAvail(), который проверяет, что в Parcel не осталось непрочитанных данных, который, по-видимому, будет использоваться AIDL после чтения аргументов RPC-вызова. Обычно мои эксплойты полагались на тот факт, что после чтения всех данных из Parcel все остальное игнорировалось; это больше не будет так, хотя я думаю, что во многих случаях можно сконструировать данные, которые в конце приведут к переходу в конечную позицию, так что это не является сильной мерой смягчения. В любом случае, иногда такие проблемы возникают естественным образом и остаются незамеченными, так что это позволит их выявлять. Дальнейшее обсуждение этого вопроса находится в issue #3
  • Каждый элемент в Bundle будет иметь свою длину, сохраняемую отдельно. Это практически убивает весь класс ошибок, о которых я сообщал в Google в частном порядке с 2014 года, опубликовал описание в 2017 году и сам код примерно год спустя. Если я правильно считаю, это будет 8 лет жизни класса ошибок (честно говоря, я понятия не имею, много это или нет, хотя все еще могут существовать варианты эксплойтов, не связанные с Bundle, например, именно этот (хотя именно этот уже был исправлен)).
Скачать инструмент
не равен null, он используется вместо
из
(проверка
ничего не делает, потому что когда
не находит класс, он выбрасывает исключение, а не возвращает null)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3457-3462;drc=58787794eb5c879f6e39ee58c0071b36e337f8e3). Однако обходной путь довольно прост: нам просто нужно обернуть
в какой-нибудь
, который выполняет
без указания
. Здесь и пригодится описанный выше класс
system_server
Parcel
PackageManagerException
ClassNotFoundException
обернутый в RuntimeException
перехваченный CREATOR из OutputConfiguration
ClipData
Intent
handleReceiver