Открыть список
Как стать автором
Обновить

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

Делал для STM32 загрузчик с возможностью обновления прошивки в шифрованном виде через USB. Шифрование — свое, из нескольких логических и математических операций над каждым байтом на основе ключа (ключ — длиной несколько сотен байт). Конечно, это не такой стойкий ко взлому алгоритм, как AES, но вполне достаточный для того, чтобы зашифрованные им обновления можно было выкладывать в паблик для пользователей :) Шьется так же блоками по размеру страницы памяти контроллера. Для общения с компьютером написал свой пакетный протокол.
Со стороны компьютера — исполняемый файл с прошивками (разными для разных вариантов устройств), содержащимися в ресурсах. Программа обнаруживает подключенное устройство, запрашивает у него данные о нем и выбирает соответствующую прошивку.
Этой же программой передается и содержимое EPROM-памяти (которая отдельным чипом в устройстве) — настройки, калибровка и т.д., это уже идет без шифрования :)
Протокол предусматривает запрос от устройства серийного номера, уникального идентификатора микроконтроллера, версий загрузчика и прошивки, задание нового серийного номера, обновление и считывание EPROM, обновление прошивки.
Единственный минус — девайс подключается по USB как CDC (COM-порт), что требует драйверов, поэтому сейчас перевожу все это дело на HID :)
Взаимодействие с USB сделано на HAL?
На SPL — и CDC и HID. Правда, пока я наладил работу HID, пришлось досконально изучить все внутренности библиотечных функций, взаимодействие их между собой и с регистрами. Плюс этот пресловутый дескриптор HID, в котором без литра самогона не разобраться :)
Заявлен stdperiph, а в коде используется cmsis — в чем соль? 64 кБ загрузчик это тоже сильно))
CRC_ResetDR() есть за то. Да и весь остальной проект на stdperiph.
64 Кб — для примера и для ровного счета. Хотя, почему нет? Может, там в процессе обновления вывод на экран какой хитрый нужен, с картинками и шрифтами.
CRC_ResetDR() есть за то. Да и весь остальной проект на stdperiph.
И вас не смущает, что смешивать 2 стиля/библиотеки программирования это плохо?
Скорректировал текст.
Вообще, вопрос не однозначный — ведь stdperiph базируется на CMSIS. Это не HAL и stdperiph смешивать, как некоторые делают.
У меня загрузчик (с AES'ом, USB HID'ом и gzip'ом) влезает в «ровные» 16к (и ещё 4к остаётся).
А вывод на экран лучше всё ж таки не делать. Захочется завтра экран поменять, или косяк какой вылезет — а загрузчик уже есть, и безболезненной процедуры апгрейда загрузчика нету (которую может сделать пользователь со 100% гарантией получения живого устройства). Минимум функционала (получил прошивку — залил куда следует, и хватит).
Обеспечить шифрование прошивки для исключения клонирования устройства.

Я правильно понял, что под этим понимается шифрование файла с прошивкой на SD-карте, а не шифрование прошивки в устройстве?
Да, всё верно.
Т.е. при желании пользователь спокойно слить прошивку (и загрузчик заодно) с устройства через отладочный разъем и склонировать его?
Именно об этом я рассказал в последнем предложении данной статьи.
Да, прошу прощения, пропустил.
Другое дело, что такая защита действительно не является гарантией, как справедливо пишут в комментарии ниже.

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

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

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

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

Проблема 2:

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

Проблема 3:

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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

Мой шифрованный (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.
На сколько я помню (но ссылок предоставить не смогу), на L-сериях структура Flash отличается и оптимизирована для меньшего энергопотребления в ущерб производительности. К тому же L1 могут только 32 МГц, а F4 до 180 МГц. Плюс можно разместить загрузчик в RAM и стереть всю Flash единовременно, а не постранично, что тоже даст ускорение.
Цифры легко ищутся в соотв. даташитах по слову «programming». STM32F405 — запись 1/2/4 байт — 16 мкс, STM32L151 — 3280 мкс.

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

Вместе с самим загрузчиком и битами защиты? Так себе решение…
В L1 никак, у меня тоже медленно пишется/стирается. Примерно 1 кб/сек.
в 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: я объявил «голую» функцию, т.е. без обработки стека и тд и в неё уже перешёл в основную программу с установкой стека.
Хорошее дополнение, спасибо. Протестирую на Keil и дополню статью.
Спасибо за подсказку. У меня как раз бывает (но очень редко и в основном при нестабильной, скачкообразной подаче питания) хард фаулт. Может быть как раз в этом причина, попробую применить Ваш способ перехода на основную прошивку :)
Советовались ли вы при создании вашей схемы защиты со старшими товарищами?
Видится такая возможность атаки: записать вместо обновляемого приложения код, выдающий содержимое памяти наружу любым доступным методом, например через тот же USB. В коде загрузчика ключ шифрования хранится в явном виде, что должно позволить расшифровывать файлы обновлений прошивок.
Не советовался — я индивидуальный разработчик, мне не с кем.

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

К тому же я указывал, что эта статья не является руководством к действию для получения 100% защиты, а лишь поясняет суть. Можно добавлять различные проверки и запутывающее поведение, что серьезно усложнит взлом.
Со схемой защиты ясно. По поводу записи, имелась в виду конечно же запись по jtag/swd, в надежде что у вас не стоит защита от записи хотя бы на некоторые страницы.
Там есть общая защита от чтения и записи, ставится через Option bytes, я написал об этом в последнем предложении. Её, кстати, можно включать прямо из прошивки, чтоб не забыть.
Почитайте о защите STM'ок. Там всё просто: защита от чтения отладчиком/штатным бутлоадером (а её, очевидно, надо включать) включает в себя и отключение команд записи. Т.е. все действия должны начинаться с команды «стереть весь чип».
А записать штатным образом (через этот загрузчик) какой-то осмысленный код представляется затруднительным…
Насколько я понимаю ДШ, история несколько иная
При RDP = 0хА5 (Level 1) можно записывать в те страницы, которые не закрыты через WRP0..WRP3
поправьте если неправ
Для определённости — разговор про 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.

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

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

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

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

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

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

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

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