Загрузчик с шифрованием для STM32

в 15:51, , рубрики: bootloader, stm32, загрузчик, программирование микроконтроллеров

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

Загрузчик с шифрованием для STM32 - 1

Основная программа будет начинаться с 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->DR = t;  

  if(CRC->DR == 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->DR = 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();

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

Загрузчик с шифрованием для STM32 - 2

Вот, собственно, и вся процедура обновления. Прошивка проверяется на корректность и шифруется, все поставленные задачи выполнены. В случае потери питания в процессе обновления устройство, конечно, закирпичится, но загрузчик останется нетронутым и процедуру обновления можно будет повторить. Для особо ответственных ситуаций можно заблокировать на запись страницы, в которых находится загрузчик через 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->DR; 
  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->DR = 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

Автор: PKav

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js