Наш первоначальный пост об улучшениях безопасности памяти в ядре XNU был посвящен kalloc_type — нашему новому API распределения, который обеспечивает рандомизированную изоляцию типов с сегментами для снижения возможности эксплуатации большинства уязвимостей использования после освобождения (UAF). В этом посте мы рассмотрим практическую эффективность kalloc_type против иллюстративной уязвимости — мощной, старой и уже исправленной ошибки UAF, известной как SockPuppet.
Об уязвимости SockPuppet сообщил в 2019 году исследователь Нед Уильямсон из Google Project Zero. Он присутствовал в нескольких версиях iOS 12 и был исправлен в iOS 12.3 и iOS 12.4.1 . Эта уязвимость представляет собой особенно полезный пример для нашей работы по усилению защиты распределителя ядра, поскольку многие аспекты этой ошибки работали в пользу злоумышленника: триггер UAF и время жизни повторного использования полностью контролировались злоумышленником, структура UAF предлагала множество путей для эксплуатации, а встроенное раскрытие памяти сделало очистку кучи чрезвычайно стабильной.
Проанализировав доступные типы перераспределения в kalloc_type, мы считаем, что было бы значительно сложнее создать даже полунадежный эксплойт для SockPuppet, если бы он все еще присутствовал в iOS 16. Традиционный подход с использованием одного типа перераспределения имел бы всего 8%. уровень успеха при kalloc_type, и наш анализ показывает, что злоумышленники столкнутся с фундаментальным ограничением надежности для многих уязвимостей использования после освобождения. Например, мы подсчитали, что SockPuppet имеет встроенный предел успеха эксплойта около 92%, а это означает, что даже самый лучший эксплойт, который выбирает, какую стратегию эксплойта использовать на основе жизнеспособных типов перераспределения, будет давать сбой примерно в 8% случаев. Поскольку исходный эксплойт SockPuppet использовал только одну стратегию эксплойта и был почти на 100% надежен,
Уязвимость SockPuppet
Уязвимость SockPuppet представляла собой функцию использования после освобождения в in6_pcbdetach() функции ядра XNU, которая была доступна через серию системных вызовов, связанных с сокетами. Эта функция не смогла очистить поле inp->in6p_outputopts после его освобождения и оставила сокет с оборванным указателем на структуру ip6_pktopts. В результате различные операции getsockopt()и setsockopt() могли выполнять ограниченное чтение и запись в освобожденный объект ip6_pktopts, а также чтение и запись через указатели, разыменованные из освобожденного объекта.
Важно отметить, что ip6_pktopts с помощью уязвимости структуру можно было освободить только один раз, а это означало, что все повреждения памяти должны были происходить путем манипулирования освобожденной памятью через представление ip6_pktopts.
Оригинальная стратегия эксплойта
Эксплойт SockPuppet использовал следующие основные примитивы, предоставляемые уязвимостью, и системные вызовы, связанные с сокетами:
getsockopt(IPV6_USE_MIN_MTU): этот системный вызов будет читать 4-байтовое поле ip6po_minmtu со смещением 0xb4в ip6_pktoptsобъекте.
getsockopt(IPV6_PREFER_TEMPADDR): этот системный вызов будет читать 4-байтовое поле ip6po_prefer_tempaddr со смещением 0xb8 в объекте ip6_pktopts.
getsockopt(IPV6_PKTINFO): этот системный вызов будет считывать 20 байтов с адреса, на который указывает ip6po_pktinfoполе со смещением 0x10 в объекте ip6_pktopts.
setsockopt(IPV6_PKTINFO): этот системный вызов освободит указатель ip6po_pktinfo по смещению 0x10в ip6_pktoptsобъекте.
Из этих базовых примитивов эксплойт SockPuppet создал следующие возможности:
1. Раскрытие адреса произвольного порта Mach: эксплойт создал сокет с полем ip6_pktopt, а затем распространил большое количество сообщений Mach, содержащих вне очереди массивы портов Mach того же размера, что и структура ip6_pktopts. Этот спрей перераспределил висячий ip6_pktopts объект с помощью одного из этих массивов портов вне очереди. При заполнении массива портов сообщения Mach в пользовательском пространстве множеством ссылок на целевой порт Mach поля ip6po_minmtu и ip6po_prefer_tempaddr будут перекрывать верхнюю и нижнюю половины двух соседних указателей соответственно. Вызов getsockopt(IPV6_USE_MIN_MTU) и getsockopt(IPV6_PREFER_TEMPADDR) для чтения этих полей позволили эксплойту восстановить полный адрес порта Mach.
2. Чтение 20 байт по произвольному адресу ядра: эксплойт создал сокет с зависшим полем ip6_pktopts, а затем распылил большое количество буферов OSData того же размера, что и структура ip6_pktopts. Этот спрей переместил висячий объект ip6_pktopts с помощью одного из этих буферов OSData. Установив смещение 0x10 в буфере данных на целевой адрес и смещение 0xb4 на магическое значение, эксплойт смог полностью контролировать значения полей ip6_pktopt' ip6po_pktinfoи ip6po_minmtu. Затем эксплойт вызывался getsockopt(IPV6_USE_MIN_MTU), чтобы проверить, что ip6po_minmtu действительно содержит магическое значение, которое указывало, что зависшая структура ip6_pktopts была успешно перераспределена и, таким образом, ip6po_pktinfo содержала целевой адрес. Наконец, эксплойт под вызывает getsockopt(IPV6_PKTINFO) чтобы скопироватьскопировать 20 байт с этого адреса в пользовательское пространство.
3. Освобождение произвольного адреса ядра: этот примитив работал так же, как описанный выше примитив чтения ядра, за исключением того, что на последнем шаге вместо вызова для чтения getsockopt(IPV6_PKTINFO) с целевого адреса эксплойт вызывал setsockopt(IPV6_PKTINFO) для передачи целевого адреса kfree() функции XNU.
Первый примитив основывался на перераспределении зависщей структуры ip6_pktopts с внешним массивом портов Mach, который представляет собой массив указателей. Второй и третий примитивы полагались на перераспределение ip6_pktopts структуры с буфером OSData, и происходит выделение данных без указателей.
И теперь мы начинаем понимать, как может помочь kalloc_type. Начиная с iOS 16 массивы внешних портов Mach принадлежат зоне kalloc_type_var, а буферы OSData выделяются в подкарте данных. Ни один из вышеперечисленных примитивов не будет работать под kalloc_type, потому что замещающие выделения обслуживаются из отдельных частей виртуального адресного пространства, а не выделения ip6_pktopts. И даже если бы у нас был какой-то другой механизм для создания произвольного свободного примитива, kfree_type()теперь он расширяется до вызова zfree(kt_view, ptr), который освобождает «произвольный» указатель на определенную зону и вызывает панику, если переданный указатель не из этой зоны.
Ландшафт kalloc_type
Ломать эксплойт-примитив бесполезно, если есть простой способ перестроить примитив, используя слегка измененную технику, например, нацелившись на другое поле в структуре UAF или выбрав другое размещение. По-настоящему эффективная защита ограничила бы возможность использования уязвимости SockPuppet для всех стратегий эксплойтов, а не только для той, что указана в исходном отчете. Давайте оценим, чего в этом плане достигает kalloc_type.
Мы можем представить, что уязвимость SockPuppet присутствует в бета-версии iOS 16 1 (сборка 20A5283p) — после того, как был введен kalloc_type — и повторить анализ базовых примитивов, которые мы получаем, управляя различными полями завсишей структуры посредством перераспределения ip6_pktopts. Мы упрощаем наш анализ для краткости, и мы ошибемся, предположив, что что-то можно использовать, если мы не можем показать, что это не так.
Структура ip6_pktopts имеет размер ровно 192 ( 0xc0) байта и будет назначена обычной зоне kalloc_type, такой как kalloc.typeN.192. На iPhone 13 Pro под управлением iOS 16 beta 1 имеется 194 обычных (не переменной длины) типа в этом классе размеров, среди которых 119 уникальных подписей и 90 групп подписей. 90 сигнатурных групп равномерно распределены по 11 зонам, названным kalloc.type0.192 через kalloc.type10.192. А поскольку метаданные распределителя для kalloc_type находятся полностью за пределами выделенной области, эти 194 типа — единственные вещи, которые могут когда-либо совмещаться со структурой ip6_pktopts.
В этом конкретном случае ip6_pktoptsструктура попала в корзину 8 вместе с 10 другими типами.
Что важно для остальной части нашего анализа, ip6_pktopts группа сигнатур не используется совместно с каким-либо другим типом, поэтому нет другой структуры, которая гарантированно всегда будет размещаться в одном и том же сегменте. Если бы в той же группе сигнатур был другой тип, что и ip6_pktopts, то этот тип был бы основным кандидатом на перераспределение, поскольку его всегда можно было бы использовать для перераспределения ip6_pktopts. Однако совместное использование группы сигнатур также будет означать, что две структуры имеют указатели и данные в одних и тех же слотах, что должно устранить простой способ обнаружения перекрытия указателя/данных между двумя структурами, предполагая, что подпись является точной.
Структурные поля и полезные перекрытия
В структуре есть много интересных полей ip6_pktopts для создания гаджетов чтения или записи:
1. ip6po_hlim( 0x8): В это 4-байтовое поле можно записать любое значение в диапазоне 0–255.
2. ip6po_pktinfo( 0x10): 20 байт по адресу, на который указывает это поле, могут быть прочитаны. Адрес может быть либо записан с 16 нулевыми байтами, за которыми следует 4-байтовое целое число примерно в диапазоне 1-25 (разрешенный диапазон может варьироваться), либо адрес может быть освобожден в кучу данных с помощью kfree_data(pktinfo, 32), что освобождает зону data.kalloc.32.
3. ip6po_nhi_nexthop( 0x18): количество байтов, зависящее от данных, по адресу, указанному в этом поле, может быть прочитано. С привилегиями root адрес может быть освобожден с помощью вызова, kfree_data_addr(nexthop)а при необходимости поле может быть перезаписано указателем на новое выделение, содержащее данные, контролируемые злоумышленником.
4. ip6po_hbh( 0x58): Это поле ведет себя аналогично ip6po_nhi_nexthop.
5. ip6po_dest1( 0x60): Это поле ведет себя аналогично ip6po_nhi_nexthop.
6. ip6po_rhi_rthdr( 0x68): Это поле ведет себя аналогично ip6po_nhi_nexthop.
7. ip6po_dest2( 0xa8): Это поле ведет себя аналогично ip6po_nhi_nexthop.
8. ip6po_tclass( 0xb0): Это 4-байтовое поле может быть прочитано, если оно неотрицательно, и может быть записано с любым значением в диапазоне 0-255.
9. 10ip6po_minmtu( 0xb4): это 4-байтовое поле может быть прочитано и записано со значениями -1, 0 и 1.
10. ip6po_prefer_tempaddr( 0xb8): Это поле ведет себя аналогично ip6po_minmtu.
11. ip6po_flags( 0xbc): Это 4-байтовое поле можно частично читать и записывать побитово.
Это довольно большое количество кандидатов для построения стратегии эксплойта. И каждый из этих доступов достигается независимым системным вызовом, поэтому их можно произвольно смешивать и сопоставлять.
Здесь мы видим, как все типы и группы сигнатур соответствуют каждому из этих полей в ip6_pktopts. Мы перечисляем оба, потому что злоумышленнику важно, какие типы могут образовывать перекрытие, которое можно использовать, но распределитель назначает типы в сегменты на основе групп сигнатур, что гораздо более грубо: любые типы, принадлежащие к одной и той же группе сигнатур, всегда будут принадлежать к одному и тому же сегменту.
Мы можем видеть, например, что существует 173 различных типа C/C++, принадлежащих к 73 различным группам сигнатур, с указателем («1») в поле, которое перекрывает ip6po_pktinfo. Почему это полезно? Он показывает, какие поля с большей вероятностью будут иметь полезные коллизии. Например, если каждый другой тип в пуле ip6_pktopts имеет указатель в грануле перекрытия ip6po_pktinfo, всегда будет безопасно разыменовать это поле. Используя информацию в таблице, мы можем вычислить, что существует 23%-ная вероятность того, что расположение корзин гарантирует, что разыменование ip6po_pktinfo всегда будет безопасным.
Эксплуатационные свойства
Несколько вещей выделяются, когда вы начинаете анализировать эти поля для эксплуатации.
Во-первых, существует так много вариантов как для прямого, так и для косвенного чтения, что мы великодушно предположим, что злоумышленник всегда может создать произвольный примитив чтения ядра и, в частности, что злоумышленник может определить, какие типы совместно используют бакет с ip6_pktopts.
Далее, примитивы прямой записи (те, которые записывают в ip6_pktopts само выделение, а не через указатель внутри этого выделения) не подходят для манипулирования перекрывающимися указателями: поле ip6po_flags может сдвигать указатели не менее чем на 4 ГБ, что кажется слишком грубым и многообещающим в соответствии с расположением памяти ядра, в то время как все другие поля прямой записи ограничены по значению, так что 3 из 8 байтов результирующего указателя не будут контролироваться.
Тем не менее, это поле ip6po_hlim интересно тем, что оно перекрывает retainCountполе OSObject, которое является базовым типом почти для всех объектов C++ в ядре. Перераспределение ip6_pktoptsс любым типом OSObject на основе и запись в ip6po_hlim может привести к появлению нового UAF в этом OSObject, что может позволить перенести UAF в другое ведро.
Но наиболее многообещающим способом построения чтения/записи являются 6 непрямых примитивов. Примитив ip6po_pktinf пoможет либо обнулить примерно 20 байтов памяти, либо освободить указатель data.kalloc.32 , в то время как другие примитивы могут освободить и заменить указатель data.kallo (возможно, NULL) новым выделением, содержащим контролируемые данные.
Кандидаты на замену в XNU
В качестве грубого приближения для набора всех 194 типов в приведенной ниже таблице показаны все основные типы XNU, которые когда-либо могли совмещаться, ip6_pktopts а также поля каждого из них, которые потенциально могут быть полезны для эксплойта. Столбец «Allocatable» описывает, может ли пространство пользователя выделять экземпляры этого типа, а столбец «Privileges» описывает любые специальные привилегии, необходимые для этого. Другие столбцы представляют поля ip6_pktopts, а цвет каждой ячейки показывает, как путаница типов будет манипулировать перекрывающимся полем замещающего типа. Все пустые ячейки считались неинтересными.
В качестве примера рассмотрим смещение таргетинга 0x10 в necp_client_flow_registration. Примитив ip6_pktopts, которым мы должны управлять смещением 0x10 позволяет нам либо прочитать 20 байтов по адресу, хранящемуся в этом поле, либо записать 20 неуправляемых байтов по этому адресу, либо освободить этот адрес в куче данных. Смещение объекта 0x10 necp_client_flow_registration — это красно-черная запись дерева, fd_link.rbe_parent которая является указателем на другой объект necp_client_flow_registration. Мы не можем использовать эту путаницу типов напрямую как произвольное чтение, потому что мы не контролируем значение этого указателя: он будет установлен в допустимую красно-черную запись дерева при создании объекта. Мы также не получаем каких-либо полезных эффектов при использовании примитива записи, поскольку первые 20 байтов содержат только другие красно-черные элементы дерева, а наш примитив записи не может создать допустимое значение указателя. Наконец, свободный примитив не будет работать, потому что мы освободим указатель kalloc_type на зонуdata.kalloc.32, принадлежащий владельцу, что вызовет панику. Вот почему ячейка в столбце 0x10 окрашена в оранжевый цвет. Напротив, ячейка в столбце 0x18 зеленая, потому что есть правдоподобный путь эксплуатации: если fd_link.rbe_left уже равен нулю, то примитив свободного и перераспределения с контролируемыми данными, который у нас есть для смещения 0x18 можно использовать для вставки подделки necp_client_flow_registration в красно-черное дерево.
Самые интересные поля — это синее и зеленое. Синий указывает, что значением поля потенциально можно управлять, чтобы оно содержало указатель, а зеленый указывает, что полем потенциально можно манипулировать с помощью примитива записи. Ни один из цветов не означает, что это поле определенно можно использовать для создания примитива чтения или записи ядра: многие поля окажутся тупиковыми из-за ограничений, налагаемых окружающим кодом. Однако цвета должны давать верхнюю границу возможностей, получаемых при нацеливании на каждое поле каждого типа.
Давайте посмотрим на некоторые статистические данные из этой таблицы.
- 13 из 18 типов XNU (72%) являются потенциально выделяемыми. 7 из них (39%), вероятно, могут быть размещены способом, способствующим heap spraying. 4 из них (22%), скорее всего, не нуждаются в каких-либо особых привилегиях для их размещения.
- 6 из 12 кандидатов на замену (50%) имеют какое-то поле, которое потенциально можно использовать для создания произвольного гаджета чтения. 5 из них (42%), вероятно, можно распылять, а 2 из них (17%) не нуждаются в привилегиях.
- У 7 из 12 кандидатов на замену (58%) есть какое-то поле, которое потенциально можно использовать для создания гаджета записи. 3 из них (25%), вероятно, можно распылять, а 2 из них (17%) не нуждаются в привилегиях для распределения. Однако, если мы также учтем, что для изменения некоторых полей ip6_pktopts требуются привилегии суперпользователя, число кандидатов на замену, которым не нужны специальные привилегии для потенциального создания примитива записи, упадет до 1 (8%).
Таблица и статистика выше, вероятно, являются верхними границами для создания фактического эксплойта. Например, IOPowerConnectionобъекты следует выделять только тогда, когда к системе подключается новое устройство, требующее питания, поэтому удаленному злоумышленнику будет очень сложно их выделить. Кроме того, маловероятно, что изменение смещения retainCountat 0x8 такого объекта будет действительно полезным, учитывая то, как используется этот тип.
Другая сложность заключается в том, что доступ к некоторым типам и полям возможен только с повышенными привилегиями. Например, оба типа dn_flow_setи dn_flow_queue являются частью функции dummynet (см. Ресурсы dnctl(8)), которая требует привилегий root. И весь вертикальный средний блок таблицы, соответствующий смещениям 0x18 насквозь 0xa8, требует привилегий суперпользователя для выполнения гаджета записи через ip6_pktopts, поэтому и строки, и столбцы таблицы выглядят гораздо более разреженными для непривилегированного злоумышленника.
Как может выглядеть надежная эксплуатация?
Чтобы упростить анализ, мы предположим, что набор кандидатов на замену из ядра XNU представляет полноразмерный класс для всего кэша ядра. Это приблизительное значение, но мы считаем его консервативным, поскольку большинство типов IOKit должны иметь более ограниченную доступность, чем ядро XNU.
В самой широкой интерпретации, которая служит верхней границей, мы ожидаем, что 68 из 194 типов в кэше ядра предложат потенциально полезный примитив чтения, а 80 типов предложат потенциальный путь для превращения гаджета записи в дальнейшее повреждение памяти. Группы сигнатур для этих типов случайным образом распределяются между 11 сегментами класса размера. Нет никаких преимуществ в том, чтобы иметь несколько уязвимых типов в группе сигнатур, поэтому вместо этого мы сосредоточимся на количестве групп сигнатур, которые можно использовать. Основываясь на размере каждой группы сигнатур в этом классе размеров, 68 типов с полезными перекрытиями указателей и данных в среднем попадают в 44 группы сигнатур, а 80 типов, которые могут создавать примитив записи, попадают в 49 групп.
Конечно, мы ожидаем, что реальное количество достижимых типов на практике будет намного меньше. Если мы экстраполируем на основе количества типов XNU, которые с достаточной вероятностью могут быть распылены, вместо этого мы ожидаем, что будет 38 групп сигнатур, содержащих типы, которые предлагают потенциальные примитивы чтения, и 26 групп сигнатур, содержащих типы, которые предлагают потенциальные примитивы записи. А если рассматривать атаку из непривилегированного положения, то это еще больше сокращается до 19 групп сигнатур с потенциальными примитивами чтения и 10 групп сигнатур с потенциальными примитивами записи.
Теперь, сколько типов-кандидатов на замену нам потребуется, чтобы получить фиксированную вероятность того, что один из кандидатов делит бакеет с ip6_pktopts? Если мы посчитаем, нам понадобится 15 кандидатов для вероятности столкновения 75% и 30 кандидатов для вероятности столкновения 95%. То есть, если вы хотите, чтобы ваш эксплойт работал успешно по крайней мере в 95% случаев, вам нужно написать код для выделения 30 различных типов из доступных групп сигнатур, повреждения 30 различных полей с помощью примитивов, а затем из каждого из этих ip6_pktopts немного отличающиеся поврежденные состояния создают полезный примитив записи ядра — и все это при том, что вы узнаете, какой из этих 30 кандидатов действительно будет работать на целевом устройстве, только после запуска эксплойта.
Консервативный анализ верхней границы предполагает, что для создания примитива записи может быть до 49 групп сигнатур, так что это теоретически достижимо. Однако, если мы возьмем более реалистичную оценку 26 групп сигнатур для типов, которые реально могут быть выделены на практике, у нас больше не будет достаточно групп сигнатур, чтобы гарантировать 95% успеха: 26 типов замещения дают только 92% вероятность коллизии, это означает, что наилучший из возможных эксплойтов, использующий все 26 возможных типов замены, все равно не сработает как минимум на 8% загружаемых систем. А еще хуже обстоит дело с непривилегированного положения. Без повышения привилегий каким-либо другим способом попытка использовать эту ошибку ядра дает расчетную вероятность успеха 59%, а это означает, что любой эксплойт не будет работать как минимум в 41% случаев.
Из этого анализа можно сделать три ключевых вывода.
Во-первых, несмотря на то, что существует много потенциальных типов замен, мы оцениваем, что на практике максимальная вероятность успеха, достижимая любым эксплойтом для этой ошибки, ограничена около 92%. Иными словами, неотъемлемые ограничения типов, которые могут быть выделены, и типов, которые можно использовать для замены выделения, ip6_pktopts означают, что самый лучший эксплойт, использующий все доступные возможности, по-прежнему, вероятно, потерпит неудачу примерно в 8% загружаемых систем.
Во-вторых, kalloc_type и песочница дополняют друг друга и значительно усложняют использование этой ошибки с непривилегированного положения. По нашим оценкам, вероятность успеха непривилегированного эксплойта может быть ограничена примерно 59%. Для злоумышленников, стремящихся надежно использовать UAF ядра, может быть более привлекательно сначала попытаться выйти из песочницы, чтобы получить доступ к более широкому набору типов замены.
В-третьих, типы XNU не казались настолько похожими, что существовал очевидный способ создания обобщенной техники эксплойта, которая была бы общей для нескольких типов замены. Создателю эксплойта, вероятно, придется разработать около 15 отдельных стратегий использования эксплойтов, чтобы иметь возможность использовать все 15 типов замен, необходимых для достижения 75% надежности. Разработка 15 стратегий эксплойтов с разными типами замены для одной и той же ошибки не будет такой трудоемкой, как написание 15 отдельных эксплойтов, поскольку некоторый код можно будет повторно использовать в разных стратегиях, и потоки эксплойтов в конечном итоге сойдутся на общих примитивах. Тем не менее, это представляет собой радикальное и потенциально недопустимое увеличение объема необходимых усилий по разработке эксплойтов по сравнению с эксплуатацией системы pre-kalloc_type, где эксплойты иногда могут повторно использоваться стратегии эксплойтов и потоки эксплойтов даже через разные уязвимости.
Заключение
Уязвимость SockPuppet значительно сложнее использовать под kalloc_type. С рандомизированным группированием kalloc_type единственные известные нам надежные стратегии эксплойтов требуют написания нескольких разных эксплойтов, а затем динамического выбора того, какой из них развертывать, на основе назначения бакета во время загрузки уязвимых типов. Кроме того, мы ожидаем, что на практике недостаточно достижимых типов замены, чтобы гарантировать высокую надежность, особенно без предварительного выхода из песочницы.
Помимо SockPuppet, этот анализ подтверждает, что коллизии сигнатур являются важным фактором в понимании возможности эксплуатации данного UAF и что нацеливание на перекрытие указателя, вероятно, будет практичным. Для UAF в классах с разреженным размером мы ожидаем, что практически достижимая вероятность успеха эксплойта, вероятно, будет ограничена количеством доступных типов замены, и что песочница, в частности, является сильным ограничивающим фактором. Конечно, коллизии сигнатур, подкарта данных и UAF внутри типа — все это по-прежнему привлекательные цели для kalloc_type. Однако мы ожидаем, что дальнейшее внедрение и усовершенствование kalloc_type сделает эксплуатацию большинства уязвимостей ядра, связанных с использованием после освобождения, непривлекательной.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://security.apple.com/blog/what-if-we-had-sockpuppet-in-ios16/
Об уязвимости SockPuppet сообщил в 2019 году исследователь Нед Уильямсон из Google Project Zero. Он присутствовал в нескольких версиях iOS 12 и был исправлен в iOS 12.3 и iOS 12.4.1 . Эта уязвимость представляет собой особенно полезный пример для нашей работы по усилению защиты распределителя ядра, поскольку многие аспекты этой ошибки работали в пользу злоумышленника: триггер UAF и время жизни повторного использования полностью контролировались злоумышленником, структура UAF предлагала множество путей для эксплуатации, а встроенное раскрытие памяти сделало очистку кучи чрезвычайно стабильной.
Проанализировав доступные типы перераспределения в kalloc_type, мы считаем, что было бы значительно сложнее создать даже полунадежный эксплойт для SockPuppet, если бы он все еще присутствовал в iOS 16. Традиционный подход с использованием одного типа перераспределения имел бы всего 8%. уровень успеха при kalloc_type, и наш анализ показывает, что злоумышленники столкнутся с фундаментальным ограничением надежности для многих уязвимостей использования после освобождения. Например, мы подсчитали, что SockPuppet имеет встроенный предел успеха эксплойта около 92%, а это означает, что даже самый лучший эксплойт, который выбирает, какую стратегию эксплойта использовать на основе жизнеспособных типов перераспределения, будет давать сбой примерно в 8% случаев. Поскольку исходный эксплойт SockPuppet использовал только одну стратегию эксплойта и был почти на 100% надежен,
Уязвимость SockPuppet
Уязвимость SockPuppet представляла собой функцию использования после освобождения в in6_pcbdetach() функции ядра XNU, которая была доступна через серию системных вызовов, связанных с сокетами. Эта функция не смогла очистить поле inp->in6p_outputopts после его освобождения и оставила сокет с оборванным указателем на структуру ip6_pktopts. В результате различные операции getsockopt()и setsockopt() могли выполнять ограниченное чтение и запись в освобожденный объект ip6_pktopts, а также чтение и запись через указатели, разыменованные из освобожденного объекта.
Важно отметить, что ip6_pktopts с помощью уязвимости структуру можно было освободить только один раз, а это означало, что все повреждения памяти должны были происходить путем манипулирования освобожденной памятью через представление ip6_pktopts.
Оригинальная стратегия эксплойта
Эксплойт SockPuppet использовал следующие основные примитивы, предоставляемые уязвимостью, и системные вызовы, связанные с сокетами:
getsockopt(IPV6_USE_MIN_MTU): этот системный вызов будет читать 4-байтовое поле ip6po_minmtu со смещением 0xb4в ip6_pktoptsобъекте.
getsockopt(IPV6_PREFER_TEMPADDR): этот системный вызов будет читать 4-байтовое поле ip6po_prefer_tempaddr со смещением 0xb8 в объекте ip6_pktopts.
getsockopt(IPV6_PKTINFO): этот системный вызов будет считывать 20 байтов с адреса, на который указывает ip6po_pktinfoполе со смещением 0x10 в объекте ip6_pktopts.
setsockopt(IPV6_PKTINFO): этот системный вызов освободит указатель ip6po_pktinfo по смещению 0x10в ip6_pktoptsобъекте.
Из этих базовых примитивов эксплойт SockPuppet создал следующие возможности:
1. Раскрытие адреса произвольного порта Mach: эксплойт создал сокет с полем ip6_pktopt, а затем распространил большое количество сообщений Mach, содержащих вне очереди массивы портов Mach того же размера, что и структура ip6_pktopts. Этот спрей перераспределил висячий ip6_pktopts объект с помощью одного из этих массивов портов вне очереди. При заполнении массива портов сообщения Mach в пользовательском пространстве множеством ссылок на целевой порт Mach поля ip6po_minmtu и ip6po_prefer_tempaddr будут перекрывать верхнюю и нижнюю половины двух соседних указателей соответственно. Вызов getsockopt(IPV6_USE_MIN_MTU) и getsockopt(IPV6_PREFER_TEMPADDR) для чтения этих полей позволили эксплойту восстановить полный адрес порта Mach.
2. Чтение 20 байт по произвольному адресу ядра: эксплойт создал сокет с зависшим полем ip6_pktopts, а затем распылил большое количество буферов OSData того же размера, что и структура ip6_pktopts. Этот спрей переместил висячий объект ip6_pktopts с помощью одного из этих буферов OSData. Установив смещение 0x10 в буфере данных на целевой адрес и смещение 0xb4 на магическое значение, эксплойт смог полностью контролировать значения полей ip6_pktopt' ip6po_pktinfoи ip6po_minmtu. Затем эксплойт вызывался getsockopt(IPV6_USE_MIN_MTU), чтобы проверить, что ip6po_minmtu действительно содержит магическое значение, которое указывало, что зависшая структура ip6_pktopts была успешно перераспределена и, таким образом, ip6po_pktinfo содержала целевой адрес. Наконец, эксплойт под вызывает getsockopt(IPV6_PKTINFO) чтобы скопироватьскопировать 20 байт с этого адреса в пользовательское пространство.
3. Освобождение произвольного адреса ядра: этот примитив работал так же, как описанный выше примитив чтения ядра, за исключением того, что на последнем шаге вместо вызова для чтения getsockopt(IPV6_PKTINFO) с целевого адреса эксплойт вызывал setsockopt(IPV6_PKTINFO) для передачи целевого адреса kfree() функции XNU.
Первый примитив основывался на перераспределении зависщей структуры ip6_pktopts с внешним массивом портов Mach, который представляет собой массив указателей. Второй и третий примитивы полагались на перераспределение ip6_pktopts структуры с буфером OSData, и происходит выделение данных без указателей.
И теперь мы начинаем понимать, как может помочь kalloc_type. Начиная с iOS 16 массивы внешних портов Mach принадлежат зоне kalloc_type_var, а буферы OSData выделяются в подкарте данных. Ни один из вышеперечисленных примитивов не будет работать под kalloc_type, потому что замещающие выделения обслуживаются из отдельных частей виртуального адресного пространства, а не выделения ip6_pktopts. И даже если бы у нас был какой-то другой механизм для создания произвольного свободного примитива, kfree_type()теперь он расширяется до вызова zfree(kt_view, ptr), который освобождает «произвольный» указатель на определенную зону и вызывает панику, если переданный указатель не из этой зоны.
Ландшафт kalloc_type
Ломать эксплойт-примитив бесполезно, если есть простой способ перестроить примитив, используя слегка измененную технику, например, нацелившись на другое поле в структуре UAF или выбрав другое размещение. По-настоящему эффективная защита ограничила бы возможность использования уязвимости SockPuppet для всех стратегий эксплойтов, а не только для той, что указана в исходном отчете. Давайте оценим, чего в этом плане достигает kalloc_type.
Мы можем представить, что уязвимость SockPuppet присутствует в бета-версии iOS 16 1 (сборка 20A5283p) — после того, как был введен kalloc_type — и повторить анализ базовых примитивов, которые мы получаем, управляя различными полями завсишей структуры посредством перераспределения ip6_pktopts. Мы упрощаем наш анализ для краткости, и мы ошибемся, предположив, что что-то можно использовать, если мы не можем показать, что это не так.
Структура ip6_pktopts имеет размер ровно 192 ( 0xc0) байта и будет назначена обычной зоне kalloc_type, такой как kalloc.typeN.192. На iPhone 13 Pro под управлением iOS 16 beta 1 имеется 194 обычных (не переменной длины) типа в этом классе размеров, среди которых 119 уникальных подписей и 90 групп подписей. 90 сигнатурных групп равномерно распределены по 11 зонам, названным kalloc.type0.192 через kalloc.type10.192. А поскольку метаданные распределителя для kalloc_type находятся полностью за пределами выделенной области, эти 194 типа — единственные вещи, которые могут когда-либо совмещаться со структурой ip6_pktopts.
В этом конкретном случае ip6_pktoptsструктура попала в корзину 8 вместе с 10 другими типами.
Что важно для остальной части нашего анализа, ip6_pktopts группа сигнатур не используется совместно с каким-либо другим типом, поэтому нет другой структуры, которая гарантированно всегда будет размещаться в одном и том же сегменте. Если бы в той же группе сигнатур был другой тип, что и ip6_pktopts, то этот тип был бы основным кандидатом на перераспределение, поскольку его всегда можно было бы использовать для перераспределения ip6_pktopts. Однако совместное использование группы сигнатур также будет означать, что две структуры имеют указатели и данные в одних и тех же слотах, что должно устранить простой способ обнаружения перекрытия указателя/данных между двумя структурами, предполагая, что подпись является точной.
Структурные поля и полезные перекрытия
В структуре есть много интересных полей ip6_pktopts для создания гаджетов чтения или записи:
1. ip6po_hlim( 0x8): В это 4-байтовое поле можно записать любое значение в диапазоне 0–255.
2. ip6po_pktinfo( 0x10): 20 байт по адресу, на который указывает это поле, могут быть прочитаны. Адрес может быть либо записан с 16 нулевыми байтами, за которыми следует 4-байтовое целое число примерно в диапазоне 1-25 (разрешенный диапазон может варьироваться), либо адрес может быть освобожден в кучу данных с помощью kfree_data(pktinfo, 32), что освобождает зону data.kalloc.32.
3. ip6po_nhi_nexthop( 0x18): количество байтов, зависящее от данных, по адресу, указанному в этом поле, может быть прочитано. С привилегиями root адрес может быть освобожден с помощью вызова, kfree_data_addr(nexthop)а при необходимости поле может быть перезаписано указателем на новое выделение, содержащее данные, контролируемые злоумышленником.
4. ip6po_hbh( 0x58): Это поле ведет себя аналогично ip6po_nhi_nexthop.
5. ip6po_dest1( 0x60): Это поле ведет себя аналогично ip6po_nhi_nexthop.
6. ip6po_rhi_rthdr( 0x68): Это поле ведет себя аналогично ip6po_nhi_nexthop.
7. ip6po_dest2( 0xa8): Это поле ведет себя аналогично ip6po_nhi_nexthop.
8. ip6po_tclass( 0xb0): Это 4-байтовое поле может быть прочитано, если оно неотрицательно, и может быть записано с любым значением в диапазоне 0-255.
9. 10ip6po_minmtu( 0xb4): это 4-байтовое поле может быть прочитано и записано со значениями -1, 0 и 1.
10. ip6po_prefer_tempaddr( 0xb8): Это поле ведет себя аналогично ip6po_minmtu.
11. ip6po_flags( 0xbc): Это 4-байтовое поле можно частично читать и записывать побитово.
Это довольно большое количество кандидатов для построения стратегии эксплойта. И каждый из этих доступов достигается независимым системным вызовом, поэтому их можно произвольно смешивать и сопоставлять.
Здесь мы видим, как все типы и группы сигнатур соответствуют каждому из этих полей в ip6_pktopts. Мы перечисляем оба, потому что злоумышленнику важно, какие типы могут образовывать перекрытие, которое можно использовать, но распределитель назначает типы в сегменты на основе групп сигнатур, что гораздо более грубо: любые типы, принадлежащие к одной и той же группе сигнатур, всегда будут принадлежать к одному и тому же сегменту.
Мы можем видеть, например, что существует 173 различных типа C/C++, принадлежащих к 73 различным группам сигнатур, с указателем («1») в поле, которое перекрывает ip6po_pktinfo. Почему это полезно? Он показывает, какие поля с большей вероятностью будут иметь полезные коллизии. Например, если каждый другой тип в пуле ip6_pktopts имеет указатель в грануле перекрытия ip6po_pktinfo, всегда будет безопасно разыменовать это поле. Используя информацию в таблице, мы можем вычислить, что существует 23%-ная вероятность того, что расположение корзин гарантирует, что разыменование ip6po_pktinfo всегда будет безопасным.
Эксплуатационные свойства
Несколько вещей выделяются, когда вы начинаете анализировать эти поля для эксплуатации.
Во-первых, существует так много вариантов как для прямого, так и для косвенного чтения, что мы великодушно предположим, что злоумышленник всегда может создать произвольный примитив чтения ядра и, в частности, что злоумышленник может определить, какие типы совместно используют бакет с ip6_pktopts.
Далее, примитивы прямой записи (те, которые записывают в ip6_pktopts само выделение, а не через указатель внутри этого выделения) не подходят для манипулирования перекрывающимися указателями: поле ip6po_flags может сдвигать указатели не менее чем на 4 ГБ, что кажется слишком грубым и многообещающим в соответствии с расположением памяти ядра, в то время как все другие поля прямой записи ограничены по значению, так что 3 из 8 байтов результирующего указателя не будут контролироваться.
Тем не менее, это поле ip6po_hlim интересно тем, что оно перекрывает retainCountполе OSObject, которое является базовым типом почти для всех объектов C++ в ядре. Перераспределение ip6_pktoptsс любым типом OSObject на основе и запись в ip6po_hlim может привести к появлению нового UAF в этом OSObject, что может позволить перенести UAF в другое ведро.
Но наиболее многообещающим способом построения чтения/записи являются 6 непрямых примитивов. Примитив ip6po_pktinf пoможет либо обнулить примерно 20 байтов памяти, либо освободить указатель data.kalloc.32 , в то время как другие примитивы могут освободить и заменить указатель data.kallo (возможно, NULL) новым выделением, содержащим контролируемые данные.
Кандидаты на замену в XNU
В качестве грубого приближения для набора всех 194 типов в приведенной ниже таблице показаны все основные типы XNU, которые когда-либо могли совмещаться, ip6_pktopts а также поля каждого из них, которые потенциально могут быть полезны для эксплойта. Столбец «Allocatable» описывает, может ли пространство пользователя выделять экземпляры этого типа, а столбец «Privileges» описывает любые специальные привилегии, необходимые для этого. Другие столбцы представляют поля ip6_pktopts, а цвет каждой ячейки показывает, как путаница типов будет манипулировать перекрывающимся полем замещающего типа. Все пустые ячейки считались неинтересными.
В качестве примера рассмотрим смещение таргетинга 0x10 в necp_client_flow_registration. Примитив ip6_pktopts, которым мы должны управлять смещением 0x10 позволяет нам либо прочитать 20 байтов по адресу, хранящемуся в этом поле, либо записать 20 неуправляемых байтов по этому адресу, либо освободить этот адрес в куче данных. Смещение объекта 0x10 necp_client_flow_registration — это красно-черная запись дерева, fd_link.rbe_parent которая является указателем на другой объект necp_client_flow_registration. Мы не можем использовать эту путаницу типов напрямую как произвольное чтение, потому что мы не контролируем значение этого указателя: он будет установлен в допустимую красно-черную запись дерева при создании объекта. Мы также не получаем каких-либо полезных эффектов при использовании примитива записи, поскольку первые 20 байтов содержат только другие красно-черные элементы дерева, а наш примитив записи не может создать допустимое значение указателя. Наконец, свободный примитив не будет работать, потому что мы освободим указатель kalloc_type на зонуdata.kalloc.32, принадлежащий владельцу, что вызовет панику. Вот почему ячейка в столбце 0x10 окрашена в оранжевый цвет. Напротив, ячейка в столбце 0x18 зеленая, потому что есть правдоподобный путь эксплуатации: если fd_link.rbe_left уже равен нулю, то примитив свободного и перераспределения с контролируемыми данными, который у нас есть для смещения 0x18 можно использовать для вставки подделки necp_client_flow_registration в красно-черное дерево.
Самые интересные поля — это синее и зеленое. Синий указывает, что значением поля потенциально можно управлять, чтобы оно содержало указатель, а зеленый указывает, что полем потенциально можно манипулировать с помощью примитива записи. Ни один из цветов не означает, что это поле определенно можно использовать для создания примитива чтения или записи ядра: многие поля окажутся тупиковыми из-за ограничений, налагаемых окружающим кодом. Однако цвета должны давать верхнюю границу возможностей, получаемых при нацеливании на каждое поле каждого типа.
Давайте посмотрим на некоторые статистические данные из этой таблицы.
- 13 из 18 типов XNU (72%) являются потенциально выделяемыми. 7 из них (39%), вероятно, могут быть размещены способом, способствующим heap spraying. 4 из них (22%), скорее всего, не нуждаются в каких-либо особых привилегиях для их размещения.
- 6 из 12 кандидатов на замену (50%) имеют какое-то поле, которое потенциально можно использовать для создания произвольного гаджета чтения. 5 из них (42%), вероятно, можно распылять, а 2 из них (17%) не нуждаются в привилегиях.
- У 7 из 12 кандидатов на замену (58%) есть какое-то поле, которое потенциально можно использовать для создания гаджета записи. 3 из них (25%), вероятно, можно распылять, а 2 из них (17%) не нуждаются в привилегиях для распределения. Однако, если мы также учтем, что для изменения некоторых полей ip6_pktopts требуются привилегии суперпользователя, число кандидатов на замену, которым не нужны специальные привилегии для потенциального создания примитива записи, упадет до 1 (8%).
Таблица и статистика выше, вероятно, являются верхними границами для создания фактического эксплойта. Например, IOPowerConnectionобъекты следует выделять только тогда, когда к системе подключается новое устройство, требующее питания, поэтому удаленному злоумышленнику будет очень сложно их выделить. Кроме того, маловероятно, что изменение смещения retainCountat 0x8 такого объекта будет действительно полезным, учитывая то, как используется этот тип.
Другая сложность заключается в том, что доступ к некоторым типам и полям возможен только с повышенными привилегиями. Например, оба типа dn_flow_setи dn_flow_queue являются частью функции dummynet (см. Ресурсы dnctl(8)), которая требует привилегий root. И весь вертикальный средний блок таблицы, соответствующий смещениям 0x18 насквозь 0xa8, требует привилегий суперпользователя для выполнения гаджета записи через ip6_pktopts, поэтому и строки, и столбцы таблицы выглядят гораздо более разреженными для непривилегированного злоумышленника.
Как может выглядеть надежная эксплуатация?
Чтобы упростить анализ, мы предположим, что набор кандидатов на замену из ядра XNU представляет полноразмерный класс для всего кэша ядра. Это приблизительное значение, но мы считаем его консервативным, поскольку большинство типов IOKit должны иметь более ограниченную доступность, чем ядро XNU.
В самой широкой интерпретации, которая служит верхней границей, мы ожидаем, что 68 из 194 типов в кэше ядра предложат потенциально полезный примитив чтения, а 80 типов предложат потенциальный путь для превращения гаджета записи в дальнейшее повреждение памяти. Группы сигнатур для этих типов случайным образом распределяются между 11 сегментами класса размера. Нет никаких преимуществ в том, чтобы иметь несколько уязвимых типов в группе сигнатур, поэтому вместо этого мы сосредоточимся на количестве групп сигнатур, которые можно использовать. Основываясь на размере каждой группы сигнатур в этом классе размеров, 68 типов с полезными перекрытиями указателей и данных в среднем попадают в 44 группы сигнатур, а 80 типов, которые могут создавать примитив записи, попадают в 49 групп.
Конечно, мы ожидаем, что реальное количество достижимых типов на практике будет намного меньше. Если мы экстраполируем на основе количества типов XNU, которые с достаточной вероятностью могут быть распылены, вместо этого мы ожидаем, что будет 38 групп сигнатур, содержащих типы, которые предлагают потенциальные примитивы чтения, и 26 групп сигнатур, содержащих типы, которые предлагают потенциальные примитивы записи. А если рассматривать атаку из непривилегированного положения, то это еще больше сокращается до 19 групп сигнатур с потенциальными примитивами чтения и 10 групп сигнатур с потенциальными примитивами записи.
Теперь, сколько типов-кандидатов на замену нам потребуется, чтобы получить фиксированную вероятность того, что один из кандидатов делит бакеет с ip6_pktopts? Если мы посчитаем, нам понадобится 15 кандидатов для вероятности столкновения 75% и 30 кандидатов для вероятности столкновения 95%. То есть, если вы хотите, чтобы ваш эксплойт работал успешно по крайней мере в 95% случаев, вам нужно написать код для выделения 30 различных типов из доступных групп сигнатур, повреждения 30 различных полей с помощью примитивов, а затем из каждого из этих ip6_pktopts немного отличающиеся поврежденные состояния создают полезный примитив записи ядра — и все это при том, что вы узнаете, какой из этих 30 кандидатов действительно будет работать на целевом устройстве, только после запуска эксплойта.
Консервативный анализ верхней границы предполагает, что для создания примитива записи может быть до 49 групп сигнатур, так что это теоретически достижимо. Однако, если мы возьмем более реалистичную оценку 26 групп сигнатур для типов, которые реально могут быть выделены на практике, у нас больше не будет достаточно групп сигнатур, чтобы гарантировать 95% успеха: 26 типов замещения дают только 92% вероятность коллизии, это означает, что наилучший из возможных эксплойтов, использующий все 26 возможных типов замены, все равно не сработает как минимум на 8% загружаемых систем. А еще хуже обстоит дело с непривилегированного положения. Без повышения привилегий каким-либо другим способом попытка использовать эту ошибку ядра дает расчетную вероятность успеха 59%, а это означает, что любой эксплойт не будет работать как минимум в 41% случаев.
Из этого анализа можно сделать три ключевых вывода.
Во-первых, несмотря на то, что существует много потенциальных типов замен, мы оцениваем, что на практике максимальная вероятность успеха, достижимая любым эксплойтом для этой ошибки, ограничена около 92%. Иными словами, неотъемлемые ограничения типов, которые могут быть выделены, и типов, которые можно использовать для замены выделения, ip6_pktopts означают, что самый лучший эксплойт, использующий все доступные возможности, по-прежнему, вероятно, потерпит неудачу примерно в 8% загружаемых систем.
Во-вторых, kalloc_type и песочница дополняют друг друга и значительно усложняют использование этой ошибки с непривилегированного положения. По нашим оценкам, вероятность успеха непривилегированного эксплойта может быть ограничена примерно 59%. Для злоумышленников, стремящихся надежно использовать UAF ядра, может быть более привлекательно сначала попытаться выйти из песочницы, чтобы получить доступ к более широкому набору типов замены.
В-третьих, типы XNU не казались настолько похожими, что существовал очевидный способ создания обобщенной техники эксплойта, которая была бы общей для нескольких типов замены. Создателю эксплойта, вероятно, придется разработать около 15 отдельных стратегий использования эксплойтов, чтобы иметь возможность использовать все 15 типов замен, необходимых для достижения 75% надежности. Разработка 15 стратегий эксплойтов с разными типами замены для одной и той же ошибки не будет такой трудоемкой, как написание 15 отдельных эксплойтов, поскольку некоторый код можно будет повторно использовать в разных стратегиях, и потоки эксплойтов в конечном итоге сойдутся на общих примитивах. Тем не менее, это представляет собой радикальное и потенциально недопустимое увеличение объема необходимых усилий по разработке эксплойтов по сравнению с эксплуатацией системы pre-kalloc_type, где эксплойты иногда могут повторно использоваться стратегии эксплойтов и потоки эксплойтов даже через разные уязвимости.
Заключение
Уязвимость SockPuppet значительно сложнее использовать под kalloc_type. С рандомизированным группированием kalloc_type единственные известные нам надежные стратегии эксплойтов требуют написания нескольких разных эксплойтов, а затем динамического выбора того, какой из них развертывать, на основе назначения бакета во время загрузки уязвимых типов. Кроме того, мы ожидаем, что на практике недостаточно достижимых типов замены, чтобы гарантировать высокую надежность, особенно без предварительного выхода из песочницы.
Помимо SockPuppet, этот анализ подтверждает, что коллизии сигнатур являются важным фактором в понимании возможности эксплуатации данного UAF и что нацеливание на перекрытие указателя, вероятно, будет практичным. Для UAF в классах с разреженным размером мы ожидаем, что практически достижимая вероятность успеха эксплойта, вероятно, будет ограничена количеством доступных типов замены, и что песочница, в частности, является сильным ограничивающим фактором. Конечно, коллизии сигнатур, подкарта данных и UAF внутри типа — все это по-прежнему привлекательные цели для kalloc_type. Однако мы ожидаем, что дальнейшее внедрение и усовершенствование kalloc_type сделает эксплуатацию большинства уязвимостей ядра, связанных с использованием после освобождения, непривлекательной.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://security.apple.com/blog/what-if-we-had-sockpuppet-in-ios16/