Усиление защиты: MTE в распределителях кучи

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Введение
В 2018 году, с выпуском ARMv8.5-A, появилась совершенно новая функция безопасности чипа "MTE" ( Memory Tagging Extensions - Расширения тегов памяти). Пять лет спустя, в 2023 году, был выпущен первый смартфон с поддержкой этой функции — Google Pixel 8, что ознаменовало официальный выход MTE на потребительский рынок. Хотя эта функция еще не включена по умолчанию, разработчики могут включить ее самостоятельно для тестирования.

В Интернете пока еще не проводился всесторонний анализ возможностей MTE в качестве средства защиты от повреждения памяти, границ этой защиты и его влияния на производительность. Ранее Google Project Zero опубликовал серию статей о MTE, в которых основное внимание уделялось более низкоуровневым аспектам безопасности MTE. Однако фактическое влияние MTE на реальную безопасность программного обеспечения остается загадкой. Для обсуждения этой темы распределители кучи являются отличной отправной точкой. Проблемы с повреждением памяти кучи постепенно стали основным типом бинарных уязвимостей. Для справки смотрите презентацию MSRC на CppCon 2019:
3.png




Среднестатистические разработчики обычно напрямую не используют инструкции сборки, связанные с MTE, а вместо этого полагаются на поддержку MTE, косвенно предоставляемую распределителями кучи. Распределители кучи действуют как щиты беря на себя задачу защиты программного обеспечения. MTE обеспечивает фундаментальную поддержку низкоуровневого управления памятью. Ответственность за внедрение расширенных функций безопасности, основанных на аппаратных возможностях MTE, возлагается на разработчиков программного обеспечения. Основные распределители кучи в сообществе разработчиков с открытым исходным кодом активно отреагировали, внедрив улучшения безопасности на основе функций MTE, тем самым повысив безопасность памяти в пространстве кучи.

Эта статья предназначена для трех основных разработчиков MTE: PartitionAlloc в Chrome, Ptmalloc в Glibc и Scudo в Android, чтобы обсудить и сравнить их соответствующие реализации MTE. В ходе нашего исследования мы обнаружили проблемы с реализацией в PartitionAlloc и сообщили о них в Google, которые были подтверждены командой Chrome.

Обзор MTE

MTE использует функцию TBI (игнорирование верхнего байта) ARMv8, используя старшие 4 бита указателя для хранения тега. В каждом процессе существует выделенная карта памяти для хранения этих тегов. Как только памяти назначается определенный тег, программа должна получить доступ к памяти с правильным тегом; если тег неверный, программа выдает сигнал ошибки SIGSEGV, как показано ниже:

2.png



Набор инструкций предоставляет серию инструкций для манипулирования тегами. Вот пример, демонстрирующий базовое использование MTE:

Код: Скопировать в буфер обмена
Код:
; x0 is a pointer
irg  x1, x0
stg  x1, [x1]
ldr  x0, [x1]

1. Инструкция IRG (Insert Random Tag) генерирует случайный тег для указателя x0 и сохраняет результат в x1.
2. Инструкция STG (Storage Allocation Tag) применяет тег к памяти с эффективной длиной, зависящей от степени детализации, которая обычно составляет 16 байт.
3. Инструкция LDR (Load Register) считывает память, используя указатель с правильным тегом.

Набор команд обеспечивает базовую поддержку, но существует большая свобода в использовании каждой инструкции. То, как конкретно используется MTE, по-прежнему во многом зависит от разработчиков программного обеспечения.

Распределитель

Chrome - PartitionAlloc

Распределение

Распределения в PartitionAlloc можно грубо разделить на три случая:
1. Распределение из ThreadCache возвращается напрямую, без изменения тега.
2. Распределение из свободного SlotSpan возвращается напрямую, без изменения тега.
3. Если ни одно из двух вышеупомянутых условий не выполнено, выделите новый SlotSpan и пометьте все свободные ячейки кучи внутри него случайными тегами.


Код: Скопировать в буфер обмена
Код:
 if (PA_LIKELY(use_tagging)) {
            next_slot_ptr =
          TagMemoryRangeRandomly(next_slot, TagSizeForSlot(root, slot_size));

Релиз
Увеличьте метку освобождаемого слота.

Код: Скопировать в буфер обмена
Код:
void* retagged_slot_start = internal::TagMemoryRangeIncrement(
          ObjectToTaggedSlotStart(object), tag_size);
      object = TaggedSlotStartToObject(retagged_slot_start);

(Прошлые) Потенциальные риски
Мы заметили, что операция увеличения тега при освобождении является детерминированным поведением, и очень вероятно, что тег не будет изменен во время выделения. Эти два момента делают управление тегами в PartitionAlloc довольно хрупким, предоставляя злоумышленникам возможность злоупотребления.

Предположим, что злоумышленник теперь имеет уязвимость UAF и может свободно контролировать время запуска этого UAF (что является обычным явлением в реальных эксплойтах). Тогда злоумышленник может обойти проверку MTE следующими способами:
1. Запускаем уязвимость для получения объекта-жертвы UAF, но не запускаем UAF сразу.
2. Непрерывно выделяем и освобождаем объект размером с жертву 15 раз, чтобы метка объекта, контролируемого злоумышленником, совпадала с меткой жертвы.
3. Запускаем UAF.

В реальных сценариях атаки злоумышленнику легко получить два эксплуатационных примитива, необходимых для вышеупомянутой атаки:
1. Выделение ячейки кучи определенного размера любое количество раз и контролировать их содержимое.
2. Иметь возможность освобождать выделенные ими слоты кучи.

Например, в процессе браузера, с помощью объекта Blob можно достичь двух вышеуказанных условий. В процессе рендеринга это также можно сделать с помощью AudioArray.

Более подробный отчет и пример PoC доступны на странице проблемы: 1512538.

Анализ

Поддержка MTE в PartitionAlloc не такая мощная, как мы себе представляли. Она в меньшей степени управляет тегами, в наибольшей степени отдавая приоритет эффективности. Подробные сравнения приведены в следующем разделе.

Glibc - Ptmalloc
Реализация в Ptmalloc является наиболее простой и рудиментарной. Его стратегия настолько проста, что ее можно резюмировать всего в нескольких предложениях.

Распределение
Для всех распределений после получения адреса распределения генерируется случайный ненулевой тег для обозначения всего выделенного фрагмента (фактическая логика в коде заключается в генерации значения тега, отличного от заголовка фрагмента. В проанализированной нами версии 2.38, память, управляемая libc, такая как заголовки блоков, имеет фиксированное значение тега 0. Этот конкретный момент больше не будет уточняться в остальной части этой статьи).

Код: Скопировать в буфер обмена
Код:
victim = tcache_get (tc_idx);
      return tag_new_usable (victim);
  // ...
  victim = _int_malloc (ar_ptr, bytes);
  // ...
  victim = tag_new_usable (victim);

Релиз
Установите для тега блока значение 0.

Код: Скопировать в буфер обмена
Код:
(void)tag_region (chunk2mem (p), memsize (p));

      ar_ptr = arena_for_chunk (p);
      _int_free (ar_ptr, p, 0);

Анализ
При такой стратегии распределения создается ощущение чрезмерной силы одним движением. Соблюдая баланс между производительностью и безопасностью, Glibc выбрала безопасность: независимо от размера выделения или источника выделения (tcache, fastbin, smallbin ...), оно будет повторно помечено случайным тегом.

Память, управляемая самим libc, такая как заголовки блоков, освобожденные блоки и верхний блок, использует 0 в качестве тега. На первый взгляд, фиксированный тег 0 кажется очень небезопасным, поскольку это информация, известная злоумышленнику. Однако при ближайшем рассмотрении становится очевидно, что это не так. Этот механизм обеспечивает, по крайней мере, следующие два момента:

1. Определенно существует заголовок блока или свободный блок (тег 0), действующий как барьер между каждыми двумя блоками (тег ненулевой), играющий роль, аналогичную защитной странице, которая может эффективно смягчать линейные переполнения.
2. Тег освобожденного фрагмента (тег 0) всегда отличается от тега используемого фрагмента (тег ненулевой), что может эффективно смягчить уязвимости UAF.

Android - Scudo
Для сравнения, реализация в Scudo является наиболее сложной.

Распределение
1. Scudo помечает только фрагменты основного типа (размером < 0x10000). Для большего вторичного типа он выделяет пространство посредством сопоставления памяти и в настоящее время не поддерживает пометку таких пространств.
2. Когда Scudo повторно использует освобожденный фрагмент, он напрямую сохраняет и использует тег UAF, присвоенный ему во время освобождения. В противном случае он выделит случайный тег.

Релиз
Назначьте фрагменту блока случайный тег, отличный от предыдущего, чтобы предотвратить его повторное использование UAF'ом.

Код: Скопировать в буфер обмена
Код:
if (Header->ClassId) {
        if (!TSDRegistry.getDisableMemInit()) {
          uptr TaggedBegin, TaggedEnd;
          const uptr OddEvenMask = computeOddEvenMaskForPointerMaybe(
              Options, reinterpret_cast<uptr>(getBlockBegin(Ptr, Header)),
              Header->ClassId);
          setRandomTag(Ptr, Size, OddEvenMask | (1UL << PrevTag), &TaggedBegin,
                       &TaggedEnd);
        }
      }

Анализ
В реализации Scudo есть уникальная опция конфигурации: UseOddEvenTags . Когда эта опция активирована, Scudo уделяет особое внимание четности тегов для каждого блока в процессе выделения памяти. Это означает, что оно гарантирует, что соотношения тегов для смежных блоков кучи различается.

Для достижения этой функциональности, computeOddEvenMaskForPointerMaybe используется для вычисления маски нечетного или четного тега:

Код: Скопировать в буфер обмена
Код:
uptr computeOddEvenMaskForPointerMaybe(const Options &Options, uptr Ptr,
                                         uptr ClassId) {
    if (!Options.get(OptionBit::UseOddEvenTags))
      return 0;
    return 0x5555U << ((Ptr >> SizeClassMap::getSizeLSBByClassId(ClassId)) & 1);
  }

Эта конфигурация предполагает компромисс между обнаружением UAF и обнаружением переполнения буфера. Когда включено UseOddEvenTags, четность тегов для соседних блоков отличается, что исключает возможность того, что случайно распределенные теги окажутся одинаковыми, тем самым увеличивая вероятность обнаружения переполнения буфера. Однако, с другой стороны, в этом случае четность каждого случайно выделенного тега фиксирована, что приводит к сокращению вдвое пространства тегов, затрудняя обнаружение UAF.

Этот дизайн подчеркивает ключевое соображение при реализации Scudo: как сбалансировать стратегии смягчения последствий для различных типов уязвимостей памяти при минимизации влияния на производительность. Это демонстрирует предпочтения и компромиссы, на которые идет распределитель кучи, когда имеет дело с безопасностью памяти.

Сравнение

В этой таблице сравнивается только реализация MTE в различных распределителях кучи и не отражает общую безопасность распределителей кучи.

1.png



  • Максимальный размер блока с тегами
Ptmalloc применяет теги к фрагментам кучи любого размера. Однако по соображениям производительности Scudo и PartitionAlloc защищают только фрагменты кучи размером меньше 0x10000 и 0x400.

  • Возможности защиты от повреждения памяти
Линейное переполнение
И Ptmalloc, и Scudo используют 0 в качестве тега для заголовков блоков, гарантируя, что между двумя блоками кучи всегда есть красная зона, которая может эффективно смягчать линейные переполнения; В то время как метаданные PartitionAlloc не находятся в начале блока кучи, таким образом, существует определенная вероятность того, что соседние блоки кучи могут по совпадению иметь один и тот же тег.
Нелинейный OOB
Для нелинейного доступа за пределы границ четность тегов Scudo гарантирует, что соседние фрагменты кучи определенно будут иметь разные теги, тем самым увеличивая размер красной зоны вокруг фрагмента кучи, повышая вероятность обнаружения мелкомасштабного OOB.
UAF
Потенциальные риски уязвимостей UAF в PartitionAlloc были подробно описаны в предыдущих разделах. Следует дополнительно отметить, что PartitionAlloc не полагается исключительно на MTE для защиты от уязвимостей UAF. Он также обладает очень сильными возможностями защиты UAF благодаря miraclept; Когда Scudo включает четность тегов, пространство тегов сокращается вдвое, увеличивая вероятность столкновений тегов.
Неинициализированная память
MTE существенно не устраняет проблемы с неинициализированной памятью. Распределители, такие как PartitionAlloc и Ptmalloc, не выполняют инициализацию памяти.
Стратегия управления тегами
Распределение и освобождение блоков кучи оцениваются здесь как единое целое. PartitionAlloc не восстанавливает новый тег при повторном использовании фрагмента кучи из кэша, но продолжает использовать старый тег и увеличивает тег только после освобождения; Два других распределителя восстанавливают теги.
Защита метаданных
Ни Ptmalloc, ни Scudo не используют теги для защиты метаданных, таких как заголовки блоков, при этом по умолчанию используется тег 0, который может служить промежутком между соседними блоками кучи, но в то же время может быть подвержен риску вредоносного вмешательства. Примечательно, что метаданные PartitionAlloc не хранятся в начале фрагмента кучи, что делает его менее подверженным повреждению.
Освобождение адреса с неправильным тегом
При освобождении фрагмента кучи PartitionAlloc и Scudo не проверяют правильность тега, а выполняют операцию отмены тега напрямую, что позволяет освободить адрес с неправильным тегом. Однако сценарии атак, возникающие в результате этого недостатка, ограничены, и их необходимо будет комбинировать с другими методами атаки для использования, поэтому мы не классифицируем это как проблему высокого риска; Ptmalloc, с другой стороны, проверяет, верен ли тег, и вызовет исключение, если это не так.

Код: Скопировать в буфер обмена
Код:
if (__glibc_unlikely (mtag_enabled))
    *(volatile char *)mem;

Выводы
В этой статье представлен подробный анализ реализации MTE в трех основных распределителях кучи, что дает читателям интуитивное представление об их безопасности. MTE, несомненно, является значительным шагом вперед в обеспечении безопасности памяти на платформе ARM. Очевидно, что с включенным MTE традиционные проблемы с повреждением памяти кучи эффективно устраняются, а некоторые даже становятся неосуществимыми для использования. Однако безопасность памяти развивалась на протяжении многих лет и сопряжена со сложными проблемами, многие из которых еще предстоит решить:

  • Защита от неинициализированной памяти по-прежнему зависит от программной реализации.
  • Будут ли переменные стека усилены с помощью MTE и могут ли соображения производительности помешать его реализации.
  • Маркировка mmapped памяти не поддерживается на уровне ядра.
  • Большие буферы памяти, такие как кольцевые буферы и разделяемая память, трудно эффективно защитить с помощью MTE.
  • Сегмент данных программ не защищен MTE.
Эволюция нападения и защиты привела к тому, что злоумышленники много лет назад могли компрометировать системы с помощью единственного переполнения стека, а теперь им требуется цепочка уязвимостей для прорыва многоуровневой защиты систем. Но мы также видим, что даже у таких передовых технологий, как MTE, есть "слепые зоны", и путь к безопасности памяти по-прежнему долгий и сложный. Мы с нетерпением ожидаем более интересных разработок в будущем.

Автор: DARKNAVY
Оригинал: www.darknavy[.]org/blog/strengthening_the_shield_mte_in_memory_allocators/
Перевёл специально для XSS: TROUBLE
 
Сверху Снизу