В данной статье хотел бы написать о своем опыте создания загрузчика для STM32 с шифрованием прошивки. Я являюсь индивидуальным разработчиком, поэтому нижеприведенный код может не соответствовать каким-либо корпоративным стандартам

В процессе работы ставились следующие задачи:

  • Обеспечить обновление прошивки пользователем устройства с SD-карты.
  • Обеспечить контроль целостности прошивки и исключить запись некорректной прошивки в память контроллера.
  • Обеспечить шифрование прошивки для исключения клонирования устройства.

Код писался в Keil uVision с использованием библиотек stdperiph, fatFS и tinyAES. Подопытным микроконтроллером был STM32F103VET6, но код может быть легко адаптирован под другой контроллер STM. Контроль целостности обеспечивается алгоритмом CRC32, контрольная сумма расположена в последних 4 байтах файла с прошивкой.

В статье не описано создание проекта, подключение библиотек, инициализация периферии и прочие тривиальные этапы.

Для начала стоит определиться с тем, что такое загрузчик. Архитектура STM32 подразумевает плоскую адресацию памяти, когда в одном адресном пространстве находится Flash-память, RAM, регистры периферии и всё остальное. Загрузчик — это программа, которая начинает выполняться при запуске микроконтроллера, проверяет, нужно ли выполнить обновление прошивки, если нужно — выполняет его, и запускает основную программу устройства. В данной статье будет описан механизм обновления с SD-карты, но можно использовать любой другой источник.

Шифрование прошивки производится алгоритмом AES128 и реализовано при помощи библиотеки tinyAES. Она представляет из себя всего два файла, один с расширением .c, другой с расширением .h, поэтому проблем с её подключением возникнуть не должно.

После создания проекта следует определиться с размерами загрузчика и основной программы. Для удобства размеры следует выбирать кратно размеру страницы памяти микроконтроллера. В данном примере загрузчик будет занимать 64 Кб, а основная программа займет оставшиеся 448 Кб. Загрузчик будет размещаться в начале Flash-памяти, а основная программа сразу после загрузчика. Это следует указать в настройках проекта в Keil. Загрузчик у нас начинается с адреса 0x80000000 (именно с него STM32 начинает выполнение кода после запуска) и имеет размер 0x10000, указываем это в настройках.



Основная программа будет начинаться с 0x08010000 и заканчиваться на 0x08080000 для удобства сделаем define со всеми адресами:

#define MAIN_PROGRAM_START_ADDRESS 0x08010000
#define MAIN_PROGRAM_END_ADDRESS 0x08080000

Так же внесем в программу ключи шифрования и инициализационный вектор AES. Данные ключи лучше сгенерировать случайным образом.

static const uint8_t AES_FW_KEY[] = {0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF, 0xAF};
static const uint8_t AES_IV[] = {0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA};

В данном примере вся процедура обновления прошивки построена в виде конечного автомата. Это позволяет в процессе обновления отображать что-то на экране, сбрасывать Watchdog и выполнять любые другие действия. Для удобства сделаем define с основными состояниями автомата, чтобы не путаться в числах:

#define FW_START 5
#define FW_READ 1000
#define FW_WRITE 2000
#define FW_FINISH 10000
#define FW_ERROR 100000

После инициализации периферии нужно проверить необходимость обновления прошивки. В первом состоянии производится попытка чтения SD-карты и проверка наличия файла на ней.

uint32_t t; /* Временная переменная */ 
uint32_t fw_step; /* Индекс состояния конечного автомата */ 

uint32_t fw_buf[512]; /* Буфер для считанного блока прошивки */ 
uint32_t aes_buf[512]; /* Буфер для расшифрованного блока прошивки равен */ 
/* Буферы равны размеру страницы Flash-памяти*/

uint32_t idx; /* Текущий адрес в памяти */ 

char tbuf[64]; /* Временный буфер для sprintf */ 

FATFS FS; /* Структура библиотеки fatFS - файловая система */ 
FIL F; /* Структура библиотеки fatFS - файл */

case FW_READ: /* Чтение прошивки */
{
 if(f_mount(&FS, "" , 0) == FR_OK) /* Пробуем смонтировать SD-карту*/  
 { /* Проверяем, есть ли файл с прошивкой. */   
  if(f_open(&F, "FIRMWARE.BIN", FA_READ | FA_OPEN_EXISTING) == FR_OK) 
  {
   f_lseek(&F, 0); /* Переходим в начало файла */ 
   CRC_ResetDR(); /* Сбрасываем аппаратный счетчик CRC */ 
   lcd_putstr("Обновление прошивки", 1, 0); /* Выводим сообщение на экран */
   /* Устанавливаем адрес чтения на начало основной программы */
   idx = MAIN_PROGRAM_START_ADDRESS; 
   fw_step = FW_READ + 10; /* Переходим к следующему состоянию */
  } else {fw_step = FW_FINISH;} /* Если файла нет - завершаем загрузчик */ 
        
 } else {fw_step = FW_FINISH;} /* Если нет SD-карты - завершаем загрузчик */
 
 break;
}

Теперь нам нужно провести проверку прошивки на корректность. Здесь сначала идет код проверки контрольной суммы, выполняющийся при окончании чтения файла, а потом само чтение. Возможно, так писать не следует, напишите в комментариях что вы об этом думаете. Чтение производится по 2 Кб для удобства работы с Flash-памятью, т.к. у STM32F103VET6 размер страницы памяти 2 Кб.

case FW_READ + 10: /* Проверка корректности файла с прошивкой */ 
{
 /* В процессе показываем на экране, сколько байт считано */	
 sprintf(tbuf, "Проверка: %d", idx - MAIN_PROGRAM_START_ADDRESS); 
 lcd_putstr(tbuf, 2, 1); 

 if (idx > MAIN_PROGRAM_END_ADDRESS) /* Если прочитаи весь файл прошивки */
 {        							 
  f_read(&F, &t, sizeof(t), &idx); /* Считываем 4 байта контрольной суммы */ 
  
  /* Записываем считанные 4 байта в регистр данных периферийного блока CRC */
  CRC_CalcCRC(t);  

  if(CRC_GetCRC() == 0) /* Если результат 0, то файл не поврежден */ 
  {
   /* Устанавливаем адрес записи на адрес начала основной программы */
   idx = MAIN_PROGRAM_START_ADDRESS;       
   f_lseek(&F, 0); /* Переходим в начало файла */
   fw_step = FW_READ + 20; /* Переходим к следующему состоянию */    
   break;
  } else 
  {
   lcd_putstr("Файл поврежден", 3, 2); /* Выводим сообщение на экран */
   fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */
   break;
  }
 }   
  
 f_read(&F, &fw_buf, sizeof(fw_buf), &t); /* Считываем 2 Кб из файла в буфер */ 
 if(t != sizeof(fw_buf)) /* Если не получилось считать */ 
 {
  lcd_putstr("Ошибка чтения", 3, 2); 
  fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */
  break;
 }
 
 /* Расшифровываем считанный блок прошивки */
 AES_CBC_decrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV);

 for(t=0;t<NELEMS(aes_buf);t++) /* Записываем блок в регистр CRC */ 
 {
  CRC_CalcCRC(aes_buf[t]); /* Запись ведем по 4 байта */
 }    
 
 idx+=sizeof(fw_buf); /* Сдвигаем адрес на следующие 2 Кб */
 
 break;
}

Теперь, если прошивка не повреждена, то нужно её снова прочитать, но на этот раз уже записать во Flash — память.

case FW_READ + 20: // Flash Firmware
{
  /* В процессе показываем на экране, сколько байт записано */	
 sprintf(tbuf, "Запись: %d", idx - MAIN_PROGRAM_START_ADDRESS);
 lcd_putstr(tbuf, 4, 2);  

 if (idx > MAIN_PROGRAM_END_ADDRESS) /* Когда записали всю прошивку */ 
 {
  lcd_putstr("Готово", 7, 3); /* Выводим сообщение на экран */
  f_unlink("FIRMWARE.BIN");  /* Удаляем файл прошивки с SD-карты */  
  fw_step = FW_FINISH; /* Завершаем загрузчик */
  break;
 }
 
 f_read(&F, &fw_buf, sizeof(fw_buf), &t); /* Считываем блок 2 Кб */
 if(t != sizeof(fw_buf)) /* Если не получилось считать */ 
 {
  lcd_putstr("Ошибка чтения", 3, 3); /* Выводим сообщение на экран */
  fw_step = FW_ERROR; /* Переходим к шагу обработки ошибки обновления */
  break;
 }  

 /* Расшифровываем считанный блок прошивки */
 AES_CBC_decrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV);   
 
 FLASH_Unlock(); /* Разблокируем FLash-память на запись */ 
 FLASH_ErasePage(idx); /* Стираем страницу памяти */ 
 for(t=0;t<sizeof(aes_buf);t+=4) /* Записываем прошивку по 4 байта */ 
 {
  FLASH_ProgramWord(idx+t, aes_buf[t/4]);
 }    
 
 FLASH_Lock(); /* Блокируем прошивку на запись */
 
 idx+=sizeof(fw_buf); /* Переходим к следующей странице */ 
 
 break;
}

Теперь для красоты создадим состояния для обработки ошибки и успешного обновления:

case FW_ERROR:
{
 /* Можно что-то сделать при ошибке обновления */ 
 break;
}

case FW_FINISH:
{
 ExecMainFW(); /* Запускаем основную программу */
 /* Дальнейший код выполнен не будет */
 break;
}

Функцию запуска основной программы ExecMainFW() стоит рассмотреть подробнее. Вот она:

void ExecMainFW()
{
 /* Устанавливаем адрес перехода на основную программу */
 /* Переход производится выполнением функции, адрес которой указывается вручную */
 /* +4 байта потому, что в самом начале расположен указатель на вектор прерывания */ 
 uint32_t jumpAddress = *(__IO uint32_t*) (MAIN_PROGRAM_START_ADDRESS + 4); 
 pFunction Jump_To_Application = (pFunction) jumpAddress;
 
 /*Сбрасываем всю периферию на APB1 */
 RCC->APB1RSTR = 0xFFFFFFFF; RCC->APB1RSTR = 0x0; 
/*Сбрасываем всю периферию на APB2 */ 
 RCC->APB2RSTR = 0xFFFFFFFF; RCC->APB2RSTR = 0x0; 
 RCC->APB1ENR = 0x0; /* Выключаем всю периферию на APB1 */ 
 RCC->APB2ENR = 0x0; /* Выключаем всю периферию на APB2 */
 RCC->AHBENR = 0x0; /* Выключаем всю периферию на AHB */
 /* Сбрасываем все источники тактования по умолчанию, переходим на HSI*/
 RCC_DeInit();  
 
 /* Выключаем прерывания */
 __disable_irq(); 
 /* Переносим адрес вектора прерываний */
 NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS);  
 /* Переносим адрес стэка */ 
  __set_MSP(*(__IO uint32_t*) MAIN_PROGRAM_START_ADDRESS); 
  /* Переходим в основную программу */  
  Jump_To_Application(); 
}

Сразу после запуска startup файл все переинициализировал, поэтому основная программа должна вновь выставить указатель на вектор прерывания внутри своего адресного пространства:

__disable_irq();
 NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS);
__enable_irq();

В проекте основной программы нужно указать правильные адреса:



Вот, собственно, и вся процедура обновления. Прошивка проверяется на корректность и шифруется, все поставленные задачи выполнены. В случае потери питания в процессе обновления устройство, конечно, закирпичится, но загрузчик останется нетронутым и процедуру обновления можно будет повторить. Для особо ответственных ситуаций можно заблокировать на запись страницы, в которых находится загрузчик через Option bytes.

Однако, в случае с SD-картой можно организовать для самого себя в загрузчике одно приятное удобство. Когда тестирование и отладка новой версии прошивки завершена, можно заставить само устройство по какому-то особому условию (например, кнопка или джампер внутри) зашифровать и выгрузить на SD-карту готовую прошивку. В таком случае останется только извлечь SD-карту из устройства, вставить в компьютер и выложить прошивку в интернет на радость пользователям. Сделаем это в виде ещё двух состояний конечного автомата:

case FW_WRITE:
{
 if(f_mount(&FS, "" , 0) == FR_OK) /* Пробуем смонтировать SD-карту*/   
 {
  /* Пробуем создать файл */ 
  if(f_open(&F, "FIRMWARE.BIN", FA_WRITE | FA_CREATE_ALWAYS) == FR_OK)  
  {
   CRC_ResetDR(); /* Сбрасываем блок CRC */
   /* Устанавливаем адрес чтения на начало основной программы */
   idx = MAIN_PROGRAM_START_ADDRESS; 
   fw_step = FW_WRITE + 10; /* Переходим к следующему состоянию */    
  } else {fw_step = FW_ERROR;} /* Переходим к шагу обработки ошибки */ 
        
 } else {fw_step = FW_ERROR;} /* Переходим к шагу обработки ошибки */
 
 break;
}

case FW_WRITE + 10:
{
 if (idx > MAIN_PROGRAM_END_ADDRESS) /* Если выгрузили всю прошивку */ 
 {   
  t = CRC_GetCRC(); 
  f_write(&F, &t, sizeof(t), &idx); /* Дописываем в конец файла контрольную сумму */ 

  f_close(&F); /* Закрываем файл, сбрасываем кэш */ 
  fw_step = FW_FINISH; /* Завершаем зарузчик */ 
 }
 
 /* Считываем 2 Кб прошивки из Flash-памяти в буфер */ 
 memcpy(&fw_buf, (uint32_t *)idx, sizeof(fw_buf)); 
 
 for(t=0;t<NELEMS(fw_buf);t++) /* Вычисляем CRC для считанного блока */ 
 {
 CRC_CalcCRC(fw_buf[t]);
 }   
 
 /* Шифруем прошивку */
 AES_CBC_encrypt_buffer((uint8_t*)&aes_buf, (uint8_t *)&fw_buf, sizeof(fw_buf), AES_FW_KEY, AES_IV); 
 
 /* Записываем зашифрованный блок в файл */
 f_write(&F, &aes_buf, sizeof(aes_buf), &t); 
 
 idx+=sizeof(fw_buf); /* Сдвигаем адрес считываемого блока */ 
 
 break;
}

Вот, собственно и всё, что я хотел рассказать. В завершении статьи хотел бы пожелать вам после создания подобного загрузчика не забыть включить защиту от чтения памяти микроконтроллера в Option bytes.

Ссылки


tinyAES
FatFS

Комментарии (52)


  1. Andy_Big
    12.12.2018 19:26

    Делал для STM32 загрузчик с возможностью обновления прошивки в шифрованном виде через USB. Шифрование — свое, из нескольких логических и математических операций над каждым байтом на основе ключа (ключ — длиной несколько сотен байт). Конечно, это не такой стойкий ко взлому алгоритм, как AES, но вполне достаточный для того, чтобы зашифрованные им обновления можно было выкладывать в паблик для пользователей :) Шьется так же блоками по размеру страницы памяти контроллера. Для общения с компьютером написал свой пакетный протокол.
    Со стороны компьютера — исполняемый файл с прошивками (разными для разных вариантов устройств), содержащимися в ресурсах. Программа обнаруживает подключенное устройство, запрашивает у него данные о нем и выбирает соответствующую прошивку.
    Этой же программой передается и содержимое EPROM-памяти (которая отдельным чипом в устройстве) — настройки, калибровка и т.д., это уже идет без шифрования :)
    Протокол предусматривает запрос от устройства серийного номера, уникального идентификатора микроконтроллера, версий загрузчика и прошивки, задание нового серийного номера, обновление и считывание EPROM, обновление прошивки.
    Единственный минус — девайс подключается по USB как CDC (COM-порт), что требует драйверов, поэтому сейчас перевожу все это дело на HID :)


    1. PKav Автор
      12.12.2018 19:34

      Взаимодействие с USB сделано на HAL?


      1. Andy_Big
        12.12.2018 19:57

        На SPL — и CDC и HID. Правда, пока я наладил работу HID, пришлось досконально изучить все внутренности библиотечных функций, взаимодействие их между собой и с регистрами. Плюс этот пресловутый дескриптор HID, в котором без литра самогона не разобраться :)


  1. NordicEnergy
    12.12.2018 19:51

    Заявлен stdperiph, а в коде используется cmsis — в чем соль? 64 кБ загрузчик это тоже сильно))


    1. PKav Автор
      12.12.2018 20:33

      CRC_ResetDR() есть за то. Да и весь остальной проект на stdperiph.
      64 Кб — для примера и для ровного счета. Хотя, почему нет? Может, там в процессе обновления вывод на экран какой хитрый нужен, с картинками и шрифтами.


      1. NordicEnergy
        13.12.2018 09:37
        +2

        CRC_ResetDR() есть за то. Да и весь остальной проект на stdperiph.
        И вас не смущает, что смешивать 2 стиля/библиотеки программирования это плохо?


        1. PKav Автор
          13.12.2018 09:47

          Скорректировал текст.
          Вообще, вопрос не однозначный — ведь stdperiph базируется на CMSIS. Это не HAL и stdperiph смешивать, как некоторые делают.


      1. esaulenka
        13.12.2018 11:02

        У меня загрузчик (с AES'ом, USB HID'ом и gzip'ом) влезает в «ровные» 16к (и ещё 4к остаётся).
        А вывод на экран лучше всё ж таки не делать. Захочется завтра экран поменять, или косяк какой вылезет — а загрузчик уже есть, и безболезненной процедуры апгрейда загрузчика нету (которую может сделать пользователь со 100% гарантией получения живого устройства). Минимум функционала (получил прошивку — залил куда следует, и хватит).


  1. Amomum
    12.12.2018 20:28

    Обеспечить шифрование прошивки для исключения клонирования устройства.

    Я правильно понял, что под этим понимается шифрование файла с прошивкой на SD-карте, а не шифрование прошивки в устройстве?


    1. PKav Автор
      12.12.2018 20:33

      Да, всё верно.


      1. Amomum
        12.12.2018 22:04
        +1

        Т.е. при желании пользователь спокойно слить прошивку (и загрузчик заодно) с устройства через отладочный разъем и склонировать его?


        1. PKav Автор
          12.12.2018 22:18

          Именно об этом я рассказал в последнем предложении данной статьи.


          1. Amomum
            12.12.2018 22:24

            Да, прошу прощения, пропустил.
            Другое дело, что такая защита действительно не является гарантией, как справедливо пишут в комментарии ниже.

            Вот пара актуальных статей на тему — раз, два.


            1. PKav Автор
              12.12.2018 22:26

              Ниже в комментарии отписал что ещё можно сделать с этим.
              habr.com/post/432966/#comment_19491212


            1. esaulenka
              13.12.2018 11:12

              Вообще-то тут написано одно и то же.
              Первоисточник, в котором есть детали (а не просто красивый мультик) здесь.
              Идея весьма любопытная, но там сделано очень много допущений о защищаемой прошивке.


        1. emmibox
          12.12.2018 22:19

          При установке битов защиты отладочный интерфейс не работает.

          Другое дело, что есть специальные люди, которые жизнь потратили на то, чтоб извлекать прошивки(и загрузчики) с устройств с взведенными битами защиты — так что именно для этой архитектуры, это лишь вопрос желания, гугления, и $$$$.


          1. PKav Автор
            12.12.2018 22:24

            Если прошивка на столько ценна, то можно завязать её работу на серийный номер микроконтроллера, при чем так, чтобы адрес серийного номера не указывался в прошивке напрямую, а вычислялся алгоритмами и проверка срабатывала не сразу, а через какое-то время после запуска. Правда, внезапно отказавшие клоны устройства тоже могут нанести вред репутации компании, но это отдельная тема.


            1. emmibox
              12.12.2018 22:32

              Просто надо еще $$$$ на реверс, что в ARM слишком просто…

              Вопрос лишь в том — надо ли это кому то или нет. Если надо — это будет плохой выбор архитектуры!


  1. Inanity
    12.12.2018 22:41
    +1

    Проблема 1:

    Здесь сначала идет код проверки контрольной суммы, выполняющийся при окончании чтения файла, а потом само чтение. Возможно, так писать не следует, напишите в комментариях что вы об этом думаете.

    Вы считали прошивку и проверили CRC, потом вы ещё раз её считываете для записи во флеш. Но никто не может гарантировать, что второй раз вы считали с карточки то же, что и в первый раз, а контрольную сумму вы уже не проверяете. Таким образом мы имеет точно правильную прошивку на SD-карте, а что по факту попало во флеш — неизвестно.

    Проблема 2:

    Вы храните CRC от расшифрованной прошивки. Строго говоря, это не безопасно. Более безопасно хранить CRC от зашифрованной прошивки. Злоумышленник не должен иметь ни единой крупицы дополнительной информации о зашифрованных данных. Открытый CRC косвенно указывает на версию прошивки как минимум.

    Проблема 3:

    Целостность прошивки с криптографической точки зрения не обеспечивается вообще.
    Можно манипулировать битиками зашифрованной прошивки, пересчитывать CRC, зашивать и смотреть
    как на это реагирует ваш девайс. Зная структуру прошивки, где, например, таблицы прерываний, инициализированные данные и т.д. можно много чего наворотить, если очень захотеть.


    1. PKav Автор
      12.12.2018 22:57

      1. Можно включить проверку CRC в драйвере SD-карты, у карт это реализовано аппаратно. Или добавить ещё одну проверку.


      2. Модифицировать алгоритм для хранения зашифрованной контрольной суммы не сложно. В этой статье я хотел показать суть, как сделать загрузчик. Толком ничего не нашел, собирал информацию по крупицам пока писал этот код, вот решил скомпоновать все.



      1. Inanity
        12.12.2018 23:19

        Можно включить проверку CRC в драйвере SD-карты

        Можно, но вы уже внедрили (уровнем выше) в свой формат CRC. А раз так, то стоит это делать правильно, а иначе зачем ваш код?

        … для хранения зашифрованной контрольной суммы не сложно
        Нет смысла зашифровывать CRC. Проще держать CRC от зашифрованных данных. Это даже работать будет быстрее, потому, что не нужно будет ничего расшифровывать на фазе проверки.

        P.S. 3 пункт дописал с задержкой, обратите внимание.


        1. PKav Автор
          13.12.2018 08:32

          Ничто не мешает файлу повредиться в процессе скачивания из интернета, поэтому обе проверки будут полезны.

          А по шифрованию да, уже согласился в предыдущем комментарии, что имеет смысл считать контрольную сумму зашифрованных данных. И изменить алгоритм никакой сложности не представляет, по сути, поменять несколько строк местами.

          По 3 пункту — это вы вот сейчас знаете, что контрольная сумма в конце и она не зашифрована, потому, что видите исходник с комментариями. В реальной ситуации никто не знает о том, что контрольная сумма в конце. Она может быть в начале, в середине, после каждой страницы памяти, и вообще в любом месте или в нескольких местах сразу и по разным алгоритмам, всё зависит от фантазии разработчика. Эта статья ведь не руководство к действию как получить 100% защиту, а просто описание метода, который тоже можно доработать.


          1. Inanity
            13.12.2018 13:30

            это вы вот сейчас знаете, что контрольная сумма в конце и она не зашифрована, потому, что видите исходник с комментариями
            Security through obscurity не работает. Это плохая практика.

            В реальной ситуации никто не знает о том, что контрольная сумма в конце.
            Если известно, что в бинарнике есть контрольная сумма, то поиск её не такая уж и сложная задача. Тем более, что она очень хорошо параллелится.

            Она может быть в начале, в середине, после каждой страницы памяти
            Да пожалуйста, т.е. где-то среди 2048+4 байт есть контрольная сумма. Найти её не так сложно, даже при условии, что полином заранее неизвестен.


            1. PKav Автор
              13.12.2018 13:41

              Тут мы уже выходим на очень высокий уровень квалификации специалистов, стоимость труда которых начинает приближаться к стоимости разработки аналогичной прошивки с нуля.

              Вы пишете о том, что любую защиту можно взломать. Я с вами согласен, даже очень дорогая и запутанная защита компьютерных игр часто взламывается в первые дни после релиза, но вопрос о наличии специалистов и стоимости взлома тут на первом месте.


    1. esaulenka
      13.12.2018 23:39

      Но никто не может гарантировать, что второй раз вы считали с карточки то же, что и в первый раз

      Считайте это защитой не от взлома, а от дурака-пользователя, который скопировал только половину файла.

      Открытый CRC косвенно указывает на версию прошивки как минимум

      Тут есть бОльшая проблема — вектор инициализации прибит гвоздём в загрузчике. И, в итоге, на версию прошивки явно указывает её содержимое.
      Но вообще, по-хорошему, версию надо на видном месте писать — хоть текстом в зарезервированной (и не загружаемой в устройство) области. Это, возможно, слегка облегчит жизнь взломщику, но гарантированно уберёт кучу проблем у обычного пользователя.


      1. Inanity
        14.12.2018 00:35

        Считайте это защитой не от взлома, а от дурака-пользователя, который скопировал только половину файла.
        Зачем оправдывать неправильную схему, когда можно сделать нормально, обеспечив корректность файла и на карточке, и в памяти мк без особых усилий? Тем более если конечная цель — гарантированно залить нормальную прошивку в мк.

        Тут есть бОльшая проблема — вектор инициализации прибит гвоздём в загрузчике. И, в итоге, на версию прошивки явно указывает её содержимое.
        В данном конкретном примере это так. Но в теории можно было гвоздём прибитый вектор инициализации криптографически замешать с серийным номером.


  1. truebest
    12.12.2018 22:41

    У STM32 есть отличный загрузчик с шифрованием, кучей проверок, защит различных моделей атак на мк, и даунгрейда. www.st.com/en/embedded-software/x-cube-sbsfu.html
    Он правда под L4, но я и под L1 переделал для себя, полет нормальный, только медленнее конечно чем на L4. Скоро на остальные платформы напишут.


    1. PKav Автор
      12.12.2018 23:00

      А на F1 или F0 или не напишут вообще, или он отъест треть памяти сразу. А это тоже хорошие контроллеры, для которых есть применение.


      1. 110
        13.12.2018 10:53

        Мой шифрованный (usb/uart) загрузчик для L1 занимает 16K из 128K (12%), HAL.

        Загрузчик пишет со скоростью 82 KB за 1:24 мин.
        Сначала стираются все страницы, далее передаются блоки по 256 байт (страница), после записи и отправки подтверждения передается следующая страница. Снимается блокировка страницы один раз перед записью 256 байт и блокируется в конце. Вся задержка на 306 строке, если ее закоментировать, задержки нет. Эта одна команда (запись 32 bit во flash) блокирует программу на 233,7 ms / (256/4) = 3,7 ms.

        image

        «STM32F405: прошить 400кб за 10 секунд или быстрый UART-загрузчик заточенный под USB-UART, размером менее 4 килобайт»
        m.habr.com/post/305800

        Я не понимаю, как можно достичь такую скорость загрузки при аппаратном ограничении записи во flash.


        1. PKav Автор
          13.12.2018 13:22

          На сколько я помню (но ссылок предоставить не смогу), на L-сериях структура Flash отличается и оптимизирована для меньшего энергопотребления в ущерб производительности. К тому же L1 могут только 32 МГц, а F4 до 180 МГц. Плюс можно разместить загрузчик в RAM и стереть всю Flash единовременно, а не постранично, что тоже даст ускорение.


          1. esaulenka
            13.12.2018 23:24

            Цифры легко ищутся в соотв. даташитах по слову «programming». STM32F405 — запись 1/2/4 байт — 16 мкс, STM32L151 — 3280 мкс.

            стереть всю Flash единовременно

            Вместе с самим загрузчиком и битами защиты? Так себе решение…


        1. truebest
          13.12.2018 14:11

          В L1 никак, у меня тоже медленно пишется/стирается. Примерно 1 кб/сек.


  1. Mirn
    13.12.2018 08:01

    в void ExecMainFW() возможно падение в хард фаулт или buserror исключения в случае:
    1. компилятор не понимает что стек изменился (gcc не понимал несколько лет назад)
    2. новый стек находится в последнем верхнем слове доступной памяти и после него памяти физически не существует и её чтение ведёт к исключению (а такое часто бывает: начать стек с конца доступной ОЗУ обычная практика)
    3. компилятор реализовал void ExecMainFW() с сохранением nonvolatile регистров в стеке при входе в неё и при вызове Jump_To_Application(); их попытается восстановить из стека, т.к. не знает о Jump_To_Application(); ничего.

    при этом в пункте 3 произойдёт выход стека за пределы блока памяти.
    решение которое делал я в своих загрузчиках для стм32Ф4 и Ф7 и H7 такое:
    github.com/Mirn/Boot_F4_fast_uart/blob/master/src/sfu_commands.c#L229
    см функция jump_main: я объявил «голую» функцию, т.е. без обработки стека и тд и в неё уже перешёл в основную программу с установкой стека.


    1. PKav Автор
      13.12.2018 08:19
      +1

      Хорошее дополнение, спасибо. Протестирую на Keil и дополню статью.


    1. Andy_Big
      14.12.2018 03:37

      Спасибо за подсказку. У меня как раз бывает (но очень редко и в основном при нестабильной, скачкообразной подаче питания) хард фаулт. Может быть как раз в этом причина, попробую применить Ваш способ перехода на основную прошивку :)


  1. Disco_Cat
    13.12.2018 13:29

    Советовались ли вы при создании вашей схемы защиты со старшими товарищами?
    Видится такая возможность атаки: записать вместо обновляемого приложения код, выдающий содержимое памяти наружу любым доступным методом, например через тот же USB. В коде загрузчика ключ шифрования хранится в явном виде, что должно позволить расшифровывать файлы обновлений прошивок.


    1. PKav Автор
      13.12.2018 13:34

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

      Код, заливаемый во Flash, расшифровывается. Если подсунуть свой код, то во Flash Будет записан мусор и контроллер не запустится.

      К тому же я указывал, что эта статья не является руководством к действию для получения 100% защиты, а лишь поясняет суть. Можно добавлять различные проверки и запутывающее поведение, что серьезно усложнит взлом.


      1. Disco_Cat
        13.12.2018 13:39

        Со схемой защиты ясно. По поводу записи, имелась в виду конечно же запись по jtag/swd, в надежде что у вас не стоит защита от записи хотя бы на некоторые страницы.


        1. PKav Автор
          13.12.2018 13:48

          Там есть общая защита от чтения и записи, ставится через Option bytes, я написал об этом в последнем предложении. Её, кстати, можно включать прямо из прошивки, чтоб не забыть.


    1. esaulenka
      13.12.2018 23:11

      Почитайте о защите STM'ок. Там всё просто: защита от чтения отладчиком/штатным бутлоадером (а её, очевидно, надо включать) включает в себя и отключение команд записи. Т.е. все действия должны начинаться с команды «стереть весь чип».
      А записать штатным образом (через этот загрузчик) какой-то осмысленный код представляется затруднительным…


      1. Disco_Cat
        14.12.2018 12:06

        Насколько я понимаю ДШ, история несколько иная
        При RDP = 0хА5 (Level 1) можно записывать в те страницы, которые не закрыты через WRP0..WRP3
        поправьте если неправ


        1. esaulenka
          14.12.2018 13:56

          Для определённости — разговор про STM32F10x, про который эта статья и написана.

          RDP=0xA5 — это Level0, защиты нет. Level1 — это RDP != 0xA5.
          При установке Level1 «снаружи» (JTAG/SWD/штатный Boot/код в RAM) можно только сделать Mass Erase. «Изнутри» (из своего кода) можно стирать/записывать страницы, на которые не стоит write protect (странная особенность F10x — автоматически ставит защиту от записи на первые страницы флеша при установке зашиты от чтения. Не отключается).

          Первоисточник — PM0075 Programming manual STM32F10xxx Flash memory microcontrollers и reference manual.


  1. Electrovoicer
    13.12.2018 15:31

    Все это очень полезно, но, к сожалению, нынче заклонировать почти любой микроконтроллер можно за сумму порядка 2-5к$. Знаю не по наслышке, нечистоплотные китайские заказчики быстро с этим справились, обломав зубы только на цифровой подписи FPGA-чипа


    1. PKav Автор
      13.12.2018 15:37

      Примеров бы. А то все об этом говорят, но реально китайские и оригинальные устройства отличаются так же, как IPhone и его китайский клон.
      — Единственное, что вспомнилось — OBDII адаптер ELM327, но там, на сколько я помню, сами разработчики забыли включить защиту от чтения памяти.


      1. Electrovoicer
        13.12.2018 19:23

        Какой пример? Мой девайс содержит LPC1343, в котором юник айди и блочное шифрование, и ПЛИС Lattice, подписанная AES-128 ключем. Когда китайцы решили больше нам не платить, они быстренько склонировали микроконтроллер, обрадовались, произвели первую партию изделий и тут их ждал облом, потому что OTP-пассворд в ПЛИС ломать еще не научились… я провел небольшое исследование, и нашел несколько контор, клонирующих любые МК с флэш-памятью. Средний ценник 3к


        1. PKav Автор
          13.12.2018 19:38

          А китайцы были клиентами или подрядчиками по производству и сборке?


          1. Electrovoicer
            13.12.2018 19:43

            Клиентами


        1. Disco_Cat
          14.12.2018 12:03

          В порядке интереса, можете ли поделиться?
          Сомнение вызывает слово «любые»
          Например NEC UPD76F0012GD мне никто и ни за какие деньги не предложил считать (и камень, и изделие на его основе стоят в приборе 20-летней давности)


          1. Electrovoicer
            14.12.2018 12:29

            Рекламировать тут всяких нехороших людей? Нагуглите сами, пожалуйста.


  1. ScaryA
    13.12.2018 16:22

    Спасибо, за статью. Очень удобно, всё в одном месте, с коментариями и с описанием. Сейчас как раз пишу загрузчик для STM32 и пришлось изрядно просеивать интернет в поисках полезных материалов. Дополнение от Mirn тоже интригующе интересно (да ещё со ссылкой на реальный код).
    Я не понял только, для чего и в загрузчике и в основной программе NVIC_SetVectorTable() с одинаковыми параметрами. Если, как написано «Сразу после запуска startup файл все переинициализировал», то в загрузчике этот вызов бесполезен, получается, он же прямо перед передачей управления, фактически, этому startup.s.
    Планируется ли перешивать сам загрузчик? У меня такая возможность предполагается и я придумал два варианта: 1) выделение дополнительного места, куда будет записываться временная программа перешивающая загрузчик (как вариант — вместо основной, с последующей её перепрошивкой назад), 2) махинации со скриптом линковщика и расположением функции копирования в RAM через атрибуты (и функций ею используемых). Реализую второй вариант, попутно разбираясь в этих скриптах (тоже не сходу находится полезная информация), вроде получается. Может есть ещё какие-то нормальные способы? Как люди делают?


    1. PKav Автор
      13.12.2018 16:39

      По NVIC_SetVectorTable — я делал это из расчета «перебдеть» — ведь стартовые процедуры тоже могут опираться на этот вектор и инициализировать какие-то системные прерывания типа HardFault. Но глубоко не копал на этот счет, нужно будет глянуть.

      По перешивке загрузчика — это может сделать и непосредственно основная программа точно таким же способом. Но при отключении питания в процессе прошивки устройство закирпичится совсем. Я бы не стал это делать не установив хотя бы аккумулятор и не удостоверившись в том, что его заряда достаточно.


    1. esaulenka
      13.12.2018 23:03

      для чего и в загрузчике и в основной программе NVIC_SetVectorTable()

      Потому что стандартный ST'шный SystemInit() перезаписывает NVIC->VTOR на стандартное 0x08000000. Для правильной работы надо подправить адрес или попросту выкинуть оттуда эту строку за ненадобностью — в загрузчике это делать идеологически правильнее.