
Эксплойт для CVE-2022-20452, повышение привилегий на Android от установленного приложения до системного приложения (или другого приложения) через LazyValue с использованием Parcel после recycle()
Android 13 вносит множество улучшений, направленных на усиление защиты механизма сериализации Parcel
Вот презентация команды Android Security and Privacy о внесённых улучшениях
Это отлично, определённо устраняет или делает неэксплуатируемыми многие уязвимости. Также они описывают, как сломали мой предыдущий эксплойт, позволяющий приложениям загружать свой код в другие приложения (включая системные)
Но теперь я вернулся с новым эксплойтом, который достигает того же, хотя и другим способом. Он опирается на следующие уязвимости, которые были внесены в ходе вышеупомянутого усиления защиты Parcel:
![Скриншот приложения с текстом. Заголовок: LeakValue. Основной текст: Создано 6 ValueLeaker-s. Блокирую ActivityTaskManagerService. ActivityTaskManagerService заблокирован. Разблокирую ActivityTaskManagerService. ActivityTaskManagerService разблокирован. leakedBinders=[android.os.BinderProxy@f06702e]. Утёкший интерфейс: android.app.IApplicationThread. Запрашиваю выполнение кода. Шеллкод выполнен в uid=1000 pid=6904 packageName=com.android.settings 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. Внизу экрана две кнопки: START и MANUAL TESTING](Screenshot_20220723-081920.png)
(Также logcat от выполнения приложения: эксплуатация сильно шумит в логах)
Parcel и ParcelableКласс Parcel в Android является основой взаимодействия между процессами
Объекты могут реализовывать интерфейс Parcelable, чтобы их можно было записывать в Parcel, например (скопировано из AOSP):```java
public class UsbAccessory implements Parcelable {
public static final Parcelable.Creator CREATOR =
new Parcelable.Creator() {
public UsbAccessory createFromParcel(Parcel in) {
String manufacturer = in.readString();
String model = in.readString();
String description = in.readString();
String version = in.readString();
String uri = in.readString();
IUsbSerialReader serialNumberReader = IUsbSerialReader.Stub.asInterface(
in.readStrongBinder());
return new UsbAccessory(manufacturer, model, description, version, uri,
serialNumberReader);
}
};
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(mManufacturer);
parcel.writeString(mModel);
parcel.writeString(mDescription);
parcel.writeString(mVersion);
parcel.writeString(mUri);
parcel.writeStrongBinder(mSerialNumberReader.asBinder());
} }
Обратите внимание, что `Parcel` внутри хранит позицию, на которой выполняется запись или чтение; `readString()` разбирает данные в `String`, а также сдвигает позицию. Эту позицию можно получить/задать вручную через [`dataPosition()`](https://developer.android.com/reference/android/os/Parcel#dataPosition())/[`setDataPosition()`](https://developer.android.com/reference/android/os/Parcel#setDataPosition(int)). Реализации интерфейса `Parcelable` должны гарантировать, что их `writeToParcel` и `createFromParcel` записывают/читают одинаковый объём данных, иначе все последующие чтения будут получать данные со смещённых позиций.
[`Bundle`](https://developer.android.com/reference/android/os/Bundle) (карта «ключ-значение», которую можно передавать между процессами) может содержать [разнообразные объекты, которые можно записать в `Parcel` через `writeValue()`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=1792-1937). При чтении содержимого `Bundle` из `Parcel` может быть прочитан любой доступный в системе класс `Parcelable`.
`Bundle` откладывает фактический разбор содержимого: в `Parcel` записывается длина всех упакованных данных, а затем [соответствующая часть исходного `Parcel` просто копируется во вторичный `Parcel`, хранящийся в `mParcelledData`](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=1675-1683) (это позволяет, например, [`Activity.onSaveInstanceState()`](https://developer.android.com/reference/android/app/Activity#onSaveInstanceState(android.os.Bundle)) предоставлять `Parcelable`-объекты, недоступные в `system_server`; весь `Bundle` затем передаётся в `system_server` и обратно без изменений, без разбора содержимого).
Однако как только к какому-либо значению в `Bundle` обратились, все значения внутри `Bundle` [были распакованы](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/BaseBundle.java;l=227-313) и [каждая присутствующая пара «ключ-значение» была разобрана](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/core/java/android/os/Parcel.java;l=3613-3632). Если такая карта содержала `Parcelable` с несбалансированными методами `writeToParcel` и `createFromParcel`, и позже такой `Bundle` пересылался в другой процесс, тот другой процесс мог видеть иное содержимое `Bundle`. Это превращало все подобные [несоответствия в классах, доступных в системе, в уязвимости](https://github.com/michalbednarski/ReparcelBug), поскольку [в системе есть места, где `Bundle` проверяется на безопасность](https://cs.android.com/android/platform/superproject/+/android-12.1.0_r8:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=5037-5046), а затем пересылается в другой процесс.
В этом описании я называю такой `Bundle` самоизменяющимся `Bundle`, который показывает одно содержимое, а после пересылки — другое.
Ещё один важный момент: помимо просто байтов (строк, чисел, составленных из них объектов), `Parcel` может также содержать файловые дескрипторы и `Binder`-объекты. `Binder` — это объекты, на которых можно совершать RPC-вызовы: то есть один процесс создаёт объект `Binder` и переопределяет [метод `onTransact()`](https://developer.android.com/reference/android/os/Binder#onTransact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)). Затем `Binder` передаётся другому процессу; в примере кода выше видны вызовы `read`/`writeStrongBinder()`, используемые для чтения и записи его в `Parcel`. В другом процессе при использовании `readStrongBinder()` создаётся объект `BinderProxy` (скрытый за [интерфейсом `IBinder`](https://developer.android.com/reference/android/os/IBinder)). Затем этот другой процесс может вызвать [`transact()`](https://developer.android.com/reference/android/os/IBinder#transact(int,%20android.os.Parcel,%20android.os.Parcel,%20int)) на этом объекте, и в исходном объекте будет выполнен `onTransact()`. Обычно, впрочем, `transact()`/`onTransact()` вручную не пишут, а [используют вместо этого AIDL](https://developer.android.com/guide/components/aidl).
# Встречайте `LazyValue` — конец самоизменяющихся `Bundle`
Поскольку в прошлом было много случаев классов с несоответствием `writeToParcel`/`createFromParcel`, Android 13 решает проблему наличия любого такого класса где-либо в системе, позволяющего создание самоизменяющегося `Bundle`, [представив `LazyValue`](https://android.googlesource.com/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee%5E%21/).
Теперь при использовании `writeValue`, если записываемое значение не является примитивом, [длина значения также записывается в `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=2331-2344;drc=03c34f57c05feecfb090de3917787f049cb5f804).
Когда обычное приложение напрямую использует `Parcel.readValue()`, [всё происходит как раньше, за исключением того, что выводится предупреждение, если длина, прочитанная из `Parcel`, не совпадает с размером реально прочитанных данных](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4330-4348;drc=03c34f57c05feecfb090de3917787f049cb5f804) (Обратите, однако, внимание, что [`Slog.wtfStack` никогда не выбрасывает исключение](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/Slog.java;l=108-116;drc=23c7543b8e608ebcbb38b952761b54bb56065577)).
`Bundle` же теперь вместо этого использует [`Parcel.readLazyValue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4350-4420;drc=03c34f57c05feecfb090de3917787f049cb5f804).
Давайте подробнее рассмотрим, как это работает: в классе `LazyValue` есть [хороший комментарий, объясняющий структуру данных `LazyValue` внутри `Parcel`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4392-4399;drc=03c34f57c05feecfb090de3917787f049cb5f804):```
| 4B | 4B |
mSource = Parcel{... | type | length | object | ...}
a b c d
length = d - c
mPosition = a
mLength = d - a
mSource — это ссылка на исходный Parcel, для которого был вызван readLazyValue()
mPosition и mLength описывают расположение всех данных LazyValue в исходном Parcel, включая type и length
«length» (без «m» в начале) относится к значению длины, как оно записано в Parcel, и не включает заголовок (type и length)
Итак, вот что происходит, когда кто-то (система или приложение) извлекает значение из Bundle, который был прочитан из Parcel:
get*() класса Bundle, например новый getParcelable() с аргументом типа (поток выполнения будет одинаковым для новых и старых методов, просто новые методы гарантируют, что аргумент clazz не равен null, тогда как устаревшие устанавливают его в null)unparcel(), который проверит, есть ли у этого Bundle поле mParcelledData (это означает, что он был прочитан из Parcel, но ни одно значение ещё не запрашивалось и имена ключей ещё не распакованы; если это не так, перейдите к шагу 5)unparcel() делегирует выполнение , . устанавливается в — копию , созданную ; параметр установлен в , чтобы указать, что переданный принадлежит и что можно вызвать для негоЕсли Bundle пересылается, пока он всё ещё содержит LazyValue (это означает, что данное конкретное значение не запрашивалось, но какое-то другое значение из этого Bundle запрашивалось (то есть unparcel() был вызван, а LazyValue.apply() для этого элемента — нет)):
LazyValue обнаруживается методом Parcel.writeValue(), и запись делегируется в LazyValue.writeToParcel()LazyValue.writeToParcel() использует out.appendFrom(source, mPosition, mLength) для копирования всех данных LazyValue из исходного Parcel (опять же, mPosition и mLength включают заголовок LazyValue, так что копируются также type и length из исходного )Parcel.ReadWriteHelper и Parcel.readSquashed(Детали этих механизмов не важны для данной эксплуатации; здесь имеет значение только то, что эти механизмы существуют)
Ещё одна интересная особенность Parcel — опциональная возможность дедупликации записанных строк (String) и объектов
Дедупликация строк выполняется переопределением класса Parcel.ReadWriteHelper: Parcel.readString() на самом деле делегирует вызов в ReadWriteHelper, а стандартный хелпер напрямую читает String из Parcel
Альтернативная реализация Parcel.ReadWriteHelper может заменить вызовы readString предварительным чтением пула строк и использованием readInt для получения индексов String в пуле; однако с контролируемыми приложением Parcel такое никогда не делается
Parcel предоставляет метод hasReadWriteHelper(), который позволяет вызывающему коду обнаружить, что такой механизм дедупликации активен, и отключить несовместимые с ним функции
Другой механизм дедупликации, доступный в Parcel, — это сжатие (squashing):
Parcel.allowSquashing()Parcel.maybeWriteSquashed(this). Если этот метод вернул true, это означает, что объект уже был записан в этот Parcel, и теперь в Parcel записано только смещение к данным предыдущего объекта. В противном случае (либо сжатие не включено, либо этот объект записывается впервые) maybeWriteSquashed записывает ноль в качестве смещения, указывая, что объект не сжат, и возвращает false, сообщая вызывающему коду, что нужно записать фактические данные объектаParcel.readSquashed, и фактическая функция чтения передаётся ему в виде лямбды. readSquashed проверяет, указывает ли смещение, записанное , на то, что другой экземпляр объекта был прочитан ранее: если да, возвращается ранее прочитанный объект; в противном случае вызывается переданная лямбда для чтения сейчасParcel.recycle()На стороне Java объекты Parcel могут перерабатываться в пул: когда вы закончили работу с Parcel, вы вызываете для него recycle(), и в следующий раз, когда кто-то вызовет Parcel.obtain(), он получит ранее переработанный Parcel. Это позволяет сократить количество выделений объектов и последующую сборку мусора (Garbage Collection)
С другой стороны, такое ручное управление памятью привносит в Java возможность ошибок, похожих на Use-After-Free (хотя и с безопасностью типов, в отличие от обычного Use-After-Free в C)
Как отмечалось выше, Bundle создаёт копию Parcel и не вызовет Parcel.recycle(), если присутствует LazyValue. Однако это не так, если Parcel.hasReadWriteHelper() возвращает true; в этом случае:
initializeFromParcelLocked(parcel, /*recycleParcel=*/ false, isNativeBundle);. Это означает, что Bundle не будет перерабатывать Parcel, поскольку тот всё ещё принадлежит вызывающему коду; однако при этом создаются LazyValue, которые ссылаются на исходный Parcel и могут пережить время жизни исходного Parcelunparcel(/* itemwise */ true), который применит getValueAt() ко всем элементам, чтобы заменить все LazyValue, присутствующие в Bundle, фактическими значениямиИтак, можем ли мы заставить эти LazyValue пережить шаг 2 и превратить это поведение в Use-After-Recycle?
Если десериализация завершается неудачей (например, не удалось найти класс с именем, указанным внутри Parcel), выбрасывается BadParcelableException, которое затем перехватывается в getValueAt(). Если статическое поле BaseBundle.sShouldDefuse равно true, исключение не выбрасывается, и выполнение продолжается, оставляя Bundle, содержащий LazyValue, ссылающийся на исходный Parcel. sShouldDefuse указывает, что недоступные значения из Bundle не должны вызывать исключения в данном процессе, и устанавливается в true в
Если исходный Parcel будет переработан, а затем Bundle, прочитанный из него, будет записан в другой Parcel, содержимое исходного Parcel будет скопировано в целевой Parcel, но к этому моменту исходный Parcel может быть повторно использован для чего-то другого, и могут быть скопированы данные из несвязанной IPC-операции
Хорошо, но как нам добиться, чтобы Parcel.hasReadWriteHelper() возвращал true, пока десериализуется Bundle, предоставленный нами?
Оказывается, класс RemoteViews (обычно используемый, например, для передачи виджетов на домашний экран) явно устанавливает ReadWriteHelper при чтении вложенных в него Bundle. Этот ReadWriteHelper не выполняет дедупликацию String и присутствует только для того, чтобы заставить Bundle пропустить копирование данных во вторичный Parcel. Причина этого в том, что RemoteViews включает сжатие для дедупликации вложенных в него объектов ApplicationInfo, но это также может привести к сжатию объектов ApplicationInfo, находящихся внутри Bundle, поэтому чтение такого нельзя откладывать, потому что тогда сжатые объекты не смогли бы быть разжаты
Parcelable в system_server и их получение обратноИтак, теперь мы хотим, чтобы system_server прочитал наши RemoteViews, содержащие Bundle, содержащий LazyValue, который не удаётся десериализовать, а позже (в другой IPC-транзакции Binder) отправил этот объект обратно нам
Вероятно, это можно было бы сделать легальными способами, например зарегистрировавшись как хост виджетов приложения (но это потребовало бы взаимодействия с пользователем для предоставления нам разрешения) или опубликовав Notification с установленным contentView (но это привело бы к взаимодействию с другими процессами и/или было бы видно пользователю, и я предпочёл избежать обоих вариантов)
Вместо этого я решил создать MediaSession и вызвать для него setQueue(List<MediaSession.QueueItem> queue), чтобы отправить объект в system_server, а позже получить его обратно через метод List<MediaSession.QueueItem> getQueue() объекта MediaController (который можно получить через MediaSession.getController()). Хотя эти методы не выглядят так, будто могут принимать RemoteViews, на самом деле могут — благодаря стиранию типов в Java и тому факту, что внутри они реализованы с использованием обобщённых операций сериализации над List
Однако я не использую эти SDK-методы; я вручную формирую данные для нижележащих транзакций Binder (потому что мне нужно записать, а затем прочитать некорректно сериализованные данные), поэтому давайте посмотрим, как работают эти методы
Оба этих метода должны были учитывать тот факт, что общий размер очереди может превышать максимальный размер транзакции Binder, поэтому передача может быть разбита на несколько транзакций
Отправка «очереди» в system_server обычно происходит следующим образом:
MediaSession.setQueue() сначала вызывает ISession.getBinderForSetQueue()system_server этот метод создаёт и возвращает объект ParcelableListBinderMediaSession.setQueue() вызывает ParcelableListBinder.send(), который отправит содержимое списка в предоставленный Binder, возможно, несколькими транзакциями:
1, а сам элемент записывается через (который записывает имя отправляемого класса, а затем вызывает для отправки данных)Получение же «очереди» происходит немного иначе:
MediaController.getQueue() просто вызывает ISessionController.getQueue() и разворачивает полученный ParceledListSlicesystem_server getQueue() просто оборачивает mQueue в ParceledListSlice и возвращает егоParceledListSlice.writeToParcel() и createFromParcel(), в частности, writeToParcel() при достижении безопасного предела размера записывает объект , позволяющий получать следующие частиЧто касается того, почему они различаются: ведутся работы над тем, чтобы system_server не совершал исходящих синхронных вызовов Binder к другим приложениям, потому что если такие вызовы зависнут, это может повесить весь system_server. Это означает, что system_server не должен получать ParceledListSlice. Хотя существует код, который предупреждает об исходящих синхронных транзакциях из system_server, его пока нельзя сделать обязательным, поскольку всё ещё есть случаи, когда system_server совершает такие вызовы, например реально получая ParceledListSlice
Итак, теперь у нас есть примитивы, необходимые для того, чтобы заставить system_server выполнить parcel_that_will_be_sent_to_us.appendFrom(some_recycled_parcel, somewhat_controlled_position, controlled_size)
Мы можем либо случайным образом пытаться извлекать данные Parcel из системы, либо организовать всё так, чтобы получить что-то конкретное
Имеются следующие соображения:* Когда вызывается Parcel.recycle(), содержимое этого Parcel очищается. Это означает, что Parcel, из которого мы хотели бы скопировать данные, не должен быть освобождён через recycle(), что примерно означает, что мы не можем взять данные из транзакции Binder, которая уже завершилась
Parcel какого-нибудь Bundle, присутствующего в системе (сюда входят extras из Intent и savedInstanceState у Activity). Обычно они вообще не освобождаются через recycle() (они очищаются сборщиком мусора и не возвращаются в пул; когда пул исчерпан, Parcel.obtain() создаёт новые объекты Parcel. Конечно, Parcel, на которые мы держим ссылку, не будут собраны GC, даже если системе они больше не нужны)Parcel, используемые для входящих транзакций Binder, используют отдельный пул по сравнению с остальными Parcel в системе. Когда выполняется исходящая транзакция Binder, копирует данные во вторичный , или приложение использует для своих целей, вызывается . С другой стороны, когда есть входящая транзакция , , который . В обоих случаях после этого вызывается , которая . Это означает, что эксплойт должен заставить читать из , принадлежащего тому же пулу, из которого мы хотим утечь данные. Прежде чем я остановился на конкретном варианте, я написал оба, так что вы можете найти оба метода и в моём классе В итоге я решил попытаться перехватить Binder интерфейса IApplicationThread, который приложение отправляет в system_server при запуске процесса приложения, и system_server использует его, чтобы сообщить приложению, какие компоненты ему следует загрузить
Когда процесс приложения только запускается, одно из первых действий, которые он выполняет, — это отправка IApplicationThread в system_server через вызов attachApplication(), и именно из этой транзакции я буду перехватывать этот Binder. Есть и другие места, где IApplicationThread помещается в Parcel, например, передаётся системой для идентификации вызывающей стороны при запуске Activity (но у меня было мало контроля над тем, когда целевое приложение это делает) или отправляется системой приложению в рамках управления жизненным циклом Activity (но это делается в oneway-транзакции, исходящей из system_server, и шансы выиграть гонку с Parcel.recycle() были бы невелики)
Тем не менее перехват Binder, который получает system_server во время транзакции attachApplication(), тоже нетривиален, и пришлось преодолеть несколько проблем.
ParcelПервая проблема с перехватом Binder интерфейса IApplicationThread из Parcel, из которого получаются данные для attachApplication(), заключается в том, что этот Binder находится на довольно ранней/низкой позиции dataPosition(), значительно ниже, чем мог бы находиться наш LazyValue в Bundle внутри RemoteViews
Данные для транзакции attachApplication() состоят всего из RPC-заголовка, за которым следует Binder интерфейса IApplicationThread. RPC-заголовок (записываемый через Parcel.writeInterfaceToken()) состоит из нескольких int и имени интерфейса, в данном случае "android.app.IActivityManager"
Между тем, чтобы прочитать Bundle, встроенный в RemoteViews, нам нужно было бы пройти как минимум (несколько незначительных элементов пропущено):
readParcelableParcelable: "android.view.RemoteViews"ApplicationInfo, присутствующий в RemoteViews (кроме того, он должен не быть null и иметь не-null packageName, иначе RemoteViews.writeToParcel() завершится ошибкой, когда мы попытаемся получить этот объект для отправки обратно)Теперь в Bundle нам нужно лишь положить строковый ключ, и начинается чтение LazyValue, позиция в Parcel запоминается, но к этому моменту она находится далеко за позицией, где был бы Binder интерфейса IApplicationThread.
Можем ли мы, достигнув этой точки, перемотать позицию в Parcel назад? Другими словами, можно ли вызвать Parcel.setDataPosition() со значением, указывающим на более раннюю позицию, чем текущая?
Оказывается, можем, благодаря ещё одной ошибке в LazyValue. Вот код, используемый для его чтения:```java
public Object readLazyValue(@Nullable ClassLoader loader) {
int start = dataPosition();
int type = readInt();
if (isLengthPrefixed(type)) {
int objectLength = readInt();
int end = MathUtils.addOrThrow(dataPosition(), objectLength);
int valueLength = end - start;
setDataPosition(end);
return new LazyValue(this, start, valueLength, type, loader);
} else {
return readValue(type, loader, /* clazz */ null);
}
}
([Оригинал в AOSP](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4376-4388;drc=03c34f57c05feecfb090de3917787f049cb5f804), конструктор `LazyValue` просто присваивает параметры полям)
Дело в том, что `MathUtils.addOrThrow()` проверяет переполнение, [но вполне нормально относится к отрицательным значениям](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/util/MathUtils.java;l=276;drc=290055a76e6ef80dd8ad7bc812d78f4fc0c5be86)
Если бы мы попытались вызвать `Parcel.writeValue()` для `LazyValue` с отрицательным `mLength` (заполненным из параметра `valueLength`), то [это привело бы к исключению в `appendFrom()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4446;drc=03c34f57c05feecfb090de3917787f049cb5f804), однако, поскольку мы находимся в процессе чтения `Bundle` с `Parcel.hasReadWriteHelper()` равным `true`, все `LazyValue` распаковываются после чтения, и нам пришлось намеренно поместить внутрь повреждённый `Parcelable`, чтобы он остался `LazyValue`. Если мы поместим корректно запарцеленные данные в позицию, где находится `LazyValue`, он будет распакован, и, как уже отмечалось ранее, несоответствие длины лишь вызовет сообщение в `logcat`. Данный конкретный эксплойт устанавливает тип `VAL_MAP` и количество пар ключ-значение равное нулю. В `logcat` при чтении этого значения мы можем увидеть следующее сообщение: "`E Parcel : android.util.Log$TerribleFailure: Unparcelling of {} of type VAL_MAP consumed 4 bytes, but -540 expected.`"
(Также `LazyValue` с указанной отрицательной длиной можно использовать (без применения других багов, описанных в этом райтапе) для создания самоизменяющегося `Bundle` — той самой вещи, для устранения которой и был создан `LazyValue`. Но это другая история (и она отдельно сообщена в Google), в этом эксплойте я целюсь в большее)
Итак, насколько нам нужно отмотать назад?
После вызова `setDataPosition()` чтение продолжится со следующей пары ключ-значение в `Bundle`, поэтому нам нужно выбрать позицию, где у нас будут:
1. Ключ `Bundle`, читаемый через `Parcel.readString()`, может быть практически любым, включая указание на недопустимую длину (отрицательную или превышающую общий размер `Parcel`), в этом случае `readString()` вернёт `null`, что является допустимым ключом в `Bundle`
2. Тип значения, который должен быть одним из [типов, для которых `isLengthPrefixed()` возвращает `true`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4695-4712;drc=03c34f57c05feecfb090de3917787f049cb5f804)
3. Длина значения, которая также должна контролироваться нами: `Parcel.appendFrom()` завершится ошибкой, если длина не выровнена или превышает общий размер исходной `Parcel`
Итак, какая позиция в `Parcel` может подойти, учитывая, что те же данные уже были прочитаны и необходимы для достижения этой точки:
* Не раньше имени `Parcelable` (`"android.view.RemoteViews"`), потому что там недостаточно места
* Не внутри имени `Parcelable`, потому что мы не можем задать тип и длину
* Не сразу после имени `Parcelable`, потому что первым в `RemoteViews` идёт `mode`, который мы [должны установить в `MODE_NORMAL`, чтобы добраться до нашего кода](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/RemoteViews.java;l=3799;drc=03c34f57c05feecfb090de3917787f049cb5f804)
* Не после этого, потому что это уже за пределами точки, где находится `Binder` интерфейса `IApplicationThread`
Хм, не существует подходящего места, когда `RemoteViews` является самым внешним объектом в запарцеленных данных
Нам нужно найти какой-нибудь другой `Parcelable`, который:
1. Имеет в начале или рядом с началом место, куда мы можем поместить произвольные данные (например, `int` или `String`, которые являются просто данными и не влияют на процесс сериализации)
2. Может содержать `RemoteViews` (напрямую или через произвольный `readParcelable`)
3. Имеет не слишком длинное полное имя класса, потому что мы по-прежнему ограничены размером позиции, на которой `IApplicationThread` находится в целевой `Parcel`
Поэтому я взял список `Parcelable`-классов в системе, отсортировал его по возрастанию длины полного имени класса и начал проверять элементы этого списка на соответствие условию 2
Таким образом я добрался до [`"android.os.Message"`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;drc=afdb23ab6f909c5438fa69aad458a11497cff216), который и используется в этом эксплойте. Теперь процесс чтения нашего подготовленного объекта из `Parcel` выглядит следующим образом:
* [Флаг наличия элемента](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/session/ParcelableListBinder.java;l=85;drc=23c7543b8e608ebcbb38b952761b54bb56065577) для запуска `readParcelable`
* [Имя `Parcelable`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=4858;drc=03c34f57c05feecfb090de3917787f049cb5f804): `"android.os.Message"`
* Несколько [`int`, которым мы можем задать любые нужные значения](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=652-656;drc=afdb23ab6f909c5438fa69aad458a11497cff216), считываются в поля
* Мы доходим до вызова `readParcelable()`, который проходит весь описанный выше путь через `RemoteViews` и начинает чтение `Bundle` с `Parcel.hasReadWriteHelper` равным `true`
* Этот `Bundle` заявляет, что содержит две пары ключ-значение. В первом значении у нас `LazyValue` с отрицательной длиной, что вызывает `Parcel.setDataPosition()` в позицию, где находится строка `"android.os.Message"`
* Чтение переходит ко второй паре ключ-значение: ключ — `"android.os.Message"`, а тип, длина и данные `LazyValue` берутся из `int`, описанных в третьем пункте. Я получил `LazyValue` с нужными мне `mPosition` и `mLength`. Ура!
* После чтения `LazyValue` распаковываются. Тот, что с отрицательным размером, успешно распаковывается и заменяется пустым `Map`, а другой не проходит десериализацию, но это исключение перехватывается, и `LazyValue` просто остаётся в `Bundle`
* `readParcelable()` завершается, но это ещё не конец данных `Message`. `Message.readFromParcel()` теперь продолжает чтение данных после отмотки и видит данные, которые изначально были записаны как часть `RemoteViews`. Если на этом этапе возникнет любое исключение, весь план рухнет
* Первое возможное исключение — [вызов `readBundle()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=660;drc=afdb23ab6f909c5438fa69aad458a11497cff216). [`Bundle` имеет магическое значение, и если оно неверно, будет выброшено исключение](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1809-1815;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91). Однако этого магического значения нет, если длина [равна нулю](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1800-1804;drc=a9cb2102da997f016245da9a2a56f9ef134e8f91) или [отрицательна](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Parcel.java;l=3323-3327;drc=03c34f57c05feecfb090de3917787f049cb5f804), и так и произошло, когда длина данных `LazyValue` была установлена в значение, которое мне было нужно для захвата `IApplicationThread`. Так что здесь мне просто повезло
* Следующая возможная проблема — [вызов `Messenger.readMessengerOrNullFromParcel()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/Message.java;l=661;drc=afdb23ab6f909c5438fa69aad458a11497cff216). Это фактически обёрнутый объект `Binder`. Чтение этого `Binder` завершается неудачей, потому что `Binder` — особый объект в `Parcel`, и он должен быть аннотирован вне полосы данных для чтения. Эта проблема [обнаруживается и логируется `Parcel` на нативной стороне, однако она не распространяется как ошибка, и просто возвращается `null`](https://cs.android.com/android/platform/superproject/+/master:frameworks/native/libs/binder/Parcel.cpp;l=2473-2476;drc=8b12e8333bbe17ccf4b30efb825244e1923d3a71)
# Блокировка `attachApplication()`
Итак, на предыдущем шаге мы успешно создали объект, который позволит нам захватить объект `IApplicationThread` во время выполнения метода `attachApplication()`
Дело в том, что этот метод выполняется быстро, и наши шансы в честной гонке с его завершением были бы довольно малы
Однако этот метод захватывает несколько мьютексов (через использование Java-блоков `synchronized () {}`); если нам удастся захватить один из таких мьютексов и заблокироваться на нём, этот метод также заблокируется
Теперь вернёмся к нескольким вещам, которые уже были сказаны в этом райтапе и станут полезными для этой цели:
* `Bundle` выполняет десериализацию значений, когда к этим значениям происходит обращение
* Существует класс `ParceledListSlice`, который при десериализации выполняет блокирующий исходящий `Binder`-вызов к объекту, указанному в сериализованных данных
Сложив всё это вместе: если мы найдём в `system_server` место, где содержимое `Bundle`, предоставленного приложением, читается под мьютексом, который также используется в `attachApplication()`, мы сможем заблокировать `attachApplication()` до тех пор, пока `Binder`-транзакция к нашему процессу не завершится
[`ActivityOptions`](https://developer.android.com/reference/android/app/ActivityOptions) — это класс, описывающий различные параметры, связанные с запуском `Activity` (например, анимацию). В отличие от других классов, описывающих параметры, передаваемые в `system_server`, этот не реализует `Parcelable`, а вместо этого предоставляет метод для преобразования его в `Bundle`
На стороне `system_server` этот [`Bundle` преобразуется обратно в `ActivityOptions`, что запускает десериализацию](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityOptions.java;l=1096-1204;drc=03c34f57c05feecfb090de3917787f049cb5f804). Я нашёл место, где [эта операция выполняется при удержании мьютекса `ActivityTaskManagerService.mGlobalLock` в `ActivityTaskManagerService.moveTaskToFront()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=2088;drc=03c34f57c05feecfb090de3917787f049cb5f804)
Поэтому я вызываю [`ActivityManager.moveTaskToFront()`](https://developer.android.com/reference/android/app/ActivityManager#moveTaskToFront(int,%20int,%20android.os.Bundle)), передавая `Bundle`, который вместо значения ожидаемого типа содержит `ParceledListSlice`. Этот [`ParceledListSlice` совершает `Binder`-вызов в мой процесс](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/BaseParceledListSlice.java;l=82-105;drc=23c7543b8e608ebcbb38b952761b54bb56065577), и пока я не вернусь из этого вызова, мьютекс `ActivityTaskManagerService.mGlobalLock` останется заблокированным
# Создание нескольких `LazyValue`, указывающих на разные `Parcel`
`Parcel.recycle()` и `Parcel.obtain()` работают по принципу [«последним пришёл — первым вышел»](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))
Это означает, что если я создам подстроенный `LazyValue`, когда никакая другая `Binder`-транзакция к `system_server` не выполняется, я получу `LazyValue`, который будет указывать на `Parcel`, всегда используемую при поступлении в `system_server` только одной транзакции (до тех пор, пока не случится, что две параллельные транзакции к `system_server` начнутся и завершатся в порядке, отличном от стекового)
Поскольку я не контролирую, какие другие транзакции поступают в `system_server`, для повышения надёжности эксплойта я создал несколько `LazyValue`, указывающих на разные `Parcel`
Поскольку у меня есть возможность инициировать синхронную `Binder`-транзакцию из `system_server` в мой процесс, я использовал эту возможность для создания `LazyValue` на разных уровнях рекурсии между моим процессом и `system_server` (хотя в этот раз я делал это без удержания глобального мьютекса)
Итак:
* Я создаю `LazyValue`
* Я инициирую вызов к `system_server`, `system_server` вызывает меня обратно
* Я создаю `LazyValue`
* Я инициирую вызов к `system_server`, `system_server` вызывает меня обратно
* Я создаю `LazyValue`
* Я инициирую вызов к `system_server`, `system_server` вызывает меня обратно
* ...
Затем, когда у меня достаточно `LazyValue`, я прекращаю это делать, возвращаюсь из всех этих вызовов, и все `Parcel`, которые были зарезервированы этими вызовами, получают `recycle()`
Каждый из созданных мной `LazyValue` обёрнут в отдельный [`ParceledListSlice`, созданный `getQueue()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/media/MediaSessionRecord.java;l=1597;drc=03c34f57c05feecfb090de3917787f049cb5f804), и я могу вызвать `Binder` у `ParceledListSlice`, чтобы заставить `system_server` сериализовать его и отправить в мой процесс
(Альтернативный способ сделать это — создать несколько `MediaSession`)
# Запуск процесса целевого приложения
Теперь у нас есть всё необходимое, чтобы захватить `IApplicationThread` из `attachApplication()`, когда это произойдёт, но нам всё ещё нужно добиться, чтобы `attachApplication()` произошёл
В целом [существует несколько типов компонентов приложения, с которыми может взаимодействовать другое приложение](https://developer.android.com/guide/components/fundamentals#Components), и для каждого из них требуется запуск процесса приложения
Я хотел запустить системное приложение Настроек (которое [работает под system uid](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=5;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) и поэтому имеет доступ ко [всему, что находится за разрешениями Android](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityManager.java;l=3948-3952;drc=16c119018a80b8630c7736a5c6dc35ebef130c5b))
Изначально я пытался запустить его через `startActivity()`, однако, когда я попробовал это, процесс не запускался, пока я не освободил блокировку `ActivityTaskManagerService`. Подробности о том, почему так происходило, описаны в разделе «Дополнительное примечание: `Binder`-вызовы и реентерабельность мьютексов», но в качестве решения я решил запросить у системы [`ContentProvider` из этого приложения](https://cs.android.com/android/platform/superproject/+/master:packages/apps/Settings/AndroidManifest.xml;l=4031-4034;drc=d131bdfbec1209548db9fbb0c63cfbaa4977ef74) вместо `Activity`. Это дало дополнительное преимущество — отсутствие помех моему пользовательскому интерфейсу
Я не использовал [официальный API `ContentResolver`, предоставляемый SDK](https://developer.android.com/reference/android/content/ContentResolver), а вместо этого использовал [внутренний системный API, поскольку мне нужен был асинхронный интерфейс](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IActivityManager.aidl;l=153-154;drc=3d7be31a284d295af4c118c5675896e6fcc28907) — привязка к `ContentProvider` не завершилась бы до `attachApplication()`, который я подвешиваю, хотя запуск отдельного потока мог бы быть альтернативой
(Неважно, что предлагает этот конкретный `ContentProvider`; важно лишь то, что я могу установить соединение с ним)
Вот так я запускаю процесс приложения Настроек. Я заранее убеждаюсь, что оно ещё не запущено, используя [официально доступный метод `ActivityManager.killBackgroundProcesses()`](https://developer.android.com/reference/android/app/ActivityManager#killBackgroundProcesses(java.lang.String))
# Собираем всё вместе
Примитивы теперь описаны, так что вот как всё работает вместе (это практически транскрипция метода `MainActivity.doAllStuff()` из этого эксплойта):
1. Включаю доступ к скрытым API (скрытые API не являются границей безопасности, и [публично доступные обходные пути уже существуют](https://www.xda-developers.com/bypass-hidden-apis/), хотя здесь я использовал метод, основанный на [`Property.of()`](https://developer.android.com/reference/android/util/Property#of(java.lang.Class%3CT%3E,%20java.lang.Class%3CV%3E,%20java.lang.String)), который я не встречал в других местах)
2. (Только если мы перезапускаем эксплойт после первой попытки) Освобождаю соединение с `ContentProvider`, которое мы установили на шаге 6 в предыдущем выполнении. Мы должны сделать это, иначе `ActivityManager.killBackgroundProcesses()` не сочтёт целевой процесс «фоновым» и не убьёт его
3. Убиваю процесс приложения-жертвы с помощью `ActivityManager.killBackgroundProcesses()`, поскольку `attachApplication()` вызывается только при запуске процесса
4. Запрашиваю у `system_server` создание набора объектов, содержащих `LazyValue`, указывающий на `Parcel`, который позже будет переработан. Я получаю ссылку на `Binder` `ParceledListSlice` для каждого объекта, содержащего `LazyValue`, и могу совершить `Binder`-транзакцию к нему, чтобы заставить систему записать его обратно. Каждый объект `LazyValue` создаётся на разной глубине [взаимно-рекурсивных](https://en.wikipedia.org/wiki/Mutual_recursion) вызовов между `system_server` и моим приложением, чтобы повысить вероятность того, что каждый из этих `LazyValue` будет иметь висячую ссылку на другой объект `Parcel`
5. Блокирую `ActivityTaskManagerService.mGlobalLock`, совершая вызов `ActivityTaskManagerService.moveTaskToFront()` с передачей в аргументе `Bundle`, который при десериализации выполняет синхронную `Binder`-транзакцию в мой процесс. Следующие шаги выполняются из этого колбэка и, следовательно, при удержании этой блокировки
6. Запрашиваю у `ActivityManagerService` соединение с `ContentProvider` приложения-жертвы (обратите внимание: в названии нет «`Task`»; `ActivityTaskManagerService` — это класс, ориентированный в основном на обработку компонентов `Activity` приложений, тогда как `ActivityManagerService` обрабатывает другие [компоненты приложений](https://developer.android.com/guide/components/fundamentals#Components) (а также запуск процессов в целом); это [разделение произошло в Android 10, ранее и обработка `Activity`, и других компонентов приложений находилась в `ActivityManagerService`](https://android.googlesource.com/platform/frameworks/base/+/595070969de0a7334d251d5448b641e856e052bc))
7. Я немного `sleep()`, чтобы дать только что запущенному процессу время начать вызывать `attachApplication()`
8. Пока блокировка всё ещё удерживается, я запрашиваю у всех ранее созданных объектов `ParceledListSlice` отправку их оставшегося содержимого (не поместившегося в исходную транзакцию), то есть объектов, содержащих `LazyValue`, указывающий на переработанную `Parcel`. Затем по жёстко заданному смещению, соответствующему позиции `IApplicationThread`, переданного в `attachApplication()`, я читаю объект `Binder`. Пока я только сохраняю полученные `Binder` в `ArrayList`, чтобы не делать слишком много при удержании блокировки
9. Это конец кода, который я выполняю из колбэка, запущенного на шаге 5. `ActivityTaskManagerService.mGlobalLock` разблокируется
10. У меня есть `Binder` интерфейса `IApplicationThread`. Теперь я могу просто использовать его для загрузки моего кода в приложение-жертву, как описано в следующем разделе
# Как я использую `IApplicationThread`
Как отмечалось ранее, [`IApplicationThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/IApplicationThread.aidl;drc=45f4b4aaa5fd8af2d0c685b2fef8acf75ed37452) — это `Binder`, отправляемый приложением в `system_server` при запуске процесса приложения, после чего `system_server` использует его, чтобы сообщить приложению, какие компоненты оно должно загрузить
Предполагается, что этот объект передаётся только в `system_server`, и поэтому там нет проверок на основе `Binder.getCallingUid()`, так что мы можем просто напрямую вызывать методы, предлагаемые этим интерфейсом
Я [описал в своём предыдущем райтапе, как я получаю выполнение кода, манипулируя аргументами `scheduleReceiver()`](https://github.com/michalbednarski/ReparcelBug2#what-then-happens-within-handlereceiver). Теперь ситуация та же, за исключением того, что в этот раз я вызываю `scheduleReceiver()` сам, тогда как тогда я вмешивался в интерпретацию аргументов вызова, совершённого `system_server`
# Дополнительные примечания
В этом разделе я описываю несколько вещей, которые в итоге не оказались полезными в данном случае, хотя они могут быть особенностями, о которых стоит знать, или потенциальными багами
## Дополнительное примечание: `Bundle.clear()`Для простоты я описал здесь обновлённый `Bundle` без одного [коммита, добавленного позже, который позволяет перерабатывать `Parcel`, используемый в `Bundle` для поддержки `LazyValue`, путём вызова `Bundle.clear()`](https://android.googlesource.com/platform/frameworks/base/+/1b74a666d3b4c6a5bf063671eb5dac62a74a9c21%5E%21/)
Как отмечено в сообщении коммита, отслеживается, был ли скопирован `Bundle`; в этом случае `clear()` не будет перерабатывать `Parcel`.
Однако этот коммит также меняет семантику параметра/переменной `recycleParcel` метода [`BaseBundle.initializeFromParcelLocked()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=408-457;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)
Ранее значение `false` в `recycleParcel` означало, что `Parcel` не следует перерабатывать: либо потому что [вызывающий код устанавливал `recycleParcel` в `false`, указывая, что `Parcel` не принадлежит `Bundle`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=1842-1847;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1), либо потому что [это значение устанавливалось в `false` на основе результата `parcelledData.readArrayMap()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/BaseBundle.java;l=441;drc=52ae6c85e4151bb0d6c7700ae4f3a5eb697cd3c1)
Сейчас причины, по которым `recycleParcel` может быть `false`, те же, однако интерпретация этого изменилась: теперь это означает не «не перерабатывать этот `Parcel`», а «отложить переработку `Parcel` до вызова `Bundle.clear()`».
Это означает, что если бы `clear()` был вызван для `Bundle`, созданного при `Parcel.hasReadWriteHelper()` равном `true`, это привело бы к переработке `Parcel`, тогда как код, вызывающий создание этого `Bundle`, также переработал бы этот `Parcel`, что приводит к двойному `recycle()` и поведению, аналогичному double-free: последующие вызовы `Parcel.obtain()` возвращали бы один и тот же объект дважды.
Однако я не нашёл способа вызвать `clear()` для такого `Bundle`.
С тех пор, как я изначально это написал, [поведение `recycle()` изменилось: теперь дополнительная переработка является no-op с возможным сбоем через `Log.wtf()`](https://android.googlesource.com/platform/frameworks/base/+/64ff38669a0e1f945b54c4c62ed9316282a6588d%5E%21/) ([в зависимости от конфигурации, но никогда не приводит к падению `system_server`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=8619-8648;drc=ab23aee04d50b9bdabd52481e77265e976558056)). Я бы сказал, что новое поведение всё ещё может быть опасным, особенно когда у нас есть возможность программно задерживать десериализацию, происходящую в другом процессе, но по-настоящему хорошего способа обработать двойную переработку нет.
## Дополнительное примечание: вызовы `Binder` и реентерабельность мьютексов
Довольно малоизвестная особенность `Binder` заключается в том, что он поддерживает диспетчеризацию рекурсивных вызовов в исходный поток.
То есть если процесс A совершает синхронный вызов `Binder` к процессу B, а затем процесс B, обрабатывая его в том же потоке, совершает синхронный вызов `Binder` к процессу A, этот вызов в процессе A будет диспетчеризован в том же потоке, который ожидает завершения исходного вызова к процессу B.
Ещё один момент: секции `synchronized () {}` в Java являются реентерабельными мьютексами, а значит, если войти в них дважды из одного и того же потока, это будет разрешено, и взаимоблокировки не произойдёт.
Это означает, что теоретически, удерживая `ActivityTaskManagerService.mGlobalLock` заблокированным, мы всё ещё могли бы запустить приложение Настройки с помощью `startActivity(new Intent(Settings.ACTION_SETTINGS))` и успешно войти в [`synchonized` блок, который мы удерживаем](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java;l=626;drc=8854e6eb5960c1a9b233fd0fa6e36a366b2f802d), однако запуск этой `Activity` также включает создание `Task`, что требует вызова [`notifyTaskCreated()`, который отправляет сообщение](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=424-429;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) в [`DisplayThread`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/DisplayThread.java;l=25-31;drc=92b9365f9e1ea5d735e8acb06f790604036ee547), а [его обработка](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=207-208;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) пытается [захватить блокировку, которую мы удерживаем, из другого потока](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=320;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f). Поэтому до освобождения `ActivityTaskManagerService.mGlobalLock` поток `DisplayThread` останется заблокированным. Далее процедура запуска `Activity` включает [отправку сообщения в тот же поток для запуска процесса приложения](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java;l=4659-4664;drc=c76db81ef9d400ffed200f69c7bbda923cdb941c). Всё это означает, что в данном случае процесс приложения не будет запущен, пока мы не освободим блокировку, а причина, по которой мы вообще удерживали эту блокировку, заключалась в том, чтобы не дать транзакции `attachApplication()` завершиться и получить из неё дескрипторы, но в этом случае эта транзакция на самом деле не запустится.
Даже если мы запустим `Activity`, которая будет частью того же `Task`, что и текущая (то есть запустим другую `Activity` из приложения Настройки, которая не указывает `android:launchMode="singleTask"`), эта процедура всё равно будет включать вызов [`notifyTaskDescriptionChanged()`](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/TaskChangeNotificationController.java;l=444-450;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f), который здесь оказывает такое же влияние, как `notifyTaskCreated()`.
Итак, хотя мой поток мог бы вызывать методы, использующие `synchronized (ActivityTaskManagerService.mGlobalLock) {}`, запуск нового процесса приложения после `startActivity()` подразумевал использование этой блокировки из другого потока, и в данном случае это было бесполезно, поэтому я предпочёл запускать процесс приложения через `ContentProvider`.
## Дополнительное примечание: другие способы использования `IApplicationThread`
`IApplicationThread` — очень привилегированный дескриптор, поэтому я рассматриваю его использование после получения как постэксплуатацию.
В этом эксплойте я использовал его напрямую для запроса выполнения кода в целевом процессе, воспользовавшись тем фактом, что доступ к этой операции ограничен capability (владением объектом `Binder`, который мы здесь получили в результате утечки), а не `Binder.getCallingUid()`.
Добавление проверки `Binder.getCallingUid()` в [`ApplicationThread.scheduleReceiver()` (который мы здесь использовали для запроса выполнения кода)](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=985-997;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f) и в другие методы `ApplicationThread` (поскольку `scheduleReceiver()` — не единственный метод в `IApplicationThread`, позволяющий загружать код) всё равно не помешало бы использованию `IApplicationThread` для загрузки кода в процесс другого приложения, поскольку атакующий мог бы передать утёкший `IApplicationThread` вместо собственного в `attachApplication()`.
Помимо загрузки кода в процесс, наличие `IApplicationThread` позволяет выполнять [`grantUriPermission()` с использованием привилегий процесса, которому принадлежит этот дескриптор](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;l=5631-5632;drc=09f52aa440ab32e66dfabeb4cb40b72166930b4f)
unparcel(boolean itemwise)sourcemParcelledDataParcelBundlerecycleParceltrueParcelBundleinitializeFromParcel вызывает recycleParcel &= parcelledData.readArrayMap(map, count, !parcelledByNative, /* lazy */ true, mClassLoader) для чтения содержимого карты ключ-значение. Ключи — это String, а значения читаются с помощью readLazyValue(), что создаёт объекты LazyValue для значений тех типов, которые записываются с префиксом длины. readArrayMap() возвращает значение, указывающее, можно ли переработать Parcel. Если присутствовали какие-либо объекты LazyValue, recycleParcel устанавливается в false, и Parcel, на который ссылаются LazyValue, не будет переработан (из этого правила есть исключение, но оно здесь не важно; я опишу его в разделе «Дополнительное примечание: Bundle.clear()»)unparcel() завершён, mMap установлен (не null) и сопоставляет строковые ключи (String) либо с фактическими значениями, если они готовы, либо с объектами LazyValuegetValue(), который сопоставляет ключ (String) с индексом (int) и передаёт его в getValueAt()LazyValue.apply() перематывает Parcel к позиции LazyValue.mPosition и вызывает обычный Parcel.readValue(), о котором я уже рассказалLazyValue заменяется в mMap, так что следующий вызов Bundle.get*() для того же ключа вернёт значение напрямую, и десериализация LazyValue не будет повторяться. Когда Bundle пересылается дальше, это значение будет сериализовано заново, а не скопировано как есть из исходных данных (однако после чтения пересланного Bundle это значение снова станет LazyValue, и любые возможные несоответствия writeToParcel/createFromParcel не смогут повлиять на другие значения)ParcelmaybeWriteSquashed()system_serverBundleParcel.writeParcelable()Parcelable.writeToParcelBinder, записывается 0, чтобы указать, что в этой транзакции больше нет элементов, и следующие элементы будут отправлены в другой транзакцииParcelableListBinder получит количество элементов, указанное в первой транзакции, он вызывает лямбду, переданную в его конструктор, которая в данном случае присваивает полученный список полю MediaSessionRecord.mQueueBinderParceledListSlice читается из Parcel, он сначала читает первую часть напрямую из Parcel, а затем, если не все элементы были записаны встроенно, вызывает Binder, который был записан в Parcel, чтобы получить эти элементыBundleParcelParcelBinderParcel.recycle()RemoteViewsParcelmakeOwnedLeakermakeHolderLeakerRemoteViews.readActionsFromParcel()