Подробные статьи о технологиях, определяющих будущее.

Джейлбрейк Switch 2: цепочка эксплойтов против Secure Boot

Джейлбрейк Switch 2 связал переполнение в BASIC-интерпретаторе, баг путей fs и гонку загрузчика. Разбираем уроки для цепочек эксплойтов и secure boot.

Портативная консоль, подключённая к аппаратному управлению, где роботизированный рычаг нажимает кнопку питания на верстаке.
Современные стенды для взлома консолей сочетают KVM-over-IP с Bluetooth-рычагом, который физически нажимает кнопку питания после каждого краша.

Свежая консоль выходит с закалённым микроядром, аутентификацией указателей (pointer authentication) для каждого адреса возврата, исполняемой-только памятью, control-flow integrity и цепочкой подписанной загрузки. Через две недели энтузиаст уже запускает произвольный нативный код от имени файловой службы — самого доверенного процесса в системе. Это история первого публичного джейлбрейка Switch 2, и ей стоит смутить каждого инженера, который строит защищённые системы. Не потому что защиты провалились — большинство из них сработало ровно так, как задумано, — а потому что эксплойту вообще не пришлось лобово их ломать. Он обошёл их, по одной логической ошибке за раз.

Много лет я работаю на оборонительной стороне инфраструктурной безопасности, и этот паттерн мне знаком по разбору продакшн-инцидентов чаще, чем хотелось бы: цепочка по отдельности невпечатляющих багов, каждый из которых даёт ровно столько привилегий, чтобы добраться до следующего. Первый плацдарм был вообще не в самом Switch 2. Это была ошибка повреждения памяти в игре для Switch 1 — интерпретаторе SmileBASIC — которая работала в режиме обратной совместимости. Оттуда цепочка поднялась через файловую службу, использовала гонку в загрузчике программ и закончилась возможностью запускать нативный код под идентичностью любого sysmodule. Каждое звено заслуживает внимания, потому что каждое соответствует ошибке, которую я видел у команд и в серверных системах.

Почему дверь через браузер была закрыта, а игровая — нет

WebKit — классическая первая цель для джейлбрейков консолей, и исследователь действительно получил плацдарм через captive portal и n-day. Но тупик наступил быстро. Стек браузера Switch 2 обёрнут в три защиты, которые очень хорошо складываются вместе: исполняемая-только память (XOM) означает, что даже при произвольном чтении и записи нельзя найти полезные гаджеты кода; аутентификация указателей (PAC) означает, что нельзя подделать указатель на код без оракула подписи; а CFI означает, что даже корректно подписанный указатель не пройдёт проверки типов в большинстве точек вызова. Сломать любую из них — исследовательский проект. Сломать все три, связав их, из песочницы JavaScript — это уже карьера.

Поэтому атака переключилась на самого слабого жильца машины: сторонние игры для Switch 1. Рассуждение было холодным и верным. Игровые бинарники собирают студии, а не команда безопасности Nintendo, поэтому их гораздо реже компилируют с полным набором защит. К тому же у Switch 1 вообще не было PAC, так что всё, что работает в его окружении совместимости, наследует эту слабость. Ещё лучше то, что корень доверия Switch 1 был скомпрометирован много лет назад, а значит, игровой код можно расшифровать и проаудировать офлайн. SmileBASIC — огромный C++-интерпретатор BASIC, который уже эксплуатировали на 3DS — стал очевидной мягкой целью.

Здесь есть урок, который выходит за рамки консолей: ваш уровень безопасности — это уровень наименее защищённого кода, который вы соглашаетесь запускать. Обратная совместимость — это поверхность атаки, которую вы наследуете, а не выбираете. Модель безопасности Xbox выучила это на собственном опыте в 2000-х: эксплойты сохранений в незащищённых играх стали клином, раскрывшим куда более жёсткое ядро — об этом мы писали в статье что взлом оригинального Xbox рассказывает о моделях аппаратной безопасности. Двадцать лет спустя тот же ход всё ещё работает.

Переполнение целого числа, которое открыло первую дверь

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

// intent: ensure the removed range is inside the array
if (position + count <= array.length) {
memmove(backing + position * 8,          // start of the gap
backing + (position + count) * 8, // elements after the gap
(length - position) * 8);          // bytes to copy
}

Вызов REMOVE R, &H60000000, &H3FFFFFFF на массиве из четырёх элементов заставляет position + count переполниться до 0x9FFFFFFF, что как знаковое 32-битное значение глубоко отрицательно — то есть заведомо «меньше» длины. Умножения на 8 тоже переполняются, и в итоге выполняется memmove(backing, backing - 8, 32). Команда после этого выводит ошибку нехватки памяти, но копирование уже произошло. И вот самое красивое: 8 байт непосредственно перед любым массивом SmileBASIC содержат указатель на сам массив. После удаления R[0] интерпретируется как строка, поле длины которой — на самом деле этот указатель, то есть чудовищно длинная строка, через которую можно читать и писать. Произвольное чтение и запись, преподнесённые в подарочной упаковке собственной раскладкой памяти интерпретатора.

Если вы пишете на C или C++ для жизни, посмотрите на эту проверку минуту. position + count <= length — самая распространённая идиома проверки границ в системном коде, и она неверна всякий раз, когда сумма может переполниться. Правильная форма — position <= length && count <= length - position, либо checked-арифметика. Я находил варианты этого бага в парсере пакетов на прошлой работе и в одном открытом движке хранилища. Он переживает код-ревью, потому что выглядит очевидно правильным. Это разрыв между спецификацией и реализацией в миниатюре: спецификация говорит «проверь диапазон», реализация проверяет что-то соседнее с диапазоном, и разница оказывается эксплуатируемой. Мы писали о том, почему баги живут в разрыве между спецификацией и реализацией; консольные интерпретаторы — просто место, где их находят мотивированные подростки, а не фаззеры.

Эскалация до файловой службы через переполнение при обработке путей

Чтение и запись в пользовательском пространстве внутри игрового процесса сами по себе ничего не дают: микроядерная архитектура Switch означает, что каждая служба — отдельный процесс со своей IPC-поверхностью. Ценной целью был fs, встроенный файловый sysmodule, по очень рекурсивной причине: fs — компонент, который отдаёт расшифрованный код загрузчику. Получив fs, можно выгрузить код любого другого sysmodule для офлайн-анализа. Квартирмейстер крепости сам выдаёт вам чертежи.

Ошибка была переполнением кучи в нормализации путей. Команда IPC GetEntryType нормализует аргумент-путь, и пути с компонентом родительского каталога и обратным слешем — /..\ — попадают во вспомогательную функцию, которая выделяет фиксированный буфер в 0x301 байт и копирует туда путь без адекватной проверки длины. Достаточно длинный путь переполняет буфер и затирает соседние объекты в куче. Собственный комментарий исследователя при обнаружении был одним словом: «лол».

Это «лол» должно жечь, потому что нормализация путей — ровно та неэффектная инфраструктурная сантехника, которая никогда не получает такого внимания безопасности, как криптография. Эксплойт превратил переполнение в полноценное произвольное чтение и запись, повредив соседний кеш-объект, у которого бит dirty перенаправляет файловый ввод-вывод через указатель, контролируемый атакующим — чистый примитив для grooming кучи. Затем, сканируя память, эксплойт нашёл в таблицах fs объект соединения собственного процесса и переписал сохранённый идентификатор процесса на 1, предоставив игровому процессу «FullPermission» над файловой системой. Обратите внимание, что здесь произошло: ни одна криптографическая проверка не была сломана. Модель прав держалась на данных в памяти, а атакующий умел писать в память.

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

Обход проверок подписи подменой файловой системы на лету

Самое хитрое звено цепочки обходит подпись кода, не трогая ни одного ключа. Загрузчик программ (ldr) проверяет две подписи перед запуском чего-либо: подпись Nintendo над метаданными прав процесса и ключом разработчика, а также подпись разработчика над хешем заголовка архива содержимого. Теоретически этот хеш транзитивно фиксирует каждый байт исполняемого файла. На практике есть брешь: именно fs должна гарантировать, что отдаваемая файловая система действительно соответствует хешу, который она уже сообщила, а у атакующего было произвольное чтение и запись внутри fs.

Поэтому эксплойт ждёт, пока загрузчик запросит исполняемую файловую систему и fs уже передаст корректный хеш и подпись, а затем подменяет объект файловой системы за IPC-хендлом загрузчика на подготовленный, который отдаёт настоящий подписанный файл метаданных, но произвольный подменённый исполняемый файл. Проверки подписи проходят, потому что они касались байтов, которые атакующему вообще не нужно было менять. Таймингом управляют, переписывая хендл, на который fs ждёт соединения загрузчика, на событие, которым атакующий управляет из игрового процесса, — и состояние гонки превращается в детерминированную подмену.

Криптографическая цепочка от хеша заголовка до содержимого исполняемого файла была корректна — на бумаге. Единственное звено, которое существовало только как допущение времени выполнения, — «процесс, отдающий файлы, честен», — и оказалось тем, что сломалось.

Это ошибка time-of-check/time-of-use в плаще, и это самый переносимый урок во всём разборе. Всякий раз, когда компонент A проверяет данные, а компонент B позже действует на их основе через изменяемую ссылку, есть окно для подмены. Однажды я разбирал внутреннюю систему деплоя, которая проверяла дайджест образа контейнера, а потом при выкатке тянула образ по тегу из реестра — тот же баг, только в другой шляпе. Исправление в обоих случаях одинаковое: проверять в момент использования или передавать потребителю неизменяемый проверенный объект, а не хендл на то, что он должен доверять третьей стороне в честности. Оборонительный дизайн, который предполагает, что каждый посредник скомпрометирован, — не паранойя; на машине, где атакующий уже частично может писать, это просто точное моделирование. Это ключевой аргумент в статье оборонительная инженерия для систем, устойчивых к катастрофам, а этот эксплойт — хороший пример.

Заимствование хендла ядра и патч fs изнутри

Финальный трюк маленький и красивый. Чтобы пропатчить собственный код процесса fs, эксплойту нужен был хендл ядра на него. Он его получил, перезаписав 4-байтовое поле хендла в памяти магическим значением 0xffff8001, которое ядро интерпретирует как «хендл текущего процесса». Игровой процесс вызвал безобидный API, получил назад собственный хендл процесса, передал его злонамеренному процессу с правами sysmodule загрузки кода, и тот через легитимные системные вызовы отобразил и пропатчил код fs. Каждая отдельная операция была авторизована. Компоновка дала полную компрометацию самого доверенного процесса на устройстве.

Если это звучит знакомо, то потому что проблемы confused deputy повсюду, стоит начать смотреть: правила sudo, которые разрешают безобидный бинарник с записываемым конфигом, облачные IAM-роли, которые могут передать себя более мощному сервису, системы сборки, которые подписывают всё, что им передаёт CI-задача. Мы разбирали сторону эскалации привилегий в этом паттерне в статье история эскалации привилегий в Unix — механизмы меняются, форма бага нет. Хендлы, capabilities и токены настолько сильны, насколько целостна память, в которой они хранятся.

Что раз за разом делают неправильно разработчики Secure Boot

Отступите и посмотрите: по всей цепочке рисунок един, и он тот же, что стоит за большинством серьёзных корпоративных взломов, которые я разбирал: проверенное ядро устояло, а всё вокруг — нет. Загрузочное ПЗУ, схема подписи и защиты ядра Nintendo были в порядке. Пало то, что ниже: проверка границ в интерпретаторе, буфер нормализации путей, поле прав, хранящееся в записываемой памяти, окно TOCTOU между двумя доверенными службами и таблица хендлов, которая брала указатели ядра из структур, доступных для записи из пользовательского пространства. Ничего из этого экзотикой не назовёшь. Всё это из категории вещей, которые переживают ревью, потому что каждая отдельно выглядит локально разумно.

  • Аудитируйте скучный код. Нормализаторы путей, проверки длины и таблицы хендлов — именно там начинаются цепочки эксплойтов, а не в криптографии. Выделяйте время на ревью пропорционально досягаемости для атакующего, а не алгоритмической сложности.
  • Считайте каждую брешь в защите дорожной картой. Если какой-то процесс в системе не имеет PAC/CFI — слой совместимости, сторонний бинарник, устаревший интерпретатор, — считайте его точкой входа и моделируйте, до чего он может добраться.
  • Уберите доверие по положению. Компонент, который проверяет данные и передаёт дальше изменяемый хендл, создал окно для подмены. Передавайте неизменяемые, самоверифицирующиеся объекты или перепроверяйте при использовании.
  • Считайте, что записываемая память где угодно — записываемая память везде. Флаги прав, поля PID и значения хендлов, хранящиеся в памяти, доступной атакующему, — часть вашей поверхности атаки, а не механизм принуждения.

Другой неприятный вывод касается инструментов. Эксплойт разрабатывал AI-агент, которому дали физический доступ к железу через устройства KVM-over-IP — видео на вход, ввод с контроллера по USB и Bluetooth-рычаг, который нажимает кнопку питания после каждого краша. Несколько экземпляров агента параллельно работали над разными поверхностями атаки круглые сутки в течение двух недель. Что бы вы ни думали об AI-ассистированных исследованиях безопасности, экономика поиска уязвимостей в закрытом железе уже изменилась. Баги, которые раньше требовали терпеливого эксперта, теперь требуют терпеливого эксперта, который может руководить тремя неутомимыми помощниками. Если ваша модель угроз исходила из того, что «никто не станет реверсить этот sysmodule», это допущение истекло в этом месяце.

Цепочка и есть продукт

Соблазнительно читать разбор джейлбрейка как список ошибок Nintendo. Лучше читать его как зеркало. У каждой системы, которую я когда-либо помогал защищать, нашёлся аналог SmileBASIC (наследуемого компонента, который никто не укреплял), аналог буфера путей (инфраструктурного кода, который перерос свои допущения) и аналог гонки в загрузчике (проверки, доверявшей изменяемому посреднику). Преимущество атакующих — не гениальность на отдельном шаге, а то, что они думают цепочками, пока защитники думают компонентами.

Поэтому упражнение для вашей команды на этой неделе — не вопрос «может ли кто-то подделать наши подписи». Оно такое: выберите самый доверенный внутренний сервис, допустите, что у атакующего уже есть произвольная запись в наименее защищённом процессе, который с ним общается, и пройдите цепочку до конца. Где случится первая эскалация? До чего может дотянуться полученная identity? Собственный sysmodule fs вы найдёте довольно скоро. Лучше вы, чем кто-то с KVM, Bluetooth-рычагом и двумя неделями терпения.