Fuchsia — это операционная система общего назначения с открытым исходным кодом, разрабатываемая компанией Google. Эта операционная система построена на базе микроядра Zircon, код которого написан на C++. При проектировании Fuchsia приоритет был отдан безопасности, обновляемости и быстродействию. Как исследователь безопасности ядра Linux я заинтересовался операционной системой Fuchsia и решил посмотреть на нее с точки зрения атакующего. В этой статье я поделюсь результатами своей работы. Обзор архитектуры безопасности операционной системы Fuchsia. Сборка Fuchsia из исходного кода, создание и запуск простейшего приложения. Микроядро Zircon: основы разработки ядра для Fuchsia, его отладка с помощью GDB. Результаты моих экспериментов по эксплуатации уязвимостей в микроядре Zircon:  Попытки фаззинга. Эксплуатация повреждения памяти C++-объекта. Перехват потока управления в ядре. Установка руткита в Fuchsia. Демонстрация прототипа эксплойта. Я придерживаюсь принципов ответственного разглашения информации, поэтому сообщил мейнтейнерам Fuchsia о проблемах безопасности, обнаруженных в ходе этого исследования. Fuchsia — это операционная система общего назначения с открытым исходным кодом. Компания Google начала ее разработку в 2016 году. В декабре 2020 года этот проект был открыт для внешних участников, а в мае 2021 года Google впервые выпустила Fuchsia на устройствах Nest Hub для управления умным домом. Операционная система поддерживает микроархитектуры arm64 и x86_64. Разработка Fuchsia сейчас находится в активной фазе, проект выглядит живым, поэтому я решил поэкспериментировать с ним. Рассмотрим основные концепции, на которых базируется архитектура Fuchsia. Эта ОС разрабатывается для целого спектра устройств: IoT, смартфонов, персональных рабочих станций. Разработчики Fuchsia уделяют особое внимание ее безопасности и обновляемости. Как результат, эта операционная система имеет необычную архитектуру безопасности: Главное — в Fuchsia отсутствует концепция пользователя. Вместо этого разграничение доступа в ней основано на разрешениях (capabilities). Приложениям в пользовательском пространстве ядро предоставляет свои ресурсы в виде объектов, доступ к которым требует соответствующих разрешений. Иными словами, приложение не может использовать ядерный ресурс без выданного разрешения. Все приложения в Fuchsia имеют минимальные привилегии, необходимые для выполнения задачи. Поэтому в системе с такой архитектурой атака для повышения привилегий отличается от того, к чему мы привыкли в GNU/Linux-системах, где атакующий исполняет код как непривилегированный пользователь и эксплуатирует некоторую уязвимость для получения привилегий суперпользователя. Второй интересный аспект — Fuchsia является микроядерной ОС. Это во многом определяет ее свойства безопасности. По сравнению с ядром Linux большое количество функциональности вынесено из микроядра Zircon в пользовательское пространство. Это существенно уменьшает периметр атаки ядра. Ниже представлена схема из документации Fuchsia, которая демонстрирует, что Zircon выполняет значительно меньше функций по сравнению с классическими монолитными ядрами ОС. Вместе с тем разработчики Zircon не стремятся сделать его совсем крошечным: в нем реализовано 176 системных вызовов, что намного больше, чем обычно бывает в других микроядрах.  Еще одно архитектурное решение, которое влияет на безопасность системы, — это изоляция компонентов (sandboxing). Компонентами называются приложения и системные сервисы в Fuchsia. Каждый из них работает в изолированном окружении — песочнице (sandbox), и все межпроцессное взаимодействие (inter-process communication, IPC) между ними явно декларируется. В Fuchsia даже нет глобальной файловой системы. Вместо этого каждому компоненту выдается отдельное пространство для работы с файлами. Это архитектурное решение явно увеличивает изоляцию и безопасность программного обеспечения в пользовательском пространстве. Вместе с тем, на мой взгляд, это делает микроядро Zircon особенно интересной целью для атакующего, поскольку Zircon предоставляет интерфейсы системных вызовов всем компонентам операционной системы. Наконец, Fuchsia имеет необычную схему доставки и обновления ПО. Приложения идентифицируются с помощью URL и скачиваются системой непосредственно перед их запуском. Такое архитектурное решение было выбрано для того, чтобы программные пакеты в Fuchsia всегда были в актуальном состоянии (наподобие веб-страниц).  Из-за перечисленных свойств безопасности Fuchsia я заинтересовался этой операционной системой и решил исследовать ее с точки зрения атакующего. В документации Fuchsia представлено хорошее руководство по быстрому старту. В нем дается ссылка на скрипт, который проверит, есть ли в вашей GNU/Linux-системе полный набор инструментов для разработки Fuchsia: При запуске этот скрипт сообщает, что дистрибутивы, не родственные Debian, не поддерживаются. При этом я не заметил никаких проблем со сборкой Fuchsia на Fedora 34. В документации также объясняется, как скачать исходный код Fuchsia и настроить переменные окружения, необходимые для компиляции. Вот команды, с помощью которых выполняется сборка системы в варианте workstation product для микроархитектуры x86_64: После сборки операционная система может быть запущена в эмуляторе FEMU (Fuchsia emulator). FEMU базируется эмуляторе Android (AEMU), который, в свою очередь, является форком QEMU. Теперь давайте создадим простейшее приложение hello world для Fuchsia. Как я уже упоминал, программы для Fuchsia называются компонентами. Вот эта команда создает шаблон нового компонента на языке C++: Компонент будет писать приветствие в системный журнал (Fuchsia log): В манифесте компонента src/a13x-pwns-fuchsia/meta/a13x_pwns_fuchsia.cml должна быть разрешена работа с системным журналом: Вот команды, которые собирают Fuchsia с новым компонентом: После компиляции мы можем протестировать систему с новым компонентом: Запускаем FEMU с помощью команды fx vdl start -N в первом терминале нашей GNU/Linux-системы. Запускаем сервер публикации пакетов Fuchsia во втором терминале с помощью команды fx serve. Выполнив команду fx log, открываем системный журнал Fuchsia в третьем терминале. Запускаем новый компонент в Fuchsia с помощью команды ffx в четвертом терминале: На снимке экрана можно увидеть, как Fuchsia нашла компонент по URL, загрузила его с сервера публикации пакетов и запустила. В результате компонент напечатал сообщение Hello from a13x, Fuchsia! в системном журнале, показанном в третьем терминале. Теперь рассмотрим, какими инструментами пользуется разработчик микроядра Zircon в своей повседневной работе. Код Zircon на языке C++ является частью исходного кода Fuchsia и находится в директории zircon/kernel. Сборка микроядра происходит при компиляции Fuchsia. Для разработки и отладки требуется запускать Zircon в QEMU с помощью команды fx qemu -N, однако у меня система выдала ошибку при первом же выполнении команды: Я обнаружил, что ошибка появляется, только если на системе настроена локаль, отличная от английской. Эта неполадка известна уже давно. Понятия не имею, почему имеющееся исправление до сих пор не принято в Fuchsia OS. С ним Fuchsia успешно стартует на виртуальной машине, созданной в QEMU/KVM: Запуск Fuchsia в QEMU/KVM позволяет выполнять отладку микроядра Zircon с помощью GDB. Вот как это выглядит на практике: Запускаем Fuchsia: Аргумент -s 1 задает количество процессорных ядер у виртуальной машины. Запуск с одним vCPU существенно упрощает работу с отладчиком. Аргумент --no-kvm отключает аппаратную виртуализацию. Он полезен, если вам необходима пошаговая отладка (single-stepping). Без этого аргумента после каждой команды stepi или nexti отладчик будет проваливаться в обработчик прерывания, которое доставил гипервизор KVM. Однако, естественно, в режиме --no-kvm виртуальная машина с Fuchsia будет работать сильно медленнее, чем с аппаратной виртуализацией. Аргумент -s в конце команды задействует gdbserver, который открывает сетевой порт 1234. Разрешаем выполнение GDB-скрипта для Zircon. Он предоставляет следующие функции: Адаптация к рандомизации адресного пространства ядра (KASLR) для корректного размещения точек останова (breakpoints). Специальные команды GDB с префиксом zircon. Улучшенное отображение сообщений об отказах микроядра Zircon. Запускаем GDB-клиент и подключаемся к GDB-серверу виртуальной машины с Fuchsia: Эта процедура позволяет отлаживать микроядро Zircon в GDB, как мы привыкли это делать с ядром Linux. Однако на моей машине упомянутый GDB-скрипт для Zircon безнадежно зависал при каждом запуске — пришлось разбираться. Оказалось, что он вызывает GDB-команду add-symbol-file с параметром -readnow, который требует от отладчика немедленно обработать все символы из 110-мегабайтного исполняемого файла Zircon. По какой-то причине у GDB не получается сделать это за обозримое время, и кажется, будто отладчик завис. Без параметра -readnow проблема исчезла, и я получил нормальную отладку микроядра Zircon в GDB: KASAN (Kernel Address SANitizer) — это технология обнаружения повреждения ядерной памяти. Она позволяет находить выход за границу массива (out-of-bounds accesses) и использование памяти после освобождения (use after free). В Fuchsia поддерживается компиляция микроядра Zircon с инструментацией KASAN. Я решил испробовать эту функциональность и собрал Fuchsia в варианте core product: Чтобы протестировать, как KASAN ловит повреждения ядерной памяти, я добавил синтетическую ошибку освобождения памяти в код Fuchsia, работающий с объектом TimerDispatcher: Если таймер выставляется на задержку, значение которой заканчивается цифрами 31337, то память объекта TimerDispatcher освобождается вне зависимости от счетчика ссылок (refcount). Я захотел спровоцировать эту ядерную ошибку из моего компонента в пользовательском пространстве, чтобы увидеть, как ядро уходит в отказ и отображает отчет KASAN. Для этого я добавил следующий код в мой компонент a13x-pwns-fuchsia: Здесь компонент сначала выполняет системный вызов zx_timer_create(). Он инициализирует таймер и возвращает в пользовательское пространство специальный указатель на него (handle), имеющий тип zx_handle_t. Затем для таймера устанавливается задержка, значение которой заканчивается «элитными» цифрами 31337. Пока программа ожидает на вызове zx_nanosleep(), Zircon освобождает память сработавшего таймера. А последующий системный вызов zx_timer_cancel() для удаленного таймера приводит к использованию памяти после освобождения. KASAN обнаруживает повреждение ядерной памяти и уводит микроядро в отказ при выполнении этого кода в пользовательском пространстве. Вместе с тем в ядерном логе распечатывается вот такой замечательный отчет: Отлично, KASAN работает. Zircon также выводит трассу исполнения (backtrace), но в нечитаемом виде, как цепочку ядерных указателей. Чтобы это исправить, нужно обработать содержимое ядерного журнала с помощью специального инструмента: Вот как трасса исполнения выглядит после fx symbolize: Здесь можно видеть, что обработчик wrapper_timer_cancel() системного вызова выполняет функцию sys_timer_cancel(), где GetDispatcherWithRightsImpl<TimerDispatcher>() обращается ко счетчику ссылок (reference counter), расположенному в освобожденной памяти объекта TimerDispatcher. Эта ошибка обнаруживается в функции asan_check(), принадлежащей механизму KASAN. В итоге работа ядра прерывается с помощью вызова panic(). Эта трасса исполнения детально описывает, как на самом деле работает код функции sys_timer_cancel(): Когда я получил работающий KASAN для Fuchsia, я почувствовал, что готов начать исследование с позиции атакующего. После изучения основ ядерной разработки Fuchsia и тестирования KASAN я приступил к экспериментам с безопасностью. Я поставил цель — разработать прототип эксплойта для уязвимости в Zircon, и в первую очередь мне нужно было найти подходящую уязвимость. Для поиска я решил использовать фаззинг. Есть прекрасный фаззер для ядер операционных систем, который называется syzkaller. Мне очень нравится этот проект, я уже давно использую его для фаззинга ядра Linux. В документации говорится, что syzkaller поддерживает фаззинг Fuchsia, поэтому я сразу решил это попробовать. Однако возникли трудности из-за необычной схемы доставки ПО для Fuchsia, о которой говорилось выше. Системный образ Fuchsia для фаззинга должен содержать программу syz-executor в качестве компонента. syz-executor — это часть проекта syzkaller, которая отвечает за выполнение фаззинга системных вызовов в виртуальной машине. Мне не удалось собрать образ Fuchsia с этим компонентом. Вначале я попробовал скомпилировать Fuchsia с исходниками syzkaller, размещенными во внешней директории. Этот способ не сработал, хотя он рекомендуется в документации: Я попытался отладить систему сборки Fuchsia и выяснил, что она неправильно обрабатывает аргумент syzkaller_dir, но починить это мне не удалось. Затем я обнаружил, что в исходном коде Fuchsia в директории third_party/syzkaller/ хранится локальная копия исходников syzkaller. Система сборки использует ее, если не задан аргумент --args=syzkaller_dir, но эта копия syzkaller старая: в ней отсутствуют все коммиты после 2 июня 2020 года. Я попробовал собрать текущую версию Fuchsia с этой старой версией фаззера, что также не удалось сделать из-за перемещения файлов и множества изменений в системных вызовах Fuchsia, которые произошли с того момента. Тогда я попробовал обновить версию фаззера в директории third_party/syzkaller/ в надежде, что свежие коммиты в репозитории syzkaller помогут синхронизироваться с текущей версией Fuchsia. Но эта затея также провалилась, потому что для сборки актуальной версии syz-executor требуется внести значительные изменения в его сборочный файл BUILD.gn. В итоге ситуация выглядит так: интеграция операционной системы Fuchsia с фаззером syzkaller, возможно, и работала когда-то в 2020 году, но сейчас она сломана. По истории разработки Fuchsia в системе контроля версий я нашел авторов этого кода и отправил им электронное письмо, в котором детально описал все обнаруженные неполадки и попросил помощи, но ответа не получил. Чем больше времени я тратил на борьбу с системой сборки Fuchsia, тем больше начинал сердиться. Тогда я крепко задумался о стратегии моих дальнейших исследований. Без фаззинга для успешного поиска уязвимостей обязательно требуются: хорошее знание кодовой базы атакуемой системы; глубокое понимание ее периметра атаки. Чтобы приобрести эти знания об операционной системе Fuchsia, мне пришлось бы потратить много времени и сил. Хотел ли я этого при моем первом знакомстве с Fuchsia? Пожалуй, нет, потому что: неразумно тратить большое количество ресурсов на первое ознакомительное исследование; по первому впечатлению, Fuchsia оказалась менее подготовлена к промышленному использованию, чем я ожидал. Поэтому скрепя сердце я решил пока не жадничать и отложить поиск уязвимостей нулевого дня (zero-day) в микроядре Zircon. Вместо этого я задумал разработать прототип эксплойта для той синтетической уязвимости, которую я использовал при тестировании KASAN. В конечном итоге это оказалось удачным решением, поскольку я относительно быстро получил результат, а также смог найти несколько проблем безопасности в Zircon. Таким образом, я сосредоточился на эксплуатации использования памяти после освобождения для объекта TimerDispatcher. Моя стратегия состояла в том, чтобы перезаписать освобожденный TimerDispatcher контролируемыми данными и тем самым спровоцировать нештатную работу микроядра Zircon, которой я как атакующий смогу управлять. В первую очередь для перезаписи объекта TimerDispatcher мне нужно было реализовать технику эксплуатации heap spraying, которая: может быть использована атакующим из непривилегированного кода в пользовательском пространстве; заставляет Zircon выделить множество новых ядерных объектов (вот почему это называется спреем), один из которых с большой вероятностью попадет на место освобожденного; заставляет Zircon наполнить этот новый ядерный объект данными атакующего, скопированными из пользовательского пространства. Из своего опыта эксплуатации уязвимостей для ядра Linux я знал, что heap spraying обычно конструируется с помощью средств межпроцессного взаимодействия (IPC). Базовые системные вызовы, предоставляющие IPC, доступны непривилегированным программам, что соответствует первому из трех названных мной свойств heap spraying. Такие системные вызовы копируют пользовательские данные в адресное пространство ядра, чтобы затем передать их получателю, — это свойство номер три. И, наконец, некоторые системные вызовы, предоставляющие IPC, позволяют задавать размер передаваемых данных, что дает атакующему возможность контролировать поведение ядерного аллокатора и позволяет перезаписать освобожденный целевой объект, — это соответствует свойству номер два. Чтобы сконструировать heap spraying для микроядра Zircon, я принялся изучать его системные вызовы, предоставляющие IPC, и отыскал Zircon FIFO. Это очереди для передачи сообщений, с помощью которых отлично получилось реализовать технику heap spraying. Когда выполняется системный вызов zx_fifo_create(), Zircon создает пару объектов FifoDispatcher (этот код можно посмотреть в файле zircon/kernel/object/fifo_dispatcher.cc). Для каждого из них выделяется запрашиваемое количество ядерной памяти под данные: С помощью отладчика я определил, что размер освобожденного объекта TimerDispatcher составляет 248 байт. Я попробовал создать несколько FIFO-объектов такого же размера, и это сработало: в отладчике я увидел, что TimerDispatcher перезаписан данными одного из объектов FifoDispatcher! Вот код, выполняющий heap spraying в моем прототипе эксплойта: Здесь системный вызов zx_fifo_create() выполняется десять раз. При каждом вызове создается пара очередей из 31 элемента по 8 байт. То есть при выполнении этого кода в ядре создается 20 объектов FifoDispatcher с буферами данных размером 248 байт. Zircon размещает один из этих буферов на месте освобожденного TimerDispatcher, который имел такой же размер. Далее очереди наполняются данными, специально подготовленными для перезаписи содержимого объекта TimerDispatcher: их называют heap spraying payload. Хорошо. Я получил возможность изменить содержимое ядерного объекта TimerDispatcher. Но что же нужно в него записать, чтобы атаковать Zircon? Я привык к тому, что в Linux ядерные объекты описываются структурами на языке C. Метод ядерного объекта там может быть реализован с помощью указателя на функцию, который хранится в поле соответствующей структуры. Поэтому раскладка данных объекта в памяти ядра Linux обычно простая и наглядная. Когда же я стал изучать внутреннее устройство C++-объектов микроядра Zircon, их раскладка в памяти показалась мне более сложной и запутанной. Я решил разобраться с анатомией объекта TimerDispatcher и попробовал распечатать его в отладчике с помощью команды print -pretty on -vtbl on. В ответ GDB вывел огромную иерархию вложенных друг в друга классов, которую мне не удалось соотнести с конкретными байтами в ядерной памяти. Затем для класса TimerDispatcher я попробовал применить утилиту pahole. Получилось лучше: она распечатала отступы полей внутри классов, но не помогла мне понять, как там реализованы методы. Наследование классов сильно усложняло всю картину. Тогда я решил не тратить время на изучение анатомии объекта TimerDispatcher и вместо этого пошел напролом. С помощью heap spraying я заменил все содержимое TimerDispatcher нулями и стал смотреть, что произойдет. Микроядро Zircon ушло в отказ на проверке счетчика ссылок в zircon/system/ulib/fbl/include/fbl/ref_counted_internal.h:57: Это не проблема. С помощью отладчика я определил, что этот счетчик хранится по отступу в 8 байт от начала объекта TimerDispatcher. Чтобы Zircon не падал на данной проверке, я задал ненулевое значение в соответствующем байте для heap spraying: Тогда запуск прототипа эксплойта на Fuchsia прошел дальше по коду ядра и окончился уже другим падением Zircon, которое оказалось более интересным с точки зрения атакующего. Микроядро выполнило разыменование нулевого указателя в функции HandleTable::GetDispatcherWithRights<TimerDispatcher>. Пошаговая отладка в GDB помогла мне выяснить, что ошибка происходит вот в этом чародействе на C++: Здесь Zircon вызывает публичный метод get_type() для класса TimerDispatcher. Адрес этой функции определяется с помощью таблицы виртуальных методов (или C++ vtable). Указатель на такую таблицу находится в самом начале объекта TimerDispatcher. Эту функциональность можно использовать для перехвата потока управления (control-flow hijacking), и тогда не нужно искать подходящие ядерные объекты, содержащие указатели на функции (как это требуется в аналогичных атаках для ядра Linux). Для перехвата потока управления нужно знать адреса ядерных функций, которые зависят от KASLR — защитного механизма, выполняющего рандомизацию расположения адресного пространства ядра (kernel address space layout randomization). С его помощью код ядра располагается по случайному отступу от фиксированного адреса. В исходном коде Zircon механизм KASLR упоминается множество раз. Вот пример из файла zircon/kernel/params.gni: Чтобы обойти защиту KASLR, я решил применить один из своих трюков для ядра Linux. Мой прототип эксплойта для CVE-2021-26708 использовал утечку информации из журнала ядра Linux, чтобы определить секретный отступ KASLR. В операционной системе Fuchsia ядерный журнал также содержит ценную информацию для атакующего. Поэтому я решил попытаться прочитать журнал микроядра Zircon из моего непривилегированного компонента в пользовательском пространстве. Для этого я добавил строку use: [ { protocol: "fuchsia.boot.ReadOnlyLog" } ] в манифест компонента и попробовал открыть ядерный журнал: В этом коде создается специальный канал (Fuchsia channel), который затем используется для протокола ReadOnlyLog. Для этого вызываются функции из библиотеки fdio, которая предоставляет единый интерфейс для файлов, сокетов, каналов, сервисов в Fuchsia. При запуске компонента система выдает следующую ошибку: Это корректное поведение. Мой компонент не имеет заявленных привилегий. Со стороны системы нет разрешения offer на использование протокола fuchsia.boot.ReadOnlyLog, поэтому Fuchsia возвращает ошибку при подключении канала к ядерному журналу. Не судьба... Я отбросил мысль об обходе KASLR с помощью утечки информации из ядерного журнала и стал бродить по исходному коду Zircon в ожидании новой идеи. Тут вдруг я наткнулся на системный вызов zx_debuglog_create(), который дает совсем другой способ доступа к ядерному журналу: В документации по системным вызовам Fuchsia говорится, что аргумент resource обязательно должен иметь тип ZX_RSRC_KIND_ROOT. Мой прототип эксплойта, конечно же, не обладал таким ресурсом, но я все равно попробовал вызвать zx_debuglog_create() наудачу: И этот код сработал! Мой непривилегированный компонент получил доступ к журналу Zircon без необходимых привилегий и при отсутствии ресурса ZX_RSRC_KIND_ROOT. Что за чудеса? Я нашел код Fuchsia, который отвечает за обработку этого системного вызова, и рассмеялся: Здесь функция validate_resource() проверяет, что ресурс имеет тип ZX_RSRC_KIND_ROOT, только если он ненулевой. Прекрасная проверка доступа (сарказм)! В трекере Fuchsia я хотел посмотреть задачи 32044 и 30918, которые указаны в комментариях к коду, но получил access denied. Похоже, процесс разработки Fuchsia не вполне открыт для сообщества, как было заявлено Google. Тогда я создал в трекере security bug и описал, что ошибка проверки доступа в sys_debuglog_create() приводит к утечке информации из ядерного журнала (для корректного отображения информации в трекере нажмите кнопку Markdown в правом верхнем углу). Мейнтейнеры проекта Fuchsia подтвердили проблему безопасности и назначили для нее идентификатор CVE-2022-0882. Поскольку мой прототип эксплойта теперь мог выполнять чтение ядерного журнала, я извлек из него несколько ядерных указателей, чтобы вычислить секретный отступ KASLR. Но как же я удивился, когда повторил эту операцию при следующем запуске Fuchsia. Несмотря на KASLR, ядерные адреса не изменялись при перезапуске Fuchsia. Ниже представлен пример. Как говорится, найдите пять отличий. Загрузка № 1: Загрузка № 2: Здесь видно, что ядерные адреса совпадают, то есть KASLR не работает. В трекере Fuchsia я создал security bug с описанием этой неполадки, на что мейнтейнеры ответили, что эта проблема им уже известна. Операционная система Fuchsia оказалась более экспериментальной, чем я ожидал. Обнаружив, что функции микроядра имеют постоянные адреса, я понял, что для перехвата потока управления нет препятствий. Поэтому я стал разбираться, как устроены таблицы виртуальных методов в C++-объектах Zircon, чтобы воспользоваться этим для развития атаки. Указатель на таблицу виртуальных методов хранится в самом начале объекта. Вот что отладчик показывает для объекта TimerDispatcher: В vtable хранятся странные значения типа 0xffdcb5a4ffe00454, определенно не являющиеся ядерными адресами. Чтобы понять, как это работает, я стал смотреть код, использующий таблицу виртуальных методов объекта TimerDispatcher: В этой высокоуровневой мудрости на C++ я ничего не понял и стал смотреть код на языке ассемблера: Здесь все проще. Регистр r13 содержит адрес объекта TimerDispatcher. Указатель на vtable находится в самом начале объекта, поэтому после первой инструкции mov он попадает в регистр rax. Затем инструкция movsxd помещает значение 0xffdcb5a4ffe00454 из таблицы виртуальных методов в регистр r11. При этом movsxd выполняет знаковое расширение 32-битного источника до 64-битного приемника. Таким образом 0xffdcb5a4ffe00454 превращается в 0xffffffffffe00454. Затем к получившемуся значению в r11 прибавляется адрес самой таблицы виртуальных методов. В результате этого сложения в регистре r11 оказывается адрес метода get_type(): Получается, таблица виртуальных методов адресует методы объекта относительно своего собственного расположения в памяти ядра. Итак, я решил сконструировать фальшивую таблицу виртуальных методов для объекта TimerDispatcher, чтобы перехватить поток управления в микроядре Zircon. Возник вопрос: где мне ее разместить? Самый простой путь — расположить ее в пользовательском пространстве, в памяти эксплойта. Однако Zircon для x86_64 поддерживает аппаратную функцию SMAP (Supervisor Mode Access Prevention), которая блокирует ядру доступ к данным в пользовательском пространстве. В моей карте средств защиты ядра Linux вы можете увидеть SMAP и другие средства защиты от перехвата потока управления. Я придумал два способа обойти защиту SMAP: Можно попробовать реализовать атаку ret2dir для микроядра Zircon, так как в нем тоже используется отображение памяти physmap. Можно использовать утечку информации из ядерного журнала, чтобы найти ядерный адрес, указывающий на данные под контролем атакующего. Размещение фальшивой vtable по этому адресу в пространстве ядра также позволит обойти SMAP. Но чтобы излишне не усложнять свой первый эксперимент с безопасностью Zircon, я решил пока отключить функции SMAP и SMEP для виртуальной машины с Fuchsia и разместил vtable в эксплойте в пользовательском пространстве: Затем я использовал указатель на мою фальшивую таблицу виртуальных методов при heap spraying: Это очень интересный фрагмент эксплойта. Здесь массив spray_data содержит данные для системного вызова zx_fifo_write(), с помощью которого выполняется heap spraying и перезаписывается TimerDispatcher. Поскольку адрес таблицы виртуальных методов должен находиться в начале объекта TimerDispatcher, указатель vtable_ptr устанавливается на начало массива spray_data. Затем с помощью vtable_ptr в данные для спрея записывается адрес фальшивой таблицы fake_vtable. Позже при перехвате потока управления в ядерной функции DownCastDispatcher() этот адрес fake_vtable окажется в регистре rax. Поэтому элемент fake_vtable[1], адресуемый с помощью DWORD PTR [rax+0x8], должен содержать значение, используемое ядром для вычисления адреса метода TimerDispatcher.get_type(). А в качестве этого метода get_type() я хочу вызвать свою функцию pwn() из эксплойта, поэтому в элемент fake_vtable[1] я записываю разность между адресом функции pwn() и адресом моей фальшивой таблицы виртуальных методов. Рассмотрим реальный пример того, как ядро работает с этой фальшивой таблицей виртуальных методов, когда исполняется эксплойт: Массив fake_vtable расположен по адресу 0x35aa74aa020, а функция pwn() — по адресу 0x35aa74a80e0. В элементе fake_vtable[1] содержится значение 0x35aa74a80e0 - 0x35aa74aa020 = 0xffffffffffffe0c0. При выполнении ядерной функции DownCastDispatcher() на это значение будет указывать адрес rax+0x8. После того как Zircon выполнит инструкцию movsxd r11, DWORD PTR [rax+0x8], регистр r11 также будет содержать 0xffffffffffffe0c0. Суммирование r11 и rax, в котором содержится адрес 0x35aa74aa020 массива fake_vtable, в результате даст значение 0x35aa74a80e0. Это адрес функции pwn(). Когда Zircon вызовет __x86_indirect_thunk_r11, функция pwn() из эксплойта получит управление. Когда я добился исполнения произвольного кода в микроядре Zircon, я стал думать: что с помощью этого можно атаковать? Первой моей мыслью было подделать тот суперресурс ZX_RSRC_KIND_ROOT, который я видел в zx_debuglog_create(). Однако я не смог придумать, как с его помощью повысить привилегии, потому что ZX_RSRC_KIND_ROOT нечасто используется в исходном коде Fuchsia. Понимая, что Zircon — это микроядро, я осознал, что для повышения привилегий в Fuchsia потребуется атаковать средства межпроцессного взаимодействия. Другими словами, мне нужно было из микроядра перехватить IPC между компонентами Fuchsia, например между моим непривилегированным эксплойтом и менеджером компонентов (component manager), обладающим высокими привилегиями. Поэтому я вернулся к изучению устройства пользовательского пространства Fuchsia. Это было сложно и скучновато... Но внезапно мне в голову пришла идея. А почему бы не поставить руткит в микроядро Zircon? Это показалось мне более интересным, и я стал разбираться, как Zircon обрабатывает свои системные вызовы. Жизненный цикл системного вызова в Fuchsia кратко описан в ее документации. В Zircon тоже есть таблица системных вызовов (syscall table), как и в ядре Linux. Для микроархитектуры x86_64 в Zircon определена функция x86_syscall() из файла fuchsia/zircon/kernel/arch/x86/syscall.S, реализованная на языке ассемблера: Вот как этот код выглядит в отладчике: Обратите внимание: отладчик показывает, что таблица системных вызовов расположена по адресу 0xffffffff003c49f8. Посмотрим ее содержимое: Первый адрес 0xffffffff00307040 в таблице системных вызовов указывает на функцию x86_syscall_call_bti_create(). Это обработчик системного вызова номер ноль, он определен в автоматически сгенерированном файле kernel-wrappers.inc в директории gen/zircon/vdso/include/lib/syscalls/. А последний системный вызов в таблице — это x86_syscall_call_vmo_create_physical() номер 175 по адресу 0xffffffff00307d10. Поэтому константа ZX_SYS_COUNT имеет значение 176. Распечатаем в отладчике всю таблицу системных вызовов (и немного больше): Здесь видно, что ранее упомянутый указатель на функцию 0xffffffff00307d10 — это последний системный вызов в таблице. Этого знания мне было достаточно для экспериментов с постановкой руткита. В качестве первого эксперимента я перезаписал всю таблицу системных вызовов значением 0x41, получив управление в моей функции pwn(). Как было сказано выше, эта функция выполняется в результате перехвата потока управления в Zircon. Для того чтобы перезаписать таблицу системных вызовов, которая доступна только для чтения, я использовал старый добрый прием со сбросом бита WP в контрольном регистре CR0: Функции записи и чтения контрольного регистра CR0: Результат перезаписи таблицы системных вызовов виден в отладчике: Это сработало. Отлично. Я стал думать, как осуществить перехват системных вызовов микроядра. Сделать это по аналогии с поведением ядерных руткитов для Linux было невозможно. Дело в том, что обычно руткит для Linux — это ядерный модуль, в котором хуки (функции-перехватчики) реализованы как функции этого модуля в пространстве ядра. А в моем случае я пытался поставить руткит в микроядро из эксплойта в пользовательском пространстве. Код из эксплойта не мог работать как ядерный хук, потому что он присутствовал только в адресном пространстве моего пользовательского процесса. Поэтому я решил превратить в хук руткита какой-либо имеющийся в Zircon код. Первым кандидатом на перезапись стала ядерная функция assert_fail_msg(), которая меня конкретно достала, пока я разрабатывал свой прототип эксплойта. Размер этой функции был достаточно большим, чтобы разместить вместо нее мой хук для руткита. Сначала я написал хук для системного вызова zx_process_create() на языке C, но мне совсем не понравился исполняемый код, который сгенерировал компилятор. Из-за этого я переписал его на языке ассемблера: Получилось здорово: Хук сохраняет в ядерном стеке значения всех регистров, которые могут быть испорчены (clobbered) при последующих вызовах функций. Подготавливается и вызывается ядерная функция printf(): Первый аргумент этой функции передается через регистр rdi. В него помещается адрес строки, которую я хочу напечатать в ядерном журнале. Подробнее про эту строку расскажу дальше. Трюк с макросами STR и XSTR называется стрингификацией (stringizing). Она служит для преобразования фрагмента кода в строковую константу. Нулевой al указывает, что векторные аргументы не передаются функции printf(), имеющей переменное количество аргументов. В регистр r11 помещается адрес функции printf() микроядра Zircon, которую затем вызывает инструкция callq *%r11. После вызова printf() восстанавливаются начальные значения регистров. Наконец хук выполняет прыжок на настоящий обработчик системного вызова zx_process_create(). А теперь перейдем к самой интересной части — постановке руткита. Функция pwn() из эксплойта копирует исполняемый код хука на место функции assert_fail_msg() в микроядре Zircon: Рассмотрим этот процесс подробнее: Переменная hook_addr инициализируется адресом ядерной функции assert_fail_msg(). Первый байт функции перезаписывается значением 0xc3, что соответствует инструкции ret. С помощью этого я избегаю отказа микроядра при неуспешной проверке assertion. Теперь, если Zircon вызывает assert_fail_msg(), происходит немедленный возврат из функции, и ядро продолжает работать. Вслед за байтом 0xc3 эксплойт располагает исполняемый код хука process_create_hook() для системного вызова zx_process_create(). Устройство хука я описал выше. После кода хука эксплойт располагает строку сообщения, которую я хочу печатать в ядерном журнале на каждом системном вызове zx_process_create(). Когда хук выполнит инструкцию mov $" XSTR(ZIRCON_ASSERT_FAIL_MSG + 1 + HOOK_CODE_SIZE) ",%rdi, адрес этой строки попадет в регистр rdi. Здесь один байт прибавлен к адресу строки из-за дополнительной инструкции ret, которая была записана в начале функции assert_fail_msg(). После размещения хука и его строки в ядерном коде функция pwn() записывает адрес хука ZIRCON_ASSERT_FAIL_MSG + 1 в 102-й элемент таблицы системных вызовов, который должен указывать на обработчик для zx_process_create(). Наконец, функция pwn() эксплойта возвращает число 42. Зачем? Как было описано выше, Zircon использует мою фальшивую таблицу виртуальных методов и вызывает pwn() в качестве метода TimerDispatcher.get_type(). Настоящий метод get_type() для этого ядерного объекта возвращает число 16, чтобы пройти проверку типа и продолжить исполнение. А я возвращаю 42, чтобы, напротив, проверка типа не сработала, обработка системного вызова zx_timer_cancel() поскорее закончилась и из-за атакованного объекта TimerDispatcher в системе больше ничего не сломалось. Вот и все. Руткит установлен в микроядро Zircon операционной системы Fuchsia! Для этой демонстрации я реализовал второй аналогичный хук для системного вызова zx_process_exit() и разместил его на месте ядерной функции assert_fail(). Таким образом, при создании и завершении процессов руткит печатает в ядерном журнале сообщения. Демонстрация работы эксплойта: Вот так я познакомился с операционной системой Fuchsia и ее микроядром Zircon. Я давно хотел применить свои навыки и посмотреть на эту интересную ОС с точки зрения атакующего. В статье я сделал обзор архитектуры безопасности Fuchsia, описал инструментарий для разработки ОС и рассказал про свои эксперименты с эксплуатацией уязвимостей для микроядра Zircon. Я сообщил мейнтейнерам Fuchsia о проблемах безопасности, обнаруженных в ходе исследования. Это одна из первых публичных работ по безопасности операционной системы Fuchsia. Думаю, она будет полезна сообществу исследователей безопасности, поскольку освещает практические аспекты эксплуатации уязвимостей и защиты в микроядерной ОС. Буду рад, если эта статья вдохновит вас на эксперименты с безопасностью операционных систем. Update: 17 июня по инициативе Google был организован видеозвонок с командой разработчиков Fuchsia. Мне было интересно получить обратную связь по своему исследованию, пообщаться о модели угроз и средствах защиты микроядра Zircon. По словам архитектора безопасности Fuchsia OS, он бы атаковал систему так же, как и я, но на последнем этапе вместо постановки руткита он попробовал бы закрепиться в системе (persistence) или атаковать IPC.   Linux Kernel Developer & Security Researcher