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

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

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

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

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

Категории

Все категории
Loading categories
TheLastBundleMismatch — Writeup и эксплойт для CVE-2023-45777, обход проверки Intent внутри AccountManagerService на Android 13, несмотря на смягчение "Lazy Bundle" | Kitploit
Инструменты/GitHubGitHub/michalbednarski/thelastbundlemismatch
Безопасность AndroidАнализ уязвимостейЭксплуатацияАнализ Бинарных ФайловСтатьи и Исследования
GitHubmichalbednarski/thelastbundlemismatch

TheLastBundleMismatch

Writeup и эксплойт для CVE-2023-45777, обход проверки Intent внутри AccountManagerService на Android 13, несмотря на смягчение "Lazy Bundle"

Репозиторий

Популярное

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

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

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

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

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

Загадочный патч

Начнём на этот раз с патча, который появился как исправление для CVE-2023-45777 в Android Security Bulletin:```diff diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 7a19d034c2c8..5238595fe2a2 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4923,7 +4923,7 @@ public class AccountManagerService p.setDataPosition(0); Bundle simulateBundle = p.readBundle(); p.recycle();

  • root@kitploit:~
           Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
    
  • root@kitploit:~
           Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
           if (intent != null && intent.getClass() != Intent.class) {
               return false;
           }
    
root@kitploit:~
Few people were puzzled by it enough to ask me, previously I've replied to them with some hints and now I'm publishing full writeup for this issue

But first lets provide some context about what is going on in this patch

This is change in [`checkKeyIntent()` method](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4938-4954;drc=47de64a38aa1799cb41f41b2ea0c539ee61de64d). This method performs multiple checks to ensure that `Intent` provided by application is safe for system to launch (using privileges of system)

First, this method uses `checkKeyIntentParceledCorrectly()` which serializes and deserializes again `Bundle` which we're checking and checks if `Intent` taken from `Bundle` before that matches `Intent` from `Bundle` after such cycle. Since launch of `Intent` happens in other system app processes than one which performs validation, it was previously [possible to construct `Bundle`-s which appeared safe during validation inside `AccountManagerService`, but contained different `Intent` after being sent to next process](https://github.com/michalbednarski/IntentsLab/issues/2#issuecomment-344365482). This simulates sending `Bundle` to next process in order to detect such situations.

After `checkKeyIntentParceledCorrectly()` we have `bundle.getParcelable()` call, which this patch switches from deprecated version that could construct any object to one that validates that object that is about to be deserialized is of type which was specified in second parameter

That version with type parameter was introduced in Android 13, as part of larger `Parcel`/`Bundle` hardening. In particular, before Android 13 when `Bundle` was sent between processes, it kept raw copy of whole serialized data until any item was accessed, at which point every value was deserialized. Now when any value is accessed for first time after `Bundle` has been received, only `String` keys and the values of primitive types are deserialized, while non-primitive values are left as `LazyValue`-s, which have their length stored as part of serialized data in order to ensure that even when serialization/deserialization logic is mismatched, such mismatches won't affect other entries

Before we dive in, lets have a look at `LazyValue`: In it's source code [we've got nice comment explaining it's data structure](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

mPosition и mLength описывают расположение всех данных LazyValue в исходном Parcel, включая type и length. "length" (без "m" в начале) относится к значению длины, записанному в Parcel, и исключает заголовок (type и length)

Если Bundle, содержащий LazyValue, пересылается в другой процесс, весь LazyValue, включая поля type и length, копируется дословно из Bundle.mParcelledData в целевой Parcel

Когда элемент Bundle, представленный LazyValue, запрашивается, Parcel перематывается к mPosition и вызывается readValue(). Если в bundle.getParcelable() передаётся аргумент типа, он передаётся в readValue(), который как гарантирует, что тип, подлежащий распаковке, является ожидаемым, так и проверяет после распаковки, что тип распакованного значения является ожидаемым. После распаковки LazyValue заменяется, поэтому при следующей записи Bundle в Parcel значение будет сериализовано через writeValue() снова

Использование типизированного параметра Bundle.get*()/Parcel.read*() в основном актуально для таких методов, как Parcel.readParcelableList(), который возвращает ArrayList, и из-за стирания типов в Java, даже если вы сделали что-то вроде List<SomeParcelableType> field = parcel.readParcelableList();, часть <SomeParcelableType> не проверялась во время выполнения, и такой List мог содержать любые классы Parcelable, доступные в системе, и поэтому все createFromParcel/writeToParcel, доступные в системе, могли использоваться как часть сериализации/десериализации типа, содержащего такой List

Вам также может быть интересна презентация команды Android Security and Privacy о внедрении этих механизмов (слайды, видео)

Однако здесь использование типизированной версии выглядит избыточным, поскольку мы также явно проверяем тип возвращаемого объекта. Так что же происходит и какая уязвимость здесь исправляется?

Побочные эффекты

Взгляните ещё раз на патч с самого начала

  • Если значение, десериализованное под ключом "intent", является Intent
    • Оно будет проверено на соответствие компоненту, который системе безопасно запускать
    • Если бы несоответствие можно было спровоцировать из объекта Intent, у нас была бы гораздо более серьёзная проблема
  • Если десериализуемое значение не является Intent
    • Чтобы сделать что-то плохое, нам нужно было бы иметь Intent внутри Bundle после его отправки в другой процесс, но тип Parcelable сохраняется на более раннем смещении, чем любое возможное несоответствие, а префикс длины LazyValue не позволяет нам изменять следующие пары ключ-значение в случае несоответствия writeToParcel/createFromParcel

Итак, что опасного мог бы здесь сделать вызов bundle.getParcelable(AccountManager.KEY_INTENT) без аргумента типа?

[Ответ в следующем абзаце, попробуйте угадать, прежде чем читать дальше. Если бы у меня была фурсона, здесь было бы место для какого-нибудь арта]

Ответ заключается в вызове несвязанного createFromParcel(), который фактически изменяет необработанные данные LazyValue, хранящегося под другим ключом и передаваемого дословно в следующий процесс

У нас есть реализация createFromParcel(), которая может фактически вызывать writeInt() на предоставленном Parcel

Но не из-за ошибочно размещённого writeInt, а из-за неограниченной рефлексии. В частности, внутри PackageParser у нас есть следующий код:```java final Class cls = (Class) Class.forName(componentName); final Constructor cons = cls.getConstructor(Parcel.class);

intentsList = new ArrayList<>(N); for (int i = 0; i < N; ++i) { intentsList.add(cons.newInstance(in)); }

root@kitploit:~
Мы можем иметь объект `Parcel`, который был передан в `createFromParcel`, переданный в любой доступный в системе `public` конструктор, который принимает единственный аргумент `Parcel`

И затем [у нас есть следующий код](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/PooledStringWriter.java;l=51-56;drc=782d49826862cbdc9d020fc9d85f8a6f64675dcb):```java
public PooledStringWriter(Parcel out) {
    mOut = out;
    mPool = new HashMap<>();
    mStart = out.dataPosition();
    out.writeInt(0); // reserve space for final pool size.
}

У нас есть конструктор, который вызывает writeInt(0) для предоставленного Parcel, однако есть несколько моментов, которые усложняют эксплуатацию

Прежде всего, хотя это напрямую не видно в исходном коде, сразу после вызова newInstance() выполняется приведение типов и выбрасывается ClassCastException

Проглатывание исключения

Мне нужно было что-то, что во время createFromParcel вызывало бы createFromParcel другого класса внутри блока try, а затем не смогло бы распространить перехваченное исключение

Это та часть, где эксплойт на самом деле не работает на чистом AOSP — я использовал специфичный для Samsung класс

Я включил копию соответствующих частей этого класса в этот репозиторий

Этот репозиторий также включает скрипт, который интегрирует его в AOSP, так что для тестирования вы можете запустить его (передайте путь к вашему AOSP-чекауту в качестве аргумента, например ./make-aosp-buggy.sh /path/to/aosp), откатите изменение, описанное в начале статьи, и запустите этот эксплойт против вашей AOSP-сборки

Ранее я использовал класс OutputConfiguration из AOSP для проглатывания исключений; до Android 13 проглатывание исключения в createFromParcel() в сочетании с разрешением создания других Parcelable-объектов само по себе является уязвимостью, однако в случае SemImageClipData проглатывание исключений не присутствовало на этих версиях Android

Однако есть важное различие между SemImageClipData и ранее использованным OutputConfiguration: даже несмотря на то, что SemImageClipData перехватывает исключение, он всё равно возвращает ненулевой объект, и если позже он будет приведён к другому типу, это вызовет ClassCastException, чего мы и пытаемся избежать

Стирание типов в Java даёт о себе знать

Стирание типов в Java означает, что обобщённые методы на самом деле не знают об обобщённом типе, используемом вызывающим кодом. Обычно это помогало эксплуатации```java // When we read some List, this actually didn't check if list contains only SomeParcelableType List myList = sourceParcel.readParcelableList();

// Above is why Android 13 has introduced typed methods that enforce type at runtime List myList = sourceParcel.readParcelableList(SomeParcelableType.class);

// If untyped method was used when reading, list can contain non-SomeParcelableType // items and they would be written without errors targetParcel.writeParcelableList(myList, 0);

// However if List contains non-SomeParcelableType item, this would throw during item access // (That however commonly didn't happen if we used Parcelable object only as container in gadget chain) SomeParcelableType myItem = myList.get(0);

root@kitploit:~
В этот раз стирание типов сыграло против нас. Сначала у нас был метод, который фактически вызывал конструктор через рефлексию```java
private static <T extends IntentInfo> ArrayList<T> createIntentsList(Parcel in) {
    // ...
    final ArrayList<T> intentsList;
    // ...
    intentsList.add(cons.newInstance(in));
    // ...
    return intentsList;
}

This method has generic parameter T. It doesn't matter what parameter type was used by caller, however since in declaration of this method there's <T extends IntentInfo>, the line with newInstance() call becomes intentsList.add((IntentInfo) cons.newInstance(in));, even though newInstance() returns Object and ArrayList.add() accepts Object as argument. This introduced need to wrap call of that with some Parcelable that swallows Exception

Then we have the bundle.getParcelable() call```java @Deprecated @Nullable public T getParcelable(@Nullable String key) { unparcel(); Object o = getValue(key); if (o == null) { return null; } try { return (T) o; } catch (ClassCastException e) { typeWarning(key, o, "Parcelable", e); return null; } }

root@kitploit:~
Процедура десериализации выполняется вызовом `getValue()`, который фактически приводит к вызову `createFromParcel()`. Если там произойдёт `ClassCastException`, он не будет перехвачен. `getValue()` теперь возвращает то значение, которое было десериализовано для этого ключа через [`parcel.readValue()`](https://developer.android.com/reference/android/os/Parcel#readValue(java.lang.ClassLoader))

Однако если мы поместим `SemImageClipData` в качестве значения, внутри блока `try`-`catch` мы попытаемся привести его к `T`, которым в данном случае является `Parcelable`, как объявлено в обобщённом объявлении метода. Вызывающий код использует этот метод как обобщённый с `T` в качестве `Intent`, однако `getParcelable()` об этом не знает, и приведение к `Intent` происходит в вызывающем коде, поэтому `ClassCastException` выбрасывается за пределами `try`

Однако мы можем обернуть наш `SemImageClipData` в массив `Parcelable[]`, тогда приведение к `T` внутри `getParcelable()` не сможет привести `Parcelable[]` к `Parcelable` и выбросит `ClassCastException` внутри `try`, это `Exception` будет записано в журнал, и будет возвращён `null`, который затем будет принят `checkKeyIntent()`

# Раскладка

Итак, теперь нам нужно выровнять содержимое внутри `Bundle`, чтобы после цикла `writeToParcel`/`createFromParcel` его содержимое было тем, которое мы подготовили

Но в отличие от типичного "`Bundle` FengShui", где триггером является то, что `createFromParcel` читает больше или меньше данных, чем ранее записал соответствующий `writeToParcel`, здесь у нас `writeInt(0)` перезаписывает часть не десериализованного `LazyValue`

Вот как выглядит `Bundle.mParcelledData`, когда он впервые распаковывается `AccountManagerService` (смещения получены вызовом `dataPosition()` через отладчик, подключённый к `system_server`)

<table>
<tr><th>Смещение</th><th>Значение</th><th>Примечание</th></tr>
<tr><td>0</td><td>3</td><td>Количество пар ключ-значение</td></tr>
<tr><td>4</td><td>"intent"</td><td>Первый ключ в <code>Bundle</code>, тот, к которому будет обращение через <code>getParcelable(AccountManager.KEY_INTENT)</code></td></tr>
<tr><td>24</td><td>16</td><td>Первый <code>LazyValue</code> начинается здесь, тип — <code>VAL_PARCELABLEARRAY</code></td></tr>
<tr><td>28</td><td>340</td><td>Объявленная длина <code>LazyValue</code>, используется для поиска следующего ключа в <code>Bundle</code>. Наш <code>LazyValue</code> на самом деле не будет иметь такой размер после чтения, но <code>LazyValue.apply</code> <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Parcel.java;l=4501-4505;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">сообщает об этом через <code>Slog.wtfStack()</code></a>, который <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Slog.java;l=230-235;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">не выбрасывает исключение</a></td></tr>
<tr><td>32</td><td>1</td><td>Длина массива <code>Parcelable[]</code>, массив содержит только один элемент и присутствует, чтобы <code>ClassCastException</code> произошло внутри блока <code>try</code>, который находится в <code>bundle.getParcelable()</code></td></tr>
<tr><td>36</td><td>"com.samsung.android.<br>content.clipboard.data.<br>SemImageClipData"</td><td>Имя класса <code>Parcelable</code>, это класс-обёртка, который проглотит исключение</td></tr>
<tr><td>160</td><td>2</td><td>Тег типа, используемый <code>createClipBoardData()</code></td></tr>
<tr><td>164</td><td></td><td>Элементы, которые читаются конструктором суперкласса <code>SemImageClipData</code> (включая вызов <code>readParcelable()</code>, однако это происходит за пределами блока <code>try</code>). Не очень важно, но нам нужно пройти через них, прежде чем добраться до интересной части <code>createFromParcel()</code></td></tr>
<tr><td>252</td><td></td><td>Данные, читаемые <code>SemImageClipData.readFromSource()</code></td></tr>
<tr><td>272</td><td>"android.content.pm.<br>PackageParser&#36;Activity"</td><td>Имя <code>Parcelable</code>, читаемое через <code>mExtraParcelFd = in.readParcelable()</code>. Тип не совпадает, однако до приведения всё равно будет выброшено исключение</td></tr>
<tr><td>360</td><td></td><td>Поля <code>className</code> и <code>metadata</code> класса <code>PackageParser$Component</code></td></tr>
<tr><td>368</td><td>1</td><td>Количество элементов в <code>createIntentsList()</code></td></tr>
<tr><td>372</td><td>"android.os.<br>PooledStringWriter"</td><td>Имя класса, который будет создан через <code>Class.forName().getConstructor(Parcel.class).newInstance()</code>. На этой позиции заканчивается первый <code>LazyValue</code>, однако его разбор продолжается, так как <code>readValue()</code> не достиг конца. Также интерпретируется как второй ключ в <code>Bundle</code> при первоначальном <code>unparcel()</code></td></tr>
<tr><td>436</td><td>4</td><td>Второй <code>LazyValue</code> начинается здесь, эта 4 — <code>VAL_PARCELABLE</code>, для которого <code>Parcel.isLengthPrefixed()</code> вернёт <code>true</code>. Это значение позже будет перезаписано конструктором <code>PooledStringWriter</code>, после чего будет выброшено исключение, и <code>getParcelable(AccountManager.KEY_INTENT)</code> завершится</td></tr>
<tr><td>440</td><td>240</td><td>Длина <code>LazyValue</code>, тип которого был объявлен как <code>VAL_PARCELABLE</code>, используется для определения позиции следующей записи и того, сколько данных нужно скопировать в целевой <code>Bundle</code> при повторной сериализации. Этот <code>LazyValue</code> фактически не распаковывается и используется как контейнер необработанных данных</td></tr>
<tr><td>684</td><td>"1&y~pw"</td><td rowspan="2">Третья пара ключ-значение, ключ сгенерирован случайным образом, чтобы его Java <code>hashCode()</code> был выше ранее использованных (элементы, хранящиеся внутри <code>ArrayMap</code>, отсортированы по возрастанию <code>hashCode()</code> ключа, и в этом порядке элементы из <code>Bundle</code> будут записаны в <code>Parcel</code>). Эта пара ключ-значение присутствует здесь только для увеличения общего количества записанных пар, так как именно столько пар будет прочитано, хотя эта пара фактически не будет прочитана</td></tr>
<tr><td>704</td><td>-1 (<code>VAL_NULL</code>)</td></tr>
</table>

Затем, когда `Bundle` сериализуется снова, он выглядит так:

<table>
<tr><th>Смещение</th><th>Значение</th><th>Примечание</th></tr>
<tr><td>0</td><td>3</td><td>Количество пар ключ-значение</td></tr>
<tr><td>4</td><td>"intent"</td><td>Первый ключ в <code>Bundle</code></td></tr>
<tr><td>24</td><td>16</td><td><code>VAL_PARCELABLEARRAY</code>, ранее десериализованный массив <code>Parcelable[]</code> теперь сериализуется снова</td></tr>
<tr><td>28</td><td>196</td><td>Длина <code>LazyValue</code>, то есть нашего обёрнутого объекта <code>SemImageClipData</code>. Эта длина взята из выполнения с моим макетом <code>SemImageClipData</code>, поэтому смещения, представленные с этого момента, не будут совпадать с теми, которые появятся на реальном устройстве Samsung, однако этот <code>LazyValue</code> больше не будет десериализован, так что для выполнения эксплойта это не имеет значения</td></tr>
<tr><td>224</td><td>"android.os.<br>PooledStringWriter"</td><td>Второй ключ в <code>Bundle</code></td></tr>
<tr><td>288</td><td>0</td><td>Второй <code>LazyValue</code> начинается здесь, элемент под ключом <code>"android.os.PooledStringWriter"</code> не был доступен, поэтому этот <code>LazyValue</code> копируется из исходных данных, однако тег типа был перезаписан вызовом <code>writeInt(0)</code>, выполненным конструктором <code>PooledStringWriter</code>, и при достижении целевого процесса это больше не интерпретируется как <code>LazyValue</code></td></tr>
<tr><td>292</td><td>240</td><td>Это была длина второго <code>LazyValue</code>, скопированного из исходного <code>Bundle</code>, однако, поскольку тег типа был перезаписан с помощью <code>writeInt(0)</code>, что является <code>VAL_STRING</code>, это значение теперь читается через <code>readString()</code>. Ранее для <code>LazyValue</code> длина выражалась в байтах, но теперь для <code>String</code> она выражается в двухбайтовых символах. В исходном <code>Parcel</code> недостаточно данных для этого, поэтому <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/native/libs/binder/Parcel.cpp;l=2221-2226;drc=4d6b008243a5b1b1fb4e725e37e14651a24a4a4d">нативный <code>parcel->readString16Inplace()</code> завершается ошибкой после чтения длины</a>, однако это не вызывает исключение на стороне Java</td></tr>
<tr><td>296</td><td>"intent"</td><td>"Третий" ключ в <code>Bundle</code>. Фактически перезаписывает первый ключ: поскольку <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=651-659;drc=584140c83a456b5de99880b440c2d5dfc3c70506">"intent" имеет меньший <code>hashCode()</code>, чем ранее виденный ключ, метод <code>ArrayMap.append()</code> использует <code>put()</code>, который позволяет заменять значения</a>, иначе у нас был бы <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/ArrayMap.java;l=667-675;drc=584140c83a456b5de99880b440c2d5dfc3c70506">дублирующийся ключ, который позже был бы отклонён <code>validate()</code></a></td></tr>
<tr><td>316</td><td>4</td><td><code>VAL_PARCELABLE</code>, здесь начинается <code>LazyValue</code>, содержащий фактический <code>Intent</code>, который будет запущен</td></tr>
<tr><td>536</td><td>"1&y~pw"</td><td>Заполняющий элемент, который был записан, но не читается, поскольку все 3 пары ключ-значение уже были прочитаны. <a href="https://cs.android.com/android/_/android/platform/system/tools/aidl/+/96a02f50fdfa4d20aa46ae2dde927257eac46d4a">В отличие от интерфейсов AIDL</a>, для <code>Bundle</code> не выполняется проверка <code>enforceNoDataAvail()</code> (но даже если бы она была, её <a href="https://github.com/michalbednarski/ReparcelBug2/issues/3">можно было бы обойти, вставив фиктивную запись, указывающую ожидаемую длину</a>)</td></tr>
</table>

# Как это произошло дважды

Давайте теперь обсудим четыре патча, два из которых исправляют уязвимость, о которой идёт речь в этом отчёте

* CVE-2023-20944 ([бюллетень](https://source.android.com/docs/security/bulletin/2023-02-01#framework), [патч](https://android.googlesource.com/platform/frameworks/base/+/d0bc9026e2e62e09fa88c1bcbf1dc1c3fb001375%5E%21/)): Это ещё одна уязвимость, найденная мной. Как и в этом случае, из патча не очевидно, как её можно эксплуатировать, но <a href="https://konata.github.io/posts/creator-mismatch/">похоже, кто-то другой это выяснил (запись в блоге на китайском)</a>
* CVE-2023-21098 ([бюллетень](https://source.android.com/docs/security/bulletin/2023-04-01#framework), [патч](https://android.googlesource.com/platform/frameworks/base/+/107e6377328486fca55131ea06ca9d6a3c1585e0%5E%21/)): Это первый раз, когда я сообщил об эксплойте, представленном здесь. Этот патч также вводит исправление для обхода `checkKeyIntentParceledCorrectly()`, применимое к версиям Android до 13
* CVE-2023-35669 ([бюллетень](https://source.android.com/docs/security/bulletin/2023-09-01#framework), [патч](https://android.googlesource.com/platform/frameworks/base/+/f810d81839af38ee121c446105ca67cb12992fc6%5E%21/)): Этот не является ответом на мой отчёт, но я думаю, что он был сделан для исправления той же проблемы, о которой была CVE-2023-20944, но для случаев, когда `AccountManager.KEY_INTENT` запускается Activities, отличными от `ChooseTypeAndAccountActivity` (например, [`AddAccountSettings`](https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Settings/src/com/android/settings/accounts/AddAccountSettings.java;l=95-107;drc=32813a2bef49b172aed89122b4eb50bf14026ddc), которое я пропустил при первом сообщении об ошибке). Это изменение заменило использование типизированного `bundle.getParcelable()` на нетипизированный с ручной проверкой `getClass() != Intent.class`, что фактически откатило исправление для CVE-2023-21098
* CVE-2023-45777 ([бюллетень](https://source.android.com/docs/security/bulletin/2023-12-01#framework), [патч](https://android.googlesource.com/platform/frameworks/base/+/f4644b55d36a549710ba35b6fb797ba744807da6%5E%21/)): Это второй раз, когда я сообщил об этом эксплойте. Патч сохранил ручную проверку `getClass() != Intent.class`, но в дополнение к этому вернул использование типизированного `bundle.getParcelable()`, что является хорошим способом исправить обе проблемы

Хотя один и тот же эксплойт работает как для CVE-2023-21098, так и для CVE-2023-45777, способ обхода `checkKeyIntentParceledCorrectly()` отличается

В случае CVE-2023-21098 [`checkKeyIntent()` фактически не вызывался, если проверяемый `Bundle` не содержал `Intent`](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=3519-3521;drc=cdd30b5c040ba7ebd0a1cc6009183ff602434fc0). Поскольку `checkKeyIntent()` — это то, что вызывает `checkKeyIntentParceledCorrectly()`, в случае, когда исходный `Bundle` не содержал `Intent`, `Bundle` после повторной сериализации не проверялся

В случае CVE-2023-45777 `checkKeyIntentParceledCorrectly()` вызывался корректно, однако [`writeBundle()` происходил там до вызова `getParcelable()` без аргумента типа (до которого `Bundle` не менял своё содержимое)](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/accounts/AccountManagerService.java;l=4921-4929;drc=b0f6558fb36eb76df35c516ec5a65030a34a8734)
Скачать инструмент