Logo Zéphyrnet

Comment utiliser la rétro-ingénierie dynamique pour les appareils embarqués | TechTarget

Date :

La prolifération de l'IoT s'est accompagnée d'une prolifération de failles de sécurité. Sans contrôle, des attaquants malveillants peuvent utiliser ces faiblesses pour infiltrer les systèmes des organisations.

Un coupage régulier tests de pénétration, reconnues depuis longtemps comme une bonne pratique de sécurité, aident les équipes de sécurité à identifier et à atténuer les vulnérabilités et les faiblesses des appareils embarqués. Cependant, de nombreuses organisations limitent les tests d'intrusion à enquêter sur les réseaux et infrastructure — les appareils IoT sont souvent négligés.

Pour mettre les équipes de sécurité au courant des tests d'intrusion sur les appareils intégrés, Jean-Georges Valle, vice-président senior de Kroll, un cabinet de conseil en cyber-risque et services financiers, a écrit Pratique Hardware Pentesting: Apprenez les techniques d'attaque et de défense pour les systèmes embarqués dans l'IoT et d'autres appareils.

Dans l'extrait suivant du chapitre 10, Valle détaille comment les testeurs de stylet peuvent utiliser l'ingénierie inverse dynamique pour voir comment le code se comporte pendant l'exécution sur les appareils intégrés. Valle fournit un exemple de rétro-ingénierie dynamique pour montrer aux testeurs de stylet les défis qui peuvent survenir lors de l'observation du comportement du code.

Note de la rédaction: L'extrait suivant provient d'une version à accès anticipé de Pentesting matériel pratique, deuxième édition et est sujet à changement.

Utilisation de la rétro-ingénierie dynamique — un exemple

J'ai préparé une variante de l'exemple précédent qui va nous poser quelques défis. Je vais vous montrer comment surmonter ces défis à la fois statiquement et dynamiquement afin que vous puissiez comparer la quantité d'efforts nécessaires dans les deux cas.

La règle de base lors de la comparaison des approches dynamiques et statiques est que 99 % du temps, les approches dynamiques sont simplement plus faciles et doivent être prioritaires si possible (n'oubliez pas que vous ne pourrez peut-être pas accéder à JTAG/SWD ou à d'autres protocoles de débogage sur puce).

Dans cette section, nous apprendrons également comment casser où nous voulons, inspecter la mémoire avec GDB, et toutes ces bonnes choses !

Le programme cible se trouve ici dans le dossier que vous avez cloné, dans le dossier ch12.

Tout d'abord, commençons par le charger dans Ghidra et inspectons-le superficiellement. Faites attention à définir la bonne architecture et l'adresse de base dans la fenêtre de chargement de Ghidra (reportez-vous au chapitre précédent si vous ne vous souvenez plus comment faire cela ou la valeur de l'adresse de base).

Première inspection Ghidra

À première vue, la fonction principale ressemble beaucoup à la fonction principale du chapitre précédent. Nous pouvons trouver la référence à la fonction principale en recherchant une chaîne PASSWORD comme dans le chapitre précédent et en analysant sa structure.

Je vous laisse travailler sur les compétences que vous avez acquises dans le chapitre précédent pour retrouver les différentes fonctions. Dans cet exécutable, vous retrouverez ceci :

  • Un grand tandis que (vrai) boucle qui agit comme la boucle d'événement principale et fait clignoter la LED du bluepill tout en agissant sur la saisie d'un mot de passe
  • Une fonction pour initialiser l'horloge
  • Une fonction pour initialiser les GPIO
  • Une fonction pour initialiser l'UART
  • Une valeur dépendant de l'identifiant unique de la puce est recalculée à peu près de la même manière (calculez cette valeur pour votre puce et notez cette valeur)
  • Une fonction valide le mot de passe (juste avant un gros if qui déclenche soit l'impression de VOUS GAGNEZ or NON)
  • Une fonction déchiffre la chaîne gagnante si la fonction de validation renvoie une (uint16_t ) 0 valeur.

La similitude de la structure est intentionnelle car c'est votre première fois. Si je devais répéter exactement les mêmes étapes que dans le chapitre précédent, cela ne vous donnerait rien de nouveau à apprendre, n'est-ce pas ?

Passons maintenant en revue plusieurs méthodes pour contourner cette validation de mot de passe grâce à une interaction dynamique avec le système. Nous irons du plus complexe au plus simple afin de vous garder concentré et d'acquérir un savoir-faire (si vous êtes comme moi, s'il existe un moyen facile de contourner quelque chose, pourquoi aller à la dure ?).

Inverser le mot de passe attendu

La première chose que nous allons faire est d'essayer de voir comment le mot de passe est validé pour comprendre comment générer un mot de passe qui passe les tests.

Jetons un coup d'œil au code C équivalent de la fonction de validation qui est produit par Ghidra :

Screenshot of Ghidra output of decompiled code
Figure 12.2 — La fonction de validation décompilée ne fait pas vraiment ce que vous pensez !

Humm… cela ne fait rien directement avec les paramètres. Il s'agit de copier le contenu d'un Assistance (71) long tableau statique d'octets dans la RAM (et PAS) puis l'appelle en tant que fonction.

Cela est étrange.

Ou est-ce?

Il s'agit d'une technique très courante pour camoufler le code (bien sûr, une version très simple de celle-ci). Si une version claire de l'opcode n'est pas présente dans le fichier .bin (et donc pas dans le flash du MCU), un outil de rétro-ingénierie comme Ghidra ne peut pas détecter qu'il s'agit de code ! Ici, nous avons deux approches possibles :

  • Soit nous extrayons manuellement le contenu du tampon du fichier .bin, le déchiffrons (ici, le chiffrement ne se fait PAS octet par octet, c'est volontairement trivial), et le faisons décompiler par Ghidra.
  • Ou, puisque nous avons un accès JTAG à la puce, nous pouvons simplement mettre un point d'arrêt sur la bonne adresse en mémoire et laisser le MCU faire le travail acharné pour nous.

Je vous laisse la première solution à mettre en œuvre comme exercice. Cela devrait prendre plus ou moins 10 lignes de code Python ou C pour une tâche aussi simple ! Vous voulez être un hacker ? Hack loin!

Moi? Je suis un gars paresseux. Si un ordinateur peut fonctionner pour moi, eh bien… qu'il en soit ainsi ! Je vais opter pour la deuxième solution.

Tout d'abord, lançons une session d'écran dans un terminal afin que nous puissions entrer des mots de passe et voir comment il réagit :

écran /dev/ttyUSB0 115200

Lançons OpenOCD et GDB dans un second terminal, comme nous l'avons fait au début du chapitre, et fouillons :

openocd -f ./ftdi2232h.cfg.tcl -f ./clone_CSK.cfg & gdb-multiarch -x ./gdbinit
lancement de #openocd
[...]
cible arrêtée en raison d'une demande de débogage, mode actuel : Thread xPSR : 0x01000000 pc : 0x080013b8 msp : 0x20005000
[...]

Et… et putain ! Cela ne me redonne pas le contrôle ! Pas de problème si cela vous arrive — un peu Ctrl + C vous redonnera immédiatement le contrôle :

^C
Programmer le signal reçu SIGINT, Interruption.
0x080003aa dans ?? ()
(Gdb)

Après notre Ctrl + C (^c), gdb nous indique que l'exécution est arrêtée à l'adresse 0x080003aa dans une fonction inconnue (??).

Selon votre état spécifique, vous pouvez casser à une autre adresse.

Ne paniquez pas — mettez votre chapeau de réflexion et emportez votre serviette avec vous (toujours).

Ce n'est pas un problème. Il y a de fortes chances que vous soyez très proche de cette adresse car elle se trouve dans la boucle d'attente qui fait clignoter la LED, attendant qu'un mot de passe soit reçu sur l'interface série.

Tout d'abord, jetons un coup d'œil à nos registres :

(gdb) ir
r0 0x0 0
r1 0x8001a1d 134224413
r2 0x5b8d7f 5999999
r3 0x335d7 210391
r4 0x20004f88 536891272
r5 0x8001a74 134224500
r6 0x0 0
r7 0x20004f88 536891272
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0xf 15
sp 0x20004f88 0x20004f88
lr 0x80003bf 134218687
ordinateur 0x80003aa 0x80003aa
xPSR 0x81000000 -2130706432
msp 0x20004f88 0x20004f88
[...]

Nous voyons que le pc est en effet là où il est censé être, tout semble bien et dandy. Alors, essayons maintenant d'entrer un mot de passe.

Et… rien ne fonctionne sur la fenêtre de l'interface série ! En pensant à… GDB bloque en fait l'exécution du code ; l'interface série ne réagira pas à vos entrées. C'est normal.

Alors, laissons-le continuer (continuer or c dans l' gdb fenêtre) et voyez si la série fonctionne maintenant. Oui. Cassons-le à nouveau et mettons un point d'arrêt sur l'adresse de la fonction de validation du mot de passe, d'accord ?

Dans Ghidra, nous pouvons voir que l'adresse de la première instruction de la fonction est 0x080002b0:

Screenshot of finding a function address in Ghidra
Figure 12.3 — Recherche d'une adresse de fonction dans Ghidra

Mettons un point d'arrêt là, laissez gdb reprendre l'exécution et saisir un mot de passe factice :

(gdb) b * 0x080002b0
#1
Point d'arrêt 1 à 0x80002b0
#2
(gdb)c
#3
Continuant.
Remarque : utilisation automatique de points d'arrêt matériels pour les adresses en lecture seule.
#4
[saisir 'aaa' dans la console série et entrer]
Point d'arrêt 1, 0x080002b0 dans ?? ()
#5
(Gdb)

Décortiquons cela :

  • b * 0x080002b0 demande gdb pour mettre un point d'arrêt sur l'instruction stockée à l'adresse 0x080002b0. Vérifiez vos pointeurs.
  • gdb me dit, d'accord, j'ai mis un point d'arrêt ici.
  • Continuez l'exécution s'il vous plaît, mon cher gdb et il dit qu'il est heureux de le faire.
  • MAIS il m'informe qu'il ne peut pas écrire à l'adresse 0x080002b0 (il est en flash et le flash ne peut pas être écrit comme ça ; il doit être déverrouillé et écrit morceau par morceau). Afin d'éviter de faire autant d'allers-retours, les puces ARM sont livrées avec des systèmes de débogage internes qui leur permettent de se casser lorsque le PC atteint des adresses spécifiques sur lesquelles il est difficile d'écrire).
  • Bam ! Le point d'arrêt a été atteint ! L'exécution est arrêtée après que j'ai entré un mot de passe factice.

Bon, maintenant qu'est-ce qu'on peut faire avec ça ?

Tout d'abord, si vous vous souvenez du code de la fonction de validation, ses arguments ont été passés directement au code décodé. Voyons ce qu'ils peuvent être (rappelez-vous la convention d'appel des fonctions : les arguments sont dans r0-3):

(gdb) p/x $r0
2 $ = 0x20000028
(gdb) p/x $r1
3 $ = 0x2169

Le premier argument est quelque chose dans la RAM, et le second est une sorte de valeur. (Il s'agit de la valeur UUID transformée pour votre puce, que vous avez notée, n'est-ce pas ?)

Maintenant, qu'est-ce qui est stocké à cette première adresse ? Examinons-le :

(gdb) x/x 0x20000028
0x20000028 : 0x00616161
(gdb) x/s 0x20000028
0x20000028 : "aaa"

Ah ! Ah ! Ah ! (Vous voyez ce que j'ai fait là-bas ?) C'est notre mot de passe. Veuillez noter l'utilisation du modificateur de format pour la commande x.

Donc, c'est prévu.

Examinons maintenant le code déchiffré.

Ghidra nous dit que l'instruction qui suit les boucles de décodage est à 0x080002f0. Arrêtons-nous là :

(gdb) b * 0x080002f0
Point d'arrêt 2 à 0x80002f0
(gdb)c
Continuant.
Point d'arrêt 2, 0x080002f0 dans ?? ()
(gdb)c
(bdb) x/4i $pc
=> 0x80002f0 : movs r0, #0
   0x80002f2 : blx r3
   0x80002f4 : mouvement r3, r0
   0x80002f6 : mouvement r0, r3

Ainsi, l'adresse du code déchiffré est dans r3. Nous avons vu que le tampon était Assistance (71) longue. On est en mode pouce (donc consigne taille 2). Cela devrait être 47/2 : environ 35 instructions. Le dernier bit de l'adresse est pour le mode ; on peut s'en débarrasser :

(gdb) x/35i ($r3 & (~1))
   0x20000128 : appuyez sur {r4, r5, r6, r7, lr}
   0x2000012a : eors r4, r4
   0x2000012c : eors r3, r3
   0x2000012e : eors r5, r5
   0x20000130 : ldrb r5, [r1, r4]
   0x20000132 : mov r8, r5
   0x20000134 : mov r6, r8
   0x20000136 : lsrs r6, r6, #4
   0x20000138 : lsls r5, r5, #4
   0x2000013a : orrs r5, r6
   0x2000013c : movs r6, #255 ; 0xff
   0x2000013e : et r5, r6
   0x20000140 : movs r6, #15
   0x20000142 : mov r8, r4
   0x20000144 : mov r7, r8
   0x20000146 : et r7, r6
   0x20000148 : ajouter r6, pc, #16 ; (adr r6, 0x2000015c) #1
   0x2000014a : ldrb r6, [r6, r7]
   0x2000014c : eors r5, r6
   0x2000014e : ajoute r0, r0, r5
   0x20000150 : ajoute r4, #1
   0x20000152 : ldrb r5, [r1, r4]
   0x20000154 : cmp r5, r3
   0x20000156 : bgt.n 0x20000132
   0x20000158 : eors r0, r2
   0x2000015a : pop {r4, r5, r6, r7, pc}
   0x2000015c : chaîne r5, [r4, #36] ; 0x24
   0x2000015e : ldrb r4, [r6, #5]
   0x20000160 : ldr r7, [r6, #32]
   0x20000162 : sous r2, #55 ; 0x37
   0x20000164 : ldr r4, [r2, r5]
   0x20000166 : ldr r5, [r1, #100] ; 0x64
   0x20000168 : ajouter r3, r12
   0x2000016a : ajoute r4, #68 ; 0x44
   0x2000016c : vqadd.u8 q0, q8,

C'est plus comme ça! Nous voyons un prélude de fonction normal (sauvegarde des registres intra-fonction dans la pile), un traitement et un retour de fonction. Mais GDB nous met en garde contre les paramètres d'instruction illégaux (0x2000016c).

En regardant la liste, nous voyons que GDB indique l'utilisation d'une donnée relative au PC :

#1 : commenté : adr r6, 0x2000015c)

Ceci est très souvent utilisé pour stocker des données dans un programme d'assemblage. adr est une pseudo-instruction qui indique à l'assembleur d'ajouter le décalage à une étiquette (une position nommée) dans le code.

Regardons ce qui y est stocké :

(gdb) x/4wx 0x2000015c
0x2000015c: 0x79746265 0x3a376a37 0x6e4d5954 0x34444463
(gdb) x/s 0x2000015c
0x2000015c: "ebty7j7:TYMncDD4"

Il s'agit en effet d'une chaîne qui est utilisée dans le processus d'une manière ou d'une autre.

Passons en revue les premières instructions, comme exemple de la façon de suivre un flux d'exécution. Nous allons d'abord mettre en place gdb il nous montre donc les registres intéressants, contenus sur chaque étape :

(gdb) affichage/x $r0
1 : /x $r0 = 0x20000028
(gdb) affichage/x $r1
2 : /x $r1 = 0x20000028
(gdb) affichage/x $r2
3 : /x $r2 = 0x2169
(gdb) affichage/x $r3
4 : /x $r3 = 0x20000129
(gdb) affichage/x $r4
5 : /x $r4 = 0x20004f88
(gdb) affichage/x $r5
6 : /x $r5 = 0x8001a74
(gdb) affichage/x $r6
7 : /x $r6 = 0x0
(gdb) affichage/x $r7
8 : /x $r7 = 0x20004f70
(gdb) affichage/x $r8
9 : /x $r8 = 0x2
(gdb) affichage/i $pc
10 : x/i $pc
=> 0x80002f0 : movs r0, #0
=> 0x80002f2 : blx r3

Nous sommes maintenant prêts à utiliser stepi (instruction étape) pour voir ce qui se passe :

0x2000012b : eors r4, r4
0x2000012d : eors r3, r3
0x2000012f : eors r5, r5

Ce zéros r4, r3et r5 (x^x = 0) :

0x20000130 : ldrb r5, [r1, r4]
0x20000132 : mov r8, r5
0x20000134 : mov r6, r8

Cela charge le premier caractère de la chaîne de mot de passe dans r5 (r1 est l'adresse et r4 est remis à zéro à ce stade) et le copie dans r8 ainsi que r6:

0x20000136 : lsrs r6, r6, #4
0x20000138 : lsls r5, r5, #4
0x2000013a : orrs r5, r6
0x2000013c : movs r6, #255 ; 0xff
0x2000013e : et r5, r6

Cela change r6 4 bits à droite, r5 4 bits vers la gauche, et met leur valeur ORed dans r4. Il masque ensuite le résultat OR avec 0xff, en échangeant essentiellement les 4 bits inférieurs et les 4 bits supérieurs du caractère de mot de passe et en nettoyant les bits en excès !

0x20000140 : movs r6, #15
0x20000142 : mov r8, r4
0x20000144 : mov r7, r8
0x20000146 : et r7, r6

Cela se déplace de 15 pouces r6, copies r4 in r8 ainsi que r7, et masques r7 avec 15. Mais pourquoi ? À ce point, r4 est 0 ! Cela peut être utilisé plus tard - puisque nous avons vu que r4 a été utilisé comme décalage lors du chargement du caractère du mot de passe, r4 est probablement un compteur! Si tel est le cas, ce masquage peut être utilisé comme une sorte de modulo… (il est très courant d'utiliser le masquage pour un modulo une puissance de deux -1) :

0x20000148 : ajouter r6, pc, #16 ; (adr r6, 0x2000015c)
0x2000014a : ldrb r6, [r6, r7]

Cela charge le premier caractère de la chaîne qui était caché dans r6 et utilise r7 et un décalage ! r4 est définitivement un compteur ici et r7 une version modulo de celui-ci. Il s'agit d'une manière de programmation très typique d'aborder cela :

0x2000014c : eors r5, r6
0x2000014e : ajoute r0, r0, r5
0x20000150 : ajoute r4, #1

Il s'agit de XORing la valeur du caractère de mot de passe échangé avec les rangs actuels de la chaîne étrange, en ajoutant ceci à r0 et en incrémentant le r4 compteur:

0x20000152 : ldrb r5, [r1, r4]
0x20000154 : cmp r5, r3
0x20000156 : bgt.n 0x20000132

Cela charge un nouveau caractère de mot de passe avec le nouveau décalage r5. r3 est 0 donc le cmp vérifie r5-r3 et attendre … bgt.n? Qu'est-ce que c'est? Vous rappelez-vous quoi faire lorsque vous avez des doutes ? Allez lire la documentation ici : https://community.arm.com/developer/ip-products/processors/b/processors-ip-blog/posts/condition-codes-1-condition-flags-and-codes.

Donc, ça saute si r5 > r3. Et r3 is 0, donc? C'est un test pour un 0 chaîne terminée !

C'est la boucle logique de validation principale !

Une fois que c'est fait, ça fait ça :

0x20000158 : eors r0, r2
0x2000015a : pop {r4, r5, r6, r7, pc}

Il XORs cette somme avec l'UUID en fonction de la valeur qu'il a calculée, restaure les valeurs du registre de l'appelant et renvoie cette valeur. Le code C vérifie ensuite si cette valeur est nulle pour afficher réellement la chaîne gagnante. Il suffit alors de s'arranger pour que notre somme soit égale à la valeur dépendante de l'UUID pour que le XOR soit nul !

Nous avons toute la logique !

spot_img

Dernières informations

spot_img