СофтХабрахабр

[Перевод] Изюминка Zircon: vDSO (virtual Dynamic Shared Object)

Zircon? Что это?

Эта ОС основана на микроядре под названием Zircon, которое в свою очередь основано на LK (Little Kernel). В августе 2016 года, без каких-либо официальных объявлений со стороны Google, были обнаружены исходники новой операционной системы Fuchsia.

Fuchsia is not Linux

Примечания переводчика

Тест под катом является компиляцией частичных переводов: официальной документации Zircon vDSO и статьи Admiring the Zircon Part 1: Understanding Minimal Process Creation от @depletionmode, куда было добавлено немного отсебятины (которая убрана под спойлеры). Я не настоящий сварщик являюсь разработчиком и/или экспертом Zircon. Поэтому конструктивные предложения по улучшению статьи, как и всегда, приветствуются.

О чем пойдет речь в статье?

vDSO в Zircon является единственным средством доступа к системным вызовам (syscalls).

Нет, эти инструкции процессора не являются частью системного ABI. А разве нельзя из нашего кода напрямую вызвать инструкции процессора SYSENTER/SYSCALL? Пользовательскому коду запрещено напрямую выполнять такие инструкции.

Желающих узнать больше деталей о таком архитектурном шаге приглашаю под кат.

Zircon vDSO (virtual Dynamic Shared Object)

Аббревиатура vDSO расшифровывается virtual Dynamic Shared Object:

  • Dynamic Shared Object это термин, используемый для обозначения разделяемых библиотек для формата ELF (.so-файлы).
  • Виртуальным (virtual) этот объект является из-за того, что он не загружается из существующего отдельного файла на файловой системе. Образ vDSO предоставляется непосредственно ядром.

Поддержка со стороны ядра

Поддержка vDSO в качестве единственного контролируемого ABI для приложений пользовательского режима реализуется двумя способами:

  1. Проецирование объекта виртуальной памяти (VMO, Virtual Memory Object).

    Это (в том числе) гарантирует только одно проецирование vDSO в память процесса. Когда zx_vmar_map обрабатывает VMO для vDSO (и в аргументах запрашивается ZX_VM_PERM_EXECUTE), ядро требует, что бы смещение и размер строго совпадали с исполняемым сегментом vDSO. А попытка повторного проецирования vDSO в память процесса, попытки удаления спроецированного VMO для vDSO или проецирование с неправильными смещением и/или размером завершаются с ошибкой ZX_ERR_ACCESS_DENIED.
    Смещение и размер кода vDSO еще на этапе компиляции извлекаются из ELF-файла и затем используются в коде ядра для выполнения вышеописанных проверок. После первого успешного проецирования vDSO в процесс его уже нельзя удалить. После первого успешного проецирования vDSO ядро ОС запоминает адрес для целевого процесса, что бы ускорить проверки.

  2. Проверка адресов возврата для функций системных вызовов.

    Низкоуровневые системные вызовы являются внутренним (приватным) интерфейсом между vDSO и ядром Zircon. Когда код пользовательского режима вызывает ядро, в регистре передается номер низкоуровневого системного вызова. Исходный код для vDSO определяет внутренние символы, идентифицирующие каждое такое местоположение. Одни (большинство) напрямую соответствуют системным вызовам публичного ABI, а другие нет.
    Для каждого низкоуровневого системного вызова в коде vDSO есть фиксированный набор смещений в коде, которые совершают этот вызов. Эти предикаты позволяют быстро проверять вызывающий код на валидность, учитывая смещение от начала сегмента кода vDSO.
    Если по предикату определяется, что вызывающему коду не разрешается производить системный вызов, генерируется синтетическое исключение, аналогично тому, как если бы вызывающий код попытался исполнить несуществующую или привилегированную инструкцию. Во время компиляции эти местоположения извлекаются из таблицы символов vDSO и используются для генерации кода ядра, который определяет предикат валидности адреса кода для каждого низкоуровневого системного вызова.

vDSO при создании нового процесса

Последним параметром этого системного вызова (смотри arg2 в документации) передается аргумент для первой нити создаваемого процесса. Для запуска исполнения первой нити (thread) новосозданного процесса используется системный вызов zx_process_start. Этот адрес является адресом заголовка ELF-файла, по которому могут быть найдены необходимые именованные функции для совершения системных вызовов. По принятому соглашению загрузчик программ отображает vDSO в адресное пространство нового процесса (в случайное место, выбранное системой) и передает базовый адрес отображения аргументом arg2 в первую нить (thread) создаваемого процесса.

Карта памяти (layout) vDSO

Но для vDSO намеренно выбрано небольшое подмножество из всего формата ELF. vDSO это обычная разделяемая библиотека EFL, которая может быть рассмотрена, как любая другая. Это дает несколько преимуществ:

  • Отображение такого ELF в процесс является простым и не включает в себя каких-либо сложных граничных случаев, которые требуются для полноценной поддержки ELF программ.
  • Использование vDSO не требует полнофункционального динамического связывания ELF. В частности, vDSO не имеет динамических перемещений (relocations). Проецирование PT_LOAD сегментов ELF файла является единственным требуемым действием.
  • Код vDSO не имеет состояния и реэнтерабелен. Он работает исключительно с регистрами процессора и стеком. Это делает его пригодным для использования в широком разнообразии контекстов с минимальными ограничениями, что соответствует обязательному ABI операционной системы. А так же упрощает анализ и проверку кода на предмет надежности и безопасности.

Вся память vDSO представлена двумя последовательными сегментами, каждый из которых содержит выровненные целые страницы:

  1. Первый сегмент доступен только для чтения и включает в себя заголовки ELF, а также константные данные.
  2. Второй сегмент является исполняемым и содержит код vDSO.

Для отображения памяти vDSO требуются только два значения, извлеченные из заголовков ELF: количество страниц в каждом сегменте. Весь образ vDSO состоит только из страниц этих двух сегментов.

Константные данные времени загрузки ОС

Эти значения либо фиксируются в ядре во время компиляции, либо определяются ядром во время начальной загрузки (загрузочные параметры и параметры аппаратного обеспечения). Некоторые системные вызовы просто возвращают значения, которые являются постоянными (значения должны запрашиваться во время выполнения и не могут быть скомпилированы в код пользовательского режима). На возвращаемое значение последней функции, например, влияет параметр командной строки ядра. Например: zx_system_get_version(), zx_system_get_num_cpus() и zx_ticks_per_second().

Подождите, количество CPU это константа?

Интересно, что и в описании функции zx_system_get_num_cpus() так же явно указано, что ОС не поддерживает горячее изменение количества процессоров:

This number cannot change during a run of the system, only at boot time.

Это, как минимум, косвенно указывает на то, что ОС не позиционируется, как серверная.

Вместо этого их реализация — простые функции C++, которые возвращают данные, считанные из сегмента констант vDSO. Поскольку эти значения постоянны, то и нет смысла платить за реальные системные вызовы в ядро ОС. Значения, зафиксированные во время компиляции (такие как строка версии системы), просто компилируются в vDSO.

Это выполняется с помощью кода, исполняемого на раннем этапе, который формирует VMO vDSO, прежде чем ядро запустит первый пользовательский процесс (и передаст ему дескриптор VMO). Для значений, определенных во время загрузки, ядро должно изменить содержимое vDSO. А во время загрузки ядро временно отображает страницы, охватывающие vdso_constants, в свое собственное адресное пространство для до-инициализации структуры правильными значениями (для текущего запуска системы). Во время компиляции смещения из образа vDSO (vdso_constants) извлекается из ELF-файла, а затем встраиваются в ядро.

К чему вся эта головная боль?

То есть, если злоумышленнику удастся исполнить произвольный (shell-) код, ему придется использовать функции vDSO для вызова системных функций. Одна из важнейших причин — безопасность. И поскольку за VMO (virtual memory object) vDSO'а отвечает ядро ОС, оно может выбрать отображение совершенно другого vDSO в конкретный процесс, тем самым запрещая опасные (и не нужные конкретному процессу) системные вызовы. Первой преградой будет вышеупомянутая рандомизация адреса загрузки vDSO для каждого создаваемого процесса. Это отличный инструмент уменьшения поверхности атаки. Например: можно запретить драйверам порождать дочерние процессы или обрабатывать проецирование областей MMIO.

Уже существует реализация концепции (proof-of-concept) и простые тесты, но требуется больше работы для улучшения надежности реализации и определения того, какие варианты будут доступны. Замечание: на текущий момент поддержка нескольких vDSO активно разрабатывается. Текущая концепция предоставляет варианты образа vDSO, которые экспортируют только подмножество полного интерфейса системных вызовов vDSO.

А что у других операционных систем?

Например, в Windows есть ProcessSystemCallDisablePolicy: Следуют отметить, что подобные техники уже успешно используются в других ОС.

Win32k System Call Disable Restriction to restrict ability to use NTUser and GDI

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть