Embedded HandbookПриложения
Все разделы › II. Ядро и системные механизмы
STM32F103 · уровень продвинутый · 15 мин чтения
Открыть в интерактивном справочнике

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) обещает ровно следующее:

Для разработки это удобно: плату не «запирают навсегда», просто каждая перепрошивка после снятия защиты начинается с чистого листа. Стёртый байт конфигурации 0xFF даёт защищённый камень, а не открытый, — это тоже из 11.6.

Лестница мер защиты изделия — от нулевого уровня до уникального ключа — на рисунке. Светлые ступени добавляют удобство и защиту от случайного копирования, зелёные закрывают и противника с доступом к плате.

Лестница мер защиты изделия: RDP 0 — защиты чтения нет; RDP 1 на F1 — защита от случайного копирования по SWD, снятие со стиранием Flash; подпись и шифрование образа — чужую прошивку не запустят и образ в канале не прочтут; RDP 2, TrustZone и OTP — отладочный порт закрыт, у F4 RDP 2 необратим; ключ на изделие с привязкой к UID — вскрытие одного прибора не раскрывает партию


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 — да

Главное из таблицы:


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 не спасает — нужен камень, где отладочный путь закрывается аппаратно.

Ни одна из мер не делает извлечение невозможным против лаборатории с микрозондами — но поднимает цену до уровня, на котором оно теряет смысл.


11в.6 Что делать: не полагаться на один байт

Защита камня — только один слой. Прошивку защищают так, чтобы её чтение само по себе ничего не давало противнику, а подмена — не проходила.

То есть RDP уровня 1 на F1 остаётся полезным — как защита от случайного копирования и как гигиена, — но несущие слои для ценного изделия это подпись, шифрование и уникальный ключ, а не один байт конфигурации.


11в.7 Грабли


11в.8 Итоги

Полные исходники

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 и часы реального времени.

Открыть в интерактивном справочнике