Chargeur de démarrage chiffré pour STM32

Dans cet article, je voudrais parler de mon expérience dans la création d'un chargeur de démarrage pour STM32 avec cryptage du firmware. Je suis un développeur individuel, donc le code ci-dessous peut ne pas être conforme aux normes de l'entreprise

Au cours du processus, les tâches suivantes ont été définies:

  • Fournissez une mise à jour du firmware pour l'utilisateur de l'appareil à partir de la carte SD.
  • Assurez le contrôle de l'intégrité du firmware et excluez l'enregistrement d'un firmware incorrect dans la mémoire du contrôleur.
  • Fournissez le cryptage du firmware pour empêcher le clonage de l'appareil.

Le code a été écrit dans Keil uVision en utilisant les bibliothèques stdperiph, fatFS et tinyAES. Le microcontrôleur expérimental était STM32F103VET6, mais le code peut être facilement adapté à un autre contrôleur STM. Le contrôle d'intégrité est assuré par l'algorithme CRC32, la somme de contrôle se trouve dans les 4 derniers octets du fichier du firmware.

L'article ne décrit pas la création d'un projet, la connexion de bibliothèques, l'initialisation de périphériques et d'autres étapes triviales.

Vous devez d'abord décider ce qu'est le chargeur de démarrage. L'architecture STM32 implique un adressage plat de la mémoire quand dans un espace d'adressage il y a la mémoire Flash, la RAM, les registres périphériques et tout le reste. Le bootloader est un programme qui démarre au démarrage du microcontrôleur, vérifie s'il est nécessaire de mettre à jour le firmware, si nécessaire, de l'exécuter et lance le programme principal de l'appareil. Cet article décrira le mécanisme de mise à jour de la carte SD, mais vous pouvez utiliser n'importe quelle autre source.

Le cryptage du firmware est effectué par l'algorithme AES128 et implémenté à l'aide de la bibliothèque tinyAES. Il ne comprend que deux fichiers, l'un avec l'extension .c, l'autre avec l'extension .h, il ne devrait donc pas y avoir de problèmes de connexion.

Après avoir créé le projet, vous devez décider de la taille du chargeur et du programme principal. Pour plus de commodité, les tailles doivent être sélectionnées en multiples de la taille de la page mémoire du microcontrôleur. Dans cet exemple, le chargeur de démarrage occupera 64 Ko et le programme principal occupera les 448 Ko restants. Le chargeur de démarrage sera situé au début de la mémoire flash et le programme principal immédiatement après le chargeur de démarrage. Cela doit être spécifié dans les paramètres du projet dans Keil. Le bootloader commence par l'adresse 0x80000000 (c'est de lui que STM32 commence à exécuter le code après le lancement) et a une taille de 0x10000, nous l'indiquons dans les paramètres.



Le programme principal commencera par 0x08010000 et se terminera par 0x08080000 pour plus de commodité, nous définirons avec toutes les adresses:

#define MAIN_PROGRAM_START_ADDRESS 0x08010000 #define MAIN_PROGRAM_END_ADDRESS 0x08080000 

Nous ajoutons également des clés de chiffrement et le vecteur d'initialisation AES au programme. Il est préférable de générer ces clés de manière aléatoire.

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

Dans cet exemple, toute la procédure de mise à jour du micrologiciel est conçue comme une machine à états. Cela permet au processus de mise à jour d'afficher quelque chose à l'écran, de réinitialiser le chien de garde et d'effectuer toute autre action. Pour plus de commodité, nous définirons avec les états de base de l'automate, afin de ne pas se confondre en chiffres:

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

Après avoir initialisé les périphériques, vous devez vérifier la nécessité des mises à jour du firmware. Dans le premier état, une tentative est faite pour lire la carte SD et vérifier la présence d'un fichier dessus.

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

Maintenant, nous devons vérifier l'exactitude du firmware. Ici, vient d'abord le code de vérification de la somme de contrôle, exécuté lorsque la lecture du fichier est terminée, puis la lecture elle-même. Peut-être que vous ne devriez pas écrire comme ça, écrire dans les commentaires ce que vous en pensez. La lecture se fait à 2 Ko pour la commodité de travailler avec la mémoire Flash, car STM32F103VET6 a une taille de page de mémoire de 2 Ko.

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

Maintenant, si le firmware n'est pas endommagé, vous devez le relire, mais cette fois-ci, écrivez-le dans la mémoire 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; } 

Maintenant, pour la beauté, nous allons créer des états pour la gestion des erreurs et les mises à jour réussies:

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

La fonction de lancement du programme principal ExecMainFW () mérite d'être examinée plus en détail. Le voici:

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

Immédiatement après le démarrage du fichier de démarrage, il a tout réinitialisé, de sorte que le programme principal devrait à nouveau définir le pointeur sur le vecteur d'interruption dans son espace d'adressage:

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

Dans le projet du programme principal, vous devez spécifier les bonnes adresses:



Voici, en fait, toute la procédure de mise à jour. Le micrologiciel est vérifié pour l'exactitude et est crypté, toutes les tâches sont terminées. En cas de coupure de courant pendant le processus de mise à jour, le périphérique se brique bien sûr, mais le chargeur de démarrage reste intact et la procédure de mise à jour peut être répétée. Pour les situations particulièrement critiques, vous pouvez verrouiller les pages dans lesquelles se trouve le chargeur via les octets d’option à écrire.

Cependant, dans le cas d'une carte SD, vous pouvez organiser vous-même dans le chargeur de démarrage une belle commodité. Lorsque le test et le débogage de la nouvelle version du micrologiciel sont terminés, vous pouvez forcer l'appareil lui-même à crypter et télécharger le micrologiciel terminé sur la carte SD pour une condition spéciale (par exemple, un bouton ou un cavalier à l'intérieur). Dans ce cas, il ne reste plus qu'à retirer la carte SD de l'appareil, à l'insérer dans l'ordinateur et à mettre le firmware sur Internet pour le plus grand plaisir des utilisateurs. Nous le ferons sous la forme de deux autres états de la machine à états finis:

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

En fait, c'est tout ce que je voulais dire. A la fin de l'article, je voudrais vous souhaiter après avoir créé un tel bootloader de ne pas oublier d'inclure la protection contre la lecture de la mémoire du microcontrôleur dans les octets d'options.

Les références


tinyAES
FatFS

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


All Articles