Хабрахабр

[Из песочницы] Загрузчик с шифрованием для STM32

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

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

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

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

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

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

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

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

#define MAIN_PROGRAM_START_ADDRESS 0x08010000
#define MAIN_PROGRAM_END_ADDRESS 0x08080000

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

static const uint8_t AES_FW_KEY[] = ;
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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть