Программирование под ARM TrustZone. Часть 1: Secure Monitor
Продолжаем наш цикл статей, посвященный столетию Великой Октябрьской… ARM TrustZone.
Сегодня мы разберемся, что такое Secure World, Normal World, как на программном уровне взаимодействуют две ОС — доверенная (TEE) и гостевая. Узнаем, для чего нужен и как работает Secure Monitor, как обрабатываются прерывания от устройств.
Если готовы — добро пожаловать под кат.
В прошлой статье я рассказывал об аппаратной реализации. Там есть все про аппаратное разделение миров, как не допустить гостевую ОС к доверенной памяти и периферии и так далее. Возьмем оттуда одну мысль в качестве связующей:
- Secure/Non-Secure — это режим процессора. Он задается битом NS (Non-Secure) в регистре SCR (Secure Configuration Register). Если NS=1, мы в режиме Non-Secure, если NS=0, мы в доверенном, то есть Secure-режиме.
С точки зрения программной реализации это все, что нам нужно. Аппаратное обеспечение находится по ту сторону абстракции, наблюдая только за NS, а программное обеспечение не только исполняется по-разному при NS=0 и 1, но и может этот бит менять.
В обоих режимах (NS=0 и NS=1) процессор может полноценно работать, настолько, что в каждом режиме может существовать своя ОС:
- NS=0: доверенная ОС или Trusted OS, или Trusted Execution Environment (TEE);
- NS=1: гостевая ОС, или Rich OS, или Normal World OS.
У каждой ОС будут своя карта виртуальной памяти, свои приложения, прерывания, драйверы и так далее.
Конечно, не всегда у нас на ARM крутятся две операционки. Доверенный код может быть и не полноценной ОС, а каким-то маленьким монитором безопасности. Или может полностью отсутствовать. Но в смартфонах и планшетах отсутствие доверенной ОС — редкость, там в основном есть и TEE (доверенная ОС) и обычная ОС (например, Android).
Только не принимайте за чистую монету название TEE, Trusted Execution Environment. Если TEE называется доверенной — значит, кто-то ее коду доверяет, потому что код ведет к достижению его целей. Может быть, его цель — уничтожить Вселенную, как знать? Вы же исходников не видите.
Процесс загрузки
Процессоры всегда стартуют в режиме Secure. Есть много процессоров ARMv7A, где Security Extensions отключены. И тогда они всегда работают как Secure. Например, всеми любимая Sitara.
Но в любом случае — процессоры всегда стартуют в режиме Secure.
Первым в процессе загрузки участвует загрузчик, и в случае с TrustZone используется одна из реализаций идеи Trusted Boot — механизма, проверяющего подпись образа перед его запуском. Общий алгоритм тут таков:
- считать в память образ загрузчика с внешнего носителя, например, SD, eMMC, NAND, QSPI;
- проверить его подпись открытым ключом, прошитым в процессор на этапе производства изделия;
- если подпись верна, передать управление загрузчику.
Открытый ключ для проверки подписи в процессор прошивается однократно, и после этого только первичный загрузчик, подписанный закрытой частью этого ключа, может быть запущен. Здесь есть и поле для злоупотреблений со стороны крупных производителей.
Подробнее о загрузке ARM — в этой статье.
Далее загрузчик проверит подпись доверенной ОС (TEE) и запустит TEE. Та инициализирует все, что нужно в TrustZone, покидает режим Secure и передаст управление гостевой ОС (например, Linux).
Если же никакая TEE не используется, и управление прямо из загрузчика передается в Linux, то Linux так и работает в режиме Secure. Это, однако, не делает его безопасной ОС: без барьера между Secure World и Normal World нет и доверенной ОС.
Заметим, что без Trusted Boot безопасность TEE была бы скомпрометирована, так как ее можно было бы подменить, подменив загрузчик. Важна вся цепочка удостоверений подлинности, обеспечиваемая подписями бинарников.
Что мы хотим понять в этой статье
На картинке изображены две ОС, которые мы только что загрузили. Гостевая ОС может вызывать функции TEE, для этого она использует Secure Monitor.
В этой статье мы разберемся, что это за Secure Monitor, как его используют и как он работает.
Режимы процессора
В ARMv7A есть довольно много режимов работы. На картинке они разделены на уровни PL0, PL1, PL2 и некоторые из уровней Secure, а некоторые — Non-secure.
PL0 — это непривилегированный (unprivileged) режим, в котором в ОС исполняются обычные программы. Каждая программа запущена со своей картой памяти, настроенной через MMU, так что к другим программам вот так просто залезть не может. Но и в ОС она также залезть не может, потому что так настроила сама ОС. Чтобы обратиться к ОС, приложение делает системный вызов (Supervisor Call, команда SVC), и процессор перепрыгивает в режим Supervisor, PL1.
Весь основной код ОС исполняется в режиме Supervisor (SVC), на уровне PL1. Тут у ядра ОС тоже есть своя таблица MMU, и ядро видит память не так, как приложения. Кстати, ядро не обязательно должно видеть все страницы памяти приложения, это будет менее безопасно.
Другой важный режим ядра — это IRQ. Туда попадают при срабатывании прерываний. IRQ находится на уровне PL1, и поэтому-то все нормальные драйверы устройств в Linux работают на уровне ядра. Парный IRQ режим FIQ — это быстрое прерывание. В Linux он совсем не используется, но в реализации TrustZone ему нашли применение — об этом мы поговорим позднее.
Еще есть режимы Undef, Abort — это исключения при работе программы. Если приложение ОС (или код ядра) попытается выполнить недопустимую команду, произойдет Undef, если обратится к запрещенной ему памяти — будет Abort. Об этом я уже писал в прошлой статье. В реализации TrustZone мы можем выбрать, будет Abort обрабатываться в гостевой ОС (Linux) или перенаправляться в доверенную ОС (TEE). В последнем случае мы можем, например, запротоколировать попытку гостевой ОС залезть в область доверенной ОС.
Всеми покинутый и забытый режим System редко, если вообще, используется.
Все вышеперечисленные режимы есть как в Secure, так и в Non-Secure-режимах работы. При этом, например, Secure Supervisor и Non-Secure Supervisor — отдельные режимы. У них разные таблицы MMU, разные права доступа (из-за NS-бита), их данные хранятся в разных линейках кеша и т.п.
Именно из-за дублирования режимов Secure и Non-Secure на одном ядре и можно запустить две ОС.
Особые режимы процессора
На рисунке выше была еще пара режимов:
- Non-Secure Hypervisor (HYP), PL2;
- Secure Monitor (SMC), PL1.
Режим HYP используется для аппаратной виртуализации, как в VMWare. Он находится на уровне PL2, — он даже главнее ядра гостевой ОС и может там все разрешить и запретить, прямо как TEE. Но мы совсем не будем в этой статье говорить про виртуализацию по двум причинам: во-первых, мало процессоров ARMv7 и софта с ее поддержкой, во-вторых, от Virtualization Extensions в ARM все становится еще запутанней. Так что лучше оставить виртуализацию пока в стороне.
А вот режим Secure Monitor нам очень нужен, он сделан для переключения между Secure и Non-Secure OC. Давайте его рассмотрим со всех сторон.
Secure Monitor
У нас есть две полноценные ОС, и глобально отличаются они только битом NS:
- Secure OS, NS=0 — TEE;
- Non-Secure OS, NS=1 — гостевая, например, Linux.
Ведь логично, что гостевая ОС не может поменять себе бит NS и получить привилегии Secure? Абсолютно логично. Менее ожидаемо, что и Secure OS не может вот так взять и переключиться в режим Non-Secure, поменяв NS на 1. Но это тоже так.
Дело в том, что переключение между режимами оказалось несколько сложнее, чем один бит поменять:
- Для переключения между режимами нужно еще и сохранить/восстановить контекст. Почти все регистры у Secure и Non-secure общие, и их нужно сохранять и восстанавливать.
- Кроме того, обращение из Normal World в Secure World нужно, чтобы сделать какую-то операцию, а у операции обычно есть параметры и возвращаемое значение. Это тоже нужно учитывать.
Вот для этого и придумали режим Secure Monitor. Попадают туда с помощью вызова «SMC #0», что расшифровывается как Secure Monitor call. Причем и Secure код должен вызывать «SMC #0», чтобы переключиться в Non-Secure. И Non-Secure в Secure перепрыгивает также.
В целом, вызов SMC подобен системному вызову операционной системы (SVC):
- системный вызов SVC позволяет приложению ОС из непривилегированного режима (PL0) вызвать функцию ОС (PL1);
- вызов монитора SMC позволяет коду гостевой ОС (Non-Secure PL1) вызвать функцию TEE (Secure PL1).
Различие заключается в том, что возврат из системного вызова осуществляется не так, как сам вызов, а вот переход между Secure и Non-Secure — симметрично, через SMC #0.
Три особенности режима Secure Monitor позволяют ему выполнять переключение контекста Secure/Non-Secure.
- У него есть свой собственный стек, относящийся к области памяти Secure. Стек доступен сразу по входу в режим Secure Monitor, и в него можно сразу сохранить все регистры (контекст) вызывающей стороны, причем неважно, какой.
- В режиме Secure Monitor мы можем изменять бит NS, как нам заблагорассудится.
- Меняя бит NS в режиме Secure Monitor, мы можем видеть регистры и периферию то из режима Secure, то из режима Non-Secure. NS при этом будет реально меняться, и это отразится на работе всей аппаратной обвязки. Однако все это будет в рамках выполнения одной последовательной подпрограммы. Благодаря этому Secure Monitor может подготовить все необходимое для переключения контекста.
Пример вызова TEE
Например, мы хотим, чтобы TEE нам подписала какой-то документ. Данные о документе мы положим в регистры процессора, например, так:
- R0 — код операции: подписать документ в памяти;
- R1 — начальный адрес документа в памяти Normal World (помним, что представление памяти в Secure и Non-Secure отличается);
- R2 — длина документа;
- R3 — начальный адрес буфера, куда упадет подпись. Считаем, что, если буфера не хватит, это не проблема TEE.
Мы вызываем SMC #0 для вызова TEE. В ответ мы ожидаем от TEE подпись в указанном буфере и код результата в регистре R0, чтобы понять — успешно прошла операция или нет.
То есть, налицо некий протокол обмена между гостевой ОС и TEE. В ARM в общем случае можно вести себя как угодно и придумывать любые концепции обмена, но есть принятый всеми механизм обмена, описанный в ARM SMC calling convention. Там описано, какие регистры используются для передачи кода команды, данных, для возвращаемых значений и так далее.
Что делает Secure Monitor
Начнем с того, что код инициализации TEE записывает адрес точки входа в Secure Monitor (адрес подпрограммы) в таблицу векторов исключений режима Monitor, на которую указывает регистр MVBAR.
Регистр MVBAR доступен только в режиме Secure и указывает на особую таблицу векторов исключений, используемую только при переходе в режим Secure Monitor.
У ARM есть и обычная таблица векторов, в которой указаны точки входа в SVC, IRQ, FIQ и так далее. Эта таблица расположена по умолчанию по адресу 0×00000000, но адрес может быть настроен регистром VBAR.
Разумеется, для работы двух ОС там предусмотрено два регистра: Secure VBAR и Non-secure VBAR. Какой из них доступен, зависит от бита NS.
Так вот, MVBAR используется не для SVC, IRQ и так далее, а только для SMC и пары исключений, которые можно настроить на попадание в Monitor Mode. Например, мы можем настроить Abort и FIQ на попадание в Secure Monitor, и благодаря этому перехватывать эти исключения.
При инициализации TEE также устанавливает адрес головы стека для Secure Monitor, и you«re all set, как говорят за океаном.
Пример реализации Secure Monitor можете посмотреть в исходниках OP-TEE, код действительно несложный: https://github.com/OP-TEE/optee_os/blob/master/core/arch/arm/sm/sm_a32.S.
Посмотрим теперь, что произойдет при вызове команды SMC #0 из гостевой ОС.
- Управление перейдет на адрес, указанный в таблице MVBAR — на подпрограмму Secure Monitor. При этом
- режим выполнения будет уже SMC;
- в регистр SPSR (Saved Program Status Register) запишется CPSR (Current Program Status Register) вызывающего кода, включая режим, в котором тот был: SVC, IRQ или что-то другое;
- в регистр LR (Link Register) запишется адрес, откуда произошел вызов.
- режим выполнения будет уже SMC;
- SPSR и LR пригодятся, чтобы потом осуществить возврат из вызова, поэтому они записываются в контекст вызывающей стороны. Пока это можно сделать только в стек Secure Monitor.
srsdb sp!, #CPSR_MODE_MON // Записать на стек LR и SPSR
- Потом нужно разобраться, с какой стороны растет на пнях мох вызвали SMC — Secure или Non-Secure. Для этого читаем SCR и проверяем бит NS. Если NS=1, значит нас вызвали из Non-Secure. В нашем примере это так, и мы переключаемся в Secure. Ставим NS=0.
- Сохраняем контекст:
- сохраняем все оставшиеся регистры в стек, а r0-r7 там уже лежат;
- туда же сохраняются регистры CPSR других режимов процессора (IRQ, FIQ и т.п.);
- копируем содержимое стека в контекст Non-Secure.
- сохраняем все оставшиеся регистры в стек, а r0-r7 там уже лежат;
- Восстанавливаем контекст Secure:
- восстанавливаем регистры CPSR всех режимов;
- загружаем содержимое регистров из контекста Secure;
- загружаем точку входа (будущие PC и CPSR) на стек Secure Monitor.
- восстанавливаем регистры CPSR всех режимов;
- Выпрыгиваем из режима Secure Monitor, считав PC и CPSR со стека:
rfefd sp! // rfe=return from exception
Вот очень упрощенное описание того, что вы увидите в приведенном выше по ссылке коде. Там операции даже не все выполняются в таком же порядке. Цель была передать общий смысл, не более того.
На самом деле, это почти все, что делает Secure Monitor — передает управление Secure OS. Когда Secure OS закончит с вызовом, она так же вызовет SMC #0. Secure Monitor поймет по NS=0, что сейчас он Secure, и нужно возвращаться в Non-Secure, и сделает те же команды, но немного наоборот.
Если вы полезли разбираться с кодом, то вот еще хинт под спойлером:
- Standard Call — вызов, требующий создания в TEE потока для его обработки. Например, обращение к функции доверенного приложения, или запуск вообще любой функции TEE, которая потребует ожидания, блокировок, семафоров и т.д.
- Fast Call — быстрый вызов TEE, который всего перечисленного выше не требует. Например, мы просим TEE включить для нас еще пару ядер процессора.
Fast Call — это как прерывание, он обязательно возвратит управление достаточно быстро. Standard Call — как RPC, после его вызова TEE начинает работать на полную катушку, выполнять разные операции, переключать контексты, может быть и ждать результатов операции.
В принципе, Secure Monitor мог бы и оставить эту проверку на TEE и сразу переключить туда, но тут такая реализация. Важно не запутаться в этом коде и увидеть, что оба вызова исполняются в режиме Secure Supervisor, а не в Secure Monitor.
Еще раз: все вызовы из Non-Secure World попадают на обработку в Secure, а Secure Monitor сам ничего не обрабатывает. Привратник.
Обработка прерываний и исключений
Гостевая ОС, как правило, не очень-то и знает, что она гостевая. Настраивает себе память, прерывания, выполняет задачи. Все работает как надо, пока не налетит на какое-то ограничение, наложенное на нее TEE. Если налетит — произойдет Abort, как мы и писали в прошлой статье.
При этом гостевая ОС загрузит кучу драйверов, будет назначать устройствам прерывания и прерывания эти будут приходить в гостевую ОС. А почему, спрашивается, в нее? Вполне может быть, что TEE хочет управлять какими-то устройствами в монопольном режиме и получать от них свои прерывания. Сейчас мы разберемся, как две операционки делят между собой прерывания.
В процессоре ARM основной контроллер прерываний — один (пусть это GICv2), нет отдельных контроллеров для Secure и Non-Secure.
Если происходит прерывание, то GICv2 по умолчанию доставит его в режим Secure. Тогда, если произошло прерывание — будет загружен вектор из Secure VBAR.
Но если мы запускаем TEE и Linux параллельно, то нужно как-то поделить прерывания. Не дело, если все прерывания будут приходить только в TEE (Secure) или в Linux (Non-secure).
Поэтому в GICv2 в рамках поддержки Security Extensions придумали сделать группировку прерываний (регистр GICD_IGROUP):
- Group 0 — это Secure прерывание, генерирует IRQ или FIQ, это можно настроить;
- Group 1 — Non-Secure прерывание, генерирует только IRQ.
При такой реализации можно запустить Linux без всяких TEE — и тогда он будет запущен в режиме Secure по умолчанию, настроит себе Secure VBAR, все прерывания будут идти к нему (про VBAR мы писали выше). А если Linux запущен в гостевом режиме, то TEE заранее настроит все ненужные ему прерывания на Group 1, а Linux запустит в Non-Secure режиме. Linux настроит себе Non-Secure VBAR, и все его прерывания будут идти к нему. Идиллия и программная совместимость, драйвер GIC в Linux и знать не должен, работает он в Secure или гостевом режиме.
Ну, казалось бы, все хорошо и понятно. Если произошло Secure-прерывание — будет загружен вектор из Secure VBAR, иначе Non-Secure VBAR.
Так нет же! Мы помним, что просто так перейти из Secure-режима в Non-Secure нельзя, для этого у нас есть Secure Monitor.
Поэтому:
- если прерывание произошло в режиме Non-Secure, и оно Non-Secure, исполняется как обычно, через Non-Secure VBAR;
- если прерывание произошло в режиме Secure, и оно Secure, тоже исполняется как обычно, через Secure VBAR;
- а вот если Secure прерывание произошло в режиме Non-Secure, то происходит вызов Secure Monitor, для того, чтобы сначала перейти в режим Secure;
- про то, что происходит в паре Non-Secure→Secure, можете догадаться.
Сухой остаток — Secure-прерывание может произойти в режиме Non-Secure, и тогда оно пойдет через Secure Monitor. Механизм его работы, описанный выше, теперь должен разобраться, не в прерывание ли он послан, и соответственно все обработать. И там все в коде у OP-TEE есть, посмотрите.
Очень полезная таблица на этот счет есть здесь: http://infocenter.arm.com/help/topic/com.arm.doc.faqs/ka16352.html
Но и это еще не все! На самом деле, чтобы это работало, нужно еще кое-что настроить. В регистре SCR, уже знакомом нам, есть биты, настраивающие, какие прерывания и исключения направлять в Secure Monitor, а какие — обрабатывать через VBAR.
На картинке — SCR от ARM Cortex-A5. Биты EA, FIQ и IRQ влияют на маршрутизацию, соответственно, External Abort, и FIQ, и IRQ.
К сожалению, там нет IRQ Group 0 и IRQ Group 1, и можно только либо все IRQ направить в Secure Monitor, либо оставить как есть, через VBAR. Как есть — нас не устроит. Поэтому все разработчики с подачи ARM используют такую схему:
- в GIC для всех Secure прерываний настраивается Group 0;
- для Group 0 настраивается генерация FIQ, а не IRQ;
- в регистре SCR выбирается маршрутизация FIQ в Secure Monitor, а IRQ — по VBAR.
Все эти настройки гостевая ОС уже не сможет изменить. В результате Secure-прерывание всегда генерирует FIQ, а FIQ всегда попадает в Secure Monitor из режима Non-Secure.
Вот так, ARMv7 штука сложная и иногда запутанная.
Таким же образом (через регистр SCR) можно настроить и ловлю External Abort из Non-Secure-режима в Secure Monitor. Это может быть полезно, т.к. External Abort может произойти, например, при попытке доступа из режима Non-Secure к Secure-периферии.
Заключение
Описать все программирование TrustZone в одной обзорной статье не получилось, и будет продолжение.
В этот раз мы рассмотрели разделение на Secure и Normal World, поразбирались в работе Secure Monitor и узнали, как ловить прерывания в доверенной среде.
В следующей статье будет про TEE: что она делает, насколько она на самом деле самостоятельная ОС, для чего нужны трастлеты, и какой у них жизненный цикл.