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]; uint32_t idx; char tbuf[64]; FATFS FS; FIL F; case FW_READ: { if(f_mount(&FS, "" , 0) == FR_OK) { if(f_open(&F, "FIRMWARE.BIN", FA_READ | FA_OPEN_EXISTING) == FR_OK) { f_lseek(&F, 0); CRC_ResetDR(); lcd_putstr(" ", 1, 0); idx = MAIN_PROGRAM_START_ADDRESS; fw_step = FW_READ + 10; } else {fw_step = FW_FINISH;} } else {fw_step = FW_FINISH;} 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); CRC_CalcCRC(t); if(CRC_GetCRC() == 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); 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_CalcCRC(aes_buf[t]); } idx+=sizeof(fw_buf); 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:
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() { uint32_t jumpAddress = *(__IO uint32_t*) (MAIN_PROGRAM_START_ADDRESS + 4); pFunction Jump_To_Application = (pFunction) jumpAddress; RCC->APB1RSTR = 0xFFFFFFFF; RCC->APB1RSTR = 0x0; RCC->APB2RSTR = 0xFFFFFFFF; RCC->APB2RSTR = 0x0; RCC->APB1ENR = 0x0; RCC->APB2ENR = 0x0; RCC->AHBENR = 0x0; 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) { if(f_open(&F, "FIRMWARE.BIN", FA_WRITE | FA_CREATE_ALWAYS) == FR_OK) { CRC_ResetDR(); 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; } memcpy(&fw_buf, (uint32_t *)idx, sizeof(fw_buf)); for(t=0;t<NELEMS(fw_buf);t++) { 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
tinyAESFatFS