Gestor de arranque cifrado para STM32

En este artículo me gustaría escribir sobre mi experiencia en la creación de un gestor de arranque para STM32 con cifrado de firmware. Soy un desarrollador individual, por lo que el siguiente código puede no cumplir con ningún estándar corporativo

En el proceso, se establecieron las siguientes tareas:

  • Proporcione actualización de firmware para el usuario del dispositivo desde la tarjeta SD.
  • Asegure el control de la integridad del firmware y excluya la grabación de firmware incorrecto en la memoria del controlador.
  • Proporcione cifrado de firmware para evitar la clonación de dispositivos.

El código fue escrito en Keil uVision usando las bibliotecas stdperiph, fatFS y tinyAES. El microcontrolador experimental era STM32F103VET6, pero el código se puede adaptar fácilmente a otro controlador STM. El algoritmo CRC32 proporciona el control de integridad, la suma de comprobación se encuentra en los últimos 4 bytes del archivo de firmware.

El artículo no describe la creación de un proyecto, conectando bibliotecas, inicializando periféricos y otros pasos triviales.

Primero debes decidir qué es el gestor de arranque. La arquitectura STM32 implica un direccionamiento plano de la memoria cuando la memoria Flash, la RAM, los registros periféricos y todo lo demás están en el mismo espacio de direcciones. El gestor de arranque es un programa que se inicia cuando se inicia el microcontrolador, comprueba si es necesario actualizar el firmware, si es necesario, ejecutarlo e inicia el programa principal del dispositivo. Este artículo describirá el mecanismo de actualización de la tarjeta SD, pero puede usar cualquier otra fuente.

El cifrado del firmware se realiza mediante el algoritmo AES128 y se implementa utilizando la biblioteca tinyAES. Consta de solo dos archivos, uno con la extensión .c, el otro con la extensión .h, por lo que no debería haber problemas con su conexión.

Después de crear el proyecto, debe decidir el tamaño del cargador y el programa principal. Por conveniencia, los tamaños deben seleccionarse en múltiplos del tamaño de la página de memoria del microcontrolador. En este ejemplo, el gestor de arranque ocupará 64 Kb, y el programa principal ocupará los 448 Kb restantes. El gestor de arranque se ubicará al comienzo de la memoria flash y el programa principal inmediatamente después del gestor de arranque. Esto debe especificarse en la configuración del proyecto en Keil. El gestor de arranque comienza con la dirección 0x80000000 (es de él que STM32 comienza a ejecutar el código después del lanzamiento) y tiene un tamaño de 0x10000, lo indicamos en la configuración.



El programa principal comenzará con 0x08010000 y finalizará con 0x08080000 para mayor comodidad, definiremos con todas las direcciones:

#define MAIN_PROGRAM_START_ADDRESS 0x08010000 #define MAIN_PROGRAM_END_ADDRESS 0x08080000 

También agregamos claves de cifrado y el vector de inicialización AES al programa. Estas claves se generan mejor al azar.

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

En este ejemplo, todo el procedimiento de actualización del firmware se construye como una máquina de estado. Esto permite que el proceso de actualización muestre algo en la pantalla, restablezca el Watchdog y realice cualquier otra acción. Por conveniencia, definiremos con los estados básicos del autómata, para no confundirnos en números:

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

Después de inicializar los periféricos, debe verificar la necesidad de actualizaciones de firmware. En el primer estado, se intenta leer la tarjeta SD y verificar la presencia de un archivo en ella.

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

Ahora necesitamos verificar que el firmware sea correcto. Aquí, primero viene el código de verificación de suma de verificación, ejecutado cuando el archivo termina de leer, y luego la lectura misma. Quizás no deberías escribir así, escribe en los comentarios lo que piensas al respecto. La lectura se realiza a 2 KB para la conveniencia de trabajar con memoria Flash, porque STM32F103VET6 tiene un tamaño de página de memoria de 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; } 

Ahora, si el firmware no está dañado, debe volver a leerlo, pero esta vez escríbalo en la memoria 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; } 

Ahora por belleza, crearemos estados para el manejo de errores y actualizaciones exitosas:

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

Vale la pena considerar con más detalle la función de iniciar el programa principal ExecMainFW (). Aquí esta:

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

Inmediatamente después de iniciar el archivo de inicio, reinicializó todo, por lo que el programa principal debería establecer nuevamente el puntero en el vector de interrupción dentro de su espacio de direcciones:

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

En el proyecto del programa principal, debe especificar las direcciones correctas:



Aquí, de hecho, todo el procedimiento de actualización. Se verifica que el firmware sea correcto y está encriptado, todas las tareas se completan. En el caso de una pérdida de energía durante el proceso de actualización, el dispositivo, por supuesto, se bloqueará, pero el gestor de arranque permanecerá intacto y el procedimiento de actualización puede repetirse. Para situaciones especialmente críticas, puede bloquear las páginas en las que se encuentra el cargador a través de bytes de Opción para escribir.

Sin embargo, en el caso de una tarjeta SD, puede organizarlo usted mismo en el gestor de arranque una buena comodidad. Cuando se completa la prueba y la depuración de la nueva versión de firmware, puede forzar al dispositivo a encriptar y cargar el firmware terminado en la tarjeta SD por alguna condición especial (por ejemplo, un botón o un puente dentro). En este caso, solo queda quitar la tarjeta SD del dispositivo, insertarla en la computadora y poner el firmware en Internet para el deleite de los usuarios. Hacemos esto en forma de dos estados más de la máquina de estados finitos:

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

Eso, de hecho, es todo lo que quería decir. Al final del artículo, me gustaría desearle que, después de crear dicho gestor de arranque, no olvide incluir la protección contra la lectura de la memoria del microcontrolador en bytes de Opción.

Referencias


tinyAES
FatFS

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


All Articles