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

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

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

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

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

Категории

Все категории
Loading categories
blanket — CVE-2018-4280: Уязвимость замены порта Mach в launchd на iOS 11.2.6, приводящая к обходу песочницы, повышению привилегий и обходу проверки подписи кода. | Kitploit
Инструменты/GitHubGitHub/bazad/blanket
Повышение привилегийБезопасность iOSАнализ уязвимостейЭксплуатацияПост-эксплуатацияМобильная безопасностьЭксплуатация Бинарных Файлов
GitHubbazad/blanket

blanket

CVE-2018-4280: Уязвимость замены порта Mach в launchd на iOS 11.2.6, приводящая к обходу песочницы, повышению привилегий и обходу проверки подписи кода.

Репозиторий
260437 лет назадПроверено Kitploit

Популярное

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

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

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

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

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

blanket

Blanket — это эксплойт для обхода песочницы, нацеленный на iOS 11.2.6, хотя основная уязвимость была исправлена только в iOS 11.4.1. Он эксплуатирует уязвимость замены порта Mach в launchd (CVE-2018-4280), а также несколько меньших уязвимостей в других службах, чтобы выполнить код внутри процесса ReportCrash, который не имеет песочницы, работает от root и имеет привилегию task_for_pid-allow. Это дает Blanket контроль над каждым процессом, работающим на телефоне, включая критически важные для безопасности, такие как amfid.

Эксплойт состоит из нескольких этапов. В этом README будет объяснена основная уязвимость и этапы обхода песочницы шаг за шагом.

Имитация системных служб

Исследуя отчеты о сбоях на iOS, я обнаружил уязвимость замены порта Mach в launchd. Вызывая сбой определенным образом, процесс может заставить ядро отправить сообщение Mach в launchd, что приводит к чрезмерному освобождению права отправки на порт Mach в пространстве IPC launchd. Это позволяет атакующему выдавать себя за любую службу launchd, которую он может найти, перед остальной системой, что открывает множество путей для повышения привилегий.

Эта уязвимость также присутствует на macOS, но срабатывание уязвимости на iOS затруднено из-за проверок в launchd, которые гарантируют, что сообщение об исключении Mach поступает от ядра.

CVE-2018-4280: чрезмерное освобождение порта Mach в launchd при обработке сообщений об исключениях EXC_CRASH

Launchd мультиплексирует несколько различных обработчиков сообщений Mach через свой основной порт, включая обработчик MIG для сообщений об исключениях. Если процесс отправляет сообщение mach_exception_raise или mach_exception_raise_state_identity на свой собственный порт начальной загрузки, launchd получит и обработает это сообщение как исключение на уровне хоста.

К сожалению, обработка этих сообщений в launchd содержит ошибки. Если тип исключения — EXC_CRASH, то launchd освободит порты потока и задачи, отправленные в сообщении, а затем вернет KERN_FAILURE из сервисной процедуры, что заставит систему MIG снова освободить порты потока и задачи. (Предполагается, что если сервисная процедура возвращает успех, то она приняла владение всеми ресурсами в сообщении Mach, а если сервисная процедура возвращает ошибку, то она не приняла владение ни одним из ресурсов.)

Вот код из сервисной процедуры launchd для сообщений mach_exception_raise, декомпилированный с помощью IDA/Hex-Rays и слегка отредактированный для удобочитаемости:

cC kern_return_t __fastcall catch_mach_exception_raise( // (a) The service routine is mach_port_t exception_port, // called with values directly mach_port_t thread, // from the Mach message mach_port_t task, // sent by the client. The exception_type_t exception, // thread and task ports could mach_exception_data_t code, // be arbitrary send rights. mach_msg_type_number_t codeCnt) { __int64 __stack_guard; // ST28_8@1 kern_return_t kr; // w0@1 MAPDST kern_return_t result; // w0@4 __int64 codes_left; // x25@6 mach_exception_data_type_t code_value; // t1@7 int pid; // [xsp+34h] [xbp-44Ch]@1 char codes_str[1024]; // [xsp+38h] [xbp-448h]@7

root@kitploit:~
__stack_guard = *__stack_chk_guard_ptr;
pid = -1;
kr = pid_for_task(task, &pid);
if ( kr )
{
    _os_assumes_log(kr);
    _os_avoid_tail_call();
}
if ( current_audit_token.val[5] )                   // (b) If the message was sent by
{                                                   //     a process with a nonzero PID
    result = KERN_FAILURE;                          //     (any non-kernel process),
}                                                   //     the message is rejected.
else
{
    if ( codeCnt )
    {
        codes_left = codeCnt;
        do
        {
            code_value = *code;
            ++code;
            __snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", code_value);
            --codes_left;
        }
        while ( codes_left );
    }
    launchd_log_2(
        0LL,
        3LL,
        "Host-level exception raised: pid = %d, thread = 0x%x, "
            "exception type = 0x%x, codes = { %s }",
        pid,
        thread,
        exception,
        codes_str);
    kr = deallocate_port(thread);                   // (c) The "thread" port sent in
    if ( kr )                                       //     the message is deallocated.
    {
        _os_assumes_log(kr);
        _os_avoid_tail_call();
    }
    kr = deallocate_port(task);                     // (d) The "task" port sent in the
    if ( kr )                                       //     message is deallocated.
    {
        _os_assumes_log(kr);
        _os_avoid_tail_call();
    }
    if ( exception == EXC_CRASH )                   // (e) If the exception type is
        result = KERN_FAILURE;                      //     EXC_CRASH, then KERN_FAILURE
    else                                            //     is returned. MIG will
        result = 0;                                 //     deallocate the ports again.
}
*__stack_chk_guard_ptr;
return result;

}

root@kitploit:~
Вот что делает этот код:

1. Эта функция является служебной процедурой Mach для сообщений об исключениях `mach_exception_raise`: она вызывается непосредственно системой Mach, когда launchd обрабатывает сообщение об исключении Mach `mach_exception_raise`. Аргументы служебной процедуры извлекаются из сообщения Mach и, следовательно, контролируются отправителем сообщения.
2. В пункте (b) launchd проверяет, что сообщение об исключении Mach было отправлено ядром. Аудит-токен отправителя содержит PID отправляющего процесса в поле 5, которое будет равно нулю только для ядра. Если сообщение было отправлено не ядром, оно отклоняется.
3. Порты потока и задачи из сообщения явно освобождаются в пунктах (c) и (d).
4. В пункте (e) launchd проверяет, является ли тип исключения `EXC_CRASH`, и если да, возвращает `KERN_FAILURE`. Предполагается, что это делается для того, чтобы не обрабатывать сообщения `EXC_CRASH`, вероятно, чтобы ReportCrash вызывался как обработчик тел. Однако возврат `KERN_FAILURE` в этой точке приведет к повторному освобождению портов задачи и потока при последующей очистке сообщения об исключении. Это означает, что эти два порта будут освобождены дважды.

Чтобы эта уязвимость была полезной, мы хотим освободить право отправки launchd'а на сервис Mach, который он предоставляет, чтобы затем выдать себя за этот сервис перед остальной системой. Это означает, что нам нужно, чтобы порты задачи и потока в сообщении об исключении на самом деле были правами отправки на порт сервиса Mach, который мы хотим освободить в launchd. Затем, отправив launchd'у вредоносное сообщение об исключении и освободив порт сервиса, мы попытаемся повторно использовать то же имя порта, но на этот раз для порта Mach, на который у нас есть право получения. Таким образом, когда клиент попросит launchd дать ему право отправки на порт Mach для сервиса, launchd вместо этого даст ему право отправки на наш порт, позволяя нам выдать себя за этот сервис перед клиентом. После этого существует множество различных путей получения привилегий системы.

### Запуск уязвимости

Чтобы фактически запустить уязвимость, нам нужно обойти проверку того, что сообщение было отправлено ядром. Это связано с тем, что если мы отправим сообщение об исключении напрямую launchd'у, оно будет просто отброшено. Нам нужно каким-то образом заставить ядро отправить «вредоносное» сообщение об исключении, содержащее право отправки Mach для системного сервиса вместо реальных портов потока и задачи.

Как оказалось, существует ловушка Mach `task_set_special_port`, которую можно использовать для установки пользовательского права отправки, которое будет использоваться вместо настоящего порта задачи в определенных ситуациях. Одна из таких ситуаций — когда ядро генерирует сообщение об исключении от имени задачи: вместо размещения настоящего права отправки задачи в сообщении об исключении ядро использует право отправки, предоставленное `task_set_special_port`. Более конкретно, если задача вызывает `task_set_special_port`, чтобы установить пользовательское значение для своего специального порта `TASK_KERNEL_PORT`, а затем задача аварийно завершается, сообщение об исключении, сгенерированное ядром, будет содержать право отправки на пользовательский порт, а не на настоящий порт задачи, в поле «task». Аналогичный API, `thread_set_special_port`, можно использовать для установки пользовательского порта в поле «thread» сгенерированного сообщения об исключении.

Из-за такого поведения на самом деле совсем несложно заставить ядро сгенерировать «вредоносное» сообщение об исключении, содержащее порт сервиса Mach вместо порта задачи и потока. Однако нам всё ещё нужно гарантировать, что сгенерированное нами сообщение об исключении будет доставлено launchd'у.

Опять же, обеспечить доставку «вредоносного» сообщения об исключении launchd'у несложно, если знать правильный API. Функция `thread_set_exception_ports` устанавливает любое право отправки Mach в качестве порта, на который доставляются сообщения об исключениях для этого потока. Таким образом, всё, что нам нужно сделать, — это вызвать `thread_set_exception_ports` с bootstrap-портом, и тогда любое сгенерированное нами исключение заставит ядро отправить сообщение об исключении launchd'у.

Последняя часть головоломки — получение правильного типа исключения. Уязвимость срабатывает только для исключений `EXC_CRASH`. Немного проб и ошибок показывает, что мы можем легко генерировать исключения `EXC_CRASH`, вызвав стандартную функцию `abort`.

Итак, резюмируя: мы можем использовать существующие и хорошо задокументированные API, чтобы заставить ядро сгенерировать вредоносное сообщение об исключении `EXC_CRASH` от нашего имени и доставить его launchd'у, запуская уязвимость и освобождая порт сервиса Mach:

1. Используйте `thread_set_exception_ports`, чтобы установить launchd в качестве обработчика исключений для этого потока.
2. Вызовите `bootstrap_look_up`, чтобы получить порт сервиса, который мы хотим подменить, от launchd.
3. Вызовите `task_set_special_port`/`thread_set_special_port`, чтобы использовать этот порт сервиса вместо настоящих портов задачи и потока в сообщениях об исключениях.
4. Вызовите `abort`. Ядро отправит сообщение об исключении `EXC_CRASH` launchd'у, но порты задачи и потока в сообщении будут портом целевого сервиса.
5. Launchd обработает сообщение об исключении и освободит порт сервиса.

### Выполнение кода после аварийного завершения

В описанной выше стратегии есть проблема: вызов `abort` убьет наш процесс. Если мы хотим иметь возможность выполнять какой-либо код после запуска уязвимости, нам нужен способ выполнить аварийное завершение в другом процессе.

(С другими типами исключений процесс мог бы фактически восстановиться после исключения. Способ восстановления процесса заключается в том, чтобы установить обработчик исключений потока на launchd, а обработчик исключений задачи — на самого себя. После того как launchd обработает и не сможет обработать исключение, ядро отправит исключение обработчику задачи, который сбросит состояние потока и сообщит ядру, что исключение обработано. Однако процесс не может перехватывать собственные исключения `EXC_CRASH`, поэтому нам действительно нужны два процесса.)

Одна из стратегий — сначала использовать уязвимость в другом процессе на iOS и заставить этот процесс установить свои порты ядра и аварийно завершиться. Однако для доказательства концепции проще создать расширение приложения.

Расширения приложений, представленные в iOS 8, предоставляют способ упаковать некоторую функциональность приложения, чтобы она была доступна вне приложения. Код расширения приложения выполняется в отдельном изолированном процессе. Это позволяет очень легко запустить процесс, который установит свои специальные порты, зарегистрирует launchd в качестве обработчика исключений для `EXC_CRASH`, а затем вызовет `abort`.

Не существует поддерживаемого способа для приложения программно запустить собственное расширение и общаться с ним. Однако Иан Макдауэлл написал [отличную статью][Multi-Process iOS App Using NSExtension], описывающую, как использовать частный API `NSExtension` для запуска и общения с процессом расширения приложения. Я использовал почти идентичную стратегию здесь. Единственное отличие в том, что нам нужно передать порт Mach процессу расширения приложения, что включает регистрацию фиктивного сервиса в launchd, к которому подключается расширение приложения.

[Multi-Process iOS App Using NSExtension]: https://ianmcdowell.net/blog/nsextension/

### Предотвращение повторного использования порта в launchd

Одна проблема, которую вы заметите, если запустите эксплойт, как описано, заключается в том, что иногда вам не удастся повторно получить освобожденный порт. Причина в том, что ядро отслеживает свободные записи IPC процесса в списке свободных, и поэтому только что освобожденное имя порта будет повторно использовано (с другим номером поколения) при выделении нового порта в таблице IPC. Таким образом, мы сможем перераспределить нужное нам имя порта, только если launchd не использует этот слот записи IPC для другого порта первым.

Обходной путь — затолкать свободный слот записи IPC глубже в список свободных, чтобы при выделении launchd'ом новых портов сначала использовались другие слоты. Как это сделать? Мы можем зарегистрировать в launchd несколько фиктивных сервисов Mach с портами, на которые у нас есть право получения. Когда мы вызываем `abort`, сначала срабатывает обработчик исключений, а затем очищается состояние процесса, включая порты Mach. Когда launchd получает исключение `EXC_CRASH`, он непреднамеренно освобождает целевой порт сервиса, помещая слот записи IPC, соответствующий этому имени порта, в начало списка свободных. Затем, когда остальные порты Mach нашего расширения приложения уничтожаются, launchd получает уведомления и освобождает фиктивные порты сервисов, закапывая целевой слот записи IPC за слотами только что освобожденных портов. Таким образом, пока launchd выделяет меньше портов, чем количество зарегистрированных нами фиктивных сервисов, целевой слот всё ещё будет в списке свободных, а значит, мы всё еще можем заставить launchd перераспределить слот с тем же именем порта, что и исходный сервис.

Ограничение этой стратегии в том, что нам нужно иметь право `com.apple.security.application-groups` для регистрации сервисов в launchd. Есть и другие способы спрятать порты Mach в launchd, но использование групп приложений — безусловно, самый простой, и его достаточно для этой доказательной концепции.

### Подмена освобожденного сервиса

После того как мы породили расширение приложения-крашера и освободили право отправки Mach в launchd, нам нужно перераспределить это имя порта Mach с правом отправки, на которое у нас есть право получения. Таким образом, любые сообщения, которые launchd отправляет на это имя порта, будут получены нами, и всякий раз, когда launchd делится этим именем порта с клиентом, клиент получит право отправки на наш порт. В частности, если мы сможем освободить право отправки launchd'а на сервис Mach, то любой процесс, запрашивающий этот сервис у launchd'а, получит право отправки на наш собственный порт вместо реального порта сервиса. Это позволяет нам подменить сервис или выполнить атаку «человек посередине», просматривая все сообщения, которые клиент отправляет сервису.

Добиться повторного использования освобожденного имени порта, чтобы оно указывало на порт, которым мы владеем, также довольно просто, учитывая, что мы уже решили использовать право `application-groups`: просто регистрируйте фиктивные сервисы Mach в launchd, пока один из них не повторно использует исходное имя порта. Нам нужно делать это пакетами, регистрируя большое количество фиктивных сервисов вместе, проверяя, удалось ли какому-либо из них успешно повторно использовать освобожденное имя порта, а затем отменяя их регистрацию. Причина в том, что нужно убедиться, что наши регистрации проходят по всему списку свободных портов IPC, чтобы добраться до затребованного зарытого имени порта.

Мы можем проверить, удалось ли нам успешно повторно использовать освобожденное имя порта, выполнив поиск исходного сервиса с помощью `bootstrap_look_up`: если он возвращает один из наших зарегистрированных портов сервисов, цель достигнута.

Как только нам удалось зарегистрировать новый сервис, который получает то же имя порта, что и исходный, любые клиенты, которые ищут исходный сервис в launchd, получат право отправки на наш порт, а не на реальный порт сервиса. Таким образом, мы фактически выдаем себя за исходный сервис перед остальной системой (или, по крайней мере, перед теми процессами, которые ищут этот сервис после нашей атаки).


Этап 1: Получение порта host-priv
---------------------------------------------------------------------------------------------------

Как только у нас есть возможность подменять произвольные системные сервисы, следующим шагом является получение порта host-priv. Этот шаг прост и не затрагивается изменениями в iOS 11.3. Основная идея этой атаки — подменить SafetyNet, аварийно завершить ReportCrash, а затем извлечь порт host-priv из умирающего порта задачи ReportCrash, отправленного в сообщении об исключении.

### О ReportCrash и SafetyNet

ReportCrash отвечает за создание отчетов о сбоях на iOS. Этот один бинарный файл фактически предоставляет 4 различных сервиса (каждый в отдельном процессе, хотя не все могут быть запущены в любой момент времени):

1. `com.apple.ReportCrash` отвечает за создание отчетов о сбоях для аварийно завершающихся процессов. Это обработчик исключений на уровне хоста для исключений `EXC_CRASH`, `EXC_GUARD` и `EXC_RESOURCE`.
2. `com.apple.ReportCrash.Jetsam` обрабатывает отчеты Jetsam.
3. `com.apple.ReportCrash.SimulateCrash` создает отчеты для симулированных сбоев.
4. `com.apple.ReportCrash.SafetyNet` является зарегистрированным обработчиком исключений для сервиса `com.apple.ReportCrash`.

Интересующие нас сервисы — `com.apple.ReportCrash` и `com.apple.ReportCrash.SafetyNet`, в дальнейшем называемые просто ReportCrash и SafetyNet. Оба являются сервисами на основе MIG и выполняют по сути один и тот же код.

Когда ReportCrash запускается, он находит сервис SafetyNet в launchd и устанавливает возвращенный порт в качестве обработчика исключений уровня задачи. Предположительно, это сделано для того, чтобы если сам ReportCrash аварийно завершится, отдельный процесс создал бы для него отчет о сбое. Однако этот путь кода выглядит нефункциональным: ReportCrash регистрирует SafetyNet для сообщений `mach_exception_raise`, хотя и ReportCrash, и SafetyNet обрабатывают только сообщения `mach_exception_raise_state_identity`. Тем не менее, оба сервиса все еще присутствуют и доступны из песочницы контейнера iOS.

### Примитивы манипуляции ReportCrash

Для выполнения следующей атаки нам нужно уметь манипулировать ReportCrash (или SafetyNet), чтобы заставить их вести себя нужным нам образом. В частности, нам нужны следующие возможности: запускать ReportCrash по требованию, принудительно завершать ReportCrash, аварийно завершать ReportCrash и гарантировать, что ReportCrash не завершится, пока мы его используем. Здесь я опишу, как мы достигаем каждой цели.

Чтобы запустить ReportCrash, нам просто нужно отправить ему сообщение Mach: launchd запустит его по требованию. Однако из-за его своеобразной архитектуры любой тип сообщения, кроме `mach_exception_raise_state_identity`, заставит ReportCrash перестать отвечать на новые сообщения и в конечном итоге завершиться. Таким образом, если мы хотим, чтобы он оставался живым после этого, нам нужно отправить сообщение `mach_exception_raise_state_identity`.

Чтобы завершить ReportCrash, мы можем просто отправить ему любой другой тип сообщения Mach.

Существует много способов аварийно завершить ReportCrash. Самый простой — отправить сообщение `mach_exception_raise_state_identity` с портом потока, установленным в `MACH_PORT_NULL`.

Наконец, нам нужно гарантировать, что ReportCrash не завершится, пока мы его используем. Каждое обработанное сообщение `mach_exception_raise_state_identity` заставляет его порождать новый поток для прослушивания следующего сообщения, в то время как исходный поток генерирует отчет о сбое. ReportCrash завершится, когда все незавершенные потоки, генерирующие отчет о сбое, закончат работу. Таким образом, если мы сможем заблокировать один из этих потоков во время генерации отчета о сбое, мы сможем предотвратить его завершение.

Самый простой способ, который я нашел для этого, — отправить сообщение `mach_exception_raise_state_identity` с пользовательским портом в полях задачи и потока. Когда ReportCrash попытается сгенерировать отчет о сбое, он вызовет `task_policy_get` для порта «task», что заставит его отправить сообщение Mach на порт, который мы отправили, и ожидать ответа. Но поскольку порт «task» — это просто обычный порт Mach, мы можем просто не отвечать на сообщение Mach, и ReportCrash будет бесконечно ждать возврата `task_policy_get`.

### Извлечение host-priv из ReportCrash

Для первого этапа эксплойта план атаки относительно прост:

1. Запустите сервис SafetyNet и заставьте его оставаться активным на время нашей атаки.
2. Используйте примитив подмены сервиса launchd, чтобы выдать себя за SafetyNet. Это дает нам новый порт, на котором мы можем получать сообщения, предназначенные для реального сервиса SafetyNet.
3. Завершите любой существующий экземпляр ReportCrash. Таким образом, мы можем гарантировать, что ReportCrash найдет наш порт SafetyNet на следующем шаге.
4. Запустите ReportCrash. ReportCrash найдет SafetyNet в launchd и установит полученный порт (который является поддельным портом SafetyNet, на который у нас есть право получения) в качестве назначения для сообщений `EXC_CRASH`.
5. Вызовите аварийное завершение ReportCrash. Увидев, что нет зарегистрированных обработчиков для исходного типа исключения, ReportCrash перейдет в фазу завершения процесса. На этом этапе XNU увидит, что ReportCrash зарегистрировал поддельный порт SafetyNet для получения исключений `EXC_CRASH`, поэтому он сгенерирует сообщение об исключении и отправит его на этот порт.
6. Затем мы прослушиваем поддельный порт SafetyNet на предмет сообщения `EXC_CRASH`. Оно будет типа `mach_exception_raise`, что означает, что оно будет содержать порт задачи ReportCrash.
7. Наконец, мы используем `task_get_special_port` для порта задачи ReportCrash, чтобы получить хост-порт ReportCrash. Поскольку ReportCrash не изолирован и работает от root, это порт host-priv.

В конце этого этапа побега из песочницы мы получаем рабочий порт host-priv. Уже одно это демонстрирует, что это серьезная проблема безопасности.


Этап 2: Побег из песочницы
---------------------------------------------------------------------------------------------------

Несмотря на то, что у нас есть порт host-priv, наша цель — полностью вырваться из песочницы и выполнять код от root с правом `task_for_pid-allow`. Первый шаг к этому — просто вырваться из песочницы.

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

Основная атака заключается в повторном использовании той же уязвимости launchd для подмены системного сервиса. Однако на этот раз наша цель — подменить сервис, которому клиент отправит свой порт задачи в сообщении Mach. Экспериментальным путем на iOS 11.2.6 легко обнаружить, что если мы подменим `com.apple.CARenderServer` (далее CARenderServer), размещенный backboardd, а затем свяжемся с `com.apple.DragUI.druid.source`, демон druid без песочницы отправит свой порт задачи в сообщении Mach на поддельный порт сервиса.

Этот шаг эксплойта сломан на iOS 11.3, потому что druid больше не отправляет свой порт задачи в сообщении Mach серверу CARenderServer. Несмотря на это, я уверен, что эту уязвимость все еще можно использовать для побега из песочницы. Один из способов — искать службы без песочницы, которые доверяют вводу от других служб. Такие «уязвимости» никогда не были бы эксплуатируемы без возможности замены системных служб, что означает, что они, вероятно, являются низкоприоритетной поверхностью атаки как внутри Apple, так и снаружи.

### Аварийное завершение druid

Как и в случае с ReportCrash, нам нужно иметь возможность принудительно перезапустить druid, если он уже запущен, чтобы он нашел наш поддельный порт CARenderServer в launchd. Для этой цели я решил использовать ошибку в libxpc, которую уже планировалось исправить.

Просматривая libxpc, я нашел чтение за пределами буфера, которое можно использовать для принудительного аварийного завершения любого XPC-сервиса:```C
void _xpc_dictionary_apply_wire_f
(
        OS_xpc_dictionary *xdict,
        OS_xpc_serializer *xserializer,
        const void *context,
        bool (*applier_fn)(const char *, OS_xpc_serializer *, const void *)
)
{
...
    uint64_t count = (unsigned int)*serialized_dict_count;
    if ( count )
    {
        uint64_t depth = xserializer->depth;
        uint64_t index = 0;
        do
        {
            const char *key = _xpc_serializer_read(xserializer, 0, 0, 0);
            size_t keylen = strlen(key);
            _xpc_serializer_advance(xserializer, keylen + 1);
            if ( !applier_fn(key, xserializer, context) )
                break;
            xserializer->depth = depth;
            ++index;
        }
        while ( index < count );
    }
...
}

Проблема в том, что использование непроверенного strlen на данных, контролируемых злоумышленником, позволяет ключу сериализованной записи словаря выйти за границы буфера данных. Это означает, что XPC-сервис, десериализующий словарь, упадет, когда strlen выполнит чтение памяти за пределами границ или когда _xpc_serializer_advance попытается продвинуть сериализатор за конец предоставленных данных.

Этот баг уже был исправлен в iOS 11.3 Beta к моменту его обнаружения, поэтому я не сообщал о нем Apple. Эксплойт доступен в виде отдельного проекта в моем репозитории xpc-crash.

Чтобы использовать этот баг для сбоя druid, нам просто нужно отправить сервису druid поврежденное XPC-сообщение, в котором ключ словаря не завершен и простирается до последнего байта сообщения.

Получение порта задачи druid

Получение порта задачи druid на iOS 11.2.6 с помощью нашего примитива подмены сервиса просто:

  1. Используйте возможность подмены Mach-сервиса, чтобы подменить CARenderServer.
  2. Отправьте сообщение сервису druid, чтобы он запустился.
  3. Если мы не получим порт задачи druid через несколько секунд, убейте druid с помощью XPC-бага и перезапустите его.
  4. Druid отправит нам свой порт задачи на поддельный порт CARenderServer.

Обход ограничений порта задачи для платформенных двоичных файлов

Получив порт задачи druid, нам все еще нужно выяснить, как выполнить код внутри процесса druid.

Проблема в том, что XNU защищает порты задач для платформенных двоичных файлов от модификации неплатформенными двоичными файлами. Защита реализована в функции task_conversion_eval, которая вызывается convert_port_to_locked_task и convert_port_to_task_with_exec_token:```C kern_return_t task_conversion_eval(task_t caller, task_t victim) { /* * Tasks are allowed to resolve their own task ports, and the kernel is * allowed to resolve anyone's task port. */ if (caller == kernel_task) { return KERN_SUCCESS; }

root@kitploit:~
if (caller == victim) {
	return KERN_SUCCESS;
}

/*
 * Only the kernel can can resolve the kernel's task port. We've established
 * by this point that the caller is not kernel_task.
 */
if (victim == kernel_task) {
	return KERN_INVALID_SECURITY;
}

#if CONFIG_EMBEDDED /* * On embedded platforms, only a platform binary can resolve the task port * of another platform binary. / if ((victim->t_flags & TF_PLATFORM) && !(caller->t_flags & TF_PLATFORM)) { #if SECURE_KERNEL return KERN_INVALID_SECURITY; #else if (cs_relax_platform_task_ports) { return KERN_SUCCESS; } else { return KERN_INVALID_SECURITY; } #endif / SECURE_KERNEL / } #endif / CONFIG_EMBEDDED */

root@kitploit:~
return KERN_SUCCESS;

}

root@kitploit:~
MIG-процедуры преобразования, которые полагаются на эти функции, включая `convert_port_to_task` и `convert_port_to_map`, следовательно, потерпят неудачу, когда мы вызываем их для задачи druid. Например, `mach_vm_write` не позволит нам манипулировать памятью druid.

Однако, изучая MIG-файл `osfmk/mach/task.defs` в XNU, я заметил кое-что интересное:```C
/*
 *	Returns the set of threads belonging to the target task.
 */
routine task_threads(
		target_task	: task_inspect_t;
	out	act_list	: thread_act_array_t);

Функция task_threads, которая перечисляет потоки в задаче, на самом деле принимает task_inspect_t, а не task_t, что означает, что MIG преобразует её с помощью convert_port_to_task_inspect, а не convert_port_to_task. Быстрый взгляд на convert_port_to_task_inspect показывает, что эта функция не выполняет проверку task_conversion_eval, то есть мы можем успешно вызывать её для платформенных бинарников. Это интересно, потому что возвращаемые потоки — это не права thread_inspect_t, а полные права thread_act_t. Иными словами, task_threads повышает немодифицируемое право задачи до модифицируемых прав потоков. А так как эквивалента thread_conversion_eval не существует, это означает, что мы можем использовать Mach API потоков для модификации потоков в задаче, даже если эта задача является платформенным бинарником.

Чтобы воспользоваться этим, я написал библиотеку под названием threadexec, которая строит полнофункциональную возможность вызова функций поверх API потоков Mach. Проект threadexec сам по себе был значительной работой, но поскольку он имеет лишь косвенное отношение к данной эксплуатации, я опущу подробное объяснение его внутреннего устройства.

Этап 3: Установка нового обработчика исключений на уровне хоста

Как только мы получили порт привилегий хоста и непесочничное выполнение кода внутри druid, следующий этап полного обхода песочницы — установка нового обработчика исключений на уровне хоста. Этот процесс является прямолинейным с учётом наших текущих возможностей:

  1. Получить текущий обработчик исключений уровня хоста для EXC_BAD_ACCESS, вызвав host_get_exception_ports.
  2. Выделить порт Mach, который станет новым обработчиком исключений уровня хоста для EXC_BAD_ACCESS.
  3. Отправить порт привилегий хоста и право отправки на только что выделенный порт Mach в druid.
  4. Используя наш контекст выполнения в druid, заставить druid вызвать host_set_exception_ports, чтобы зарегистрировать наш порт Mach как обработчик исключений уровня хоста для EXC_BAD_ACCESS.

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

Этап 4: Получение порта задачи ReportCrash

Следующий этап — вызвать исключение EXC_BAD_ACCESS в ReportCrash, чтобы его порт задачи был отправлен в сообщении об исключении на наш новый порт обработчика исключений:

  1. Вызвать сбой ReportCrash с помощью ранее описанной техники. Это приведёт к тому, что ReportCrash сгенерирует исключение EXC_BAD_ACCESS. Поскольку у ReportCrash нет зарегистрированного обработчика для EXC_BAD_ACCESS (напомним, SafetyNet зарегистрирован для EXC_CRASH), исключение будет доставлено обработчику исключений уровня хоста.
  2. Прослушивать сообщения об исключениях на нашем порте обработчика исключений хоста.
  3. Когда мы получим сообщение об исключении от ReportCrash, сохранить порты задачи и потока. Приостановить падающий поток и вернуть KERN_SUCCESS, чтобы указать ядру, что исключение обработано и ReportCrash может быть возобновлён.
  4. Использовать порты задачи и потока для установки контекста выполнения внутри ReportCrash, как мы это делали с druid.

На этом этапе мы получаем выполнение кода внутри процесса без песочницы, с правами root и с разрешением task_for_pid-allow.

Этап 5: Восстановление исходного обработчика исключений уровня хоста

Следующие два этапа не являются строго необходимыми, но их всё же стоит выполнить.

Как только мы получили выполнение кода внутри ReportCrash, следует сбросить обработчик исключений уровня хоста для EXC_BAD_ACCESS, используя druid:

  1. Отправить старый порт обработчика исключений уровня хоста в druid.
  2. Вызвать host_set_exception_ports в druid, чтобы повторно зарегистрировать старый обработчик исключений уровня хоста для EXC_BAD_ACCESS.

Это предотвратит получение нашим портом обработчика исключений сообщений об исключениях от других падающих процессов.

Этап 6: Исправление launchd

Последний шаг — восстановить повреждения, которые мы нанесли launchd, освободив порты служб в его пространстве IPC, чтобы выдать себя за них:

  1. Вызвать task_for_pid в ReportCrash, чтобы получить порт задачи launchd.
  2. Для каждой службы, от имени которой мы выдавали себя:
    1. Получить в launchd имя права отправки на поддельный порт службы. Это исходное имя реального порта службы.
    2. Уничтожить поддельный порт службы, отменив регистрацию поддельной службы в launchd.
    3. Вызвать mach_port_insert_right в ReportCrash, чтобы поместить реальный порт службы в IPC-пространство launchd под исходным именем.

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

Постэксплуатация

Blanket также включает полезную нагрузку постэксплуатации, которая обходит amfid и запускает оболочку с привязкой к порту. В этом разделе описывается, как это достигается.

Запуск процесса с полезной нагрузкой

Даже после получения возможности выполнять код в ReportCrash использовать эту возможность непросто: мы ограничены выполнением отдельных вызовов функций внутри процесса, что затрудняет выполнение сложных задач. В идеале хотелось бы иметь способ запускать код нативно с привилегиями ReportCrash, либо внедряя код в ReportCrash, либо порождая новый процесс с теми же (или более высокими) привилегиями.

Blanket выбирает путь порождения процесса. Мы используем task_for_pid и наш статус платформенного бинарника в ReportCrash, чтобы получить порт задачи launchd и создать новый поток внутри launchd, которым мы можем управлять. Затем мы используем этот поток для вызова posix_spawn для запуска нашего бинарника с полезной нагрузкой. Полезная нагрузка может быть подписана с ограниченными привилегиями, включая task_for_pid-allow, чтобы предоставить дополнительные возможности.

Обход amfid

Чтобы iOS приняла наш вновь порождённый бинарник, необходимо обойти проверку кода. За годы было предложено множество стратегий, но наиболее распространённой текущей стратегией является регистрация обработчика исключений для amfid и последующее патчение данных, чтобы amfid падал при попытке вызова MISValidateSignatureAndCopyInfo. Это позволяет нам подделать реализацию этой функции, чтобы симулировать действительную подпись кода.

Однако существует и другой подход, который, на мой взгляд, является более надёжным и гибким: вместо патчинга amfid можно просто зарегистрировать новый порт amfid в ядре.

Ядро отслеживает, какой порт отправлять сообщения amfid, с помощью специального порта хоста под названием HOST_AMFID_PORT. Если у нас есть непесочничное выполнение кода с правами root, мы можем установить этот порт на новое значение. Apple защитилась от этой атаки, проверяя, действительно ли ответ на запрос проверки пришёл от amfid: cdhash отправителя сравнивается с cdhash amfid. Однако это на самом деле не предотвращает отправку сообщения процессу, отличному от amfid; это только предотвращает получение ответа от не-amfid процесса. Если мы организуем треугольник, где ядро отправляет сообщения нам, мы генерируем ответ и передаём его amfid, а затем amfid отправляет ответ ядру, тогда мы сможем обойти проверку отправителя.

У этого подхода есть множество преимуществ, самым большим из которых, вероятно, является доступ к дополнительным флагам в сервисной процедуре verify_code_directory. Хотя amfid использует не все из них, существует множество других выходных флагов, которые amfid мог бы устанавливать для управления поведением подписывания кода. Вот частичный прототип verify_code_directory:```C kern_return_t verify_code_directory( mach_port_t amfid_port, amfid_path_t path, uint64_t file_offset, int32_t a4, int32_t a5, int32_t a6, int32_t * entitlements_valid, int32_t * signature_valid, int32_t * unrestrict, int32_t * signer_type, int32_t * is_apple, int32_t * is_developer_code, amfid_a13_t a13, amfid_cdhash_t cdhash, audit_token_t audit);

root@kitploit:~
Особый интерес для разработчиков джейлбрейков представляет параметр `is_apple`. Похоже, что этот параметр не используется amfid, но если он установлен, он заставляет ядро установить флаг подписи кода `CS_PLATFORM_BINARY`, что предоставляет приложению привилегии платформенного бинарника. В частности, это означает, что приложение теперь может использовать порты задач для непосредственного изменения платформенных бинарников.

Лазейки, используемые в этой атаке
---------------------------------------------------------------------------------------------------

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

В ядре:

1. `task_threads` может повысить инспективный `task_inspect_t` до модифицирующего `thread_act_t`.
2. Отсутствует `thread_conversion_eval`, выполняющий роль `task_conversion_eval` для потоков.
3. Неплатформенный бинарник может использовать право `task_inspect_t` для платформенного бинарника.
4. Сообщения об исключениях для процессов без песочницы могут доставляться в процессы с песочницей, хотя это предоставляет способ обхода песочницы. Неясно, существует ли чистое исправление для этой лазейки.
5. Выполнение кода без песочницы, порт host-priv и возможность аварийно завершить процесс `task_for_pid-allow` можно объединить для создания обходного пути `task_for_pid`. (Обходной путь: вызвать `host_set_exception_ports`, чтобы установить новый обработчик исключений на уровне хоста, затем аварийно завершить процесс `task_for_pid-allow`, чтобы получить его порт задачи и выполнить код с соответствующими привилегиями.)

В расширениях приложений:

1. Расширения приложений, которые используют общую группу приложений, могут обмениваться сообщениями Mach, несмотря на то, что документация предполагает невозможность связи между основным приложением и расширением.

Рекомендуемые исправления и смягчения
---------------------------------------------------------------------------------------------------

Я рекомендую следующие исправления, примерно в порядке важности:

1. Освобождать порты Mach в служебных процедурах launchd только при возврате `KERN_SUCCESS`. Это исправит уязвимость замены порта Mach.
2. Закрыть лазейку `task_threads`, позволяющую неплатформенному бинарнику использовать порт задачи платформенного бинарника для достижения выполнения кода.
3. Исправить проблемы с аварийным завершением в ReportCrash.
4. Набор служб Mach, доступных из песочницы контейнера, следует минимизировать. Я не вижу законных причин для большинства приложений iOS взаимодействовать с ReportCrash или SafetyNet.
5. Как можно больше процессов должно быть помещено в песочницу. Я не уверен, нужно ли druid быть без песочницы для правильной работы, но если нет, его следует поместить в соответствующую песочницу.
6. Мертвый код следует устранить. SafetyNet, похоже, не выполняет свои предполагаемые функции. Если он больше не нужен, его следует удалить.
7. Закрыть обходной путь `task_for_pid`, основанный на `host_set_exception_ports`. Например, рассмотреть, стоит ли ограничить `host_set_exception_ports` для root или ограничить использование порта host-priv в некоторых конфигурациях. Это нарушает элегантный дизайн Mach, основанный на возможностях, но `host_set_exception_ports` может быть многообещающей целью для злоупотреблений.
8. Рассмотреть, стоит ли добавить `task_conversion_eval` в `task_inspect_t`.

Запуск blanket
---------------------------------------------------------------------------------------------------

Blanket должен работать на любом устройстве под управлением iOS 11.2.6.

1. Загрузите проект:   ```
   git clone https://github.com/bazad/blanket
   cd blanket
  1. Загрузите и соберите библиотеку threadexec, которая необходима для blanket для внедрения кода в процессы и задачи: ``` git clone https://github.com/bazad/threadexec cd threadexec make ARCH=arm64 SDK=iphoneos EXTRA_CFLAGS='-mios-version-min=11.1 -fembed-bitcode' cd ..
    root@kitploit:~
  2. Скачайте iOS binpack Джонатана Левина, который содержит бинарные файлы, которые будут использоваться привязанной оболочкой. Если вы измените полезную нагрузку для других целей, binpack не понадобится. ``` mkdir binpack curl http://newosxbook.com/tools/binpack64-256.tar.gz | tar -xf- -C binpack
    root@kitploit:~
  3. Откройте Xcode и настройте проект. Вам потребуется изменить идентификатор подписи и указать собственное разрешение группы приложений.
  4. Отредактируйте файл headers/config.h и измените APP_GROUP на идентификатор группы приложений, который вы указали ранее.

После этого вы сможете собрать и запустить проект на устройстве.

Если blanket работает успешно, он запускает полезный двоичный файл (исходный код в blanket_payload/blanket_payload.c), который по умолчанию создает привязанную оболочку на порту 4242. Вы можете подключиться к этому порту с помощью netcat и выполнять произвольные команды оболочки.

Благодарности

Большое спасибо Яну Биру и Джонатану Левину за их превосходные исследования безопасности и внутреннего устройства iOS.

Хронология

Я обнаружил эту уязвимость в январе 2018 года и начал разрабатывать эксплойт в конце февраля. Я сообщил об этой проблеме Apple 13 апреля. Apple присвоила уязвимости замены порта Mach в launchd номер CVE-2018-4280, и она была исправлена в iOS 11.4.1 и macOS 10.13.6 9 июля.

Лицензия

Blanket распространяется под лицензией MIT.


Brandon Azad

Скачать инструмент