Информация [IOS] Реверс-инжиниринг и анализ доменов защиты SPTM и TXM.

sashka123555

Участник
Автор темы
42
50

Источники.​

В частности анализировал два модуля сам, потратил где-то неделю, хоть и данная статья является введением, но укажу источники, где сам проверял свои догадки:
1. https://www.alphaxiv.org/ru/abs/2510.09272
2. https://support.apple.com/ru-ru/guide/security/sec8b776536b/web
3. https://df-f.com/blog/ios17

Про статью.​

Привет, данную статью я изначально писал для Хабра, она валялась у меня в черновиках. Последнюю неделю я активно анализировал защитные модули iOS, хотел написать огромную статью для Хабра, но решил, что лучше будет не выкладывать ее туда. Стиль в статье получился сильно академическим, потому что модерация Хабра уж сильно требовательная к оформлению статьи... Поисковики в целом любят статьи с бластхака - особенно Яндекс. На вопросы отвечу в комментариях, конечно же) Если статья будет интересна - дропну еще части. Также вместе с ментором планируем написать большие статьи на тему архитектуры iOS без воды(привет Джонатан Левин). Если какие-то термины непонятно, повторюсь, что статья писалось изначально для людей с Хабра, всегда можете уточнить либо у меня, либо у нейросети.

Введение.​

Представим ситуацию, в которой отсутствуют как PPL, так и SPTM/TXM. У злоумышленника имеется примитив записи в адресное пространство ядра XNU. В таком случае важные механизмы безопасности, включая структуры MACF и связанные с ними функции обратного вызова, фактически находятся в том же адресном пространстве ядра и не имеют отдельного аппаратно защищённого уровня изоляции. При этом именно на таких механизмах завязаны проверки, связанные с политиками безопасности, в том числе контролем подписи кода и применением sandbox-политик, в результате чего наличие произвольной записи в память ядра потенциально позволяет злоумышленнику модифицировать соответствующие структуры и указатели на каллбек-функции - вплоть до их обнуления или подмены. Иными словами, получив примитив записи в ядро, атакующий получает возможность напрямую вмешиваться в реализацию политик безопасности, поскольку сама эта логика размещена внутри защищаемого лишь на уровне EL1 адресного пространства ядра.

До iOS 17 в системе использовался механизм защиты PPL (Page Protected Layer -защищённый слой страниц). Это был программно-аппаратный механизм безопасности, предназначенный для изоляции критически важных таблиц страниц памяти, благодаря этому даже скомпрометированное ядро не могло напрямую изменять защищённые области памяти и тем самым вмешиваться в механизмы, отвечающие за безопасность системы. Но у PPL была принципиальная проблема - несмотря на наличие дополнительного уровня защиты, сам механизм выполнялся в контексте ядра. Это означало, что ошибки в его реализации или логике взаимодействия с ядром потенциально могли привести к обходу предусмотренных ограничений. Подобные проблемы действительно находили исследователи, в том числе в виде CVE, связанных с обходом или нарушением механизмов защиты памяти.

CVE-2023-38606 - наиболее показательный пример. Уязвимость позволяла через аппаратные MMIO-регистры получить запись в физическую память и обойти PPL. Она использовалась в цепочке Operation Triangulation и была исправлена в iOS 16.6.

В результате в iOS 17 Apple решила существенно пересмотреть архитектуру защиты таблиц страниц и заменить PPL двумя новыми механизмами защиты:

1. TXM (Trust Exuction Monitor) - привилегированный компонент, выполняющий проверки и операции, связанные с защищёнными механизмами безопасности системы;
2. SPTM (Secure Page Table Monitor) - механизм, отвечающий за контроль операций с таблицами страниц и критически важными областями памяти.

После появления этих механизмов Apple существенно усложнила построение цепочек эксплуатации для разработчиков шпионского ПО. Как правило, современное шпионское ПО представляет собой не одну уязвимость, а последовательность эксплойтов, каждый из которых позволяет перейти на следующий уровень привилегий. Раньше условная цепочка могла выглядеть следующим образом:
1790435094501.png

Диаграмма демонстрирующая цепочку уязвимостей.
​
С появлением SPTM и TXM эта схема стала значительно сложнее. Даже получив примитив записи в ядро, злоумышленник уже не получает прежнего контроля над критическими структурами памяти - операции с защищёнными областями дополнительно контролируются механизмами, находящимися за пределами обычного доверенного ядра. В результате одного успешного эксплойта ядра становится недостаточно - атакующему необходимо найти способ обойти и новый уровень защиты. Именно это существенно повышает стоимость и сложность построения полноценной цепочки эксплуатации iOS.

Теперь же, по состоянию на 2026 год, согласно моему анализу iOS 26.5, механизмы, связанные с проверкой цифровой подписи кода (AMFI) и применением политик песочницы, реализованы в защищённом контексте TXM. Соответственно, обычное ядро XNU не имеет прямого доступа к защищённой памяти TXM и не может просто изменить находящиеся там структуры или функции обратного вызова. Теперь углубимся в это и изучим базу, для начала с управлением и таблиц страниц в XNU.

Управление памятью в XNU.​

Если не брать в расчет кастомные аппаратные защиты Apple, базовая концепция работы с памятью в XNU мало чем отличается от условного Linux или ядра условной NT, под капотом у нас всегда живет дуализм - физическое железо (планки ОЗУ) и виртуальные абстракции, которые ядро подсовывает процессам.
Когда компилятор собирает под iOS исполняемый файл в формате Mach-O, в его заголовках (Load Commands, если конкретнее - LC_SEGMENT_64) уже намертво прописываются базовые виртуальные адреса для каждого сегмента бинаря (__TEXT, __DATA, __LINKEDIT). Например, загрузчик видит стартовый адрес 0x1000050000. Процесс искренне считает, что ег о память начинается именно отсюда и полностью принадлежит ему.

Физический же адресный спектр оперативки в девайсах Apple жестко привязан к архитектуре конкретного SoC так называемый DRAM_BASE (исторически во многих чипах Apple A-серии он брал начало с региона 0x800000000, хотя в современных ревизиях Apple Silicon он сдвинут выше). Если бы два разных процесса попытались одновременно писать по одному и тому же жесткому адресу вроде 0x1000050000, система бы моментально схлопнулась в панику ядра из-за пересечения и затирания чужих данных.

Чтобы этого не происходило, XNU нарезает всю память на куски страницы. В iOS стандартный размер страницы равен 16 КБ (0x4000 байт в шестнадцатеричной системе), в отличие от классических 4 КБ в x86 архитектурах или старых чипах Apple до процессора A8. Переход на 16-килобайтные страницы позволил Apple снизить нагрузку на кэш TLB. Пример работы виртуальной памяти:

- Страница №1 => 0x1000050000
- Страница №2 => 0x1000054000
- Страница №3 => 0x1000058000

Спокойно можно бахнуть стандартный memcpy() размером 0xC000 байт (ровно 3 страницы), скопировав данные за один проход, и программа даже не заметит подвоха. Но если мы снимем маску виртуализации и заглянем в физические ячейки ОЗУ, то увидим, что эти три страницы раскиданы по чипу памяти в абсолютно хаотичном порядке:

- Виртуальная Страница №1 мапится на физический 0x800004000
- Виртуальная Страница №2 улетает вообще в другой конец - 0x8018C0000
- Виртуальная Страница №3 приземляется на 0x8000C4000

Процессы могут работать с непрерывными областями памяти и заранее определенными базовыми адресами. При разыменовании указателя на виртуальный адрес этот адрес преобразуется в физический, из которого затем производится чтение или запись.

Таблицы страниц. То, что так настоятельно защищает SPTM.​

Преобразованием виртуальных адресов в физические занимается MMU, думаю всем он знаком, кто хоть раз взаимодействовал с x86_64. Через эту технологию проходят все операции с памятью, конкретно он генерирует разного рода ислкючения - например, если процесс пытается прочитать адрес, которого нет в таблицах, или к которому у него нет прав. Кстати, интересно то, что MMU инициализирует непосредственно сам SPTM:
C++:
__text:FFFFFFF0270A32A0 loc_FFFFFFF0270A32A0                    ; CODE XREF: __start+2FF8↓j
__text:FFFFFFF0270A32A0                 ADD             X2, X2, #4,LSL#12
__text:FFFFFFF0270A32A4                 MOV             X13, #1
__text:FFFFFFF0270A32A8                 CMP             X13, #1
__text:FFFFFFF0270A32AC                 MOV             X11, #0x7FF000000000
__text:FFFFFFF0270A32B0                 MOV             X12, #0xFFE000000
__text:FFFFFFF0270A32B4                 CSEL            X10, X11, X12, EQ
__text:FFFFFFF0270A32B8                 AND             X10, X10, #0x7FFFFFFFFF
__text:FFFFFFF0270A32BC                 MOV             X11, #0x24 ; '$'
__text:FFFFFFF0270A32C0                 MOV             X12, #0x19
__text:FFFFFFF0270A32C4                 CSEL            X11, X11, X12, EQ
__text:FFFFFFF0270A32C8                 AND             X10, X10, X14
__text:FFFFFFF0270A32CC                 LSR             X10, X10, X11
__text:FFFFFFF0270A32D0                 LSL             X10, X10, #3
__text:FFFFFFF0270A32D4                 ADD             X10, X3, X10
__text:FFFFFFF0270A32D8                 MOV             X11, #3
__text:FFFFFFF0270A32DC                 AND             X12, X2, #0xFFFFFFFFF000
__text:FFFFFFF0270A32E0                 ORR             X12, X11, X12
__text:FFFFFFF0270A32E4                 STR             X12, [X10]
__text:FFFFFFF0270A32E8                 CMP             X5, X9
__text:FFFFFFF0270A32EC                 CSEL            X9, X5, X9, LT
__text:FFFFFFF0270A32F0                 MOV             X16, #0
__text:FFFFFFF0270A32F4                 CMP             X16, #0
__text:FFFFFFF0270A32F8                 MOV             X11, #0xFFE000000
__text:FFFFFFF0270A32FC                 MOV             X12, #0x1FFC000
__text:FFFFFFF0270A3300                 CSEL            X10, X11, X12, EQ
__text:FFFFFFF0270A3304                 MOV             X11, #0x19
__text:FFFFFFF0270A3308                 MOV             X12, #0xE
__text:FFFFFFF0270A330C                 CSEL            X11, X11, X12, EQ
__text:FFFFFFF0270A3310                 AND             X10, X10, X14
__text:FFFFFFF0270A3314                 LSR             X10, X10, X11
__text:FFFFFFF0270A3318                 LSL             X10, X10, #3
__text:FFFFFFF0270A331C                 ADD             X10, X2, X10
__text:FFFFFFF0270A3320                 MOV             X11, #0x601
__text:FFFFFFF0270A3324                 CBNZ            X16, loc_FFFFFFF0270A332C
__text:FFFFFFF0270A3328                 ORR             X11, X11, #0x40000000000000
__text:FFFFFFF0270A332C
__text:FFFFFFF0270A332C loc_FFFFFFF0270A332C                    ; CODE XREF: __start+2F9C↑j
__text:FFFFFFF0270A332C                 ORR             X11, X11, X16,LSL#1
__text:FFFFFFF0270A3330                 CMP             X16, #0
__text:FFFFFFF0270A3334                 MOV             X12, #0xFFFFFE000000
__text:FFFFFFF0270A3338                 MOV             X13, #0xFFFFFFFFC000
__text:FFFFFFF0270A333C                 CSEL            X12, X12, X13, EQ
__text:FFFFFFF0270A3340                 AND             X12, X12, X15
__text:FFFFFFF0270A3344                 ORR             X12, X11, X12
__text:FFFFFFF0270A3348                 MOV             X11, X9
__text:FFFFFFF0270A334C                 CMP             X16, #0
__text:FFFFFFF0270A3350                 MOV             X13, #0x2000000
__text:FFFFFFF0270A3354                 MOV             X17, #0x4000
__text:FFFFFFF0270A3358                 CSEL            X13, X13, X17, EQ
__text:FFFFFFF0270A335C
__text:FFFFFFF0270A335C loc_FFFFFFF0270A335C                    ; CODE XREF: __start+2FE0↓j
__text:FFFFFFF0270A335C                 STR             X12, [X10],#8
__text:FFFFFFF0270A3360                 ADD             X12, X12, X13
__text:FFFFFFF0270A3364                 SUBS            X11, X11, #1
__text:FFFFFFF0270A3368                 B.NE            loc_FFFFFFF0270A335C
__text:FFFFFFF0270A336C                 SUBS            X5, X5, X9
__text:FFFFFFF0270A3370                 B.EQ            loc_FFFFFFF0270A3384
__text:FFFFFFF0270A3374                 ADD             X14, X14, X9,LSL#25
__text:FFFFFFF0270A3378                 ADD             X15, X15, X9,LSL#25
__text:FFFFFFF0270A337C                 MOV             X9, #0x800
__text:FFFFFFF0270A3380                 B               loc_FFFFFFF0270A32A0
1790435241593.png

При попытке разыменовать указатель, например 0x1000000000, MMUвыполняет так называемый Page Table Walk (проход по таблицам страниц). В архитектуре ARM64 под управлением IOS эти таблицы представляют собой древовидную структуру, состоящую из 64-битных дескрипторов (записей), а сама конфигурация жестко завязана на выбранный Apple размер страницы (гранулу) в 16 КБ. Поскольку каждая запись в таблице занимает 8 байт, одна 16-килобайтная страница таблицы вмещает в себя ровно 2048 дескрипторов. В результате выстраивается строгая трехуровневая иерархия, которая управляет виртуальным пространством юзерленда (обычно простирающимся от 0x0 до 0x8000000000):

- Уровень 1 (L1 Descriptor): Каждая запись на этом уровне покрывает огромный регион в 64 ГБ (0x1000000000 байт) виртуальной памяти;
- Уровень 2 (L2 Descriptor): Каждая запись здесь контролирует блок размером в 32 МБ (0x2000000 байт);
- Уровень 3 (L3 Descriptor): Каждая запись указывает на конкретную физическую страницу размером 16 КБ (0x4000 байт).

301310

Диаграмма демонстрирующая процесс трансляции.​


ARM64 предоставляет MMU два основных способа интерпретации этих записей за счет младших битов дескриптора. А именно:

1. Block Mapping% Если в дескрипторе L2 выставлен тип Block, MMU прекращает дальнейший спуск по дереву, он берет базовый физический адрес из этой записи (допустим, 0x800004000) и напрямую проецирует на него весь непрерывный виртуальный кусок в 32 МБ. В таком сценарии диапазон виртуальных адресов от 0x1000000000 до 0x1002000000 монолитно отобразится на физический регион 0x800004000–0x802004000. Это экономит память под таблицы, но лишает гибкости;
2. Table Descriptor: При установке типа Table в дескриптор уровня L2, MMU считывает содержащийся в нем адрес не как указатель на физические данные процесса, а как физический адрес начала следующей таблицы в цепочке - таблицы уровня L3.

После этого MMU берет следующую группу битов из исходного виртуального адреса, использует их как индекс для поиска нужной записи уже внутри этой новой L3-таблицы и переходит к ней. Каждая запись в таблице L3 отвечает ровно за одну страницу размером 16 КБ. Так как в рамках одной L2-записи (32 МБ) помещается ровно 2048 таких страниц, L3-таблица позволяет задать индивидуальные параметры для каждого 16-килобайтного куска отдельно.
Конкретно в дескрипторе L3 процессор проверяет финальные атрибуты доступа к конкретной странице памяти: бит запрета исполнения кода (NX), а также биты прав на чтение и запись (RO/RW). Если выполняемый код попытается совершить действие, нарушающее эти атрибуты (например, записать данные в страницу с флагом RO), MMU мгновенно прервет операцию и сгенерирует аппаратное исключение.

Исключения в ARM.​

В архитектуре ARM предусмотрено несколько уровней привилегий выполнения, называемых Exception Levels (EL). Они определяют, какие системные ресурсы и инструкции доступны коду, выполняющемуся на конкретном уровне. Чем выше уровень EL, тем больше привилегий имеет выполняющийся на нём код. Основными уровнями являются EL0, EL1, EL2 и EL3.

Для сравнения, в x86-64 существует другая модель разделения привилегий - кольца. Например, Ring 3 обычно используется пользовательскими приложениями, а Ring 0 ядром операционной системы. Для гипервизора часто используется условное обозначение Ring -1, хотя это уже расширение классической модели колец, а SMM и Intel ME вообще работают в отдельных механизмах аппаратного доверия и не являются обычными кольцами. Классификация исключений в ARM:
  1. EL0 - уровень работы пользовательских приложений, у которых нет прав взаимодействия с системными компонентами (API для взаимодействия с процессором, например - платформенных экспертов, аналоги HAL в Windows). У подобных приложений также нет возможности читать или изменять физическую память; они могут работать только с виртуальной памятью в контексте песочницы;
  2. EL1 - привилегированный уровень выполнения, на котором в контексте данной статьи работает ядро операционной системы XNU. На этом уровне выполняются важные для ОС операции - управление виртуальной памятью и адресными пространствами процессов, взаимодействие с аппаратными механизмами платформы, обработка исключений, инициализация и регистрация различных механизмов обратного вызова и т.д;
  3. EL2 - уровень привилегий ARM, предназначенный прежде всего для работы гипервизора и механизмов виртуализации. В архитектуре защиты современных Apple Silicon также используются специальные механизмы изоляции и защиты памяти;
  4. EL3 - наиболее привилегированный уровень выполнения в архитектуре ARMv8-A, предназначенный прежде всего для управления переходами между Secure World и Normal World. На этом уровне может выполняться доверенный код, отвечающий за безопасное переключение контекстов и предоставление защищённых сервисов менее привилегированным уровням.
1790435362648.png

Guarded Execution Feature.​

Также Apple внедрила собственный аппаратный механизм изоляции - GXF. Начиная с iOS 17, он позволяет разделять память и права доступа между различными изолированными компонентами системы. Благодаря этому даже привилегированный код ядра XNU не получает автоматически доступ ко всем областям памяти и ресурсам системы. На подобных механизмах строится архитектура SPTM и TXM. За счёт аппаратного разделения критически важные компоненты могут работать в изолированной среде, недоступной обычному ядру. Если EL определяют общий уровень привилегий процессора, то GL определяют положение компонента внутри изолированной среды GXF и его возможности по отношению к другим защищённым компонентам:
  1. GL0 - базовый уровень, на котором выполняются менее привилегированные компоненты. На данном уровне выполняются изолированные задачи TXM, отвечающие за проверку подписей и прав, а также пользовательское пространство подсистемы Exclaves (компоненты вроде pmm_exclave, roottask, scheduler).
  2. GL1 - промежуточный уровень с дополнительными правами доступа к защищённым ресурсам. Здесь работает так называемое Защищённое ядро (exclavecore), которое берет на себя микроядерные функции и координирует безопасные домены, не доверяя основному ядру XNU;
  3. GL2 - более привилегированный уровень, предназначенный для компонентов, которым требуется контролировать защищённые области системы. На данном уровне работает компонент SPTM. Отсюда осуществляется монопольный контроль над физическими фреймами памяти и модификацией таблиц страниц. Обычное ядро XNU из своего дефолтного состояния не имеет физического доступа к регистрам уровня;
  4. GL3 - наиболее привилегированный уровень GXF. Код, работающий на этом уровне, имеет максимальные возможности внутри модели изоляции GXF, управляя глобальной конфигурацией самого механизма защиты и распределением аппаратных ресурсов между нижестоящими GL-доменами.
Исключения в ARM и GXF являются разными механизмами. EL0–EL3 являются стандартными уровнями исключений архитектуры ARM, тогда как GL представляют дополнительную модель аппаратной изоляции Apple.
1790435388951.png

​
Механизм переключения.
GXF использует собственные аппаратные инструкции для управления переходами между защищёнными доменами и уровнями изоляции:
  1. GENTER - быстрый контролируемый вход в изолированный домен. В регистре X16 передается специальный упакованный дескриптор (диспетчерский вызов), который железо проверяет на валидность перед тем, как повысить уровень до GL2;
  2. GEXIT - безопасный возврат в менее привилегированное состояние с автоматической очисткой или блокировкой конфиденциальных регистров текущего домена. Также из ARM:
  3. SVC - вызов привилегированного кода операционной системы из пользовательского режима. В контексте GXF она позволяет инициировать переход к обработчику, работающему с более высокими привилегиями;
  4. ERET - возврат на более низкий уровень привилегий.
Главное различие между этими двумя системами заключается в том, что инструкции ARM и инструкции Apple GXF работают в ортогональных плоскостях:

1. По вертикали. Вызовы SVC (Supervisor Call) или HVC (Hypervisor Call) генерируют полноценное аппаратное исключение, процессор сбрасывает текущий конвейер, переключает вектор прерываний и передает управление глобальному обработчику;
2. По горизонтали. Инструкции GENTER и GEXIT не генерируют исключений. Они выполняются атомарно прямо внутри текущего уровня EL1 (или EL0 в контексте эксклавов). Железо просто переключает внутренний переключатель (стейт-машину) прав доступа процессора, изменяя маскирование системных регистров за несколько тактов.

Домены разделения.
Каждый привилегированный компонент системы имеет свою область памяти, в которой ему разрешено читать и изменять данные. Такие области разделены на несколько типов, самые важные из них в контексте статьи, это:
  1. XNU_DOMAIN - территория ядра XNU. Здесь ядро выполняет свои определенные операции;
  2. SPTM_DOMAIN - территория SPTM. Здесь выполняются операции, связанные с защитой памяти и таблиц страниц. Код за пределами этого домена не может напрямую изменять защищённые структуры;
  3. TXM_DOMAIN - территория TXM. Здесь находятся механизмы, связанные с безопасностью системы, включая проверки целостности и прав доступа;
    Каждый домен изолирован от остальных. Поэтому наличие прав на запись в одном домене не означает, что компонент получает такие же права в другом;
  4. SK_DOMAIN - территория защищенного ядра и подсистемы Exclaves. Здесь изолированы критические ресурсы устройства - от сенсоров и аудио-буферов до движка Apple Neural Engine (ANE) и биометрии. Доступ к этой памяти замаплен исключительно на доверенные уровни исполнения (GL1/GL0) и полностью закрыт для стандартных процессов ядра XNU.
Перенос фрейма памяти из одного домена в другой возможен только через строгий протокол верификации. Т.е когда ядру XNU требуется создать новую таблицу страниц, оно физически не может объявить существующий кусок памяти табличным. Ядро генерирует диспетчерский вызов через инструкцию GENTER, передавая управление SPTM в GL2. Монитор проверяет запрос, очищает целевой фрейм от старых данных ядра, аппаратно меняет его тип на Page Table Domain и возвращает управление обратно через GEXIT.

1790435453803.png

​

Trust Exuction Monitor(TXM)​

TXM работает на уровне GL0 и отвечает за ряд критически важных функций безопасности системы. В его области находятся механизмы, связанные с проверкой подписи кода, контролем прав доступа и применением политик безопасности. Важная особенность здесь заключается в том, что эти операции вынесены за пределы обычного адресного пространства XNU. TXM при этом не управляет памятью полностью самостоятельно. В этой части он опирается на SPTM, который контролирует работу с таблицами страниц и защищёнными областями памяти. SPTM следит за тем, чтобы TXM и другие защищённые компоненты получали только разрешённый им доступ к памяти.

Взаимодействие XNU с TXM также не происходит как обычный вызов функции внутри ядра. Для этого используется специальный интерфейс txm_kernel_call. Запрос из XNU проходит через установленный механизм взаимодействия с защищённым доменом, а SPTM обеспечивает соблюдение правил доступа к памяти во время такой операции. XNU может запросить у TXM выполнение определённой операции, но при этом не получает прямого доступа к внутренним данным TXM. SPTM, в свою очередь, контролирует память и таблицы страниц, не позволяя обычному коду ядра произвольно изменить область, в которой работают защищённые компоненты.

Под капотом TXM его основная логика построена вокруг функции диспетчеризации. По сути, она получает номер запрошенной операции и в зависимости от него выбирает нужный обработчик. Структурно это обычный switch/case: каждый кейс соответствует отдельной операции и выполняет свою задачу. Одни обработчики могут заниматься проверками прав доступа, другие - работой с защищёнными областями памяти, третьи - обработкой запросов, поступающих из XNU или других доверенных компонентов.

1790435478461.png

​
В ARM64 первые аргументы функции передаются через регистры X0-X7. Поэтому значение v28, которое перед вызовом находится в регистре W16, передаётся диспетчеру как первый аргумент через X0. Уже внутри функции этот аргумент представлен как a1, и именно по нему выбирается нужный case. Условно:
C:
switch (a1)
{
    case 1:
        // одна операция TXM
        break;

    case 2:
        // другая операция
        break;

    case 4:
        // обработка метаданных TXM
        break;

    case 5:
        // операция связанная с защитой
        break;
}
Задача диспетчера - определить, какую именно операцию запросили, и передать выполнение соответствующему обработчику. Возьмем в пример case 5. По результатам анализа, связанный с ним обработчик выполняет действия, после которых определённые управляющие данные в защищённой области памяти становятся недоступными для дальнейшего изменения. В частности, в коде встречаются операции записи нулевого значения через регистр WZR.
1790435515058.png

WZR - специальный нулевой регистр ARM64. При чтении из него всегда получается значение 0. Данная защита может быть реализована, например для защиты MACF-структур:

1. Инструкция STR WZR, [X8] берет этот аппаратный ноль и физически перезаписывает им адрес памяти;
2. Важные указатели затираются нулями. Они превращаются в тыкву (0x00000000).

Если бы защита сводилась лишь к изменению прав доступа к этим структурам на Read-Only, она всё равно оставляла бы принципиальную возможность атаки. Обнаружив 0-day в ядре, злоумышленник мог бы получить примитив записи, временно обойти или изменить механизм защиты памяти, вернуть нужный флаг в исходное состояние и тем самым восстановить возможность управления защищёнными структурами.

1790435557643.png

Диаграмма демонстрирующая работу диспетчера TXM.​

Secure Page Table Monitor(SPTM).​

SPTM можно рассматривать как отдельный контролирующий слой, который стоит выше обычного ядра и следит за операциями с памятью. XNU больше не может произвольно менять свойства защищённых страниц только потому, что соответствующий код выполняется с привилегиями ядра. Запрос на изменение памяти проходит через SPTM, который проверяет, кто его выполняет, с какой областью памяти производится операция и разрешено ли такое изменение вообще. Для этого SPTM ведёт собственное представление физических кадров памяти - FTE (Frame Table Entries). Для каждого кадра фиксируется его текущее назначение и состояние. Благодаря этому SPTM может определить, например, что конкретный физический кадр принадлежит защищённой области и не должен быть превращён в обычную доступную для записи память. Смысл SPTM заключается не просто в том, чтобы пометить страницы как Read-Only. Он контролирует сам процесс изменения состояния памяти. Даже если злоумышленник получил возможность выполнять код внутри XNU, ему недостаточно просто вызвать обычную функцию управления памятью или изменить таблицу страниц: запрос должен пройти через отдельный слой контроля, который находится за пределами обычной логики ядра.
1790435598737.png

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

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


Mach-порты.​

Перед тем, как объяснять про коммуникацию, объясню про Mach-порты, которые являются основой коммуникации в iOS.
Mach-порт - это защищенная ядром однонаправленная очередь сообщений в памяти EL1, которая служит основной точкой коммуникации между процессами. Процессы не могут напрямую читать память друг друга, поэтому они создают порты, отправляют в них структурированные данные и доверяют ядру XNU доставить это сообщение адресату.

Термины. Межпроцессное взаимодействие в iOS опирается на несколько ключевых компонентов:
  1. launchd - первый процесс пользовательского пространства, который запускается ядром при загрузке системы (аналог systemd/init в Linux, или smss.exe в Windows), работает он с максимальными привилегиями, управляет жизненным циклом всех остальных демонов и приложений, а также выступает главным системным брокером сообщений;
  2. bootstrap порт - специальный Mach-порт, который выдается каждому процессу при старте стороны первоначального процесса(launchd), через этот порт приложение регистрирует свои сервисы или запрашивает права на отправку сообщений к другим системным демонам;
  3. Права доступа разделяются на несколько видов:
    MACH_PORT_RIGHT_RECEIVE - право на чтение сообщений (может быть только у одного процесса);
    MACH_PORT_RIGHT_SEND - право на отправку сообщений (может быть у многих);
    MACH_PORT_RIGHT_SEND_ONCE - одноразовое право на отправку (обычно используется для ответа клиенту).
  4. Сообщения - данные, передаваемые через порты, могут содержать как обычные байты (заголовки, структуры), так и дескрипторы для передачи указателей на память или прав на другие порты.
В отличие от привычных UNIX-сокетов или пайпов, IPC в Mach построен на концепции портов и прав доступа к ним. Порт - является защищённым объектом ядра, который представляет собой очередь сообщений с набором прав - право на отправку (send right), право на получение (receive right) и право на однократную отправку (send-once right). Без соответствующего права физически нельзя взаимодействовать с портом, даже если знать его имя или номер.
Когда процесс хочет отправить сообщение, он вызывает mach_msg(). На уровне пользователя это выглядит как обычная функция, но под капотом происходит переход в ядро через специальный трап, ядро проверяет наличие send right у вызывающего процесса, копирует тело сообщения из пользовательского пространства в буфер ядра, а затем доставляет его в очередь получателя. Если получатель уже ждёт сообщение (заблокирован на mach_msg() с флагом MACH_RCV_MSG), доставка происходит синхронно. Если нет - сообщение помещается в очередь, и получатель получит его при следующем вызове.

Права на порты передаются внутри самих сообщений. Например, когда launchd запускает ваш процесс, он передаёт ему SendRight на порт bootstrap, непосредственно через этот порт процесс может запрашивать доступ к другим системным сервисам. Сервис, получив запрос, может вернуть новый send right на свой собственный порт, и теперь клиент может общаться с ним напрямую. Права никогда не передаются по ссылке или через глобальные имена, они всегда перемещаются внутри сообщений, и ядро гарантирует их целостность при передаче. Базовая работа с портами выглядит так:
C++:
mach_port_t port;

kern_return_t kr;

// создается порт с правом на получение
kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
if (kr != KERN_SUCCESS) {
  //err
}

// тут получаем send right на тот же порт для отправки сообщений себе
kr = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
  //err
}

// структура сообщения
struct
{
  mach_msg_header_t header;
  uint32_t payload;
} msg;

msg.header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
msg.header.msgh_size = sizeof(msg);
msg.header.msgh_remote_port = port;
msg.header.msgh_local_port = MACH_PORT_NULL;
msg.header.msgh_id = 0x1337;
msg.payload = 42;

// отправка
kr = mach_msg(&msg.header, MACH_SEND_MSG, sizeof(msg), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
Кстати, также мы установим IOS-SDK, но об этом позже. В целом, в сплойт-деве IOS-а можете запомнить то, что в мире iOS всё упирается в примитивы. Когда вы читаете про уязвимость в ядре XNU, ваша конечная цель почти всегда звучит одинаково - получить стабильный Kernel Read/Write. Далее.

Коммуникация между безопасными компонентами.​

Аппаратно изолированная микроядерная подсистема безопасности, функционирующая параллельно с основным ядром XNU в отдельном доверенном домене (SK_DOMAIN) под управлением собственного защищенного ядра exclavecore.

Внутри архитектуры эксклавов обмен данными между изолированными компонентами и остальной системой строится на трех основных интерфейсах связи. Для базового взаимодействия юзерленда с защищенными ресурсами, вроде сенсоров или аудио-буферов, ядро задействует стандартный механизм Mach-портов. XNU проверяет права доступа конкретного процесса, генерирует соответствующие права на отправку и передает ему имена портов для трансляции запросов.

Более сложные RPC-взаимодействия реализуются через системный вызов exclaves_endpoint_call, логика которого во многом заимствована из архитектуры микроядер seL4. В этой схеме потоки полагаются на специализированные IPC-буферы, выделенные под каждый отдельный поток. При этом промежуточный прокси-компонент xnuproxy отвечает за распределение идентификаторов контекста планирования, известных как SCID. Наконец, в актуальных версиях системы Apple внедрила специализированный IPC-фреймворк Tightbeam, ориентированный на безопасный мир. Он выступает в роли абстракции над низкоуровневым транспортом, беря на себя всю рутину по управлению конечными точками, контролю клиентских сессий и непосредственной маршрутизации сообщений между доменами.

1790435715708.png

​

На этом все.​

На этом все. Введение закончено.
Статья в Telegram - https://teletype.in/@mosohism_protocol/ziFdkP5A-jY
 

Вложения

  • 1790369588774.png
    1790369588774.png
    33.4 KB · Просмотры: 9
  • 1790435291686.png
    1790435291686.png
    105.2 KB · Просмотры: 129