11в. Защита чтения на F1: почему уровень 1 не спасает прошивку
Раздел 11 (подраздел 11.6) ввёл байты конфигурации и защиту чтения RDP:
значение 0xA5 у F1 означает «защиты нет», любое другое — защиту первого
уровня, после которой отладчик не читает Flash, а снять её можно только
вместе со стиранием всей памяти. Для отладки этого достаточно. Для изделия,
которое уедет к незнакомым людям, вопрос другой: насколько первый уровень
мешает тому, кто держит плату в руках и хочет вытащить прошивку.
Здесь — честный ответ для F1. Что первый уровень обещает и честно даёт; в чём его слабость именно на F1 (принцип давно описан публично); и что из этого следует для изделия. Тон раздела оборонительный: это про выбор камня и мер, а не про снятие чужой защиты. Готового инструмента извлечения здесь нет.
Опоры: раздел 11 (карта памяти, байты конфигурации 11.6), раздел 48 (защита загрузчика 48.9), подпись образа (48а), а также камни с сильной защитой — STM32U5 и TrustZone (2б, 2е).
Как это называется в документации
Оригинал Перевод read protection level (RDP level) уровень защиты чтения vector fetch выборка вектора: чтение адреса обработчика из таблицы exception stacking укладка кадра исключения на стек threat model модель угроз one-time programmable (OTP) однократно программируемая память device unique ID (UID) заводской номер кристалла
11в.1 Что обещает уровень 1
Защита чтения живёт в одном байте конфигурации — RDP. На F1 защиту
выключает единственное значение 0xA5; любое другое включает первый уровень.
Когда он включён, документация на F103 (RM0008, раздел про встроенную Flash)
обещает ровно следующее:
- отладчик по-прежнему подключается — ядро, регистры, периферию он видит;
- но чтение, стирание и запись основной Flash через отладчик, а также при загрузке из ОЗУ или системной памяти — запрещены; попытка чтения даёт ошибку шины;
- снять защиту можно только переводом
RDPобратно в0xA5, и это полное стирание флеша: код исчезает раньше, чем защита снимется.
Для разработки это удобно: плату не «запирают навсегда», просто каждая
перепрошивка после снятия защиты начинается с чистого листа. Стёртый байт
конфигурации 0xFF даёт защищённый камень, а не открытый, — это тоже из 11.6.
Лестница мер защиты изделия — от нулевого уровня до уникального ключа — на рисунке. Светлые ступени добавляют удобство и защиту от случайного копирования, зелёные закрывают и противника с доступом к плате.
11в.2 В чём слабость именно на F1
Обещание «отладчик не читает Flash» верно для обычного пути данных: когда отладчик просит байт по адресу Flash напрямую, контроллер памяти отвечает ошибкой. Слабость F1 в том, что это не единственный путь, которым значение из Flash попадает наружу.
Ядро Cortex-M3 само непрерывно читает Flash, чтобы работать: оно выбирает оттуда команды и — при каждом входе в исключение — адрес обработчика из таблицы векторов (выборка вектора). У F1 к Flash идут две раздельные шины ядра (RM0008, раздел про архитектуру шин): шина команд ICode для выборки инструкций и шина данных DCode для литералов и доступа отладчика. Защита первого уровня закрывает путь, по которому ходит отладчик, но ядру работать не мешает — иначе камень просто не запускался бы. А раз ядро читает Flash, прочитанное слово попадает в регистр — вектор ложится в счётчик команд, — а регистры ядра видно и через не заблокированный отладочный доступ.
Что при входе в исключение происходит именно так, подтверждает документация на ядро (Cortex-M3 Generic User Guide, раздел про вход в исключение): «процессор выполняет выборку вектора, читающую стартовый адрес обработчика из таблицы векторов», а состояние прерванной программы — включая адрес возврата — ядро укладывает на стек (укладка кадра исключения). Вектор ядро достаёт из Flash само; адрес возврата — это адрес команды во Flash, а не прочитанное из неё слово.
На этом принципе построена давно и публично описанная атака на RDP уровня 1 у F1: сдвигая у таблицы векторов базу и вызывая исключения, через не закрытый отладочный интерфейс по очереди считывают значения, которые ядро само выбирает из Flash. Ей присвоен номер CVE-2020-8004, подробный разбор опубликован исследователями. Пошаговую методику и готовый инструмент мы здесь не приводим — важен вывод, а не рецепт: на F1 первый уровень не рассчитан на противника с физическим доступом к плате.
У более новых семейств этот путь перекрыт иначе. У F4 первый уровень назван в RM0090 (3.7.3) «read protection enabled», а полную защиту даёт только второй уровень с отключением отладки. У U5 при включённой TrustZone в режиме защиты любой отладочный доступ помечается как вторжение и закрывает доступ к памяти аппаратно (RM0456, раздел про защиту чтения). F1 этих механизмов не имеет.
11в.3 Что даёт каждый уровень: F1, F4, U5
Сведём уровни трёх семейств в одну таблицу. Значения байта RDP — из
заголовков паков и справочных руководств (RM0090 для F4, RM0456 для U5);
у F1 — из заголовка семейства и его библиотеки.
| Серия | Уровень | Байт RDP | Отладчик читает Flash | Снятие | Необратим |
|---|---|---|---|---|---|
| F1 | 0 | 0xA5 | да | — | нет |
| F1 | 1 | любое, кроме 0xA5 | нет | со стиранием всей Flash | нет |
| F4 | 0 | 0xAA | да | — | нет |
| F4 | 1 | любое, кроме 0xAA и 0xCC | нет | со стиранием всей Flash | нет |
| F4 | 2 | 0xCC | отладки нет | нельзя | да |
| U5 | 0 | 0xAA | да | — | нет |
| U5 | 0.5 | 0x55 (только при TrustZone) | только небезопасную часть | через уровень 1 | нет |
| U5 | 1 | любое, кроме 0xAA, 0x55 и 0xCC | нет | со стиранием; можно запереть ключом OEM1 | нет |
| U5 | 2 | 0xCC | отладки нет | только с ключом OEM2, заданным заранее | без ключа OEM2 — да |
Главное из таблицы:
- У F1 всего один защитный уровень сверх нулевого. Второго, с отключением отладки, у F1 нет — значит, нет и аппаратной защиты от описанного принципа.
- У F4 два уровня. Второй (
0xCC) отключает отладку и загрузку из системной памяти навсегда: это необратимая операция, назад кнопки нет. - У U5 — четыре градации (0, 0.5, 1, 2) и привязка к TrustZone; второй уровень можно сделать снимаемым, только если заранее заложить ключ OEM2.
- У всех трёх серий стёртый байт
0xFFпопадает в первый уровень, а не в нулевой: почти любое значение байта защищает, «открыто» — одно особое.
11в.4 Проверка: ядро само читает вектор из Flash
Принцип из 11в.2 — «при входе в исключение ядро читает вектор из Flash, и значение оказывается в регистре и на стеке» — можно показать на своей тестовой прошивке, не трогая ничей чужой камень. Это проверка документированного поведения ядра (выборка вектора и укладка кадра), а не извлечение памяти: прошивка читает только собственную таблицу векторов.
Прошивка собрана arm-none-eabi-gcc под Cortex-M3 (176 байт) и запущена
в QEMU на модели stm32vldiscovery (STM32F100). Таблица векторов лежит во
Flash; запись 11 — обработчик SVC_Handler. Главная функция читает слово
вектора, затем вызывает исключение командой svc 0; отладчик (GDB по
удалённому протоколу) останавливается у входа в обработчик и смотрит, что
ядро сделало при входе:
/* Голый обработчик: передаёт указатель кадра (MSP) в си-часть, не трогая его. */
__attribute__((naked)) void SVC_Handler(void)
{
__asm volatile(
"mrs r0, msp \n"
"b svc_handler_c\n");
}
Си-часть обработчика получает указатель кадра и достаёт из него адрес возврата — тот самый, что ядро уложило на стек при входе:
/* Си-часть обработчика: frame — кадр исключения на стеке; frame[6] — адрес
* возврата (PC), который ядро само считало и положило на стек при входе. */
void svc_handler_c(uint32_t *frame)
{
g_stacked_pc = frame[6];
g_handler_addr = (uint32_t)SVC_Handler;
g_svc_ran = 1u;
}
Что показал прогон (значения из запуска, адреса — в области Flash
0x0800 ….):
| Что наблюдаем | Значение |
|---|---|
слово вектора 11 во Flash (адрес SVC_Handler) |
0x0800 0045 |
| счётчик команд PC на входе в обработчик | 0x0800 0044 |
| адрес возврата, уложенный ядром на стек | 0x0800 008C |
| обработчик фактически отработал | да |
Слово 0x0800 0045 — это адрес SVC_Handler с младшим битом Thumb; в PC оно
попало без этого бита — 0x0800 0044. То есть ядро само прочитало слово из
Flash и загрузило его в регистр (счётчик команд). Адрес возврата
0x0800 008C — это команда в главной функции сразу за svc; ядро уложило
на стек адрес, указывающий в Flash. Ни того, ни другого отладчик у Flash не
просил — значения дало само ядро при входе в исключение. Именно эта штатная
механика на F1 не перекрыта защитой первого уровня.
Отдельно проверена таблица уровней из 11в.3: короткий расчёт по правилам
документов даёт, что у F1 первому уровню отвечают 255 значений байта из 256,
у F4 — 254 (кроме 0xAA и 0xCC), у U5 — 253; стёртый 0xFF у всех трёх
попадает в уровень 1. Это совпадает с таблицей раздела.
11в.5 Что делать: камень с сильной защитой
Если изделие должно хранить ценную прошивку и попадёт к посторонним, защиту выбирают по модели угроз: случайное копирование соседом по цеху или противник с паяльником и отладчиком. От второго первый уровень F1 не спасает — нужен камень, где отладочный путь закрывается аппаратно.
- F4 с уровнем 2. Байт
RDP = 0xCCотключает отладку, загрузку из системной памяти и дальнейшее изменение байтов конфигурации. Это надёжно и необратимо: перепрошить такую плату из ремонта нечем, кроме как через собственный живой загрузчик изделия (раздел 48). На линию уровень 2 ставят осознанно, не в первый месяц выпуска (об этом же — раздел 201). - U5 с TrustZone. Ценный код и ключи держат в безопасной части, а защиту чтения доводят до уровня 2 с регрессией по ключу OEM2 — тогда отладочный доступ к памяти закрыт аппаратно, а для сервиса остаётся управляемый возврат по ключу. Как это настраивается на практике — разделы 2б и 2е.
- OTP и однократные биты. Там, где второго уровня нет, часть секрета можно держать в однократно программируемой памяти: записанное один раз не стирается при снятии защиты.
Ни одна из мер не делает извлечение невозможным против лаборатории с микрозондами — но поднимает цену до уровня, на котором оно теряет смысл.
11в.6 Что делать: не полагаться на один байт
Защита камня — только один слой. Прошивку защищают так, чтобы её чтение само по себе ничего не давало противнику, а подмена — не проходила.
- Подпись образа (раздел 48а): загрузчик проверяет, кто сделал прошивку, и чужую не запустит. Корень доверия — первый неизменяемый этап.
- Шифрование образа (разделы 48.9 и 48г): прошивка в канале обновления и на диске — зашифрована; само по себе чтение Flash не даёт открытого кода.
- Не хранить секреты открытым текстом во Flash F1. Массив ключей в исходниках или в открытой Flash защитой не является.
- Уникальный ключ на изделие, привязанный к UID. Общий ключ на всю партию означает, что вскрытие одного прибора компрометирует все. Ключ выводят из заводского номера кристалла (UID) и секрета, не хранящегося в открытой Flash, а правильность закладки проверяют в производстве (раздел 201).
То есть RDP уровня 1 на F1 остаётся полезным — как защита от случайного копирования и как гигиена, — но несущие слои для ценного изделия это подпись, шифрование и уникальный ключ, а не один байт конфигурации.
11в.7 Грабли
- «Поставили RDP1 на F1 — прошивка в сейфе». Нет: первый уровень на F1 — защита от случайного копирования, не от противника с отладчиком. Ценный код на F1 без подписи и шифрования — открыт для того, кто держит плату.
- Секрет в открытой Flash «под защитой RDP1». Значение, которое ядро читает при работе, проходит через регистры и стек. На F1 держать там ключи и пароли нельзя — только в зашифрованном виде или в OTP.
- Уровень 2 на F4 «чтобы наверняка» в начале выпуска.
RDP = 0xCCнеобратим: ни отладки, ни возврата. Поставленный рано, он превращает первый же дефектный борт в невосстановимый. Ставят последним шагом и не сразу. - Снятие защиты «без потерь». Перевод
RDPв открытое значение — всегда полное стирание Flash. Рассчитывать «сниму защиту и дочитаю прошивку» нельзя: к моменту снятия читать уже нечего. - Один ключ на всю партию. Удобно в производстве, опасно в поле: вскрытие одного прибора раскрывает все. Ключ — свой на изделие, с привязкой к UID.
- U5: уровень 2 без заложенного ключа OEM2. Без заранее записанного ключа OEM2 возврат с уровня 2 невозможен — так же необратимо, как у F4.
11в.8 Итоги
- У F1 защита чтения — один байт
RDPи один защитный уровень:0xA5— открыто, любое другое значение (из 255 возможных) — первый уровень. - Первый уровень честно закрывает прямой путь данных отладчика к Flash и снимается только полным стиранием; но штатную выборку вектора и команд ядром он не перекрывает, и на этом построена публично описанная атака (CVE-2020-8004).
- На модели в QEMU видно само поведение ядра: при входе в исключение слово
вектора
0x0800 0045из Flash попадает в счётчик команд (0x0800 0044), а адрес возврата0x0800 008C— на стек. Слово вектора ядро прочитало из Flash само, без запроса отладчика. - Вывод для изделия: RDP уровня 1 на F1 — защита от случайного копирования, а не от противника. Для настоящей защиты нужны камни с отключаемой отладкой: F4 с уровнем 2 (необратим), U5 с TrustZone и уровнем 2, OTP для части секрета.
- Несущие слои — подпись (48а) и шифрование образа (48г), отсутствие секретов в открытой Flash, уникальный ключ на изделие с привязкой к UID и проверкой в производстве (201).
Полные исходники
vec_demo.c — тестовая прошивка: ядро само читает вектор из Flash при входе в исключение.
/* Раздел 11в: показ документированного поведения ядра Cortex-M3 на СВОЕЙ тестовой
* прошивке (STM32F100, QEMU stm32vldiscovery). Прошивка демонстрирует, что при
* входе в исключение ядро само читает адрес обработчика из таблицы векторов во
* Flash и кладёт его в счётчик команд (регистр PC), а адрес возврата — на стек.
* Это проверка штатного стекинга и выборки вектора, а не извлечение чужой памяти:
* прошивка читает только собственную таблицу векторов и печатать содержимое чужого
* кристалла не умеет. Собирается arm-none-eabi-gcc под cortex-m3. */
#include <stdint.h>
/* --- наблюдаемые величины: GDB читает их по именам --- */
volatile uint32_t g_vtor; /* SCB->VTOR — где лежит таблица векторов */
volatile uint32_t g_vec_svc; /* слово Flash: вектор SVCall (запись 11) */
volatile uint32_t g_vec_reset; /* слово Flash: вектор сброса (запись 1) */
volatile uint32_t g_handler_addr; /* фактический адрес SVC_Handler */
volatile uint32_t g_stacked_pc; /* адрес возврата, который ядро положило на стек */
volatile uint32_t g_svc_ran; /* 1 — обработчик отработал */
#define SCB_VTOR (*(volatile uint32_t *)0xE000ED08u)
extern uint32_t _estack;
void Reset_Handler(void);
void Default_Handler(void);
void SVC_Handler(void);
/* Таблица векторов во Flash: запись 1 — сброс, запись 11 — SVCall. */
__attribute__((section(".isr_vector"), used))
uint32_t *const isr_vector[16] = {
(uint32_t *)&_estack, /* 0 начальный указатель стека */
(uint32_t *)Reset_Handler, /* 1 сброс */
(uint32_t *)Default_Handler, /* 2 NMI */
(uint32_t *)Default_Handler, /* 3 HardFault */
(uint32_t *)Default_Handler, /* 4 MemManage */
(uint32_t *)Default_Handler, /* 5 BusFault */
(uint32_t *)Default_Handler, /* 6 UsageFault */
0, 0, 0, 0, /* 7..10 резерв */
(uint32_t *)SVC_Handler, /* 11 SVCall */
(uint32_t *)Default_Handler, /* 12 DebugMon */
0, /* 13 резерв */
(uint32_t *)Default_Handler, /* 14 PendSV */
(uint32_t *)Default_Handler, /* 15 SysTick */
};
void Default_Handler(void)
{
for (;;) {
}
}
/* Си-часть обработчика: frame — кадр исключения на стеке; frame[6] — адрес
* возврата (PC), который ядро само считало и положило на стек при входе. */
void svc_handler_c(uint32_t *frame)
{
g_stacked_pc = frame[6];
g_handler_addr = (uint32_t)SVC_Handler;
g_svc_ran = 1u;
}
/* Голый обработчик: передаёт указатель кадра (MSP) в си-часть, не трогая его. */
__attribute__((naked)) void SVC_Handler(void)
{
__asm volatile(
"mrs r0, msp \n"
"b svc_handler_c\n");
}
/* Точка остановки после возврата из исключения: здесь все g_* уже заполнены. */
__attribute__((noinline)) void checkpoint(void)
{
__asm volatile("nop");
}
int main(void)
{
/* Куда ядро смотрит за таблицей векторов и какие слова там лежат. */
g_vtor = SCB_VTOR;
g_vec_reset = ((volatile uint32_t *)g_vtor)[1];
g_vec_svc = ((volatile uint32_t *)g_vtor)[11];
/* Вызов исключения: ядро выберет вектор 11 из Flash и уйдёт в SVC_Handler. */
__asm volatile("svc 0");
/* После возврата: адрес обработчика, попавший в PC, = слово вектора во Flash
* (без младшего бита Thumb); адрес возврата на стеке — тоже адрес Flash. */
checkpoint();
for (;;) {
}
}
void Reset_Handler(void)
{
main();
for (;;) {
}
}
Дальше: раздел 12 «Служебные блоки» — сторожевые таймеры, аппаратный CRC и часы реального времени.