
Это история о том, как я недавно обнаружил в iOS уязвимость, которая стала одной из моих любимых. Почему любимых? Потому что реализовать для неё эксплойт оказалось очень легко. Кроме того, она использовала публичный легаси API, на который до сих пор опираются многие компоненты ОС Apple, и о котором многие разработчики даже не слышали.
▍ Уведомления Darwin
Большинство разработчиков под iOS наверняка привыкли к
NSNotificationCenter
, а разработчики под Mac — к NSDistributedNotificationCenter
. Первый механизм работает внутри одного процесса, а второй позволяет процессам обмениваться простыми уведомлениями с возможностью включать в них строку дополнительных данных.Darwin Notification API ещё проще, так как является частью слоя CoreOS. Этот API предоставляет механизм для простого обмена сообщениями между процессами в ОС Apple. Вместо объектов или строк с каждым уведомлением может быть связано
state
, имеющее тип UInt64
и обычно используемое только для указания логического значения true
либо false
(1 или 0).В качестве простого примера использования этого API можно рассмотреть процесс, желающий уведомить другие процессы о некотором событии. Для этого он может вызвать функцию
notify_post
, которая получает строку, обычно представляющую инвертированное значение DNS вроде com.apple.springboard.toggleLockScreen
.Процессы, заинтересованные в получении такого уведомления, могут зарегистрироваться с помощью функции
notify_register_dispatch
, которая будет отправлять в заданную очередь диспетчеризации блок, как только другой процесс опубликует уведомление с указанным именем.Процесс, который хочет разместить уведомление Darwin с состоянием, должен сначала зарегистрировать соответствующего обработчика. Для этого ему нужно вызвать функцию
notify_register_check
. Эта функция получает имя уведомления и указатель на Int32
, куда ей нужно вернуть токен, необходимый для вызова notify_set_state
, которая тоже получает в качестве состояния значение UInt64
.Через тот же механизм
notify_register_check
процесс, который хочет получить состояние уведомления, вызывает notify_get_state
. Это позволяет использовать уведомления Darwin для определённых видов событий, но также хранить некое состояние, которое любой процесс в системе сможет в любой момент запросить.▍ Уязвимость
Любой процесс в ОС Apple, включая iOS, может без дополнительных требований прямо из своей собственной среды зарегистрироваться для получения любых уведомлений Darwin. И это имеет смысл с учётом того, что некоторые системные фреймворки, используемые сторонними приложениями, опираются на уведомления Darwin для реализации важной функциональности.
Поскольку через уведомления Darwin передаётся очень ограниченный объём данных, они не представляют значительного риска утечки чувствительных данных, несмотря на использование публичного API и возможность их получения автономными приложениями. Однако, поскольку любой процесс в системе может зарегистрироваться для получения уведомлений Darwin, то же касается и их отправки.
Итак, уведомления Darwin:
- Не требуют особого допуска для получения.
- Не требуют особого допуска для отправки.
- Доступны в виде публичного API.
- Не имеют механизма для верификации отправителя.
Учитывая все эти свойства, мне стало интересно, есть ли в iOS места, где такие уведомления используются для важных операций, и которые потенциально можно эксплуатировать для реализации DoS-атаки из автономного приложения.
Ну и поскольку вы читаете эту статью, то ответ, очевидно «да».
▍ Подтверждение гипотезы: EvilNotify
Озадачившись этим вопросом, я взял свежую копию корневой файловой системы iOS — одну из первых бета версий iOS 18 — и начал искать процессы, которые используют
notify_register_dispatch
и notify_check
.Я быстро нашёл множество таких процессов и создал тестовое приложение, которое назвал «EvilNotify».

Ролик можно посмотреть в оригинале статьи
К сожалению, у меня больше нет уязвимого устройства, которое бы можно было использовать для записи полноценного видео, но в демо iOS Simulator выше я продемонстрировал всё основное, что мне удалось реализовать. Некоторые процессы в симуляторе не работают, поэтому на видео их нет.
В конце ролика вы можете заметить, какой в итоге получилась DoS-атака, но я хочу также перечислить всё остальное, чего мне удалось добиться. Имейте в виду, что любое из этих действий могло повлиять на всю систему, даже если бы пользователь закрыл приложение.
- Включить отображение иконки «liquid detection» (обнаружена жидкость) в панели состояния.
- Активировать статус подключения «Display Port» в Dynamic Island.
- Заблокировать системные жесты, позволяющие свайпом сверху вниз вызвать центр управления, центр уведомлений и экран блокировки.
- Заставить систему игнорировать WiFi и использовать вместо этого сотовую связь.
- Заблокировать экран.
- Активировать режим UI «data transfer in progress» (выполняется передача данных), который не позволяет использовать остальные функции устройства, пока пользователь его не отменит.
- Симулировать вход и выход устройства из «Lost Mode» (режим утери), после которого для восстановления доступа к Apple Pay система предлагает заново ввести данные Apple ID.
- Спровоцировать вход устройством в режим «restore in progress» (восстановление).
▍ «Восстановление»
Поскольку я искал способ совершить DoS-атаку, последний пункт выглядел наиболее перспективным, так как выйти из этого режима можно только через нажатие кнопки «Перезапуск», что всегда приведёт к перезагрузке устройства.
Да и в целом это был весьма лаконичный вариант, включавший всего одну строку кода:
notify_post("com.apple.MobileSync.BackupAgent.RestoreStarted")
Вот оно! Этой строки будет достаточно, чтобы устройство вошло в режим «восстановления». По истечении периода ожидания эта операция будет неизбежно проваливаться, так как по факту устройство не восстанавливается, и исправить это можно лишь путём перезагрузки.
Судя по информации SpringBoard, это уведомление активирует UI. Оно создаётся при восстановлении устройства из локальной резервной копии или через подключённый компьютер, но, как я выяснил ранее, отправить его мог любой процесс, заставив систему войти в режим восстановления.
▍ Отказ в обслуживании: VeryEvilNotify
Теперь, когда я нашёл уведомление Darwin, которое потенциально можно было превратить в DoS-атаку, нужно было лишь придумать способ активировать его циклически после перезагрузки устройства.
Поначалу эта задача казалась мне довольно сложной, так как приложения iOS очень ограничены в возможностях совершать действия из фонового режима, и довольно много API с побочными эффектами не работают, когда приложение неактивно. Второй момент меня не беспокоил, так как я смог подтвердить работоспособность
notify_post
, даже когда приложение находилось в фоновом режиме. Что же касается возможности повторно отправлять уведомления после перезагрузки устройства, то здесь я не был уверен, но догадывался, что самым вероятным кандидатом на успех будет расширение приложения.
Некоторые типы сторонних расширений могут запускаться ещё до первой разблокировки устройства iOS, так что я решил попробовать знакомый мне вид расширения и создал виджет в новом приложении, которое назвал «VeryEvilNotify».
Виджеты периодически активируются ОС в фоновом режиме. У них есть ограниченный промежуток времени для генерации снимков системы и таймлайна, которые затем ОС выводит в различных местах, включая экран блокировки, домашний экран, центр уведомлений и центр управления.
Поскольку виджеты используются в системе повсеместно, то при установке и запуске нового приложения, включающего такое расширение, система без раздумий его выполняет. Это делает такие виджеты готовыми к добавлению пользователем в различные подходящие места.
В своей сути виджет — это просто процесс, который может выполнять код, поэтому я добавил вышеупомянутую строку кода в своё расширение. Это расширение я настроил так, чтобы оно включало всевозможные типы виджетов, с целью максимально подтолкнуть систему выполнить его как можно раньше.
Но возникла проблема: расширения виджетов создают плейсхолдеры, снимки и таймлайн, которые затем кэшируются системой для экономии ресурсов. Они не работают фоном постоянно, и даже если такое расширение запрашивает очень частое обновление, система установит временные рамки и будет просто это обновление задерживать.
Для обхода этого я решил сделать так, чтобы мой виджет всегда падал вскоре после выполнения функции
notify_post
. Для этого я вызывал функцию Swift fatalError()
в каждом из методов TimelineProvider
.Вызов
notify_post
совершался внутри точки входа расширения перед делегированием дальнейшего выполнения его окружению:import WidgetKit
import SwiftUI
import notify
struct VeryEvilWidgetBundle: WidgetBundle {
var body: some Widget {
VeryEvilWidget()
if #available(iOS 18, *) {
VeryEvilWidgetControl()
}
}
}
/// Переопределение точки входа расширения, чтобы код эксплойта запускался при каждом его пробуждении системой.
@main
struct VeryEvilWidgetEntryPoint {
static func main() {
notify_post("com.apple.MobileSync.BackupAgent.RestoreStarted")
VeryEvilWidgetBundle.main()
}
}
Когда виджет был готов, как только я установил VeryEvilNotify на свой экспериментальный девайс, отобразился режим «Восстановление», после провала которого последовало диалоговое окно с предложением перезапустить систему.
После перезагрузки система активировала расширение следом за инициализацией SpringBoard, поскольку до этого момента оно не могло создавать какие-либо записи виджета, и это всякий раз приводило к повторению всего процесса.
Результатом стало программное окирпичивание устройства, требующее очистки системы и восстановления из резервной копии. Думаю, если бы приложение оказалось в бэкапе, и устройство восстановили из него, баг продолжил бы срабатывать, став ещё более эффективной DoS-атакой.

Посмотреть ролик можно в оригинале статьи
Я ожидал, что в iOS на случай падения виджета сработает некий механизм повтора попытки, что определённо привело бы к срабатыванию ограничения частоты вызовов. И, мне кажется, так и есть, просто тот тайминг, с которым происходило падение расширения и начиналось восстановление системы с последующим провалом наверняка не дал этому ограничению возможности сработать.
Довольный тем, что мой эксперимент удался, я сообщил о проблеме в Apple.
▍ Хронология
Ниже я привёл общую хронологию формирования этого отчёта об уязвимости. Были ещё дополнительные обновления статуса посредством автоматических сообщений системы Apple, которые я не стал включать.
- 26 июня 2024: отправка первичного отчёта в Apple.
- 27 сентября 2024: получил от Apple уведомление о том, что над проблемой работают.
- 28 января 2025: проблема отмечена как решённая, и обоснованность вознаграждения подтверждена.
- 11 марта 2025: баг получил номер CVE-2025-24091 и был устранён с обновлением iOS/iPadOS 18.3
- Сумма вознаграждения за его обнаружение: $17 500
Несмотря на то, что уязвимости уже был присвоен номер CVE, и в Apple даже создали ссылку, где должно появиться её описание с упоминанием причастных к раскрытию, этого ещё не произошло. Мне сообщили, что вскоре эти сведения опубликуют, но я на всякий случай приведу информацию об уязвимости ниже.

Перевод
В Apple присвоили обозначенной проблеме номер CVE-2025-24091. CVE — это уникальные ID, используемые для идентификации уязвимостей. Ниже приведено её описание и влияние на систему:
- Влияние: приложение может вызывать состояние «отказа в обслуживании».
- Описание: приложение может имитировать системные уведомления. Чувствительные уведомления теперь требуют использования ограниченных допусков.
Обратите внимание на фразу «чувствительные уведомления теперь требуют использования ограниченных допусков», намекающую на то, как именно проблема была исправлена. Подробнее о самом исправлении я расскажу ниже.
▍ Исправление
Как сказано в справке, для отправки чувствительных уведомлений Darwin процесс теперь должен обладать ограниченным допуском. Причём имеется в виду не просто допуск, позволяющий отправлять любые чувствительные уведомления, а допуск в виде префикса
com.apple.private.darwin-notification.restrict-post.<notification>
.Насколько я понял, бегло изучив дизассемблированный код, «ограниченным» уведомление делает приставка
com.apple.private.restrict-post.
в его имени.Например, уведомление
com.apple.MobileBackup.BackupAgent.RestoreStarted
теперь отправляется по адресу com.apple.private.restrict-post.MobileBackup.BackupAgent.RestoreStarted
, после чего notifyd
сначала проверяет, чтобы отправляющий процесс обладал допуском com.apple.private.darwin-notification.restrict-post.MobileBackup.BackupAgent.RestoreStarted
, и только потом позволяет это уведомление отправить.Процессы, которые наблюдают опубликованное уведомление, также будут использовать новое имя с префиксом
com.apple.private.restrict-post
, чтобы никакое случайное приложение или процесс без допуска не могли разместить уведомление, способное оказать серьёзное побочное воздействие на систему.У меня не было возможности покопаться в старых релизах iOS, чтобы найти конкретную версию, где эта защита была введена впервые. Но с помощью ipsw-diffs мне удалось выяснить, что описанный механизм допуска появился в iOS 18.2 build 22C5125e, то есть iOS 18.2 beta 2.
Первыми его начали использовать процессы
backupd
, BackupAgent2
и UserEventAgent
, получая допуски, связанные с уведомлением системы о восстановлении устройства. Таким образом была закрыта самая грубая уязвимость из продемонстрированных в моём эксперименте.При последующем выходе различных бета-версий iOS 18 и прочих релизов новый допуск для уведомлений стали получать и другие процессы. А с выходом iOS 18.3 были решены и все другие указанные мной проблемы.
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
