D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
В этом посте я рассмотрю CVE-2023-6241, уязвимость в графическом процессоре Arm Mali, о которой я сообщил в Arm 15 ноября 2023 года и которая была исправлена в версии драйвера Arm Mali r47p0, который был опубликован 14 декабря 2023 года. Это было исправлено в Android в Мартовском обновлении системы безопасности. Эксплуатация данной уязвимости позволяет злоумышленнику используя злонамеренное приложение для Android, получить возможность выполнение произвольного кода на уровне ядра и получить root на устройстве. Уязвимость затрагивает устройства с новым Arm Mali GPU, которые используют функции Команды фронтенд потока (КСС) , такие как телефоны Google Pixel 7 и Pixel 8. Что интересно в этой уязвимости, так это то, что это логическая ошибка в блоке управления памятью графического процессора Arm Mali, и она способна обходить Расширение для тегов памяти (MTE), новое и мощное средство защиты от повреждения памяти, которое впервые было поддержано в Pixel 8. В этом посте я покажу, как использовать эту ошибку, чтобы добиться выполнения произвольного кода на уровне ядра в Pixel 8 из ненадежного пользовательского приложения. Я подтвердил, что эксплойт успешно работает даже при включенной MTE в ядре, при следовании этим инструкциям.
Arm64 MTE
MTE - это очень хорошо документированная функция на новых процессорах Arm, которая использует аппаратную реализацию для проверки на повреждение памяти. Поскольку уже есть много хороших статей о MTE, я лишь вкратце расскажу об идее MTE и объясню ее значение по сравнению с другими способами предотвращения повреждения памяти. Читатели, заинтересованные в более подробной информации, могут, к примеру, ознакомиться с этой статьей и с технической документацией, выпущенной Arm.
Хотя архитектура Arm64 использует 64-битные указатели для доступа к памяти, обычно нет необходимости использовать такое большое адресное пространство. На практике большинство приложений используют гораздо меньшее адресное пространство (обычно 52 бита или меньше). Это оставляет неиспользованными старшие биты в указателе. Основная идея тегирования памяти заключается в использовании этих старших битов в адресе для хранения "тега”, который затем может быть использован для проверки по другому тегу, хранящемуся в блоке памяти, связанном с адресом. Это помогает снизить распространенные типы повреждений памяти следующим образом:
В случае линейного переполнения указатель используется для разыменования соседнего блока памяти, который имеет другой тег по сравнению с тем, который хранится в указателе. Проверяя эти теги во время разыменования, можно обнаружить поврежденное разыменование. При повреждении памяти типа use-after-free, пока тег в блоке памяти очищается каждый раз, когда он освобождается, и новый тег переназначается при его выделении, разыменование уже освобожденного и восстановленного объекта также приведет к несоответствию между тегом указателя и тегом в памяти, что позволяет обнаружить use-after-free.
(Приведенное выше изображение взято из Расширения для тегов памяти: повышение безопасности памяти с помощью архитектуры, опубликованного Arm.)
Основная причина, по которой пометка памяти отличается от предыдущих мер по исправлению ошибок, таких как целостность потока управления ядром (kCFI), заключается в том, что, в отличие от других мер по исправлению ошибок, которые прерывают более поздние стадии эксплойта, MTE - это мера по исправлению ошибок на очень ранней стадии, которая пытается выявить повреждение памяти при первом его возникновении. Как таковой, он способен остановить эксплойт на очень ранней стадии, прежде чем злоумышленник получит какие-либо возможности, и поэтому его очень трудно обойти. Он вводит проверки, которые эффективно превращают небезопасный язык памяти в тот, который безопасен для памяти, хоть и вероятностно.
Теоретически, пометка памяти может быть реализована только программным обеспечением, заставляя распределитель памяти назначать и удалять теги каждый раз, когда выделяется или освобождается память, и добавляя логику проверки тегов при разыменовании указателей. Однако это приводит к снижению производительности, что делает метод непригодным для практического использования. В итоге на практике необходима аппаратная реализация, чтобы снизить затраты на производительность и сделать маркировку памяти пригодной для практического использования. Аппаратная поддержка была представлена в версии v8.5a архитектуры ARM, в которой были введены дополнительные аппаратные инструкции (называемые MTE) для выполнения тегов и проверки. Для устройств Android большинство наборов микросхем, поддерживающих MTE, используют процессоры Arm v9 (вместо Arm v8.5a), и в настоящее время существует лишь несколько устройств, поддерживающих MTE.
Одним из ограничений MTE является то, что количество доступных неиспользуемых битов невелико по сравнению со всеми возможными блоками памяти, которые когда-либо могут быть выделены. Таким образом, столкновение тегов неизбежно, и многие блоки памяти будут иметь один и тот же тег. Это означает, что поврежденный доступ к памяти все еще может быть успешным случайно. На практике, даже при использовании всего 4 бит для тега, вероятность успешного выполнения снижается до 1/16, что по-прежнему является довольно надежной защитой от повреждения памяти. Другое ограничение заключается в том, что, передавая значения указателя и блока памяти с использованием атаки по побочному каналу, такой как Spectre, злоумышленник может быть уверенным, что поврежденный доступ к памяти выполняется с правильным тегом, и, таким образом, обходит MTE. Однако этот тип утечки в основном доступен только локальному злоумышленнику. Серия статей "Реализация MTE " Марка Брэнда включает подробное исследование ограничений и влияния MTE на различные сценарии атак.
Помимо наличия оборудования, использующего процессоры, Arm версии 8.5a или выше, для включения MTE также требуется программная поддержка. В настоящее время только Pixel 8 от Google позволяет пользователям включать MTE в настройках разработчика, а MTE по умолчанию отключен. Также требуются дополнительные шаги для включения MTE в ядре.
Графический процессор Arm Mali
Графический процессор Arm Mali может быть интегрирован в различные устройства (например, см. “Реализации” в статье Mali (GPU) в Википедии). Он был привлекательной целью на смартфонах Android и неоднократно становился мишенью для необычных эксплойтов. Текущая уязвимость тесно связана с другой проблемой, о которой я сообщал, и представляет собой уязвимость в обработке типа памяти графического процессора, называемого JIT-памятью. Далее я кратко расскажу о памяти JIT и объясню уязвимость CVE-2023-6241.
JIT - память в Arm Mali
При использовании драйвера графического процессора Mali пользовательскому приложению сначала необходимо создать и инициализировать kbase_context объект ядра. Это включает в себя открытие пользовательского приложения файла драйвера и использование результирующего файлового дескриптора для выполнения серии ioctl вызовов. А kbase_context объект отвечает за управление ресурсами для каждого открываемого файла драйвера и уникален для каждого дескриптора файла.
В частности, kbase_context управляет различными типами памяти, которые совместно используются графическим процессором устройства и приложениями пользовательского пространства. Пользовательские приложения могут либо сопоставить свою собственную память с пространством памяти графического процессора, чтобы графический процессор мог получить доступ к этой памяти, либо они могут выделить память из графического процессора. Память, выделяемая графическим процессором, управляется kbase_context и может быть сопоставлена с пространством памяти графического процессора, а также с пользовательским пространством. Пользовательское приложение также может использовать графический процессор для доступа к отображенной памяти путем отправки команд на графический процессор. В стандартном случае память должна быть либо выделена графическим процессором и управляться им (собственная память), либо импортирована в графический процессор из пользовательского пространства, а затем сопоставлена с адресным пространством графического процессора, прежде чем к ней сможет получить доступ графический процессор. Область памяти в графическом процессоре Mali представлена символом kbase_va_region. Подобно виртуальной памяти в центральном процессоре, область памяти в графическом процессоре может иметь не весь свой диапазон, поддерживаемый физической памятью. nr_pages Поле в a kbase_va_region определяет виртуальный размер области памяти, тогда как gpu_alloc->nents это фактическое количество физических страниц, которые поддерживают эту область. Отныне я буду называть эти страницы резервными страницами области. Хотя виртуальный размер области памяти фиксирован, его физический размер может изменяться. Отныне, когда я использую такие термины, как изменение размера, рост и сжатие, применительно к области памяти, я имею в виду, что изменяется, растет или сжимается физический размер области.
Память JIT - это тип встроенной памяти, временем жизни которой управляет драйвер ядра. Пользовательские приложения запрашивают графический процессор для выделения и освобождения памяти JIT, отправляя соответствующие команды графическому процессору. Хотя большинство команд, например, использующих графический процессор для выполнения арифметических операций и доступа к памяти, выполняются на самом графическом процессоре, есть некоторые команды, например, используемые для управления памятью JIT, которые реализованы в ядре и выполняются на центральном процессоре. Они называются программными командами (в отличие от аппаратных команд, которые выполняются на графическом процессоре (аппаратном обеспечении)). На графических процессорах, использующих интерфейс потока команд (CSF), программные и аппаратные команды размещаются в очередях команд разных типов. Для отправки программной команды требуется kbase_kcpu_command_queue, и ее можно создать с помощью KBASE_IOCTL_KCPU_QUEUE_CREATE ioctl. Затем программная команда может быть помещена в очередь с помощью KBASE_IOCTL_KCPU_QUEUE_ENQUEUE команды. Для выделения или освобождения памяти JIT могут использоваться команды типа BASE_KCPU_COMMAND_TYPE_JIT_ALLOC и BASE_KCPU_COMMAND_TYPE_JIT_FREE.
BASE_KCPU_COMMAND_TYPE_JIT_ALLOC Команда использует kbase_jit_allocate для выделения памяти JIT. Аналогично, команда BASE_KCPU_COMMAND_TYPE_JIT_FREE может использоваться для освобождения памяти JIT. Как объяснялось в разделе “Жизненный цикл JIT-памяти” в одном из моих предыдущих постов, когда JIT-память освобождается, она переходит в пул памяти, управляемый kbase_context и когда kbase_jit_allocate вызывается, он сначала просматривает этот пул памяти, чтобы увидеть, есть ли какая-либо подходящая освобожденная JIT-память, которую можно использовать повторно:
Код: Скопировать в буфер обмена
Если найден существующий регион и его виртуальный размер соответствует запросу, но его физический размер слишком мал, то kbase_jit_allocate попытается выделить больше физических страниц для резервного копирования региона, вызвав kbase_jit_grow:
Код: Скопировать в буфер обмена
Если, с другой стороны, подходящая область не найдена, kbase_jit_allocate выделит память JIT с нуля:
Код: Скопировать в буфер обмена
Как мы можем видеть из комментария выше, вызов kbase_jit_grow, kbase_jit_grow может временно отбросить kctx->reg_lock:
Код: Скопировать в буфер обмена
В приведенном выше примере мы видим, что kbase_gpu_vm_unlock вызывается для временного удаления kctx->reg_lock, в то время как kctx->mem_partials_lock также удаляется во время вызова kbase_mem_pool_grow. В графическом процессоре Mali kctx->reg_lock используется для защиты параллельных обращений к областям памяти. Так, например, когда kctx->reg_lock удерживается, физический размер области памяти не может быть изменен другим потоком. В GHSL-2023-005, о котором я сообщал ранее, я смог запустить race, так что область JIT была уменьшена с помощью KBASE_IOCTL_MEM_COMMIT ioctl из другого потока во время kbase_mem_pool_grow выполнения. Это изменение размера области JIT привело к тому, что reg->gpu_alloc->nents изменилось после kbase_mem_pool_grow, что означает, что фактическое значение reg->gpu_alloc->nents тогда отличалось от значения, которое было кэшировано в old_size и delta (1. и 2. в приведенном выше примере). Поскольку эти значения позже использовались для выделения и отображения области JIT, использование этих устаревших значений вызвало несогласованность в отображении памяти графического процессора, что привело к появлению GHSL-2023-005.
Код: Скопировать в буфер обмена
После исправления GHSL-2023-005 больше не было возможности изменять размер JIT-памяти с помощью KBASE_IOCTL_MEM_COMMIT ioctl.
Уязвимость
Аналогично виртуальной памяти, когда графический процессор обращается к адресу в области памяти, которая не поддерживается физической страницей, возникает ошибка доступа к памяти. В этом случае, в зависимости от типа области памяти, может оказаться возможным выделить и сопоставить физическую страницу "на лету" для резервного копирования адреса ошибки. Ошибка доступа к памяти графического процессора обрабатывается kbase_mmu_page_fault_worker:
Код: Скопировать в буфер обмена
В обработчике ошибок выполняется ряд проверок, чтобы гарантировать, что области памяти разрешено увеличиваться в размерах. Две проверки, которые имеют отношение к памяти JIT, - это проверки на GROWABLE_FLAGS_REQUIRED и KBASE_REG_DONT_NEED флаги. GROWABLE_FLAGS_REQUIRED Определяется следующим образом:
Код: Скопировать в буфер обмена
Эти флаги добавляются в область JIT при ее создании с помощью kbase_jit_allocate и никогда не изменяются:
Код: Скопировать в буфер обмена
Хоть KBASE_REG_DONT_NEED флаг и добавляется в область JIT при ее освобождении, он удаляется в kbase_jit_grow задолго до того, как kctx->reg_lock и kctx->mem_partials_lock отбрасываются и kbase_mem_pool_grow вызывается:
Код: Скопировать в буфер обмена
В частности, во время окна race, отмеченного в приведенном выше фрагменте, разрешен рост объема памяти JIT при сбое страницы.
Итак, получая доступ к незамеченной памяти в области для создания сбоя в другом потоке во время kbase_mem_pool_grow выполнения, я могу заставить обработчик сбоев GPU увеличить область JIT во время выполнения kbase_mem_pool_grow. Затем это изменяется reg->gpu_alloc->nents и делает недействительным old_size и delta в 1. и 2. ниже:
Код: Скопировать в буфер обмена
В результате, когда delta и old_size используются в 3. и 4. для выделения резервных страниц и сопоставления страниц с пространством памяти графического процессора, их значения недопустимы.
Это очень похоже на то, что произошло с GHSL-2023-005. Поскольку kbase_mem_pool_grow требуется выделение большого объема памяти, в этой гонке можно очень легко победить. Однако здесь есть одно очень большое отличие: с GHSL-2023-005 я смог уменьшить область JIT, в то время как в этом случае я смог только увеличить область JIT. Чтобы понять, почему это важно, давайте вкратце расскажем, как работал мой эксплойт для GHSL-2023-005.
Как упоминалось ранее, физический размер или количество резервных страниц kbase_va_region хранятся в поле reg->gpu_alloc->nents. У kbase_va_region есть два kbase_mem_phy_alloc объекта: cpu_alloc и gpu_alloc, которые отвечают за управление его страницами поддержки. Для устройств Android эти два поля настроены одинаково. Внутри kbase_mem_phy_allocполе pages представляет собой массив, содержащий физические адреса резервных страниц, в то время как nents определяет длину pages массива:
Код: Скопировать в буфер обмена
При вызове kbase_alloc_phy_pages_helper_locked он выделяет страницы памяти и добавляет физические адреса, представленные этими страницами, в массив pages, поэтому новые страницы добавляются в индекс nents и далее. Затем новый размер сохраняется до nents. Например, когда он вызывается в kbase_jit_grow, delta - это количество добавляемых страниц.:
Код: Скопировать в буфер обмена
В этом случае delta страницы вставляются по индексу nents в массив pages из gpu_alloc:
После выделения резервных страниц и вставки в pages массив, новые страницы отображаются в адресное пространство графического процессора путем вызова kbase_mem_grow_gpu_mapping. Виртуальный адрес kbase_va_region в области памяти графического процессора управляется kbase_va_region самим и хранится в полях start_pfn и nr_pages:
Код: Скопировать в буфер обмена
Начало виртуального адреса kbase_va_region хранится в start_pfn (в виде фрейма страницы, поэтому фактическим адресом является start_pfn >> PAGE_SIZE), в то время как в nr_pages сохраняется размер области. Эти поля остаются неизменными после их установки. Внутри kbase_va_region начальные reg->gpu_alloc->nents страницы в виртуальном адресном пространстве поддерживаются физической памятью, хранящейся в pages массиве gpu_alloc->pages, в то время как остальные адреса не поддерживаются. В частности, резервные копии виртуальных адресов всегда являются смежными (поэтому между поддерживаемыми областями нет пробелов) и всегда начинаются с начала области. Например, возможно следующее:
Но следующий случай не возможен, потому что резервное копирование не начинается с начала области:
и этот следующий случай также не возможен из - за пробелов в адресах:
В случае, когда kbase_mem_grow_gpu_mapping вызывается в kbase_jit_grow, адреса графического процессора между (start_pfn + old_size) * 0x1000 to (start_pfn + info->commit_pages) * 0x1000 сопоставляются с недавно добавленными страницами в gpu_alloc->pages, которые являются страницами между индексами pages + old_size и pages + info->commit_pages (потому что delta = info->commit_pages - old_size):
Код: Скопировать в буфер обмена
В частности, old_size здесь используется для указания как адреса графического процессора, с которого должно начинаться новое сопоставление, так и смещения от pages массива, где должны использоваться резервные страницы.
Если изменения reg->gpu_alloc->nents после old_size и delta кэшируются, то эти смещения могут стать недействительными. Например, если kbase_va_region при сжатии и nents уменьшается после old_size и delta были сохранены, то kbase_alloc_phy_pages_helper_locked будет вставлять delta страницы reg->gpu_alloc->pages + nents:
Аналогично, в kbase_mem_grow_gpu_mapping будут отображены адреса графического процессора, начинающиеся с (start_pfn + old_size) * 0x1000, используя страницы, находящиеся между reg->gpu_alloc->pages + old_size и reg->gpu_alloc->pages + nents + delta (пунктирные линии на рисунке ниже). Это означает, что страницы между pages->nents и pages->old_size в конечном итоге не будут сопоставлены ни с какими адресами графического процессора, в то время как на некоторых адресах в конечном итоге не будет резервных страниц:
Эксплуатация GHSL-2023-005
GHSL-2023-005 позволил мне уменьшить область JIT, но CVE-2023-6241 не дает мне такой возможности. Чтобы понять, как использовать эту проблему, нам нужно знать немного больше о том, как удаляются сопоставления GPU. Функция kbase_mmu_teardown_pgd_pages отвечает за удаление сопоставлений адресов из GPU. Эта функция, по сути, просматривает диапазон адресов графического процессора и удаляет адреса из таблицы страниц графического процессора, помечая их как недопустимые. Если он обнаружит запись таблицы страниц высокого уровня (PTE), которая охватывает большой диапазон адресов, и обнаружит, что запись недопустима, то он пропустит удаление всего диапазона адресов, охватываемых записью. Например, запись таблицы страниц уровня 2 охватывает диапазон из 512 страниц, поэтому, если запись таблицы страниц уровня 2 будет признана недопустимой (1. в приведенном ниже примере), то kbase_mmu_teardown_pgd_pages предположит, что следующие 512 страниц покрыты этим уровнем 2 и, следовательно, все они уже недействительны. Таким образом, удаление этих страниц будет пропущено (2. показано ниже).
Код: Скопировать в буфер обмена
Функция kbase_mmu_teardown_pgd_pages вызывается либо при уменьшении kbase_va_region, либо при его удалении. Как объяснялось в предыдущем разделе, виртуальные адреса в kbase_va_region которые отображаются и поддерживаются физическими страницами, должны быть непрерывными с начала kbase_va_region. В результате, если сопоставлен какой-либо адрес в регионе, то должен быть сопоставлен начальный адрес и, следовательно, запись таблицы страниц высокого уровня, покрывающая начальный адрес, должна быть действительной (если ни один адрес в регионе не сопоставлен, то kbase_mmu_teardown_pgd_pages даже не будет вызван):
В приведенном выше примере PTE уровня 2, который покрывает начальный адрес региона, сопоставлен, и поэтому он действителен, поэтому в этом случае, если kbase_mmu_teardown_pgd_pages когда-либо встречается не сопоставленный PTE высокого уровня, остальные адреса в kbase_va_region, должно быть, уже не сопоставлены и могут быть безопасно пропущены.
В случае, когда область сжимается, адрес, с которого начинается разметка, находится в пределах kbase_va_region, и весь диапазон между этим начальным адресом и концом области будет размечен. Если запись таблицы страниц уровня 2, охватывающая этот адрес, недопустима, то начальный адрес должен находиться в области, которая не сопоставлена, и, следовательно, остальной диапазон адресов, который нужно отменить, также не должен быть сопоставлен. В этом случае пропуск адресов снова безопасен:
Таким образом, пока области отображаются только по их начальным адресам и не имеют пробелов в сопоставлениях, kbase_mmu_teardown_pgd_pages будет работать правильно.
В случае GHSL-2023-005 возможно создать область, которая не соответствует этим условиям. Например, уменьшив всю область до нулевого размера во время окна race, можно создать область, начало которой не отображается:
Если регион удаляется и kbase_mmu_teardown_pgd_pages пытается удалить первый адрес, поскольку PTE уровня 2 недействителен, то будут пропущены следующие 512 страниц, некоторые из которых, возможно, действительно были сопоставлены:
В этом случае адреса в “неправильно пропущенной” области останутся сопоставленными некоторым записям в pages массиве в gpu_alloc, которые уже освобождены. И эти "неправильно пропущенные” адреса графического процессора могут быть использованы для доступа к уже освобожденным страницам памяти.
Использование CVE-2023-6241
Ситуация, однако, сильно отличается, когда регион увеличивается во время окна race. В этом случае, nents больше, чем old_size когда вызываются kbase_alloc_phy_pages_helper_locked и kbase_mem_grow_gpu_mapping, и delta страницы вставляются по индексу nents pages массива:
pages массив содержит правильное количество страниц для резервного копирования как jit grow, так и для аварийного доступа, и фактически именно так и должно быть, когда kbase_jit_grow вызывается после обработчика ошибок страницы.
При kbase_mem_grow_gpu_mapping вызове delta страницы отображаются на графический процессор из (start_pfn + old_size) * 0x1000. Поскольку общее количество резервных страниц теперь увеличилось на fh + delta, где fh - количество страниц, добавленных обработчиком ошибок, это оставляет последние fh страницы в pages массиве без отображения.
Однако это, похоже, также не создает никаких проблем. В области памяти по-прежнему отображаются только начальные адреса, и в отображении нет пробелов. Страницы, которые не отображаются, просто недоступны из графического процессора и будут освобождены при удалении области памяти, так что это даже не проблема с утечкой памяти.
Однако не все потеряно. Как мы видели, когда происходит сбой страницы графического процессора, если причиной сбоя является то, что адрес не сопоставлен, тогда обработчик ошибок попытается добавить резервные страницы в регион и сопоставить эти новые страницы с экстентом региона. Если адрес ошибки, скажем, fault_addr, то минимальное количество добавляемых страниц равно new_pages = fault_addr/0x1000 - reg->gpu_alloc->nents. В зависимости от kbase_va_region также может быть добавлено некоторое дополнение. В любом случае эти новые страницы будут отображены на графический процессор, начиная с адреса (start_pfn + reg->gpu_alloc->nents) * 0x1000, чтобы сохранить фактическое отображение только адреса в начале региона.
Это означает, что если я вызову другой сбой графического процессора в области JIT, на которую повлияла ошибка, то некоторые новые сопоставления будут добавлены после области, которая не сопоставлена.
Это создает пробел в сопоставлениях графического процессора, и я начинаю получать нечто, что выглядит пригодным для использования.
Обратите внимание, что поскольку для запуска ошибки delta должно быть ненулевым значением, и поскольку delta + old_size страницы в начале региона сопоставлены, по-прежнему невозможно, чтобы начало региона не отображалось, как в случае GHSL-2023-005. Итак, мой единственный вариант здесь - уменьшить область и сделать так, чтобы результирующий размер находился где-то внутри незамеченного промежутка.
Единственный способ уменьшить область JIT - это использовать BASE_KCPU_COMMAND_TYPE_JIT_FREE команду GPU для “освобождения” области JIT. Как объяснялось ранее, это фактически не освобождает сам kbase_va_region, а скорее помещает его в пул памяти, чтобы его можно было повторно использовать при последующем выделении JIT. До этого kbase_jit_free также будет уменьшена область JIT в соответствии с initial_commit размером области, а также с trim_level который настроен в kbase_context:
Код: Скопировать в буфер обмена
В любом случае, я могу контролировать размер этого сокращения. Имея это в виду, я могу упорядочить область следующим образом:
1. Создайте область JIT и запустите ошибку. Расположите ошибку графического процессора так, чтобы обработчик ошибок добавлял fault_size страницы, достаточные для покрытия хотя бы одного PTE 2-го уровня.
После срабатывания ошибки только начальные old_size + delta страницы отображаются в адресное пространство графического процессора, в то время как у kbase_va_region всего есть old_size + delta + fault_size резервные страницы.
2. Запускает вторую ошибку со смещением, превышающим количество резервных страниц, так что страницы добавляются к области и отображаются после неподтвержденных областей, созданных на предыдущем шаге.
3. Освободите область JIT с помощью BASE_KCPU_COMMAND_TYPE_JIT_FREE, которая вызовет kbase_jit_free сжатие области и удаление страниц из нее. Контролируйте размер этой обрезки так, чтобы размер области после сжатия (final_size) резервного хранилища находился где-то в пределах незамеченной области, охватываемой PTE первого уровня.
Когда область сжимается, kbase_mmu_teardown_pgd_pages вызывается для отмены сопоставления адресов графического процессора, начиная с region_start + final_size и полностью и до конца области. Поскольку весь диапазон адресов, охватываемый 2 PTE первого уровня, не сопоставлен, при kbase_mmu_teardown_pgd_pages попытке размонтирования region_start + final_size выполняется условие !mmu_mode->pte_is_valid для PTE уровня 2, и поэтому при размонтировании будут пропущены следующие 512 страниц, начиная с region_start + final_size. Однако, поскольку адреса, принадлежащие PTE следующего уровня 2, по-прежнему отображаются, эти адреса будут пропущены неправильно (оранжевая область на следующем рисунке), в результате чего они будут отображены на страницы, которые будут освобождены:
После завершения сжатия резервные страницы освобождаются, а адреса в оранжевой области сохраняют доступ к уже освобожденным страницам.
Это означает, что освобожденную резервную страницу теперь можно повторно использовать как любую страницу ядра, что дает мне множество возможностей использовать эту ошибку. Одна из возможностей - использовать мою предыдущую технику для замены резервной страницы в качестве глобальных каталогов таблицы страниц (PGD) нашего графического процессора kbase_context.
В заключение давайте посмотрим, как распределяются резервные страницы a kbase_va_region. При выделении страниц для резервного хранилища kbase_va_region используется kbase_mem_pool_alloc_pages функция:
Код: Скопировать в буфер обмена
Входной аргумент kbase_mem_pool - это пул памяти, управляемый kbase_context объектом, связанным с файлом драйвера, который используется для выделения памяти графического процессора. Как следует из комментариев, выделение фактически выполняется по уровням. Сначала страницы будут выделены из текущего kbase_mem_pool использования kbase_mem_pool_remove_locked (1 в приведенном выше примере). Если в текущем сервере недостаточно ресурсов kbase_mem_pool для удовлетворения запроса, то pool->next_pool, используется для выделения страниц (2 в приведенном выше примере). Если даже pool->next_pool не хватает мощности, то kbase_mem_alloc_page используется для выделения страниц непосредственно из ядра через распределитель buddy (распределитель страниц в ядре).
При освобождении страницы, при условии, что область памяти не удалена, то же самое происходит в обратном направлении: kbase_mem_pool_free_pages сначала пытается вернуть страницам значение kbase_mem_pool текущего kbase_context, если пул памяти заполнен, он попытается вернуть оставшимся страницам значение pool->next_pool. Если следующий пул также заполнен, оставшиеся страницы возвращаются в ядро путем их освобождения через распределитель buddy.
Как отмечалось в моем посте, повреждение памяти без повреждения памяти, pool->next_pool это пул памяти, управляемый драйвером Mali и общий для всех kbase_context. Он также используется для выделения глобальных каталогов таблицы страниц (PGD), используемых контекстами графического процессора. В частности, это означает, что путем тщательной организации пулов памяти можно повторно использовать освобожденную резервную страницу в kbase_va_region как PGD контекста графического процессора. (Подробности о том, как этого добиться, можно найти здесь.)
После повторного использования освобожденной страницы в качестве PGD контекста GPU адреса GPU, которые сохраняют доступ к освобожденной странице, могут быть использованы для перезаписи PGD с GPU. Затем это позволяет отображать любую память ядра, включая код ядра, на графический процессор. Затем это позволяет мне переписать код ядра и, следовательно, выполнить произвольный код ядра. Это также позволяет мне читать и записывать произвольные данные ядра, поэтому я могу легко переписать учетные данные моего процесса, чтобы получить root, а также отключить SELinux.
Эксплойт для Pixel 8 можно найти здесь с некоторыми замечаниями по настройке.
Как это позволяет обойти MTE?
До сих пор я не упоминал никаких конкретных мер по обходу MTE. Фактически, MTE вообще не влияет на поток эксплойтов этой ошибки. В то время как MTE защищает от разыменований указателей на несогласованные блоки памяти, эксплойт вообще не полагается ни на одно из таких разыменований. Когда срабатывает ошибка, она создает несоответствия между pages массивом и отображениями графического процессора в области JIT. На данный момент повреждения памяти нет, и ни сопоставления графического процессора, ни pages массив, если рассматривать их отдельно, не содержат недопустимых записей. Когда ошибка используется для того, чтобы kbase_mmu_teardown_pgd_pages пропустит удаление сопоставлений GPU, ее результатом является сохранение физических адресов освобожденных страниц памяти в таблице страниц GPU. Итак, когда графический процессор обращается к освобожденным страницам, он фактически обращается напрямую к их физическим адресам, что также не требует разыменования указателя. Кроме того, я также не уверен, оказывает ли MTE какое-либо влияние на доступ к памяти графического процессора в любом случае. Итак, используя графический процессор для прямого доступа к физическим адресам, я могу полностью обойти защиту, предлагаемую MTE. В конечном счете, в коде, который управляет доступом к памяти, нет кода, защищающего память. В какой-то момент физические адреса придется использовать напрямую для доступа к памяти.
Заключение
В этом посте я показал, как CVE-2023-6241 можно использовать для выполнения произвольного кода ядра на Pixel 8 с поддержкой MTE ядра. Хотя MTE, возможно, является одним из наиболее значительных достижений в борьбе с повреждениями памяти и сделает многие уязвимости, связанные с повреждением памяти, неиспользуемыми, это не серебряная пуля, и все еще возможно добиться выполнения произвольного кода ядра с помощью одной ошибки. Ошибка в этом сообщении позволяет обойти MTE, используя сопроцессор (GPU) для прямого доступа к физической памяти (пример 4 в реализация MTE, часть 3: ядро). По мере того, как на стороне процессора внедряется все больше аппаратных и программных средств защиты, я ожидаю, что сопроцессоры и драйверы их ядра по-прежнему будут мощным средством атаки.
Автор: Man Yue Mo
Оригинал: github.blog/2024-03-18-gaining-kernel-code-execution-on-an-mte-enabled-pixel-8/
Перевёл специально для XSS: TROUBLE
Код эксплойта прикреплён в архиве.
Arm64 MTE
MTE - это очень хорошо документированная функция на новых процессорах Arm, которая использует аппаратную реализацию для проверки на повреждение памяти. Поскольку уже есть много хороших статей о MTE, я лишь вкратце расскажу об идее MTE и объясню ее значение по сравнению с другими способами предотвращения повреждения памяти. Читатели, заинтересованные в более подробной информации, могут, к примеру, ознакомиться с этой статьей и с технической документацией, выпущенной Arm.
Хотя архитектура Arm64 использует 64-битные указатели для доступа к памяти, обычно нет необходимости использовать такое большое адресное пространство. На практике большинство приложений используют гораздо меньшее адресное пространство (обычно 52 бита или меньше). Это оставляет неиспользованными старшие биты в указателе. Основная идея тегирования памяти заключается в использовании этих старших битов в адресе для хранения "тега”, который затем может быть использован для проверки по другому тегу, хранящемуся в блоке памяти, связанном с адресом. Это помогает снизить распространенные типы повреждений памяти следующим образом:
В случае линейного переполнения указатель используется для разыменования соседнего блока памяти, который имеет другой тег по сравнению с тем, который хранится в указателе. Проверяя эти теги во время разыменования, можно обнаружить поврежденное разыменование. При повреждении памяти типа use-after-free, пока тег в блоке памяти очищается каждый раз, когда он освобождается, и новый тег переназначается при его выделении, разыменование уже освобожденного и восстановленного объекта также приведет к несоответствию между тегом указателя и тегом в памяти, что позволяет обнаружить use-after-free.
(Приведенное выше изображение взято из Расширения для тегов памяти: повышение безопасности памяти с помощью архитектуры, опубликованного Arm.)
Основная причина, по которой пометка памяти отличается от предыдущих мер по исправлению ошибок, таких как целостность потока управления ядром (kCFI), заключается в том, что, в отличие от других мер по исправлению ошибок, которые прерывают более поздние стадии эксплойта, MTE - это мера по исправлению ошибок на очень ранней стадии, которая пытается выявить повреждение памяти при первом его возникновении. Как таковой, он способен остановить эксплойт на очень ранней стадии, прежде чем злоумышленник получит какие-либо возможности, и поэтому его очень трудно обойти. Он вводит проверки, которые эффективно превращают небезопасный язык памяти в тот, который безопасен для памяти, хоть и вероятностно.
Теоретически, пометка памяти может быть реализована только программным обеспечением, заставляя распределитель памяти назначать и удалять теги каждый раз, когда выделяется или освобождается память, и добавляя логику проверки тегов при разыменовании указателей. Однако это приводит к снижению производительности, что делает метод непригодным для практического использования. В итоге на практике необходима аппаратная реализация, чтобы снизить затраты на производительность и сделать маркировку памяти пригодной для практического использования. Аппаратная поддержка была представлена в версии v8.5a архитектуры ARM, в которой были введены дополнительные аппаратные инструкции (называемые MTE) для выполнения тегов и проверки. Для устройств Android большинство наборов микросхем, поддерживающих MTE, используют процессоры Arm v9 (вместо Arm v8.5a), и в настоящее время существует лишь несколько устройств, поддерживающих MTE.
Одним из ограничений MTE является то, что количество доступных неиспользуемых битов невелико по сравнению со всеми возможными блоками памяти, которые когда-либо могут быть выделены. Таким образом, столкновение тегов неизбежно, и многие блоки памяти будут иметь один и тот же тег. Это означает, что поврежденный доступ к памяти все еще может быть успешным случайно. На практике, даже при использовании всего 4 бит для тега, вероятность успешного выполнения снижается до 1/16, что по-прежнему является довольно надежной защитой от повреждения памяти. Другое ограничение заключается в том, что, передавая значения указателя и блока памяти с использованием атаки по побочному каналу, такой как Spectre, злоумышленник может быть уверенным, что поврежденный доступ к памяти выполняется с правильным тегом, и, таким образом, обходит MTE. Однако этот тип утечки в основном доступен только локальному злоумышленнику. Серия статей "Реализация MTE " Марка Брэнда включает подробное исследование ограничений и влияния MTE на различные сценарии атак.
Помимо наличия оборудования, использующего процессоры, Arm версии 8.5a или выше, для включения MTE также требуется программная поддержка. В настоящее время только Pixel 8 от Google позволяет пользователям включать MTE в настройках разработчика, а MTE по умолчанию отключен. Также требуются дополнительные шаги для включения MTE в ядре.
Графический процессор Arm Mali
Графический процессор Arm Mali может быть интегрирован в различные устройства (например, см. “Реализации” в статье Mali (GPU) в Википедии). Он был привлекательной целью на смартфонах Android и неоднократно становился мишенью для необычных эксплойтов. Текущая уязвимость тесно связана с другой проблемой, о которой я сообщал, и представляет собой уязвимость в обработке типа памяти графического процессора, называемого JIT-памятью. Далее я кратко расскажу о памяти JIT и объясню уязвимость CVE-2023-6241.
JIT - память в Arm Mali
При использовании драйвера графического процессора Mali пользовательскому приложению сначала необходимо создать и инициализировать kbase_context объект ядра. Это включает в себя открытие пользовательского приложения файла драйвера и использование результирующего файлового дескриптора для выполнения серии ioctl вызовов. А kbase_context объект отвечает за управление ресурсами для каждого открываемого файла драйвера и уникален для каждого дескриптора файла.
В частности, kbase_context управляет различными типами памяти, которые совместно используются графическим процессором устройства и приложениями пользовательского пространства. Пользовательские приложения могут либо сопоставить свою собственную память с пространством памяти графического процессора, чтобы графический процессор мог получить доступ к этой памяти, либо они могут выделить память из графического процессора. Память, выделяемая графическим процессором, управляется kbase_context и может быть сопоставлена с пространством памяти графического процессора, а также с пользовательским пространством. Пользовательское приложение также может использовать графический процессор для доступа к отображенной памяти путем отправки команд на графический процессор. В стандартном случае память должна быть либо выделена графическим процессором и управляться им (собственная память), либо импортирована в графический процессор из пользовательского пространства, а затем сопоставлена с адресным пространством графического процессора, прежде чем к ней сможет получить доступ графический процессор. Область памяти в графическом процессоре Mali представлена символом kbase_va_region. Подобно виртуальной памяти в центральном процессоре, область памяти в графическом процессоре может иметь не весь свой диапазон, поддерживаемый физической памятью. nr_pages Поле в a kbase_va_region определяет виртуальный размер области памяти, тогда как gpu_alloc->nents это фактическое количество физических страниц, которые поддерживают эту область. Отныне я буду называть эти страницы резервными страницами области. Хотя виртуальный размер области памяти фиксирован, его физический размер может изменяться. Отныне, когда я использую такие термины, как изменение размера, рост и сжатие, применительно к области памяти, я имею в виду, что изменяется, растет или сжимается физический размер области.
Память JIT - это тип встроенной памяти, временем жизни которой управляет драйвер ядра. Пользовательские приложения запрашивают графический процессор для выделения и освобождения памяти JIT, отправляя соответствующие команды графическому процессору. Хотя большинство команд, например, использующих графический процессор для выполнения арифметических операций и доступа к памяти, выполняются на самом графическом процессоре, есть некоторые команды, например, используемые для управления памятью JIT, которые реализованы в ядре и выполняются на центральном процессоре. Они называются программными командами (в отличие от аппаратных команд, которые выполняются на графическом процессоре (аппаратном обеспечении)). На графических процессорах, использующих интерфейс потока команд (CSF), программные и аппаратные команды размещаются в очередях команд разных типов. Для отправки программной команды требуется kbase_kcpu_command_queue, и ее можно создать с помощью KBASE_IOCTL_KCPU_QUEUE_CREATE ioctl. Затем программная команда может быть помещена в очередь с помощью KBASE_IOCTL_KCPU_QUEUE_ENQUEUE команды. Для выделения или освобождения памяти JIT могут использоваться команды типа BASE_KCPU_COMMAND_TYPE_JIT_ALLOC и BASE_KCPU_COMMAND_TYPE_JIT_FREE.
BASE_KCPU_COMMAND_TYPE_JIT_ALLOC Команда использует kbase_jit_allocate для выделения памяти JIT. Аналогично, команда BASE_KCPU_COMMAND_TYPE_JIT_FREE может использоваться для освобождения памяти JIT. Как объяснялось в разделе “Жизненный цикл JIT-памяти” в одном из моих предыдущих постов, когда JIT-память освобождается, она переходит в пул памяти, управляемый kbase_context и когда kbase_jit_allocate вызывается, он сначала просматривает этот пул памяти, чтобы увидеть, есть ли какая-либо подходящая освобожденная JIT-память, которую можно использовать повторно:
Код: Скопировать в буфер обмена
Код:
struct kbase_va_region *kbase_jit_allocate(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
bool ignore_pressure_limit)
{
...
kbase_gpu_vm_lock(kctx);
mutex_lock(&kctx->jit_evict_lock);
/*
* Сканирование пула на выделенную память подходящую требованиям и её удаление
*/
if (info->usage_id != 0)
/* Первичное сканирование на наличие выделенной памяти с тем же usage ID */
reg = find_reasonable_region(info, &kctx->jit_pool_head, false);
...
}
Если найден существующий регион и его виртуальный размер соответствует запросу, но его физический размер слишком мал, то kbase_jit_allocate попытается выделить больше физических страниц для резервного копирования региона, вызвав kbase_jit_grow:
Код: Скопировать в буфер обмена
Код:
struct kbase_va_region *kbase_jit_allocate(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
bool ignore_pressure_limit)
{
...
/* kbase_jit_grow() высвобождает и выделяет вновь 'kctx->reg_lock',
* дабы вне зависимости от состояния защищенности памяти, она была пересмотрена в случае добавления *дополнительного кода здесь.
/*
ret = kbase_jit_grow(kctx, info, reg, prealloc_sas,
mmu_sync_info);
...
}
Если, с другой стороны, подходящая область не найдена, kbase_jit_allocate выделит память JIT с нуля:
Код: Скопировать в буфер обмена
Код:
struct kbase_va_region *kbase_jit_allocate(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
bool ignore_pressure_limit)
{
...
} else {
/* Подходящая область JIT не найдена, так что создаем новую*/
u64 flags = BASE_MEM_PROT_CPU_RD | BASE_MEM_PROT_GPU_RD |
BASE_MEM_PROT_GPU_WR | BASE_MEM_GROW_ON_GPF |
BASE_MEM_COHERENT_LOCAL |
BASEP_MEM_NO_USER_FREE;
u64 gpu_addr;
...
mutex_unlock(&kctx->jit_evict_lock);
kbase_gpu_vm_unlock(kctx);
reg = kbase_mem_alloc(kctx, info->va_pages, info->commit_pages, info->extension,
&flags, &gpu_addr, mmu_sync_info);
...
}
Как мы можем видеть из комментария выше, вызов kbase_jit_grow, kbase_jit_grow может временно отбросить kctx->reg_lock:
Код: Скопировать в буфер обмена
Код:
static int kbase_jit_grow(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
struct kbase_va_region *reg,
struct kbase_sub_alloc **prealloc_sas,
enum kbase_caller_mmu_sync_info mmu_sync_info)
{
...
if (!kbase_mem_evictable_unmake(reg->gpu_alloc))
goto update_failed;
...
old_size = reg->gpu_alloc->nents; //commit_pages - reg->gpu_alloc->nents; //<---------2.
pages_required = delta;
...
while (kbase_mem_pool_size(pool) mem_partials_lock);
kbase_gpu_vm_unlock(kctx); //<---------- lock снят.
ret = kbase_mem_pool_grow(pool, pool_delta);
kbase_gpu_vm_lock(kctx);
...
}
В приведенном выше примере мы видим, что kbase_gpu_vm_unlock вызывается для временного удаления kctx->reg_lock, в то время как kctx->mem_partials_lock также удаляется во время вызова kbase_mem_pool_grow. В графическом процессоре Mali kctx->reg_lock используется для защиты параллельных обращений к областям памяти. Так, например, когда kctx->reg_lock удерживается, физический размер области памяти не может быть изменен другим потоком. В GHSL-2023-005, о котором я сообщал ранее, я смог запустить race, так что область JIT была уменьшена с помощью KBASE_IOCTL_MEM_COMMIT ioctl из другого потока во время kbase_mem_pool_grow выполнения. Это изменение размера области JIT привело к тому, что reg->gpu_alloc->nents изменилось после kbase_mem_pool_grow, что означает, что фактическое значение reg->gpu_alloc->nents тогда отличалось от значения, которое было кэшировано в old_size и delta (1. и 2. в приведенном выше примере). Поскольку эти значения позже использовались для выделения и отображения области JIT, использование этих устаревших значений вызвало несогласованность в отображении памяти графического процессора, что привело к появлению GHSL-2023-005.
Код: Скопировать в буфер обмена
Код:
static int kbase_jit_grow(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
struct kbase_va_region *reg,
struct kbase_sub_alloc **prealloc_sas,
enum kbase_caller_mmu_sync_info mmu_sync_info)
{
...
//Увеличиваем пул памяти
...
//Используем дельту для выделения области
gpu_pages = kbase_alloc_phy_pages_helper_locked(reg->gpu_alloc, pool,
delta, &prealloc_sas[0]);
...
//Старый размер используем для gpu mapping
ret = kbase_mem_grow_gpu_mapping(kctx, reg, info->commit_pages,
old_size);
...
}
После исправления GHSL-2023-005 больше не было возможности изменять размер JIT-памяти с помощью KBASE_IOCTL_MEM_COMMIT ioctl.
Уязвимость
Аналогично виртуальной памяти, когда графический процессор обращается к адресу в области памяти, которая не поддерживается физической страницей, возникает ошибка доступа к памяти. В этом случае, в зависимости от типа области памяти, может оказаться возможным выделить и сопоставить физическую страницу "на лету" для резервного копирования адреса ошибки. Ошибка доступа к памяти графического процессора обрабатывается kbase_mmu_page_fault_worker:
Код: Скопировать в буфер обмена
Код:
void kbase_mmu_page_fault_worker(struct work_struct *data)
{
...
kbase_gpu_vm_lock(kctx);
...
if ((region->flags & GROWABLE_FLAGS_REQUIRED)
!= GROWABLE_FLAGS_REQUIRED) {
kbase_gpu_vm_unlock(kctx);
kbase_mmu_report_fault_and_kill(kctx, faulting_as,
"Memory is not growable", fault);
goto fault_done;
}
if ((region->flags & KBASE_REG_DONT_NEED)) {
kbase_gpu_vm_unlock(kctx);
kbase_mmu_report_fault_and_kill(kctx, faulting_as,
"Don't need memory can't be grown", fault);
goto fault_done;
}
...
spin_lock(&kctx->mem_partials_lock);
grown = page_fault_try_alloc(kctx, region, new_pages, &pages_to_grow,
&grow_2mb_pool, prealloc_sas);
spin_unlock(&kctx->mem_partials_lock);
...
}
В обработчике ошибок выполняется ряд проверок, чтобы гарантировать, что области памяти разрешено увеличиваться в размерах. Две проверки, которые имеют отношение к памяти JIT, - это проверки на GROWABLE_FLAGS_REQUIRED и KBASE_REG_DONT_NEED флаги. GROWABLE_FLAGS_REQUIRED Определяется следующим образом:
Код: Скопировать в буфер обмена
#define GROWABLE_FLAGS_REQUIRED (KBASE_REG_PF_GROW | KBASE_REG_GPU_WR)
Эти флаги добавляются в область JIT при ее создании с помощью kbase_jit_allocate и никогда не изменяются:
Код: Скопировать в буфер обмена
Код:
struct kbase_va_region *kbase_jit_allocate(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
bool ignore_pressure_limit)
{
...
} else {
/* Подходящая область JIT не найдена, так что создаем новую */
u64 flags = BASE_MEM_PROT_CPU_RD | BASE_MEM_PROT_GPU_RD |
BASE_MEM_PROT_GPU_WR | BASE_MEM_GROW_ON_GPF | //jit_evict_lock);
kbase_gpu_vm_unlock(kctx);
reg = kbase_mem_alloc(kctx, info->va_pages, info->commit_pages, info->extension,
&flags, &gpu_addr, mmu_sync_info);
...
}
Хоть KBASE_REG_DONT_NEED флаг и добавляется в область JIT при ее освобождении, он удаляется в kbase_jit_grow задолго до того, как kctx->reg_lock и kctx->mem_partials_lock отбрасываются и kbase_mem_pool_grow вызывается:
Код: Скопировать в буфер обмена
Код:
static int kbase_jit_grow(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
struct kbase_va_region *reg,
struct kbase_sub_alloc **prealloc_sas,
enum kbase_caller_mmu_sync_info mmu_sync_info)
{
...
if (!kbase_mem_evictable_unmake(reg->gpu_alloc)) //<----- Убираем KBASE_REG_DONT_NEED
goto update_failed;
...
while (kbase_mem_pool_size(pool) mem_partials_lock);
kbase_gpu_vm_unlock(kctx);
ret = kbase_mem_pool_grow(pool, pool_delta); //<----- race окно: хэндлер ошибок увеличивает регион
kbase_gpu_vm_lock(kctx);
...
}
В частности, во время окна race, отмеченного в приведенном выше фрагменте, разрешен рост объема памяти JIT при сбое страницы.
Итак, получая доступ к незамеченной памяти в области для создания сбоя в другом потоке во время kbase_mem_pool_grow выполнения, я могу заставить обработчик сбоев GPU увеличить область JIT во время выполнения kbase_mem_pool_grow. Затем это изменяется reg->gpu_alloc->nents и делает недействительным old_size и delta в 1. и 2. ниже:
Код: Скопировать в буфер обмена
Код:
static int kbase_jit_grow(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
struct kbase_va_region *reg,
struct kbase_sub_alloc **prealloc_sas,
enum kbase_caller_mmu_sync_info mmu_sync_info)
{
...
if (!kbase_mem_evictable_unmake(reg->gpu_alloc))
goto update_failed;
...
old_size = reg->gpu_alloc->nents; //commit_pages - reg->gpu_alloc->nents; //<---------2.
pages_required = delta;
...
while (kbase_mem_pool_size(pool) mem_partials_lock);
kbase_gpu_vm_unlock(kctx);
ret = kbase_mem_pool_grow(pool, pool_delta); //gpu_alloc->nents changed by fault handler
kbase_gpu_vm_lock(kctx);
...
//delta use for allocating pages
gpu_pages = kbase_alloc_phy_pages_helper_locked(reg->gpu_alloc, pool, //commit_pages, //<----- 4.
old_size);
...
}
В результате, когда delta и old_size используются в 3. и 4. для выделения резервных страниц и сопоставления страниц с пространством памяти графического процессора, их значения недопустимы.
Это очень похоже на то, что произошло с GHSL-2023-005. Поскольку kbase_mem_pool_grow требуется выделение большого объема памяти, в этой гонке можно очень легко победить. Однако здесь есть одно очень большое отличие: с GHSL-2023-005 я смог уменьшить область JIT, в то время как в этом случае я смог только увеличить область JIT. Чтобы понять, почему это важно, давайте вкратце расскажем, как работал мой эксплойт для GHSL-2023-005.
Как упоминалось ранее, физический размер или количество резервных страниц kbase_va_region хранятся в поле reg->gpu_alloc->nents. У kbase_va_region есть два kbase_mem_phy_alloc объекта: cpu_alloc и gpu_alloc, которые отвечают за управление его страницами поддержки. Для устройств Android эти два поля настроены одинаково. Внутри kbase_mem_phy_allocполе pages представляет собой массив, содержащий физические адреса резервных страниц, в то время как nents определяет длину pages массива:
Код: Скопировать в буфер обмена
Код:
struct kbase_mem_phy_alloc {
...
size_t nents;
struct tagged_addr *pages;
...
}
При вызове kbase_alloc_phy_pages_helper_locked он выделяет страницы памяти и добавляет физические адреса, представленные этими страницами, в массив pages, поэтому новые страницы добавляются в индекс nents и далее. Затем новый размер сохраняется до nents. Например, когда он вызывается в kbase_jit_grow, delta - это количество добавляемых страниц.:
Код: Скопировать в буфер обмена
Код:
static int kbase_jit_grow(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
struct kbase_va_region *reg,
struct kbase_sub_alloc **prealloc_sas,
enum kbase_caller_mmu_sync_info mmu_sync_info)
{
...
//delta use for allocating pages
gpu_pages = kbase_alloc_phy_pages_helper_locked(reg->gpu_alloc, pool,
delta, &prealloc_sas[0]);
...
}
В этом случае delta страницы вставляются по индексу nents в массив pages из gpu_alloc:
После выделения резервных страниц и вставки в pages массив, новые страницы отображаются в адресное пространство графического процессора путем вызова kbase_mem_grow_gpu_mapping. Виртуальный адрес kbase_va_region в области памяти графического процессора управляется kbase_va_region самим и хранится в полях start_pfn и nr_pages:
Код: Скопировать в буфер обмена
Код:
struct kbase_va_region {
...
u64 start_pfn;
...
size_t nr_pages;
...
}
Начало виртуального адреса kbase_va_region хранится в start_pfn (в виде фрейма страницы, поэтому фактическим адресом является start_pfn >> PAGE_SIZE), в то время как в nr_pages сохраняется размер области. Эти поля остаются неизменными после их установки. Внутри kbase_va_region начальные reg->gpu_alloc->nents страницы в виртуальном адресном пространстве поддерживаются физической памятью, хранящейся в pages массиве gpu_alloc->pages, в то время как остальные адреса не поддерживаются. В частности, резервные копии виртуальных адресов всегда являются смежными (поэтому между поддерживаемыми областями нет пробелов) и всегда начинаются с начала области. Например, возможно следующее:
Но следующий случай не возможен, потому что резервное копирование не начинается с начала области:
и этот следующий случай также не возможен из - за пробелов в адресах:
В случае, когда kbase_mem_grow_gpu_mapping вызывается в kbase_jit_grow, адреса графического процессора между (start_pfn + old_size) * 0x1000 to (start_pfn + info->commit_pages) * 0x1000 сопоставляются с недавно добавленными страницами в gpu_alloc->pages, которые являются страницами между индексами pages + old_size и pages + info->commit_pages (потому что delta = info->commit_pages - old_size):
Код: Скопировать в буфер обмена
Код:
static int kbase_jit_grow(struct kbase_context *kctx,
const struct base_jit_alloc_info *info,
struct kbase_va_region *reg,
struct kbase_sub_alloc **prealloc_sas,
enum kbase_caller_mmu_sync_info mmu_sync_info)
{
...
old_size = reg->gpu_alloc->nents;
delta = info->commit_pages - reg->gpu_alloc->nents;
...
//old_size используется для растущего маппинга gpu
ret = kbase_mem_grow_gpu_mapping(kctx, reg, info->commit_pages,
old_size);
...
}
В частности, old_size здесь используется для указания как адреса графического процессора, с которого должно начинаться новое сопоставление, так и смещения от pages массива, где должны использоваться резервные страницы.
Если изменения reg->gpu_alloc->nents после old_size и delta кэшируются, то эти смещения могут стать недействительными. Например, если kbase_va_region при сжатии и nents уменьшается после old_size и delta были сохранены, то kbase_alloc_phy_pages_helper_locked будет вставлять delta страницы reg->gpu_alloc->pages + nents:
Аналогично, в kbase_mem_grow_gpu_mapping будут отображены адреса графического процессора, начинающиеся с (start_pfn + old_size) * 0x1000, используя страницы, находящиеся между reg->gpu_alloc->pages + old_size и reg->gpu_alloc->pages + nents + delta (пунктирные линии на рисунке ниже). Это означает, что страницы между pages->nents и pages->old_size в конечном итоге не будут сопоставлены ни с какими адресами графического процессора, в то время как на некоторых адресах в конечном итоге не будет резервных страниц:
Эксплуатация GHSL-2023-005
GHSL-2023-005 позволил мне уменьшить область JIT, но CVE-2023-6241 не дает мне такой возможности. Чтобы понять, как использовать эту проблему, нам нужно знать немного больше о том, как удаляются сопоставления GPU. Функция kbase_mmu_teardown_pgd_pages отвечает за удаление сопоставлений адресов из GPU. Эта функция, по сути, просматривает диапазон адресов графического процессора и удаляет адреса из таблицы страниц графического процессора, помечая их как недопустимые. Если он обнаружит запись таблицы страниц высокого уровня (PTE), которая охватывает большой диапазон адресов, и обнаружит, что запись недопустима, то он пропустит удаление всего диапазона адресов, охватываемых записью. Например, запись таблицы страниц уровня 2 охватывает диапазон из 512 страниц, поэтому, если запись таблицы страниц уровня 2 будет признана недопустимой (1. в приведенном ниже примере), то kbase_mmu_teardown_pgd_pages предположит, что следующие 512 страниц покрыты этим уровнем 2 и, следовательно, все они уже недействительны. Таким образом, удаление этих страниц будет пропущено (2. показано ниже).
Код: Скопировать в буфер обмена
Код:
static int kbase_mmu_teardown_pgd_pages(struct kbase_device *kbdev, struct kbase_mmu_table *mmut,
u64 vpfn, size_t nr, u64 *dirty_pgds,
struct list_head *free_pgds_list,
enum kbase_mmu_op_type flush_op)
{
...
for (level = MIDGARD_MMU_TOPLEVEL;
level ate_is_valid(page[index], level))
break; /* keep the mapping */
else if (!mmu_mode->pte_is_valid(page[index], level)) { //<------ 1.
/* nothing here, advance */
switch (level) {
...
case MIDGARD_MMU_LEVEL(2):
count = 512; // nr)
count = nr;
goto next;
}
...
next:
kunmap(phys_to_page(pgd));
vpfn += count;
nr -= count;
Функция kbase_mmu_teardown_pgd_pages вызывается либо при уменьшении kbase_va_region, либо при его удалении. Как объяснялось в предыдущем разделе, виртуальные адреса в kbase_va_region которые отображаются и поддерживаются физическими страницами, должны быть непрерывными с начала kbase_va_region. В результате, если сопоставлен какой-либо адрес в регионе, то должен быть сопоставлен начальный адрес и, следовательно, запись таблицы страниц высокого уровня, покрывающая начальный адрес, должна быть действительной (если ни один адрес в регионе не сопоставлен, то kbase_mmu_teardown_pgd_pages даже не будет вызван):
В приведенном выше примере PTE уровня 2, который покрывает начальный адрес региона, сопоставлен, и поэтому он действителен, поэтому в этом случае, если kbase_mmu_teardown_pgd_pages когда-либо встречается не сопоставленный PTE высокого уровня, остальные адреса в kbase_va_region, должно быть, уже не сопоставлены и могут быть безопасно пропущены.
В случае, когда область сжимается, адрес, с которого начинается разметка, находится в пределах kbase_va_region, и весь диапазон между этим начальным адресом и концом области будет размечен. Если запись таблицы страниц уровня 2, охватывающая этот адрес, недопустима, то начальный адрес должен находиться в области, которая не сопоставлена, и, следовательно, остальной диапазон адресов, который нужно отменить, также не должен быть сопоставлен. В этом случае пропуск адресов снова безопасен:
Таким образом, пока области отображаются только по их начальным адресам и не имеют пробелов в сопоставлениях, kbase_mmu_teardown_pgd_pages будет работать правильно.
В случае GHSL-2023-005 возможно создать область, которая не соответствует этим условиям. Например, уменьшив всю область до нулевого размера во время окна race, можно создать область, начало которой не отображается:
Если регион удаляется и kbase_mmu_teardown_pgd_pages пытается удалить первый адрес, поскольку PTE уровня 2 недействителен, то будут пропущены следующие 512 страниц, некоторые из которых, возможно, действительно были сопоставлены:
В этом случае адреса в “неправильно пропущенной” области останутся сопоставленными некоторым записям в pages массиве в gpu_alloc, которые уже освобождены. И эти "неправильно пропущенные” адреса графического процессора могут быть использованы для доступа к уже освобожденным страницам памяти.
Использование CVE-2023-6241
Ситуация, однако, сильно отличается, когда регион увеличивается во время окна race. В этом случае, nents больше, чем old_size когда вызываются kbase_alloc_phy_pages_helper_locked и kbase_mem_grow_gpu_mapping, и delta страницы вставляются по индексу nents pages массива:
pages массив содержит правильное количество страниц для резервного копирования как jit grow, так и для аварийного доступа, и фактически именно так и должно быть, когда kbase_jit_grow вызывается после обработчика ошибок страницы.
При kbase_mem_grow_gpu_mapping вызове delta страницы отображаются на графический процессор из (start_pfn + old_size) * 0x1000. Поскольку общее количество резервных страниц теперь увеличилось на fh + delta, где fh - количество страниц, добавленных обработчиком ошибок, это оставляет последние fh страницы в pages массиве без отображения.
Однако это, похоже, также не создает никаких проблем. В области памяти по-прежнему отображаются только начальные адреса, и в отображении нет пробелов. Страницы, которые не отображаются, просто недоступны из графического процессора и будут освобождены при удалении области памяти, так что это даже не проблема с утечкой памяти.
Однако не все потеряно. Как мы видели, когда происходит сбой страницы графического процессора, если причиной сбоя является то, что адрес не сопоставлен, тогда обработчик ошибок попытается добавить резервные страницы в регион и сопоставить эти новые страницы с экстентом региона. Если адрес ошибки, скажем, fault_addr, то минимальное количество добавляемых страниц равно new_pages = fault_addr/0x1000 - reg->gpu_alloc->nents. В зависимости от kbase_va_region также может быть добавлено некоторое дополнение. В любом случае эти новые страницы будут отображены на графический процессор, начиная с адреса (start_pfn + reg->gpu_alloc->nents) * 0x1000, чтобы сохранить фактическое отображение только адреса в начале региона.
Это означает, что если я вызову другой сбой графического процессора в области JIT, на которую повлияла ошибка, то некоторые новые сопоставления будут добавлены после области, которая не сопоставлена.
Это создает пробел в сопоставлениях графического процессора, и я начинаю получать нечто, что выглядит пригодным для использования.
Обратите внимание, что поскольку для запуска ошибки delta должно быть ненулевым значением, и поскольку delta + old_size страницы в начале региона сопоставлены, по-прежнему невозможно, чтобы начало региона не отображалось, как в случае GHSL-2023-005. Итак, мой единственный вариант здесь - уменьшить область и сделать так, чтобы результирующий размер находился где-то внутри незамеченного промежутка.
Единственный способ уменьшить область JIT - это использовать BASE_KCPU_COMMAND_TYPE_JIT_FREE команду GPU для “освобождения” области JIT. Как объяснялось ранее, это фактически не освобождает сам kbase_va_region, а скорее помещает его в пул памяти, чтобы его можно было повторно использовать при последующем выделении JIT. До этого kbase_jit_free также будет уменьшена область JIT в соответствии с initial_commit размером области, а также с trim_level который настроен в kbase_context:
Код: Скопировать в буфер обмена
Код:
void kbase_jit_free(struct kbase_context *kctx, struct kbase_va_region *reg)
{
...
old_pages = kbase_reg_current_backed_size(reg);
if (reg->initial_commit initial_commit,
div_u64(old_pages * (100 - kctx->trim_level), 100));
u64 delta = old_pages - new_size;
if (delta) {
mutex_lock(&kctx->reg_lock);
kbase_mem_shrink(kctx, reg, old_pages - delta);
mutex_unlock(&kctx->reg_lock);
}
}
...
}
В любом случае, я могу контролировать размер этого сокращения. Имея это в виду, я могу упорядочить область следующим образом:
1. Создайте область JIT и запустите ошибку. Расположите ошибку графического процессора так, чтобы обработчик ошибок добавлял fault_size страницы, достаточные для покрытия хотя бы одного PTE 2-го уровня.
После срабатывания ошибки только начальные old_size + delta страницы отображаются в адресное пространство графического процессора, в то время как у kbase_va_region всего есть old_size + delta + fault_size резервные страницы.
2. Запускает вторую ошибку со смещением, превышающим количество резервных страниц, так что страницы добавляются к области и отображаются после неподтвержденных областей, созданных на предыдущем шаге.
3. Освободите область JIT с помощью BASE_KCPU_COMMAND_TYPE_JIT_FREE, которая вызовет kbase_jit_free сжатие области и удаление страниц из нее. Контролируйте размер этой обрезки так, чтобы размер области после сжатия (final_size) резервного хранилища находился где-то в пределах незамеченной области, охватываемой PTE первого уровня.
Когда область сжимается, kbase_mmu_teardown_pgd_pages вызывается для отмены сопоставления адресов графического процессора, начиная с region_start + final_size и полностью и до конца области. Поскольку весь диапазон адресов, охватываемый 2 PTE первого уровня, не сопоставлен, при kbase_mmu_teardown_pgd_pages попытке размонтирования region_start + final_size выполняется условие !mmu_mode->pte_is_valid для PTE уровня 2, и поэтому при размонтировании будут пропущены следующие 512 страниц, начиная с region_start + final_size. Однако, поскольку адреса, принадлежащие PTE следующего уровня 2, по-прежнему отображаются, эти адреса будут пропущены неправильно (оранжевая область на следующем рисунке), в результате чего они будут отображены на страницы, которые будут освобождены:
После завершения сжатия резервные страницы освобождаются, а адреса в оранжевой области сохраняют доступ к уже освобожденным страницам.
Это означает, что освобожденную резервную страницу теперь можно повторно использовать как любую страницу ядра, что дает мне множество возможностей использовать эту ошибку. Одна из возможностей - использовать мою предыдущую технику для замены резервной страницы в качестве глобальных каталогов таблицы страниц (PGD) нашего графического процессора kbase_context.
В заключение давайте посмотрим, как распределяются резервные страницы a kbase_va_region. При выделении страниц для резервного хранилища kbase_va_region используется kbase_mem_pool_alloc_pages функция:
Код: Скопировать в буфер обмена
Код:
int kbase_mem_pool_alloc_pages(struct kbase_mem_pool *pool, size_t nr_4k_pages,
struct tagged_addr *pages, bool partial_allowed)
{
...
/* Get pages from this pool */
while (nr_from_pool--) {
p = kbase_mem_pool_remove_locked(pool); //next_pool) {
/* Allocate via next pool */
err = kbase_mem_pool_alloc_pages(pool->next_pool, //<----- 2.
nr_4k_pages - i, pages + i, partial_allowed);
...
} else {
/* Get any remaining pages from kernel */
while (i != nr_4k_pages) {
p = kbase_mem_alloc_page(pool); //<------- 3.
...
}
...
}
...
}
Входной аргумент kbase_mem_pool - это пул памяти, управляемый kbase_context объектом, связанным с файлом драйвера, который используется для выделения памяти графического процессора. Как следует из комментариев, выделение фактически выполняется по уровням. Сначала страницы будут выделены из текущего kbase_mem_pool использования kbase_mem_pool_remove_locked (1 в приведенном выше примере). Если в текущем сервере недостаточно ресурсов kbase_mem_pool для удовлетворения запроса, то pool->next_pool, используется для выделения страниц (2 в приведенном выше примере). Если даже pool->next_pool не хватает мощности, то kbase_mem_alloc_page используется для выделения страниц непосредственно из ядра через распределитель buddy (распределитель страниц в ядре).
При освобождении страницы, при условии, что область памяти не удалена, то же самое происходит в обратном направлении: kbase_mem_pool_free_pages сначала пытается вернуть страницам значение kbase_mem_pool текущего kbase_context, если пул памяти заполнен, он попытается вернуть оставшимся страницам значение pool->next_pool. Если следующий пул также заполнен, оставшиеся страницы возвращаются в ядро путем их освобождения через распределитель buddy.
Как отмечалось в моем посте, повреждение памяти без повреждения памяти, pool->next_pool это пул памяти, управляемый драйвером Mali и общий для всех kbase_context. Он также используется для выделения глобальных каталогов таблицы страниц (PGD), используемых контекстами графического процессора. В частности, это означает, что путем тщательной организации пулов памяти можно повторно использовать освобожденную резервную страницу в kbase_va_region как PGD контекста графического процессора. (Подробности о том, как этого добиться, можно найти здесь.)
После повторного использования освобожденной страницы в качестве PGD контекста GPU адреса GPU, которые сохраняют доступ к освобожденной странице, могут быть использованы для перезаписи PGD с GPU. Затем это позволяет отображать любую память ядра, включая код ядра, на графический процессор. Затем это позволяет мне переписать код ядра и, следовательно, выполнить произвольный код ядра. Это также позволяет мне читать и записывать произвольные данные ядра, поэтому я могу легко переписать учетные данные моего процесса, чтобы получить root, а также отключить SELinux.
Эксплойт для Pixel 8 можно найти здесь с некоторыми замечаниями по настройке.
Как это позволяет обойти MTE?
До сих пор я не упоминал никаких конкретных мер по обходу MTE. Фактически, MTE вообще не влияет на поток эксплойтов этой ошибки. В то время как MTE защищает от разыменований указателей на несогласованные блоки памяти, эксплойт вообще не полагается ни на одно из таких разыменований. Когда срабатывает ошибка, она создает несоответствия между pages массивом и отображениями графического процессора в области JIT. На данный момент повреждения памяти нет, и ни сопоставления графического процессора, ни pages массив, если рассматривать их отдельно, не содержат недопустимых записей. Когда ошибка используется для того, чтобы kbase_mmu_teardown_pgd_pages пропустит удаление сопоставлений GPU, ее результатом является сохранение физических адресов освобожденных страниц памяти в таблице страниц GPU. Итак, когда графический процессор обращается к освобожденным страницам, он фактически обращается напрямую к их физическим адресам, что также не требует разыменования указателя. Кроме того, я также не уверен, оказывает ли MTE какое-либо влияние на доступ к памяти графического процессора в любом случае. Итак, используя графический процессор для прямого доступа к физическим адресам, я могу полностью обойти защиту, предлагаемую MTE. В конечном счете, в коде, который управляет доступом к памяти, нет кода, защищающего память. В какой-то момент физические адреса придется использовать напрямую для доступа к памяти.
Заключение
В этом посте я показал, как CVE-2023-6241 можно использовать для выполнения произвольного кода ядра на Pixel 8 с поддержкой MTE ядра. Хотя MTE, возможно, является одним из наиболее значительных достижений в борьбе с повреждениями памяти и сделает многие уязвимости, связанные с повреждением памяти, неиспользуемыми, это не серебряная пуля, и все еще возможно добиться выполнения произвольного кода ядра с помощью одной ошибки. Ошибка в этом сообщении позволяет обойти MTE, используя сопроцессор (GPU) для прямого доступа к физической памяти (пример 4 в реализация MTE, часть 3: ядро). По мере того, как на стороне процессора внедряется все больше аппаратных и программных средств защиты, я ожидаю, что сопроцессоры и драйверы их ядра по-прежнему будут мощным средством атаки.
Автор: Man Yue Mo
Оригинал: github.blog/2024-03-18-gaining-kernel-code-execution-on-an-mte-enabled-pixel-8/
Перевёл специально для XSS: TROUBLE
Код эксплойта прикреплён в архиве.