Sploitus

Exploit for ReparcelBug2

kitploit · 2026-09-03

Exploit Code

MARKDOWN286 lines
## https://sploitus.com/exploit?id=KITPLOIT:TOOLS-GITHUB-MICHALBEDNARSKI-REPARCELBUG2
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](https://assets.kitploit.com/production/public/readmes/52440/1ac3485d3bdfec689a9fca801903e2b4ad82d45e274f8aece1d32b1241583332/c4730e746ebea187515adc59c656d61005398aca8ca491829dd33fb4e6e9efbb-display-v1.webp)

# Введение в 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`, например, именно этот (хотя именно этот уже был исправлен)).