STM32: Шифруем прошивку.

Не сильно хорошая, но все же защита от бездумного копирования устройства. А может быть и еще что-то. В общем, идея такая: зашифровать некоторые критические функции, без которых устройство работать не будет, хитрожопые алгоритмы или математика какая-нибудь. Причем, желательно не потерять удобную сборку и отладку, и использовать желательно без всяких указателей. Я использую arm-none-eabi-gcc в качестве тулчейна и CMake как систему сборки. Поэтому все нижесказанное относится именно к этой связки и для других компиляторов-сборщиков придется немного перепилить.

Итак постановка задачи: Нужно удобным образом пометить какие-то функции, которые будут зашифрованы при компиляции и расшифрованы на лету при работе процессора. Решение простое, поступим так же как компилятор поступает с инициализированными данными: вынесем нужные функции в отдельную секцию, у которой разделим LMA (Load Memory Address – адрес, по которому линковщик положит данные в скомпилированной прошивки и VMA (Virtual Memory Address – адрес, который будет использоваться при обращении к символам из этой секции). Делается это через скрипт линковки, добавлением секции по образу и подобию .data:

1
2
3
4
5
6
7
__xsectext = __etext + (__data_end__ - __data_start__);
.xsec : AT ( __xsectext )
{
    PROVIDE(__xsec_start__ = .);
    KEEP(*(.xsec));
    PROVIDE(__xsec_end__ = .);
} > RAM

Этот кусок в разделе SECTIONS скажет линковщику создать символ __xsectext с LMA адресом (и записать в него правильный адрес) и создать секцию .xsec с нужным VMA адресом, экспортировать адрес начала и конца секции и положить эту секцию в регион RAM (это у меня так оперативка в скрипте называется, что характерно). Теперь все функции с атрибутом section(«.xsec»), линковщик положит в эту секцию.

1
uint32_t secure_foo()__attribute__ ((section (".xsec")));

Осталось при запуске (или в любой другой удобный момент) скопировать нужное количество байт из LMA адрес в VMA адрес, как-то обработав при этом. Пишем функцию:

1
2
3
4
5
6
7
void prepare_xsec() {
    extern uint8_t __xsec_start__;
    extern uint8_t __xsec_end__;
    extern uint8_t __xsectext;
 
    memcpy(&__xsec_start__, &__xsectext, &__xsec_end__ - &__xsec_start__);
 }

Это пока просто копирование, без всяких расшифровок. Но и секция у нас еще не зашифрована. Добавляем вызов prepare_xsec() перед первым использованием функций содержащихся в ней – и проверяем как работает. А если все сделано правильно – то работает.
Теперь приступим к шифрации. Я просто переверну младший полубайт (out = in ^ 0x0F), этого более, чем достаточно для проверки – а дальше уже полный полет фантазии, серийник использовать, да хоть AES ключи с удаленного сервера или еще как-то.
Расшифровщик пишется просто, добавляем после memcpy:

1
2
uint8_t* ptr = &__xsec_start__;
while(rv < &__xsec_end__) *(ptr ++) ^= 0x0F;

и готово.
Теперь будем шифровать. Для этого нужно:

  1. Выковырять нужную секцию из elf
  2. Зашифровать её
  3. Запихнуть обратно.

Пункты 1 и 3 умеет (как ни странно) objcopy:
Вытаскиваем:

1
arm-none-eabi-objcopy -j .xsec -Obinary firmware.elf xsec.bin

Шифруем:

1
xotor -i xsec.bin  -o xsec_xor.bin -x 0x0F

и запихиваем обратно (ну почти обратно, лучше сделаем еще один elf)

1
arm-none-eabi-objcopy --update-section .xsec=xsec_xor.bin firmware.elf crypted.elf

после чего с crypted.elf можно делать что угодно – выковыривать hex или bin, отлаживаться итд. Ну и осталось все это автоматизировать:
к CMakeLists.txt моего шаблона добавляем

1
2
3
ADD_CUSTOM_COMMAND(TARGET ${CMAKE_PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} ARGS -j .xsec -Obinary  ${CMAKE_PROJECT_NAME}.elf xsec.bin )
ADD_CUSTOM_COMMAND(TARGET ${CMAKE_PROJECT_NAME}.elf POST_BUILD COMMAND xotor ARGS -i xsec.bin  -o xsec_xor.bin -x 0x0F)
ADD_CUSTOM_COMMAND(TARGET ${CMAKE_PROJECT_NAME}.elf POST_BUILD COMMAND ${CMAKE_OBJCOPY} ARGS --update-section .xsec=xsec_xor.bin  ${CMAKE_PROJECT_NAME}.elf crypted.elf )

Ну и для запуска (в Clion, например) указываем нужный executable файл. Готово.

Что можно еще? Добавить проверку, какая версия запускается шифрованная или нужно просто скопировать:
Добавим маркер в начало секции:

1
2
3
4
5
6
7
.xsec : AT ( __xsectext )
{
    PROVIDE(__xsec_start__ = .);
    KEEP(*(.xsec_marker));
    KEEP(*(.xsec));
    PROVIDE(__xsec_end__ = .);
} > RAM

Сделаем этот самый маркер и напишем функцию проверки:

1
2
3
4
5
#define SEC_MARKER (0xBEED)
volatile static uint32_t __attribute__ ((section (".xsec_marker"))) xsec_marker = SEC_MARKER;
uint8_t check_crypt() {
    return (uint8_t)(xsec_marker != SEC_MARKER ? 1 : 0);
}

ну и расшифровывать теперь будем только по необходимости

1
if(check_crypt()) while(ptr < &__xsec_end__) *(ptr ++) ^= 0x0F;

вроде бы все. Да, тузла xotor – просто ксорит входной файл с заданным значением и записывает в выходной. Сходу не вспомнил, чем можно воспользоваться, набросал самостоятельно.