В процессе работы ставились следующие задачи:
- Обеспечить обновление прошивки пользователем устройства с 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)
NordicEnergy
12.12.2018 19:51Заявлен stdperiph, а в коде используется cmsis — в чем соль? 64 кБ загрузчик это тоже сильно))
PKav Автор
12.12.2018 20:33CRC_ResetDR() есть за то. Да и весь остальной проект на stdperiph.
64 Кб — для примера и для ровного счета. Хотя, почему нет? Может, там в процессе обновления вывод на экран какой хитрый нужен, с картинками и шрифтами.NordicEnergy
13.12.2018 09:37+2CRC_ResetDR() есть за то. Да и весь остальной проект на stdperiph.
И вас не смущает, что смешивать 2 стиля/библиотеки программирования это плохо?PKav Автор
13.12.2018 09:47Скорректировал текст.
Вообще, вопрос не однозначный — ведь stdperiph базируется на CMSIS. Это не HAL и stdperiph смешивать, как некоторые делают.
esaulenka
13.12.2018 11:02У меня загрузчик (с AES'ом, USB HID'ом и gzip'ом) влезает в «ровные» 16к (и ещё 4к остаётся).
А вывод на экран лучше всё ж таки не делать. Захочется завтра экран поменять, или косяк какой вылезет — а загрузчик уже есть, и безболезненной процедуры апгрейда загрузчика нету (которую может сделать пользователь со 100% гарантией получения живого устройства). Минимум функционала (получил прошивку — залил куда следует, и хватит).
Amomum
12.12.2018 20:28Обеспечить шифрование прошивки для исключения клонирования устройства.
Я правильно понял, что под этим понимается шифрование файла с прошивкой на SD-карте, а не шифрование прошивки в устройстве?PKav Автор
12.12.2018 20:33Да, всё верно.
Amomum
12.12.2018 22:04+1Т.е. при желании пользователь спокойно слить прошивку (и загрузчик заодно) с устройства через отладочный разъем и склонировать его?
PKav Автор
12.12.2018 22:18Именно об этом я рассказал в последнем предложении данной статьи.
Amomum
12.12.2018 22:24Да, прошу прощения, пропустил.
Другое дело, что такая защита действительно не является гарантией, как справедливо пишут в комментарии ниже.
Вот пара актуальных статей на тему — раз, два.PKav Автор
12.12.2018 22:26Ниже в комментарии отписал что ещё можно сделать с этим.
habr.com/post/432966/#comment_19491212
emmibox
12.12.2018 22:19При установке битов защиты отладочный интерфейс не работает.
Другое дело, что есть специальные люди, которые жизнь потратили на то, чтоб извлекать прошивки(и загрузчики) с устройств с взведенными битами защиты — так что именно для этой архитектуры, это лишь вопрос желания, гугления, и $$$$.PKav Автор
12.12.2018 22:24Если прошивка на столько ценна, то можно завязать её работу на серийный номер микроконтроллера, при чем так, чтобы адрес серийного номера не указывался в прошивке напрямую, а вычислялся алгоритмами и проверка срабатывала не сразу, а через какое-то время после запуска. Правда, внезапно отказавшие клоны устройства тоже могут нанести вред репутации компании, но это отдельная тема.
emmibox
12.12.2018 22:32Просто надо еще $$$$ на реверс, что в ARM слишком просто…
Вопрос лишь в том — надо ли это кому то или нет. Если надо — это будет плохой выбор архитектуры!
Inanity
12.12.2018 22:41+1Проблема 1:
Здесь сначала идет код проверки контрольной суммы, выполняющийся при окончании чтения файла, а потом само чтение. Возможно, так писать не следует, напишите в комментариях что вы об этом думаете.
Вы считали прошивку и проверили CRC, потом вы ещё раз её считываете для записи во флеш. Но никто не может гарантировать, что второй раз вы считали с карточки то же, что и в первый раз, а контрольную сумму вы уже не проверяете. Таким образом мы имеет точно правильную прошивку на SD-карте, а что по факту попало во флеш — неизвестно.
Проблема 2:
Вы храните CRC от расшифрованной прошивки. Строго говоря, это не безопасно. Более безопасно хранить CRC от зашифрованной прошивки. Злоумышленник не должен иметь ни единой крупицы дополнительной информации о зашифрованных данных. Открытый CRC косвенно указывает на версию прошивки как минимум.
Проблема 3:
Целостность прошивки с криптографической точки зрения не обеспечивается вообще.
Можно манипулировать битиками зашифрованной прошивки, пересчитывать CRC, зашивать и смотреть
как на это реагирует ваш девайс. Зная структуру прошивки, где, например, таблицы прерываний, инициализированные данные и т.д. можно много чего наворотить, если очень захотеть.PKav Автор
12.12.2018 22:57Можно включить проверку CRC в драйвере SD-карты, у карт это реализовано аппаратно. Или добавить ещё одну проверку.
Модифицировать алгоритм для хранения зашифрованной контрольной суммы не сложно. В этой статье я хотел показать суть, как сделать загрузчик. Толком ничего не нашел, собирал информацию по крупицам пока писал этот код, вот решил скомпоновать все.
Inanity
12.12.2018 23:19Можно включить проверку CRC в драйвере SD-карты
Можно, но вы уже внедрили (уровнем выше) в свой формат CRC. А раз так, то стоит это делать правильно, а иначе зачем ваш код?
… для хранения зашифрованной контрольной суммы не сложно
Нет смысла зашифровывать CRC. Проще держать CRC от зашифрованных данных. Это даже работать будет быстрее, потому, что не нужно будет ничего расшифровывать на фазе проверки.
P.S. 3 пункт дописал с задержкой, обратите внимание.PKav Автор
13.12.2018 08:32Ничто не мешает файлу повредиться в процессе скачивания из интернета, поэтому обе проверки будут полезны.
А по шифрованию да, уже согласился в предыдущем комментарии, что имеет смысл считать контрольную сумму зашифрованных данных. И изменить алгоритм никакой сложности не представляет, по сути, поменять несколько строк местами.
По 3 пункту — это вы вот сейчас знаете, что контрольная сумма в конце и она не зашифрована, потому, что видите исходник с комментариями. В реальной ситуации никто не знает о том, что контрольная сумма в конце. Она может быть в начале, в середине, после каждой страницы памяти, и вообще в любом месте или в нескольких местах сразу и по разным алгоритмам, всё зависит от фантазии разработчика. Эта статья ведь не руководство к действию как получить 100% защиту, а просто описание метода, который тоже можно доработать.Inanity
13.12.2018 13:30это вы вот сейчас знаете, что контрольная сумма в конце и она не зашифрована, потому, что видите исходник с комментариями
Security through obscurity не работает. Это плохая практика.
В реальной ситуации никто не знает о том, что контрольная сумма в конце.
Если известно, что в бинарнике есть контрольная сумма, то поиск её не такая уж и сложная задача. Тем более, что она очень хорошо параллелится.
Она может быть в начале, в середине, после каждой страницы памяти
Да пожалуйста, т.е. где-то среди 2048+4 байт есть контрольная сумма. Найти её не так сложно, даже при условии, что полином заранее неизвестен.PKav Автор
13.12.2018 13:41Тут мы уже выходим на очень высокий уровень квалификации специалистов, стоимость труда которых начинает приближаться к стоимости разработки аналогичной прошивки с нуля.
Вы пишете о том, что любую защиту можно взломать. Я с вами согласен, даже очень дорогая и запутанная защита компьютерных игр часто взламывается в первые дни после релиза, но вопрос о наличии специалистов и стоимости взлома тут на первом месте.
esaulenka
13.12.2018 23:39Но никто не может гарантировать, что второй раз вы считали с карточки то же, что и в первый раз
Считайте это защитой не от взлома, а от дурака-пользователя, который скопировал только половину файла.
Открытый CRC косвенно указывает на версию прошивки как минимум
Тут есть бОльшая проблема — вектор инициализации прибит гвоздём в загрузчике. И, в итоге, на версию прошивки явно указывает её содержимое.
Но вообще, по-хорошему, версию надо на видном месте писать — хоть текстом в зарезервированной (и не загружаемой в устройство) области. Это, возможно, слегка облегчит жизнь взломщику, но гарантированно уберёт кучу проблем у обычного пользователя.Inanity
14.12.2018 00:35Считайте это защитой не от взлома, а от дурака-пользователя, который скопировал только половину файла.
Зачем оправдывать неправильную схему, когда можно сделать нормально, обеспечив корректность файла и на карточке, и в памяти мк без особых усилий? Тем более если конечная цель — гарантированно залить нормальную прошивку в мк.
Тут есть бОльшая проблема — вектор инициализации прибит гвоздём в загрузчике. И, в итоге, на версию прошивки явно указывает её содержимое.
В данном конкретном примере это так. Но в теории можно было гвоздём прибитый вектор инициализации криптографически замешать с серийным номером.
truebest
12.12.2018 22:41У STM32 есть отличный загрузчик с шифрованием, кучей проверок, защит различных моделей атак на мк, и даунгрейда. www.st.com/en/embedded-software/x-cube-sbsfu.html
Он правда под L4, но я и под L1 переделал для себя, полет нормальный, только медленнее конечно чем на L4. Скоро на остальные платформы напишут.PKav Автор
12.12.2018 23:00А на F1 или F0 или не напишут вообще, или он отъест треть памяти сразу. А это тоже хорошие контроллеры, для которых есть применение.
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.
«STM32F405: прошить 400кб за 10 секунд или быстрый UART-загрузчик заточенный под USB-UART, размером менее 4 килобайт»
m.habr.com/post/305800
Я не понимаю, как можно достичь такую скорость загрузки при аппаратном ограничении записи во flash.PKav Автор
13.12.2018 13:22На сколько я помню (но ссылок предоставить не смогу), на L-сериях структура Flash отличается и оптимизирована для меньшего энергопотребления в ущерб производительности. К тому же L1 могут только 32 МГц, а F4 до 180 МГц. Плюс можно разместить загрузчик в RAM и стереть всю Flash единовременно, а не постранично, что тоже даст ускорение.
esaulenka
13.12.2018 23:24Цифры легко ищутся в соотв. даташитах по слову «programming». STM32F405 — запись 1/2/4 байт — 16 мкс, STM32L151 — 3280 мкс.
стереть всю Flash единовременно
Вместе с самим загрузчиком и битами защиты? Так себе решение…
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: я объявил «голую» функцию, т.е. без обработки стека и тд и в неё уже перешёл в основную программу с установкой стека.Andy_Big
14.12.2018 03:37Спасибо за подсказку. У меня как раз бывает (но очень редко и в основном при нестабильной, скачкообразной подаче питания) хард фаулт. Может быть как раз в этом причина, попробую применить Ваш способ перехода на основную прошивку :)
Disco_Cat
13.12.2018 13:29Советовались ли вы при создании вашей схемы защиты со старшими товарищами?
Видится такая возможность атаки: записать вместо обновляемого приложения код, выдающий содержимое памяти наружу любым доступным методом, например через тот же USB. В коде загрузчика ключ шифрования хранится в явном виде, что должно позволить расшифровывать файлы обновлений прошивок.PKav Автор
13.12.2018 13:34Не советовался — я индивидуальный разработчик, мне не с кем.
Код, заливаемый во Flash, расшифровывается. Если подсунуть свой код, то во Flash Будет записан мусор и контроллер не запустится.
К тому же я указывал, что эта статья не является руководством к действию для получения 100% защиты, а лишь поясняет суть. Можно добавлять различные проверки и запутывающее поведение, что серьезно усложнит взлом.Disco_Cat
13.12.2018 13:39Со схемой защиты ясно. По поводу записи, имелась в виду конечно же запись по jtag/swd, в надежде что у вас не стоит защита от записи хотя бы на некоторые страницы.
PKav Автор
13.12.2018 13:48Там есть общая защита от чтения и записи, ставится через Option bytes, я написал об этом в последнем предложении. Её, кстати, можно включать прямо из прошивки, чтоб не забыть.
esaulenka
13.12.2018 23:11Почитайте о защите STM'ок. Там всё просто: защита от чтения отладчиком/штатным бутлоадером (а её, очевидно, надо включать) включает в себя и отключение команд записи. Т.е. все действия должны начинаться с команды «стереть весь чип».
А записать штатным образом (через этот загрузчик) какой-то осмысленный код представляется затруднительным…Disco_Cat
14.12.2018 12:06Насколько я понимаю ДШ, история несколько иная
При RDP = 0хА5 (Level 1) можно записывать в те страницы, которые не закрыты через WRP0..WRP3
поправьте если неправ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.
Electrovoicer
13.12.2018 15:31Все это очень полезно, но, к сожалению, нынче заклонировать почти любой микроконтроллер можно за сумму порядка 2-5к$. Знаю не по наслышке, нечистоплотные китайские заказчики быстро с этим справились, обломав зубы только на цифровой подписи FPGA-чипа
PKav Автор
13.12.2018 15:37Примеров бы. А то все об этом говорят, но реально китайские и оригинальные устройства отличаются так же, как IPhone и его китайский клон.
— Единственное, что вспомнилось — OBDII адаптер ELM327, но там, на сколько я помню, сами разработчики забыли включить защиту от чтения памяти.Electrovoicer
13.12.2018 19:23Какой пример? Мой девайс содержит LPC1343, в котором юник айди и блочное шифрование, и ПЛИС Lattice, подписанная AES-128 ключем. Когда китайцы решили больше нам не платить, они быстренько склонировали микроконтроллер, обрадовались, произвели первую партию изделий и тут их ждал облом, потому что OTP-пассворд в ПЛИС ломать еще не научились… я провел небольшое исследование, и нашел несколько контор, клонирующих любые МК с флэш-памятью. Средний ценник 3к
Disco_Cat
14.12.2018 12:03В порядке интереса, можете ли поделиться?
Сомнение вызывает слово «любые»
Например NEC UPD76F0012GD мне никто и ни за какие деньги не предложил считать (и камень, и изделие на его основе стоят в приборе 20-летней давности)
ScaryA
13.12.2018 16:22Спасибо, за статью. Очень удобно, всё в одном месте, с коментариями и с описанием. Сейчас как раз пишу загрузчик для STM32 и пришлось изрядно просеивать интернет в поисках полезных материалов. Дополнение от Mirn тоже интригующе интересно (да ещё со ссылкой на реальный код).
Я не понял только, для чего и в загрузчике и в основной программе NVIC_SetVectorTable() с одинаковыми параметрами. Если, как написано «Сразу после запуска startup файл все переинициализировал», то в загрузчике этот вызов бесполезен, получается, он же прямо перед передачей управления, фактически, этому startup.s.
Планируется ли перешивать сам загрузчик? У меня такая возможность предполагается и я придумал два варианта: 1) выделение дополнительного места, куда будет записываться временная программа перешивающая загрузчик (как вариант — вместо основной, с последующей её перепрошивкой назад), 2) махинации со скриптом линковщика и расположением функции копирования в RAM через атрибуты (и функций ею используемых). Реализую второй вариант, попутно разбираясь в этих скриптах (тоже не сходу находится полезная информация), вроде получается. Может есть ещё какие-то нормальные способы? Как люди делают?PKav Автор
13.12.2018 16:39По NVIC_SetVectorTable — я делал это из расчета «перебдеть» — ведь стартовые процедуры тоже могут опираться на этот вектор и инициализировать какие-то системные прерывания типа HardFault. Но глубоко не копал на этот счет, нужно будет глянуть.
По перешивке загрузчика — это может сделать и непосредственно основная программа точно таким же способом. Но при отключении питания в процессе прошивки устройство закирпичится совсем. Я бы не стал это делать не установив хотя бы аккумулятор и не удостоверившись в том, что его заряда достаточно.
esaulenka
13.12.2018 23:03для чего и в загрузчике и в основной программе NVIC_SetVectorTable()
Потому что стандартный ST'шный SystemInit() перезаписывает NVIC->VTOR на стандартное 0x08000000. Для правильной работы надо подправить адрес или попросту выкинуть оттуда эту строку за ненадобностью — в загрузчике это делать идеологически правильнее.
Andy_Big
Делал для STM32 загрузчик с возможностью обновления прошивки в шифрованном виде через USB. Шифрование — свое, из нескольких логических и математических операций над каждым байтом на основе ключа (ключ — длиной несколько сотен байт). Конечно, это не такой стойкий ко взлому алгоритм, как AES, но вполне достаточный для того, чтобы зашифрованные им обновления можно было выкладывать в паблик для пользователей :) Шьется так же блоками по размеру страницы памяти контроллера. Для общения с компьютером написал свой пакетный протокол.
Со стороны компьютера — исполняемый файл с прошивками (разными для разных вариантов устройств), содержащимися в ресурсах. Программа обнаруживает подключенное устройство, запрашивает у него данные о нем и выбирает соответствующую прошивку.
Этой же программой передается и содержимое EPROM-памяти (которая отдельным чипом в устройстве) — настройки, калибровка и т.д., это уже идет без шифрования :)
Протокол предусматривает запрос от устройства серийного номера, уникального идентификатора микроконтроллера, версий загрузчика и прошивки, задание нового серийного номера, обновление и считывание EPROM, обновление прошивки.
Единственный минус — девайс подключается по USB как CDC (COM-порт), что требует драйверов, поэтому сейчас перевожу все это дело на HID :)
PKav Автор
Взаимодействие с USB сделано на HAL?
Andy_Big
На SPL — и CDC и HID. Правда, пока я наладил работу HID, пришлось досконально изучить все внутренности библиотечных функций, взаимодействие их между собой и с регистрами. Плюс этот пресловутый дескриптор HID, в котором без литра самогона не разобраться :)