In diesem Artikel möchte ich über meine Erfahrungen beim Erstellen eines Bootloaders für STM32 mit Firmware-Verschlüsselung schreiben. Ich bin ein einzelner Entwickler, daher entspricht der folgende Code möglicherweise keinen Unternehmensstandards
Dabei wurden folgende Aufgaben gestellt:
- Stellen Sie dem Gerätebenutzer ein Firmware-Update von der SD-Karte bereit.
- Stellen Sie sicher, dass die Integrität der Firmware kontrolliert wird, und schließen Sie die Aufzeichnung falscher Firmware im Controller-Speicher aus.
- Stellen Sie eine Firmware-Verschlüsselung bereit, um das Klonen von Geräten zu verhindern.
Der Code wurde in Keil uVision mit den Bibliotheken stdperiph, fatFS und tinyAES geschrieben. Der experimentelle Mikrocontroller war STM32F103VET6, aber der Code kann leicht an einen anderen STM-Controller angepasst werden. Die Integritätskontrolle wird durch den CRC32-Algorithmus bereitgestellt. Die Prüfsumme befindet sich in den letzten 4 Bytes der Firmware-Datei.
Der Artikel beschreibt nicht die Erstellung eines Projekts, das Verbinden von Bibliotheken, das Initialisieren von Peripheriegeräten und andere triviale Schritte.
Zuerst müssen Sie entscheiden, was der Bootloader ist. Die STM32-Architektur impliziert eine flache Adressierung des Speichers, wenn sich Flash-Speicher, RAM, Peripherieregister und alles andere im selben Adressraum befinden. Der Bootloader ist ein Programm, das beim Start des Mikrocontrollers gestartet wird, prüft, ob die Firmware aktualisiert werden muss, ggf. ausgeführt und das Hauptprogramm des Geräts gestartet wird. Dieser Artikel beschreibt den Aktualisierungsmechanismus von der SD-Karte, Sie können jedoch jede andere Quelle verwenden.
Die Verschlüsselung der Firmware wird vom AES128-Algorithmus durchgeführt und mithilfe der tinyAES-Bibliothek implementiert. Es besteht nur aus zwei Dateien, eine mit der Erweiterung .c und die andere mit der Erweiterung .h, sodass es keine Probleme mit der Verbindung geben sollte.
Nach dem Erstellen des Projekts sollten Sie die Größe des Loaders und des Hauptprogramms festlegen. Der Einfachheit halber sollten Größen in Vielfachen der Größe der Speicherseite des Mikrocontrollers ausgewählt werden. In diesem Beispiel belegt der Bootloader 64 KB und das Hauptprogramm die verbleibenden 448 KB. Der Bootloader befindet sich am Anfang des Flash-Speichers und das Hauptprogramm unmittelbar nach dem Bootloader. Dies sollte in den Projekteinstellungen in Keil angegeben werden. Der Bootloader startet mit der Adresse 0x80000000 (von ihm beginnt STM32 nach dem Start mit der Ausführung von Code) und hat eine Größe von 0x10000, die wir in den Einstellungen angeben.

Das Hauptprogramm beginnt mit 0x08010000 und endet zur Vereinfachung mit 0x08080000. Wir definieren mit allen Adressen:
#define MAIN_PROGRAM_START_ADDRESS 0x08010000 #define MAIN_PROGRAM_END_ADDRESS 0x08080000
Wir fügen dem Programm auch Verschlüsselungsschlüssel und den AES-Initialisierungsvektor hinzu. Diese Schlüssel werden am besten zufällig generiert.
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 diesem Beispiel wird die gesamte Firmware-Aktualisierungsprozedur als Zustandsmaschine erstellt. Auf diese Weise kann der Aktualisierungsvorgang etwas auf dem Bildschirm anzeigen, den Watchdog zurücksetzen und andere Aktionen ausführen. Der Einfachheit halber definieren wir mit den Grundzuständen des Automaten, um nicht in Zahlen verwechselt zu werden:
#define FW_START 5 #define FW_READ 1000 #define FW_WRITE 2000 #define FW_FINISH 10000 #define FW_ERROR 100000
Nach der Initialisierung der Peripheriegeräte müssen Sie überprüfen, ob Firmware-Updates erforderlich sind. Im ersten Zustand wird versucht, die SD-Karte zu lesen und zu überprüfen, ob eine Datei darauf vorhanden ist.
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; }
Jetzt müssen wir die Firmware auf Richtigkeit überprüfen. Hier kommt zuerst der Prüfsummen-Überprüfungscode, der ausgeführt wird, wenn die Datei fertig gelesen wurde, und dann das Lesen selbst. Vielleicht solltest du nicht so schreiben, in die Kommentare schreiben, was du darüber denkst. Das Lesen erfolgt mit 2 KB, um die Arbeit mit Flash-Speicher zu vereinfachen STM32F103VET6 hat eine Speicherseitengröße von 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; }
Wenn die Firmware nicht beschädigt ist, müssen Sie sie erneut lesen, diesmal jedoch in den Flash-Speicher schreiben.
case FW_READ + 20:
Jetzt für die Schönheit werden wir Zustände für die Fehlerbehandlung und erfolgreiche Updates erstellen:
case FW_ERROR: { break; } case FW_FINISH: { ExecMainFW(); break; }
Die Funktion zum Starten des Hauptprogramms ExecMainFW () sollte genauer betrachtet werden. Da ist sie:
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(); }
Unmittelbar nach dem Start der Startdatei wurde alles neu initialisiert, sodass das Hauptprogramm den Zeiger erneut auf den Interrupt-Vektor in seinem Adressraum setzen sollte:
__disable_irq(); NVIC_SetVectorTable(NVIC_VectTab_FLASH, MAIN_PROGRAM_START_ADDRESS); __enable_irq();
Im Projekt des Hauptprogramms müssen Sie die richtigen Adressen angeben:

Hier ist in der Tat der gesamte Update-Vorgang. Die Firmware wird auf Richtigkeit überprüft und verschlüsselt, alle Aufgaben sind erledigt. Im Falle eines Stromausfalls während des Aktualisierungsvorgangs wird das Gerät natürlich blockiert, aber der Bootloader bleibt intakt und der Aktualisierungsvorgang kann wiederholt werden. In besonders kritischen Situationen können Sie die Seiten, auf denen sich der Loader befindet, über zu schreibende Optionsbytes sperren.
Im Falle einer SD-Karte können Sie jedoch im Bootloader eine nette Annehmlichkeit für sich selbst arrangieren. Wenn das Testen und Debuggen der neuen Firmware-Version abgeschlossen ist, können Sie das Gerät selbst zwingen, die fertige Firmware für eine bestimmte Bedingung (z. B. eine Schaltfläche oder einen Jumper im Inneren) zu verschlüsseln und auf die SD-Karte hochzuladen. In diesem Fall muss nur noch die SD-Karte aus dem Gerät entfernt, in den Computer eingelegt und die Firmware zur Freude der Benutzer ins Internet gestellt werden. Wir werden dies in Form von zwei weiteren Zuständen der endlichen Zustandsmaschine tun:
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; }
Das ist eigentlich alles, was ich erzählen wollte. Am Ende des Artikels möchte ich Sie bitten, nach dem Erstellen eines solchen Bootloaders nicht zu vergessen, den Schutz gegen das Lesen des Mikrocontrollerspeichers in Optionsbytes aufzunehmen.
Referenzen
tinyAESFatFS