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

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

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

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

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

Категории

Все категории
Loading categories
CVE-2022-20474 — PoC для CVE-2022-20474 | Kitploit
Инструменты/GitHubGitHub/cxxsheng/cve-2022-20474
Безопасность AndroidАнализ уязвимостейЭксплуатацияСтатьи и ИсследованияОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubcxxsheng/cve-2022-20474

CVE-2022-20474

PoC для CVE-2022-20474

Репозиторий
2011 год назадПроверено Kitploit

Популярное

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

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

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

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

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

CVE-2022-20474 Анализ — Self-changed Bundle в LazyValue

Предисловие

Пожалуйста, прочтите это предупреждение: перед чтением этой статьи следует иметь базовое представление об уязвимостях, связанных с несоответствием Bundle. Если вы еще не читали следующие справочные материалы, рекомендуется сначала прочитать их:

  1. Bundle Фэн-шуй — Подробное объяснение уязвимостей несоответствия сериализации и десериализации в Android: Классическое вводное руководство.
  2. История атак и защиты уязвимостей десериализации Android: Хорошая обзорная статья.
  3. TheLastBundleMismatch: Первая статья о Bundle Mismatch в режиме LazyValue.

Предыстория

Недавно я внимательно изучал статью LeakValue от michalbednarski. В разговоре с Canyie он упомянул, что в этой статье также описывается случай Self-changing Bundle в сценарии LazyValue. Я пошел искать оригинал, и действительно, там был такой абзац, который я упустил, читая статью Михала. В оригинале говорится:

(Also LazyValue with negative length specified can be used (without using other bugs described in this writeup) to create self-changing Bundle, the thing LazyValue was created to eliminate. But that is another story (and separately reported to Google), in this exploit I'm aiming for more)

Михаил, вероятно, имел в виду CVE-2022-20474 (бюллетень, патч). Я взглянул на патч, но функция в ссылке на патч была неполной. Я дополнил её и внимательно изучил:

root@kitploit:~
@@ -4388,6 +4388,9 @@
    public Object readLazyValue(@Nullable ClassLoader loader) {
         int start = dataPosition();
         int type = readInt();
         if (isLengthPrefixed(type)) {
             int objectLength = readInt();
+            if (objectLength < 0) {
+                return null;
+            }
             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);
         }
    }             

objectLength в коде — это length в LazyValue, но на самом деле это только длина изменяемого объекта, содержащегося в LazyValue. Всю длину LazyValue контролирует поле mLength, то есть valueLength в коде, которое в конструкторе LazyValue передаётся в mLength.

Давайте сравним с макетом LazyValue:

root@kitploit:~
       /**
         *                      |   4B   |   4B   |
         * mSource = Parcel{... |  type  | length | object | ...}
         *                      a        b        c        d
         * length = d - c
         * mPosition = a
         * mLength = d - a
         */

Исходя из вышесказанного, можно сделать следующие факты:

  1. mLength представляет всю длину LazyValue: mLength = objectLength + 8 байт.
  2. objectLength должно быть ≥ 0.
  3. Объект LazyValue сохраняет только mLength, но не objectLength, потому что копирование памяти для LazyValue выполняется на основе всего объекта.
  4. После чтения указатель перемещается вперёд; возможно, LazyValue будет прочитан снова.

Затем, после долгих размышлений, мы пришли к выводу, что эти факты нифига не полезны! Потому что на их основе можно изменить значение только при чтении, а мы знаем, что основная идея Self-changed Bundle — изменить его после завершения чтения, чтобы обойти проверку безопасности.

Когда мы уже собирались сдаться, мы внезапно заметили некоторые детали в описании патча:

Addresses a security vulnerability where a (-8) length object would cause dataPosition to be reset back to the statt of the value, and be re-read again.

Аномальный objectLength

Там упоминается, что при objectLength = -8 возникают некоторые проблемы, что дало нам дополнительные подсказки. Может ли LazyValue в этом случае нормально применяться через apply?

root@kitploit:~
        @Override
        public Object apply(@Nullable Class<?> clazz, @Nullable Class<?>[] itemTypes) {
            Parcel source = mSource;
            if (source != null) {
                synchronized (source) {
                    // Check mSource != null guarantees callers won't ever see different objects.
                    if (mSource != null) {
                        int restore = source.dataPosition();
                        try {
                            source.setDataPosition(mPosition);
                            mObject = source.readValue(mLoader, clazz, itemTypes);
                        } finally {
                            source.setDataPosition(restore);
                        }
                        mSource = null;
                    }
                }
            }
            return mObject;
        }

  	/**
     * @see #readValue(int, ClassLoader, Class, Class[])
     */
    @Nullable
    private <T> T readValue(@Nullable ClassLoader loader, @Nullable Class<T> clazz,
            @Nullable Class<?>... itemTypes) {
        int type = readInt();
        final T object;
        if (isLengthPrefixed(type)) {
            int length = readInt();
            int start = dataPosition();
            object = readValue(type, loader, clazz, itemTypes);
            int actual = dataPosition() - start;
            if (actual != length) {
                Slog.wtfStack(TAG,
                        "Unparcelling of " + object + " of type " + Parcel.valueTypeToString(type)
                                + "  consumed " + actual + " bytes, but " + length + " expected.");
            }
        } else {
            object = readValue(type, loader, clazz, itemTypes);
        }
        return object;
    }

Видно, что настоящий readValue начинает чтение с mPosition, затем последовательно читает LazyType и objectLength, после чего переходит к нормальному процессу чтения Value, например, для Parcelable читается ClassName и выполняется createFromParcel. После чтения это ничем не отличается от обычного Key-Value и не влияет на последующую сериализацию. Ещё раз вспомним: основная идея Self-changed Bundle — изменить после завершения чтения. Здесь же просто обычное чтение за пределами границ. Похоже, этот путь не работает.

Что если LazyValue не применяется apply в этом процессе? Другими словами, если он продолжает участвовать в IPC как LazyValue, то будет вызвана его функция writeToParcel:

root@kitploit:~
     public void writeToParcel(Parcel out) {
            Parcel source = mSource;
            if (source != null) {
                synchronized (source) {
                    if (mSource != null) {
                        out.appendFrom(source, mPosition, mLength);
                        return;
                    }
                }
            }
            out.writeValue(mObject);
        }

Весь LazyValue будет скопирован как есть, если только mLength == 0. Стоп! Выше упоминалось, что mLength = objectLength + 8 байт, а из информации о патче следует, что для возникновения уязвимости objectLength должен быть -8, тогда mLength = 0. То есть в этом сценарии весь LazyValue просто исчезает, копируется только String Key, что приводит к пропуску записи. Условие Self-changed Bundle напрямую выполняется.

Теперь, зная причину, мы можем приступить к воспроизведению, но перед этим нам нужно ещё немного деталей.

Деталь 1: Два типа Bundle

root@kitploit:~
 static final int BUNDLE_MAGIC = 0x4C444E42; // 'B' 'N' 'D' 'L'
 private static final int BUNDLE_MAGIC_NATIVE = 0x4C444E44; // 'B' 'N' 'D' 'N'

Как известно, структура памяти Bundle примерно такова:

root@kitploit:~
       /**
         *        |   4B   |   4B   |   4B   |
         * Bundle{| length |  MAGIC |  size  | Key  | Value | Key  | Value | ...}
         *
         */

MAGIC — это магическое число Bundle в структуре памяти, может быть BUNDLE_MAGIC или BUNDLE_MAGIC_NATIVE. Самое важное различие: BUNDLE_MAGIC приводит к пересортировке Key-Value после десериализации, как видно из следующего кода:

root@kitploit:~
 /**
     * Reads a map into {@code map}.
     *
     * @param sorted Whether the keys are sorted by their hashes, if so we use an optimized path.
     * @param lazy   Whether to populate the map with lazy {@link Function} objects for
     *               length-prefixed values. See {@link Parcel#readLazyValue(ClassLoader)} for more
     *               details.
     * @return a count of the lazy values in the map
     * @hide
     */
    int readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
            boolean lazy, @Nullable ClassLoader loader) {
        int lazyValues = 0;
        while (size > 0) {
            String key = readString();
            Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
            if (value instanceof LazyValue) {
                lazyValues++;
            }
            if (sorted) {
                map.append(key, value);
            } else {
                map.put(key, value);
            }
            size--;
        }
        if (sorted) {
            map.validate();
        }
        return lazyValues;
    }

Флаг MAGIC влияет на значение sorted в readArrayMap, определяя, будет ли выполняться сортировка map. В комментарии также говорится, что сортировка осуществляется по хэш-значению key String.

Деталь 2: Перекрытие при десериализации

root@kitploit:~
   /**
     *                 a
     * ArrayMap{| Key1 | LazyValue1 | FakeKey2 | Value2 | Key3  | Value3 |} 
     *
     */

Давайте снова представим процесс анализа ArrayMap. На первом проходе сначала анализируется Key1, затем предпринимается попытка проанализировать LazyValue1. Но так как objectLength у LazyValue1 равен -8, указатель Parcel возвращается к началу LazyValue1, то есть к точке a — это было упомянуто выше как факт 4. На этом первая пара Key-Map завершена. Вторая пара Key-Value начинает анализироваться с точки a. Сначала считывается длина String. Предположим, что LazyValue1 содержит Parcelable; тогда эта длина должна быть 4. Следовательно, настоящий Key2 начинается с точки a, то есть Key2 = + . не содержит никаких данных, поэтому его длина составляет 8 байт ( + ). Общая длина всей строки будет 4 (маркер длины) + 4*2 + 4 ("\0") = 16 байт. Убирая 8 байт из , нам нужно добавить ещё 8 байт при конструировании, то есть 2 . Затем считывается . Таким образом, при первом анализе нам нужно использовать , чтобы компенсировать смещение указателя, вызванное отрицательным у . Значение этого нас не интересует, так как оно используется только при первом анализе — это просто инструмент. Раз это одноразовый инструмент, после первого анализа мы хотим, чтобы он был подальше от наших критических данных, а в уже будут наши вредоносные данные. Лучше, чтобы хэш был больше, чем у , тогда он не будет влиять на последующий анализ. Используя , мы можем настроить хэш для достижения этой цели — подробнее в . Затем мы переходим к анализу . По стандартной схеме , в помещаем с вредоносным . Но у есть дополнительные ограничения. После завершения первого раунда десериализации структура должна быть такой: инструмент оказывается в конце:

root@kitploit:~
Key1-Value1 | Key3-Value3 | Key2-Value2

Как упоминалось в разделе Аномальный objectLength, длина LazyValue1 равна 0, поэтому при writeToParcel он вообще не копируется! Фактическая структура такая:

root@kitploit:~
Key1 | Key3-Value3 | Key2-Value2

После чтения Key1 он всё ещё ожидает чтения Value1. Здесь снова происходит чтение за пределами границ, и Key3 должен взять на себя роль чтения LazyValue1. Первый int в Key3 должен одновременно быть длиной String и типом LazyValue. Это означает, что длина Key3 не может быть слишком короткой, иначе хэш будет трудно вычислить. Просмотрев список типов LazyValue, я выбрал:

root@kitploit:~
private static final int VAL_LIST  = 11; // length-prefixed

Можно выбрать 12, 16, 17 и т.д., главное, чтобы не слишком коротко.

Второй int в Key3 должен быть Length для LazyValue. С его помощью мы контролируем длину LazyValue1, чтобы следующий указатель указывал на начало вредоносного Intent. Если содержимое LazyValue1 невалидно? Это не наша проблема — пока вы не вызовете getXXX для apply, он останется LazyValue.

Деталь 3: Реализация брутфорсера

Напишем простой брутфорсер:

root@kitploit:~
private static Pair<Integer, Integer> generateInt(){
        while (true) {
            Random random = new Random();
            int number1 = random.nextInt();
            int number2 = random.nextInt();
            Parcel parcel = Parcel.obtain();
            parcel.writeInt(11); //
            parcel.writeInt(32);
            parcel.writeInt(0);
            parcel.writeInt(0);
            parcel.writeInt(number1);
            parcel.writeInt(number2);
            parcel.writeInt(0);
            parcel.setDataPosition(0);
            String str = parcel.readString();
            if (str.hashCode() >= "Cxxsheng".hashCode() && str.hashCode() <  "Cxxsheng".hashCode() + 1000000)
            {
                parcel.recycle();
                return new Pair<>(number1, number2);
            }
            parcel.recycle();
        }

Конечно, можно подобрать и Key2, поместив его вперёд, но длина Key2 фиксирована — 4, что довольно мало, пространства для манёвра меньше. А для Key3 мы изначально зарезервировали некоторую длину, чтобы упростить брутфорс. Пусть наш Key1 — строка "Cxxsheng". Мы хотим, чтобы хэш Key3 был больше хэша "Cxxsheng", но лишь немного — я установил 1000000, чтобы они с высокой вероятностью всегда были вместе, и третьему Key2 не удалось вклиниться. Как упоминалось выше, первые два int строки фиксированы. Сначала мы записываем VAL_LIST (также длина String), вычисляем, что до вредоносного Intent осталось 32 байта. Остальные нули можно использовать для брутфорса. Выбираем любые два значения для подбора.

Воспроизведение

С помощью number1 и number2 можно управлять положением третьего значения в ArrayMap. Поскольку ArrayMap сортируется по hashcode ключа, можно сделать так, чтобы третье значение после десериализации стало вторым, сразу после первого "Cxxsheng":

root@kitploit:~
Bundle[{Cxxsheng=Supplier{VAL_PARCELABLE@28+0},[какой-то мусор]=[вредоносный ByteArray], [какой-то мусор]=0}]

Видно, что порядок чтения отличается от порядка записи. После записи, как мы анализировали выше, весь LazyValue был потерян, а третья пара Key-Value пересортирована на второе место, включая type и objectLength. Таким образом, структура страницы будет следующей:

Эксплуатация

Читатели могут самостоятельно использовать классическую цепочку эксплуатации AccountManagerService. Сможет ли она быть использована, здесь не обсуждается, так как это зависит от того, присутствует ли функция checkKeyIntentParceledCorrectly в патче ноября 2022 года. Дополнительно поясню: эта функция использует эмуляцию вызова IPC, чтобы заблокировать цепочку эксплуатации AccountManagerService. Таким образом, даже если на Android 12 или 13 существует Mismatch, это не обязательно может быть использовано успешно; требуется найти способ обхода этой функции.

Мы можем написать аналог этой функции, эмулируя процесс вызова IPC:

root@kitploit:~
    private Bundle simulateIPCBundle(Bundle originBundle){
        Parcel p = Parcel.obtain();
        p.writeBundle(originBundle);
        p.setDataPosition(0);
        byte[] bs = p.marshall(); // при отладке здесь можно увидеть данные Parcel
        // marshall не меняет указатель Parcel
        // p.setDataPosition(0); 
        Bundle simulateBundle = p.readBundle(getClass().getClassLoader());
        p.recycle();
        return simulateBundle;
    }

Затем посмотрите на вывод журнала при эмуляции процесса IPC. Подробнее можно посмотреть в моём коде на GitHub: description

Скачать инструмент
LazyValue1
FakeKey2
LazyValue1
LazyType
objectLength
LazyValue1
writeInt
Value2
Key2-Value2
objectLength
LazyValue1
Key2-Value2
Key3-Value3
Key2
Key1
деталь 1
Key3
детали 3
Key2-Value2
Bundle mismatch
Value3
ByteArray
Intent
Key3
ArrayMap
Key2-Value2
ЗначениеОписание
"Cxxsheng"Первый ключ
4Будет прочитано дважды: первый раз как VAL_PARCELABLE; второй раз как длина строки второго ключа
-8Будет прочитано дважды: первый раз как objectLength для LazyValue, что приводит к смещению указателя назад и двойному чтению; второй раз как значение строки второго ключа
0Значение строки второго Key
0Значение строки второго Key
1VAL_INTEGER
0Второе значение Value
11Длина строки третьего Key
32Значение строки третьего Key
0Значение строки третьего Key
0Значение строки третьего Key
number1Значение строки третьего Key, эти два значения используются для регулировки сортировки
number2Значение строки третьего Key, эти два значения используются для регулировки сортировки
0Значение строки третьего Key
13VAL_BYTEARRAY
Длина LazyValueВычисляется
Длина ByteArrayВычисляется
ByteArrayСодержит вредоносный Key-Value, т.е. Intent.EXTRA_INTENT и его Intent
ЗначениеОписание
"Cxxsheng"Первый ключ
11VAL_LIST
32Длина первого Value, всё последующее уже неважно (так как apply для этого LazyValue не вызывается), это указывает на начало вредоносного Intent в ByteArray
0Значение в LazyValue
0Значение в LazyValue
number1Значение в LazyValue
number2Значение в LazyValue
0Значение в LazyValue
13Значение в LazyValue
Длина LazyValueЗначение в LazyValue
Длина ByteArrayЗначение в LazyValue
Начало ByteArray / Intent.EXTRA_INTENTВторой ключ
IntentВторое значение Value
Третья пара Key-ValueОказывается в конце