Недавно попросили в двух словах рассказать серьезным людям о загрузке операционной системы на ARM и дать оценку угроз безопасности этого процесса. Вообще ARM-процессоров и вообще ОС. Вы понимаете, все ведь слышали про эти ARM, и что такое ОС тоже все знают. Желательно, на уровне квадратиков со стрелками.
Загрузка ARM в четырех прямоугольниках — под катом.
Сразу ограничим уровень детализации. Нас интересует, что происходит, а не как, то есть конкретные инструкции процессора оставим в стороне. Постараемся найти общее у всех процессоров и всех ОС. Поищем угрозы безопасности.
Разновидности процессоров ARM
Если вы знаете про ARM, то этот раздел можно смело пропустить.
В производстве и эксплуатации сейчас встречаются процессоры ARM пяти архитектур: ARMv4, ARMv5, ARMv6, ARMv7 и ARMv8. Компания ARM дает этим архитектурам коммерческие названия, поэтому ARMv4 называется, например, ARM7, ARMv5 – ARM9, а название Cortex имеют процессоры на архитектурах ARMv6, v7, v8. Следующая таблица перечисляет основные разновидности.
Архитектура | Коммерческое название | Распространенные виды | Запуск Linux |
ARMv4 | ARM7 | ARM7TDMI | Нецелесообразно |
ARMv5 | ARM9 | ARM926EJ-S | Да |
ARMv6 | ARM11 | ARM1176JZF-S | Да |
Cortex-M0 | Cortex-M0 | Нет | |
ARMv7 | Cortex-M | Cortex-M3 | Нецелесообразно |
Cortex-A | Cortex-A9 | Да | |
Cortex-R | Cortex-R4 | Да | |
ARMv8 | Cortex-A | Cortex-A53 | Да |
Например, кнопочные телефоны в основном используют ARM7, а смартфоны – Cortex-A. Современные смартфоны строятся преимущественно на ARMv8, единственных 64-битных. Процессоры ARM7 и ARM9 широко применялись в различных промышленных контроллерах, сетевом оборудовании, а сейчас фокус переходит на использование в них Cortex-A. В различной бытовой технике, мелких электронных приборах, в области безопасности и т.п. применяются микроконтроллеры Cortex-M.
Вообще все устройства ARM можно условно разбить на микроконтроллеры и Application Processor.
- Микроконтроллеры отличаются наличием на кристалле Flash-памяти и рабочего ОЗУ. Применяются для задач относительно малой автоматизации.
- Application Processor преимущественно пользуется внешней памятью — DDRAM и Flash. Мы их дальше будем называть просто — процессоры. Масштаб задач у них больше.
Долгое время одни и те же архитектуры ARM7, ARM9 использовались как для построения процессоров, так и микроконтроллеров. С появлением линейки Cortex произошло разделение, и теперь микроконтроллеры называются Cortex-M, а процессоры Cortex-A и Cortex-R.
Виды ОС
Какие есть варианты запуска ОС:
- на микроконтроллерах обычно запущена маленькая ОС реального времени (RTOS) или просто программа без ОС;
- на процессорах чаще запущена ОС общего применения (Linux, Android), иногда маленькая RTOS, иногда полнофункциональная RTOS (типа vxWORKS).
Например, в планшетах и смартфонах используется Android, iOS или вариант Linux. В телекоммуникационном оборудовании может быть Linux или один из вариантов RTOS. В более простом оборудовании может применяться RTOS или программа без ОС.
В дальнейшем мы будем говорить только о запуске ОС (Linux, Android) или RTOS на ARM. По способу запуска “большие” RTOS попадают в одну группу с Linux, а “малые” RTOS объединяются с программами без ОС.
Для запуска Linux хорошо подходят процессоры ARM9, ARM11, Cortex-A. Усеченную версию Linux также можно загрузить на ARM7, Cortex-M4 и Cortex-M7, но это нецелесообразно.
Для запуска малых RTOS подходят микроконтроллеры и процессоры ARM7, ARM9, Cortex-M. В некоторых случаях для RTOS используют начальные модели Cortex-A, например, Cortex-A5. Большинство же процессоров Cortex-A столь сложны, что их возможности можно использовать только совместно с поставляемым производителем Linux/Android SDK, что и определяет выбор в пользу Linux.
Загрузчик ОС
С точки зрения разработчика системное ПО устройства делится на загрузчик и ОС. Основную функцию всегда выполняет программа, работающая под управлением ОС или RTOS.
Загрузчик обеспечивает загрузку ОС и сервисные функции, такие, как:
- проверка целостности образа ОС перед запуском;
- обновление программ;
- сервисные функции, функции первоначальной инициализации устройства;
- самотестирование.
В случае RTOS загрузчик зачастую пишется разработчиком устройства и представляет собой небольшую специализированную программу. В случае с ОС общего применения широко применяются загрузчики с открытым исходным кодом, например, u-boot.
Таким образом, с точки зрения разработчика изделия запуск ОС выглядит следующим образом:
Здесь знаком // отмечен момент подачи питания или сброса процессора. Такой простой способ запуска был у некоторых процессоров ARM7. В последовавших за ними версиях процесс запуска в реальности сложнее, чем на приведенной схеме, но для разработчика конечного решения это обычно не существенно.
Схема “Загрузчик-ОС” очень удобна из практических соображений, ведь загрузчик берет на себя всю низкоуровневую работу:
- инициализирует память перед запуском ОС и загружает ядро ОС в память;
- инициализирует часть периферии;
- часто реализует хранение двух образов ОС: текущего и резервного, или образа для восстановления;
- контролирует образ ОС перед загрузкой;
- дает сервисный режим работы даже при испорченном образе ОС.
Например, для запуска Linux на ARM загрузчик должен инициализировать память, хотя бы один терминал, загрузить образ ядра и Device Tree в память и передать управление на ядро. Все это описано в <https://www.kernel.org/doc/Documentation/arm/Booting>. Код инициализации ядра Linux не будет делать сам то, что должен делать загрузчик.
В то же время, загрузчик зачастую слабо защищен или не защищен вовсе. В большинстве домашних роутеров достаточно открыть крышку и подключиться к разъему UART, чтобы войти в меню управления загрузчиком. В телекоммуникационном оборудовании более высокого класса вход в меню загрузчика зачастую возможен по недокументированной комбинации клавиш или нажатой кнопке в момент включения устройства. Иными словами, зачастую загрузчик не защищен от локального нарушителя.
Рассмотрим работу загрузчика на примере u-boot, загружающего Linux, по шагам.
- После включения или сброса процессор загружает образ u-boot, хранимый в Flash-памяти, в ОЗУ и передает управление на первую команду этого образа.
- u-boot инициализирует DDRAM.
- u-boot инициализирует драйверы загрузочного носителя (ЗН), например, eMMC, NAND Flash.
- u-boot читает с ЗН область переменных конфигураций. В конфигурации задан скрипт загрузки, который u-boot далее исполняет.
- u-boot выводит в консоль предложение прервать процесс загрузки и сконфигурировать устройство. Если за 2-3 секунды пользователь этого не сделает, запускается скрипт загрузки.
- Иногда скрипт начинается с поиска подходящего образа ОС для загрузки на всех доступных носителях. В других случаях ЗН задается в скрипте жестко.
- Скрипт загружает с ЗН в DDRAM образ ядра Linux (zImage), файл Device Tree с параметрами ядра (*.dtb).
- Дополнительно скрипт может загрузить в DDRAM образ initrd – маленькой файловой системы с необходимыми для старта драйверами устройств. Современные дистрибутивы Linux иногда используют initrd, а иногда – нет.
- Разместив загруженные 2 или 3 файла в памяти, скрипт передает управление на первую команду образа zImage (ядро Linux).
- zImage состоит из распаковщика и сжатого образа ядра. Распаковщик развертывает ядро в памяти, и загрузка ОС начинается.
Запуск загрузчика – предзагрузчик
Однако в реальности почти никогда не бывает, чтобы команды загрузчика выполнялись первыми после включения или сброса процессора. Это еще было на процессорах ARM7, но почти не встречалось далее.
Любое ядро процессора ARM при сбросе начинает исполнение с адреса 0, где записан вектор “reset”. Старые серии процессоров буквально начинали загружаться с внешней памяти, отображенной по нулевому адресу, и тогда первая команда процессора была командной загрузчика. Однако для такой загрузки подходит только параллельная NOR Flash или ROM. Эти типы памяти работают очень просто – при подаче адреса они выдают данные. Характерный пример параллельной NOR Flash – микросхема BIOS в персональных компьютерах.
В современных системах используются другие виды памяти, потому что они дешевле, а объем больше. Это NAND, eMMC, SPI/QSPI Flash. Эти типы памяти уже не работают по принципу: подал адрес — читаешь данные, а значит, для прямого исполнения команд из них не подходят. Даже для простого чтения тут требуется написать драйвер, и мы имеем проблему «курицы и яйца»: драйвер нужно откуда-то заранее загрузить.
По этой причине в современные процессоры ARM интегрировано ПЗУ с предзагрузчиком. ПЗУ отображено в памяти процессора на адрес 0, и именно с него начинает исполнение команд процессор.
В задачи предзагрузчика входят следующие:
- определение конфигурации подключенных устройств;
- определение загрузочного носителя (ЗН);
- инициализация устройств и ЗН;
- чтение загрузчика с ЗН;
- передача управления загрузчику.
Конфигурация предзагрузчика обычно устанавливается одним из двух способов:
- схемотехнически, подключением определенных выводов процессора к земле или шине питания;
- записывается в однократно-программируемую память процессора на этапе производства.
В целом почти всегда есть возможность задать единственный вариант загрузки или основной и несколько альтернативных. При этом в числе альтернативных может быть первичная загрузка через USB или последовательный порт, что очень удобно при первоначальной инициализации на производстве.
Подобный предзагрузчик устанавливается как в процессорах ARM, таких, как Cortex-A, так и в микроконтроллерах, даже таких маленьких, как Cortex-M0. Вместе с предзагрузчиком процедура запуска ОС выглядит так:
Анализ угроз на этом этапе
Исходный код предзагрузчика пишется производителем процессора, а не компанией ARM, является частью микросхемы как продукта компании-производителя и защищен авторским правом. Например, в процессорах ARM компаний Atmel и NXP предзагрузчики написаны, соответственно, Atmel и NXP.
В некоторых случаях предзагрузчик можно прочитать из ROM и проанализировать, но иногда доступ к нему ограничен. Например, предзагрузчик процессора серии Psoc4000 компании Cypress был закрыт несколькими слоями защиты (но был взломан талантливым хакером).
Использования предзагрузчика в большинстве сценариев избежать нельзя. Можно рассматривать его как вариант BIOS, которого в ARM-системах нет.
Сам по себе предзагрузчик в ROM несет в себе угрозу нарушения порядка загрузки и выполнения произвольного кода. Но после того как управление передано загрузчику ОС, предзагрузчик уже безвреден. Мы можем просто не передавать ему управление, перенастроить все обработчики прерываний и так далее.
В некоторые небольшие микроконтроллеры производители интегрируют в ROM-библиотеки для работы с периферийными устройствами, которые требуется вызывать на протяжении всей работы микроконтроллера. В этом случае системное ПО (загрузчик и ОС) само периодически передает управление куда-то в область предзагрузчика, и схема передачи управления получается следующей:
Это в общем случае небезопасно, но встречается только в некоторых микроконтроллерах на архитектуре ARM. На таких микроконтроллерах обычно запускаются программы без ОС или малые RTOS, и дизайнер системы может оценить риски.
Загрузка с TrustZone
В процессоры ARM Cortex-A и Cortex-R встраивается технология TrustZone. Эта технология позволяет на аппаратном уровне выделить два режима исполнения: Secure (Безопасный) и Non-Secure (Гостевой).
Эти процессоры в основном нацелены на рынок смартфонов и планшетных компьютеров, и TrustZone используется для создания в режиме Secure доверенной “песочницы” для исполнения кода, связанного с криптографией, DRM, хранением пользовательских данных.
В режиме Secure при этом запускается специальная ОС, называемая в общем случае TEE (Trusted Execution Environment, доверенная среда исполнения), а нормальная ОС, такая, как Linux, Android, iOS, запускается в режиме Non-Secure. При этом права доступа к некоторым устройствам ограничены для нормальной ОС, поэтому ее еще называют гостевой ОС.
Из-за наложенных ограничений гостевая ОС вынуждена время от времени вызывать функции TEE для исполнения некоторых операций. TEE продолжает существовать параллельно с гостевой ОС все время, и гостевая ОС не может ничего с этим поделать.
Например, гостевая ОС использует функции TEE для:
- включения и выключения ядер процессора (в ARMv8-A это происходит через PSCI — часть ARM Trusted Firmware, а в ARMv7 — по-разному для каждого производителя процессоров);
- хранения ключей, данных банковских карт и т.п.;
- хранения ключей полнодискового шифрования;
- операций с криптографией;
- отображения DRM-контента.
При этом, с точки зрения безопасности, на время таких вызовов управление передается в неизвестный нам, непроверенный код. Мы не можем однозначно сказать, что делают Samsung KNOX или QSEE от Qualcomm.
Почему же разработчики систем соглашаются на такой режим функционирования? В процессоры с поддержкой TrustZone встроен и механизм Secure Boot в том или ином виде.
С Secure Boot предзагрузчик проверяет подпись загружаемого образа с помощью прошитого на этапе производства открытого ключа. Таким образом, гарантируется, что загружен будет только подписанный образ. Это функция безопасности.
То есть загрузка ОС становится следующей:
- стартует предзагрузчик в ROM. Он загружает ключи для проверки подписи TEE из ROM;
- предзагрузчик загружает в память образ TEE, проверяет подпись. Если проверка прошла успешно, запускается TEE;
- TEE настраивает режимы Secure и Non-Secure. Далее TEE загружает основной загрузчик ОС и переходит на него в режиме Non-Secure. Сам TEE остается в режиме Secure и ждет;
- загрузчик основной ОС загружает ОС как обычно;
- ОС вынуждена время от времени вызывать функции TEE для выполнения некоторых задач.
Однако производитель, как правило, поставляет подписанные образы загрузчика и TEE в составе SDK для процессора и поставляет процессоры, уже “зашитые” ключом производителя. В этом случае предзагрузчик из ROM не станет выполнять любой загрузчик, если он не подписан производителем. Все основные процессоры для смартфонов сейчас поставляются уже “прошитыми” под исполнение собственного TEE перед исполнением загрузчика ОС.
Далее действует лень — c TEE все работает, а без TEE даже не запускается. Разработчики используют SDK с TEE, вызывают закрытый бинарный код из ядра Linux и не волнуются.
Как проверить свой проект на обращения к TrustZone
Может даже показаться, что всей этой TrustZone не существует, по крайней мере, в вашей конкретной разработке. Проверить это совсем несложно.
Дело в том, что все процессоры с TrustZone стартуют в режиме Secure, а только потом переключаются в Normal. Если ваша ОС запущена в режиме Normal, то какая-то Secure OS (TEE) существует в системе и перевела ее в этот режим.
Лакмусовой бумажкой является обращение к TEE для включения кэш-памяти 2-го уровня. По какой-то причине архитектура ARM не позволяет этого делать из Normal World. Поэтому для включения кэша ядру ОС потребуется сделать хоть один вызов к TrustZone. Делается это единственной командой: smc #0, и вы можете поискать ее сами в ядре Linux или Android.
Разумеется, мы и сами поискали, и нашли такие вызовы в коде поддержки ряда процессоров Qualcomm, Samsung, Mediatek, Rockchip, Spreadtrum, HiSilicon, Broadcom, Cavium.
Загрузка ARM Cortex-A и анализ угроз
Итак, обещанный процесс загрузки ОС на ARM (здесь — Cortex-A) в четыре блока:
На схеме пунктиром обозначен путь обращения из ядра ОС в TEE.
В двух блоках — неизвестный нам код. Посмотрим, чем это грозит.
Технически, любой из компонентов системного ПО может содержать ошибки, намеренные закладки и так далее. Однако в большинстве случаев загрузчик, ОС и системное ПО можно проверить, изучив исходные коды. Сконцентрируемся на возможных угрозах, исходящих от предзагрузчика и TEE, исходные коды к которым закрыты.
Предзагрузчик работает на самом раннем этапе, когда схема подключения к процессору различных периферийных устройств еще не известна, никакие коммуникационные устройства (WiFi, 3G и т.д.) не настроены, коммуникационные протоколы не работают. При этом предзагрузчик – небольшая программа, с размером кода порядка нескольких десятков килобайт, и сложно представить размещение в нем полных стеков протоколов или серьезной эвристики по определению подключенных устройств. Поэтому предзагрузчик вряд ли таит в себе серьезные закладки, связанные со слежкой, передачей данных и т.п.
Гораздо более интересной точкой атаки является TEE, так как его функции вызываются в процессе работы ОС, когда все периферийные устройства работают, а коммуникационные протоколы настроены. Создание шпионской закладки в коде TEE позволяет практически неограниченно следить за пользователем СВТ.
В небольшом исследовании мы показали реализуемость закладки в TEE, незаметно перехватывающей системные вызовы ОС Linux. Для активации закладки нужно только одно обращение из ядра Linux в TEE (например, то самое, для кэша второго уровня), после чего система становится полностью управляемой. Это позволяет:
- контролировать чтение и запись файлов, модифицировать данные «на лету»;
- перехватывать пользовательский ввод, причем введенные символы перехватываются даже с экранной клавиатуры;
- незаметно внедрять свои данные при коммуникации с удаленными серверами, в том числе по протоколу https, маскируя передачу шпионской информации под обычный зашифрованный Web-трафик.
Несомненно, выявленные возможности – только вершина айсберга, и создание закладок не было целью исследования.
Выводы
Мы рассмотрели процесс загрузки различных микроконтроллеров и процессоров ARM.
У микроконтроллеров наиболее уязвимым местом в процессе загрузки является загрузчик ОС.
Современные процессоры ARM Cortex-A включают в себя TrustZone — и от этого никуда не уйти. TrustZone предполагает запуск перед ОС доверенной среды исполнения TEE.
TEE является самой уязвимой точкой в процессе загрузки ОС на ARM Cortex-A, потому что обращения к TEE приводят к выполнению закрытого системного кода, известного производителю, но скрытого от нас.
Без контроля над TEE невозможно обеспечить безопасность и доверенность исполнения любой ОС на ARM Cortex-A.
Автор: vlk77