Carregador de inicialização criptografado para STM32

Neste artigo, gostaria de escrever sobre minha experiência na criação de um gerenciador de inicialização para o STM32 com criptografia de firmware. Como sou um desenvolvedor individual, o código abaixo pode não estar em conformidade com os padrões corporativos

No processo, as seguintes tarefas foram definidas:

  • Forneça atualização de firmware para o usuário do dispositivo a partir do cartão SD.
  • Garanta o controle da integridade do firmware e exclua a gravação de firmware incorreto na memória do controlador.
  • Forneça criptografia de firmware para impedir a clonagem de dispositivos.

O código foi escrito no Keil uVision usando as bibliotecas stdperiph, fatFS e tinyAES. O microcontrolador experimental era STM32F103VET6, mas o código pode ser facilmente adaptado a outro controlador STM. O controle de integridade é fornecido pelo algoritmo CRC32, a soma de verificação está localizada nos últimos 4 bytes do arquivo de firmware.

O artigo não descreve a criação de um projeto, conectando bibliotecas, inicializando periféricos e outras etapas triviais.

Primeiro você precisa decidir o que é o gerenciador de inicialização. A arquitetura STM32 implica um endereçamento plano de memória quando a memória Flash, RAM, registros periféricos e tudo o mais estão no mesmo espaço de endereço. O gerenciador de inicialização é um programa iniciado quando o microcontrolador é iniciado, verifica se é necessário atualizar o firmware, se necessário, executá-lo e iniciar o programa principal do dispositivo. Este artigo descreve o mecanismo de atualização do cartão SD, mas você pode usar qualquer outra fonte.

A criptografia do firmware é realizada pelo algoritmo AES128 e implementada usando a biblioteca tinyAES. Ele consiste em apenas dois arquivos, um com a extensão .c e outro com a extensão .h, portanto, não deve haver problemas com a conexão.

Depois de criar o projeto, você deve decidir o tamanho do carregador e o programa principal. Por conveniência, os tamanhos devem ser selecionados em múltiplos do tamanho da página de memória do microcontrolador. Neste exemplo, o gerenciador de inicialização ocupará 64 Kb e o programa principal ocupará os restantes 448 Kb. O carregador de inicialização estará localizado no início da memória flash e o programa principal imediatamente após o carregador. Isso deve ser especificado nas configurações do projeto no Keil. O carregador de inicialização inicia com o endereço 0x80000000 (é dele que o STM32 começa a executar o código após o lançamento) e tem um tamanho de 0x10000, indicamos isso nas configurações.



O programa principal começará com 0x08010000 e terminará com 0x08080000 por conveniência, definiremos com todos os endereços:

#define MAIN_PROGRAM_START_ADDRESS 0x08010000 #define MAIN_PROGRAM_END_ADDRESS 0x08080000 

Também adicionamos chaves de criptografia e o vetor de inicialização do AES ao programa. Essas chaves são melhor geradas aleatoriamente.

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

Neste exemplo, todo o procedimento de atualização de firmware é construído como uma máquina de estado. Isso permite que o processo de atualização exiba algo na tela, redefina o Watchdog e execute outras ações. Por conveniência, definiremos com os estados básicos do autômato, para não confundir em números:

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

Depois de inicializar os periféricos, é necessário verificar a necessidade de atualizações de firmware. No primeiro estado, é feita uma tentativa de ler o cartão SD e verificar a presença de um arquivo nele.

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

Agora precisamos verificar se o firmware está correto. Aqui, primeiro vem o código de verificação da soma de verificação, executado quando o arquivo termina de ler e depois a própria leitura. Talvez você não deva escrever assim, escreva nos comentários o que pensa sobre isso. A leitura é feita em 2 KB para facilitar o trabalho com memória Flash, porque STM32F103VET6 possui um tamanho de página de memória 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; } 

Agora, se o firmware não estiver danificado, você precisará lê-lo novamente, mas desta vez grave-o na memória 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; } 

Agora, para a beleza, criaremos estados para tratamento de erros e atualizações bem-sucedidas:

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

A função de iniciar o programa principal ExecMainFW () vale a pena considerar com mais detalhes. Aqui está:

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

Imediatamente após iniciar o arquivo de inicialização, ele reinicializou tudo; portanto, o programa principal deve novamente definir o ponteiro para o vetor de interrupção dentro de seu espaço de endereço:

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

No projeto do programa principal, você precisa especificar os endereços corretos:



Aqui, de fato, todo o procedimento de atualização. O firmware é verificado quanto à correção e está criptografado, todas as tarefas estão concluídas. No caso de uma perda de energia durante o processo de atualização, é claro que o dispositivo aumentará, mas o carregador de inicialização permanecerá intocado e o procedimento de atualização poderá ser repetido. Para situações especialmente críticas, você pode bloquear as páginas nas quais o carregador está localizado via bytes Option para escrever.

No entanto, no caso de um cartão SD, você pode organizar no carregador de inicialização uma boa conveniência. Ao concluir o teste e a depuração da nova versão do firmware, você pode forçar o próprio dispositivo a criptografar e carregar o firmware finalizado no cartão SD para alguma condição especial (por exemplo, um botão ou jumper interno). Nesse caso, resta apenas remover o cartão SD do dispositivo, inseri-lo no computador e colocar o firmware na Internet para o deleite dos usuários. Faremos isso na forma de mais dois estados da 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; } 

Isso, de fato, é tudo o que eu queria contar. No final do artigo, gostaria de desejar que você, após criar um gerenciador de inicialização, não esqueça de incluir a proteção contra a leitura da memória do microcontrolador nos bytes da opção.

Referências


tinyAES
FatFS

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


All Articles