📜 ⬆️ ⬇️

Encryption downloader for STM32

In this article I would like to write about my experience in creating a bootloader for STM32 with firmware encryption. I am an individual developer, so the code below may not meet any corporate standards.

In the process, the following tasks were set:


The code was written in Keil uVision using the stdperiph, fatFS and tinyAES libraries. The experimental microcontroller was STM32F103VET6, but the code can be easily adapted to another STM controller. Integrity control is provided by the CRC32 algorithm, the checksum is located in the last 4 bytes of the firmware file.
')
The article does not describe the creation of the project, the connection of libraries, the initialization of the periphery and other trivial steps.

First you need to decide what is the bootloader. The STM32 architecture implies flat addressing of memory when Flash memory, RAM, peripheral registers, and everything else are in the same address space. The loader is a program that starts to run when the microcontroller starts, checks whether it is necessary to update the firmware, if necessary, executes it, and starts the main program of the device. This article will describe the update mechanism from the SD card, but you can use any other source.

The firmware is encrypted using the AES128 algorithm and implemented using the tinyAES library. It consists of only two files, one with the extension .c, the other with the extension .h, therefore problems with its connection should not arise.

After creating the project, you should decide on the size of the loader and the main program. For convenience, sizes should be selected multiple of the size of the microcontroller memory page. In this example, the bootloader will occupy 64 Kb, and the main program will occupy the remaining 448 Kb. The loader will be located at the beginning of the flash memory, and the main program immediately after the loader. This should be indicated in the project settings in Keil. Our bootloader starts at address 0x80000000 (it is from it that the STM32 starts executing the code after launch) and has a size of 0x10000, we indicate this in the settings.



The main program will start at 0x08010000 and end at 0x08080000 for convenience, we will define with all addresses:

#define MAIN_PROGRAM_START_ADDRESS 0x08010000 #define MAIN_PROGRAM_END_ADDRESS 0x08080000 

We will also add encryption keys and an AES initialization vector to the program. These keys are best generated randomly.

 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}; 

In this example, the entire procedure for updating the firmware is built as a finite state machine. This allows the update process to display something on the screen, reset Watchdog and perform any other actions. For convenience, let's define with the basic states of the automaton so as not to be confused by the numbers:

 #define FW_START 5 #define FW_READ 1000 #define FW_WRITE 2000 #define FW_FINISH 10000 #define FW_ERROR 100000 

After initialization of the peripherals, you need to check the need to update the firmware. In the first state, an attempt is made to read the SD card and check for the presence of a file on it.

 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; } 

Now we need to check the firmware for correctness. Here, first comes the checksum verification code that runs when the file is finished reading, and then the reading itself. Perhaps you should not write like that, write in the comments what you think about it. Reading is done at 2 KB for the convenience of working with flash-memory, because the STM32F103VET6 has a memory page size of 2 Kb.

 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; } 

Now, if the firmware is not damaged, then you need to read it again, but this time it is already recorded in Flash - memory.

 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; } 

Now for beauty, we will create states for error handling and successful updating:

 case FW_ERROR: { /*  -     */ break; } case FW_FINISH: { ExecMainFW(); /*    */ /*      */ break; } 

The startup function of the main ExecMainFW () program is worth considering in more detail. Here she is:

 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(); } 

Immediately after launching the startup file, everything was re-initialized, so the main program should again set the pointer to the interrupt vector within its address space:

 __disable_irq(); NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS); __enable_irq(); 

In the draft of the main program you need to specify the correct addresses:



This is the whole update procedure. The firmware is checked for correctness and encrypted, all assigned tasks are completed. In the event of a power loss during the update process, the device will, of course, become bogged down, but the bootloader will remain intact and the upgrade procedure can be repeated. For critical situations, you can block the page in which the loader is located via Option bytes.

However, in the case of an SD card, you can organize one pleasant convenience for yourself in the bootloader. When testing and debugging a new firmware version is completed, you can force the device itself to encrypt and unload the finished firmware onto an SD card according to some special condition (for example, a button or jumper inside). In this case, it remains only to remove the SD card from the device, insert it into the computer and put the firmware on the Internet to the joy of the users. Let's do this in the form of two more states of the finite state machine:

 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; } 

That's all that I wanted to tell. At the end of the article I would like to wish you, after creating such a bootloader, not to forget to include the protection from reading the microcontrol memory in Option bytes.

Links


tinyAES
FatFS

Source: https://habr.com/ru/post/432966/


All Articles