[Перевод] Изюминка Zircon: vDSO (virtual Dynamic Shared Object)
Zircon? Что это?
В августе 2016 года, без каких-либо официальных объявлений со стороны Google, были обнаружены исходники новой операционной системы Fuchsia. Эта ОС основана на микроядре под названием Zircon, которое в свою очередь основано на LK (Little Kernel).
Fuchsia is not Linux
Я не настоящий сварщик являюсь разработчиком и/или экспертом Zircon. Тест под катом является компиляцией частичных переводов: официальной документации Zircon vDSO и статьи Admiring the Zircon Part 1: Understanding Minimal Process Creation от @depletionmode, куда было добавлено немного отсебятины (которая убрана под спойлеры). Поэтому конструктивные предложения по улучшению статьи, как и всегда, приветствуются.
О чем пойдет речь в статье?
vDSO в Zircon является единственным средством доступа к системным вызовам (syscalls).
А разве нельзя из нашего кода напрямую вызвать инструкции процессора SYSENTER/SYSCALL? Нет, эти инструкции процессора не являются частью системного ABI. Пользовательскому коду запрещено напрямую выполнять такие инструкции.
Желающих узнать больше деталей о таком архитектурном шаге приглашаю под кат.
Zircon vDSO (virtual Dynamic Shared Object)
Аббревиатура vDSO расшифровывается virtual Dynamic Shared Object:
- Dynamic Shared Object это термин, используемый для обозначения разделяемых библиотек для формата ELF (.so-файлы).
- Виртуальным (virtual) этот объект является из-за того, что он не загружается из существующего отдельного файла на файловой системе. Образ vDSO предоставляется непосредственно ядром.
Поддержка со стороны ядра
Поддержка vDSO в качестве единственного контролируемого ABI для приложений пользовательского режима реализуется двумя способами:
- Проецирование объекта виртуальной памяти (VMO, Virtual Memory Object).
Когда zx_vmar_map обрабатывает VMO для vDSO (и в аргументах запрашиваетсяZX_VM_PERM_EXECUTE
), ядро требует, что бы смещение и размер строго совпадали с исполняемым сегментом vDSO. Это (в том числе) гарантирует только одно проецирование vDSO в память процесса. После первого успешного проецирования vDSO в процесс его уже нельзя удалить. А попытка повторного проецирования vDSO в память процесса, попытки удаления спроецированного VMO для vDSO или проецирование с неправильными смещением и/или размером завершаются с ошибкойZX_ERR_ACCESS_DENIED
.
Смещение и размер кода vDSO еще на этапе компиляции извлекаются из ELF-файла и затем используются в коде ядра для выполнения вышеописанных проверок. После первого успешного проецирования vDSO ядро ОС запоминает адрес для целевого процесса, что бы ускорить проверки. - Проверка адресов возврата для функций системных вызовов.
Когда код пользовательского режима вызывает ядро, в регистре передается номер низкоуровневого системного вызова. Низкоуровневые системные вызовы являются внутренним (приватным) интерфейсом между vDSO и ядром Zircon. Одни (большинство) напрямую соответствуют системным вызовам публичного ABI, а другие нет.
Для каждого низкоуровневого системного вызова в коде vDSO есть фиксированный набор смещений в коде, которые совершают этот вызов. Исходный код для vDSO определяет внутренние символы, идентифицирующие каждое такое местоположение. Во время компиляции эти местоположения извлекаются из таблицы символов vDSO и используются для генерации кода ядра, который определяет предикат валидности адреса кода для каждого низкоуровневого системного вызова. Эти предикаты позволяют быстро проверять вызывающий код на валидность, учитывая смещение от начала сегмента кода vDSO.
Если по предикату определяется, что вызывающему коду не разрешается производить системный вызов, генерируется синтетическое исключение, аналогично тому, как если бы вызывающий код попытался исполнить несуществующую или привилегированную инструкцию.
vDSO при создании нового процесса
Для запуска исполнения первой нити (thread) новосозданного процесса используется системный вызов zx_process_start. Последним параметром этого системного вызова (смотри arg2 в документации) передается аргумент для первой нити создаваемого процесса. По принятому соглашению загрузчик программ отображает vDSO в адресное пространство нового процесса (в случайное место, выбранное системой) и передает базовый адрес отображения аргументом arg2 в первую нить (thread) создаваемого процесса. Этот адрес является адресом заголовка ELF-файла, по которому могут быть найдены необходимые именованные функции для совершения системных вызовов.
Карта памяти (layout) vDSO
vDSO это обычная разделяемая библиотека EFL, которая может быть рассмотрена, как любая другая. Но для vDSO намеренно выбрано небольшое подмножество из всего формата ELF. Это дает несколько преимуществ:
- Отображение такого ELF в процесс является простым и не включает в себя каких-либо сложных граничных случаев, которые требуются для полноценной поддержки ELF программ.
- Использование vDSO не требует полнофункционального динамического связывания ELF. В частности, vDSO не имеет динамических перемещений (relocations). Проецирование PT_LOAD сегментов ELF файла является единственным требуемым действием.
- Код vDSO не имеет состояния и реэнтерабелен. Он работает исключительно с регистрами процессора и стеком. Это делает его пригодным для использования в широком разнообразии контекстов с минимальными ограничениями, что соответствует обязательному ABI операционной системы. А так же упрощает анализ и проверку кода на предмет надежности и безопасности.
Вся память vDSO представлена двумя последовательными сегментами, каждый из которых содержит выровненные целые страницы:
- Первый сегмент доступен только для чтения и включает в себя заголовки ELF, а также константные данные.
- Второй сегмент является исполняемым и содержит код vDSO.
Весь образ vDSO состоит только из страниц этих двух сегментов. Для отображения памяти vDSO требуются только два значения, извлеченные из заголовков ELF: количество страниц в каждом сегменте.
Константные данные времени загрузки ОС
Некоторые системные вызовы просто возвращают значения, которые являются постоянными (значения должны запрашиваться во время выполнения и не могут быть скомпилированы в код пользовательского режима). Эти значения либо фиксируются в ядре во время компиляции, либо определяются ядром во время начальной загрузки (загрузочные параметры и параметры аппаратного обеспечения). Например: zx_system_get_version (), zx_system_get_num_cpus () и zx_ticks_per_second (). На возвращаемое значение последней функции, например, влияет параметр командной строки ядра.
Интересно, что и в описании функции zx_system_get_num_cpus () так же явно указано, что ОС не поддерживает горячее изменение количества процессоров:
This number cannot change during a run of the system, only at boot time.
Это, как минимум, косвенно указывает на то, что ОС не позиционируется, как серверная.
Поскольку эти значения постоянны, то и нет смысла платить за реальные системные вызовы в ядро ОС. Вместо этого их реализация — простые функции C++, которые возвращают данные, считанные из сегмента констант vDSO. Значения, зафиксированные во время компиляции (такие как строка версии системы), просто компилируются в vDSO.
Для значений, определенных во время загрузки, ядро должно изменить содержимое vDSO. Это выполняется с помощью кода, исполняемого на раннем этапе, который формирует VMO vDSO, прежде чем ядро запустит первый пользовательский процесс (и передаст ему дескриптор VMO). Во время компиляции смещения из образа vDSO (vdso_constants) извлекается из ELF-файла, а затем встраиваются в ядро. А во время загрузки ядро временно отображает страницы, охватывающие vdso_constants, в свое собственное адресное пространство для до-инициализации структуры правильными значениями (для текущего запуска системы).
К чему вся эта головная боль?
Одна из важнейших причин — безопасность. То есть, если злоумышленнику удастся исполнить произвольный (shell-) код, ему придется использовать функции vDSO для вызова системных функций. Первой преградой будет вышеупомянутая рандомизация адреса загрузки vDSO для каждого создаваемого процесса. И поскольку за VMO (virtual memory object) vDSO’а отвечает ядро ОС, оно может выбрать отображение совершенно другого vDSO в конкретный процесс, тем самым запрещая опасные (и не нужные конкретному процессу) системные вызовы. Например: можно запретить драйверам порождать дочерние процессы или обрабатывать проецирование областей MMIO. Это отличный инструмент уменьшения поверхности атаки.
Замечание: на текущий момент поддержка нескольких vDSO активно разрабатывается. Уже существует реализация концепции (proof-of-concept) и простые тесты, но требуется больше работы для улучшения надежности реализации и определения того, какие варианты будут доступны. Текущая концепция предоставляет варианты образа vDSO, которые экспортируют только подмножество полного интерфейса системных вызовов vDSO.
Следуют отметить, что подобные техники уже успешно используются в других ОС. Например, в Windows есть ProcessSystemCallDisablePolicy:
Win32k System Call Disable Restriction to restrict ability to use NTUser and GDI