Vulnérabilités aux injections de NOP sur cartes à puce Franck De Goër Tuteur : Marie-Laure Potet Ensimag - Verimag [email protected] 16 mai 2013 Introduction Les cartes à puces se sont généralisées depuis les années 80, et contiennent aujourd’hui des données sensibles (cartes bancaires, e-passeports, badges d’accès, etc.). Il est donc primordial qu’elles présentent une robustesse maximale à tout type d’attaque, que ce soit matériel ou logiciel. Par exemple, il est préférable qu’une personne malveillante ne soit pas en mesure d’utiliser une carte bleue sans connaître son code PIN. À ce titre, de même que l’on teste les serrures de coffre-fort d’une part, et la résistance de la porte dudit coffre d’autre part, la sécurité du code ainsi que la protection matérielle de la carte doivent être éprouvées. Nous nous intéressons dans ce papier à l’évaluation de la robustesse d’un code s’exécutant sur carte à puce, face aux attaques classiques du domaine (ces attaques sont introduites en section 1, et largement expliquées dans le papier de Bar-El, Choukri, Naccache, Tunstall et Whelan [1]). Nous nous intéresserons particulièrement à la possibilité de détecter au niveau source des vulnérabilités pouvant être exploitées par un attaquant durant l’exécution du programme. Les autorités de certification de cartes à puce (CESTI 1 ) ont besoin de vérifier la robustesse d’un code avant de le valider. Il est donc nécessaire de développer des outils d’analyse qui vont dans ce sens, c’est-à-dire permettant la détection automatisée de vulnérabilités. À l’heure actuelle, leur protocole de validation, détaillé dans la partie 2.1, est basé sur des tests exhaustifs en brute force, indépendamment de la sémantique du code testé (nous y reviendrons dans la section 2.1). L’approche Lazart (introduite dans le papier de Potet, Mounier et Vivien [2], et sur laquelle nous revenons en section 2.2), quant à elle, bien que non exhaustive, permet de détecter les vulnérabilités au niveau du code source. L’avantage de cette approche est qu’elle reste praticable même lorsque le modèle de faute est complexifié (e.g. l’attaquant peut introduire un nombre quelconque de fautes). Elle est complémentaire à l’approche du CESTI, en ce sens qu’elle permet de mettre en évidence des points sensibles du programme qu’il est particulièrement intéressant de tester. À l’heure actuelle, Lazart ne détecte que les vulnérabilités liées aux inversions de test, comme détaillé section 2.2. L’objectif de ce papier est d’étendre cette approche aux attaques NOP. Ces attaques, décrites plus précisément en section 3, consistent à empêcher l’exécution de certaines instructions d’un programme lors de l’exécution. Dans la section 1, nous décrirons le principe d’une attaque laser sur carte à puce et ses conséquences. En 2, nous verrons les différentes approches existantes pour détecter des vulnérabilités aux attaques laser. Les sections 3 et 4 présentent notre contribution au domaine : en 3, nous ferons un descriptif détaillé du type d’attaque étudié et ses conséquences ; et en 4 nous présenterons une implémentation s’insérant dans l’approche Lazart pour détecter ce type d’attaques. 1. Centre d’Évaluation de la Sécurité des Technologies de l’Information : organisme chargé de valider la robustesse des codes exécutés sur carte avant commercialisation 1 Franck De Goër Injections de NOP sur cartes à puce Table des matières 1 Principe d’une attaque laser 3 2 Détections de vulnérabilité aux injections 2.1 Au niveau binaire : brute-tests . . . . . . 2.2 Au niveau source : Lazart . . . . . . . . . 2.2.1 Coloration du graphe de flot . . . 2.2.2 Génération de mutants . . . . . . . 2.2.3 Tests symboliques sur les mutants 2.3 Sensibilité à l’introduction de NOP . . . . . 2.4 Pertinence des NOP . . . . . . . . . . . . . de . . . . . . . . . . . . . . faute . . . . . . . . . . . . . . . . . . . . . . . . . . . . : approches existantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4 4 4 6 6 7 7 3 Attaques de type JUMP ← NOP 3.1 Boucle while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Structure if|elsif|else . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3 Retours de fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 8 9 10 4 Implémentation et résultats 4.1 Noppy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Inclusion dans l’approche Lazart . . . . . . . . . . . . . . . . . 4.1.2 Algorithme de base d’ajout d’arcs . . . . . . . . . . . . . . . . 4.2 Sur un exemple : verify . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.1 Mutation et coloration du CFG . . . . . . . . . . . . . . . . . . 4.2.2 Production de codes mutants et analyse dynamique sur le CFG 11 11 11 12 14 15 15 . . . . . . . . . . . . . . . . . . . . . . . . . modifié . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A Efficacité des détections au niveau source et au niveau binaire 21 B Deux conséquences differentes à une attaque de type NOP sur une boucle while 21 C Mutant de verify pour analyse dynamique 22 2 Franck De Goër 1 Injections de NOP sur cartes à puce Principe d’une attaque laser Nous présentons dans cette section le principe général d’une attaque classique sur carte à puce : l’attaque au laser par injection de faute. L’idée est de modifier une valeur de la mémoire afin de modifier le comportement à l’exécution du programme à attaquer, et ce afin d’obtenir des informations ou d’outrepasser des authentifications. C’est une attaque matérielle dans le sens où elle est insérée physiquement dans le système, mais sa répercution est directement liée au logiciel qui s’exécute sur la carte. Dans les papiers de Pellegrini, Bertacco, Austin [3] et de Barenghi, Breveglieri, Koren, Pelosi, Regazzoni [4] sont décrites des attaques respectivement sur RSA et AES s’exécutant sur carte à puce. Grâce à des injections de faute dans les calculs, il est possible de récupérer la clé privée (ou la clé symétrique dans le cas de l’AES) embarquée dans la carte. Dans ces deux cas, il s’agit d’injection d’erreurs de calculs durant le chiffrement d’un même message afin de récupérer des informations sur la clef. La thèse de Maria Christofi [5] traite dans le chapitre 9 des analyses de vulnérabilité d’implémentations cryptographiques face aux injections de faute. Dans ce papier, nous nous intéresserons plutôt à l’injection de faute amenant non pas à des erreurs de calculs mais à une modification du chemin d’exécution du code sur la carte. Ci-suit un exemple illustrant le principe de ce type d’injection de faute. 1 int triesLeft = 2; 2 3 4 5 6 7 int verify(char buffer[], int ofs, int len) { int i; /* No comparison if PIN is blocked */ if(triesLeft < 0) return EXIT_FAILURE; 8 /* Main Comparison */ for (i = 0; i < len; i++) { if(buffer[ofs + i] != pin[i]) { triesLeft--; authenticated = 0; return EXIT_FAILURE; } } 9 10 11 12 13 14 15 16 17 /* Comparison is successful */ triesLeft = maxTries; authenticated = 1; return EXIT_SUCCESS; 18 19 20 21 22 } Figure 1 – Code de vérification du code PIN Ce code est chargé de vérifier le code PIN entré par l’utilisateur de la carte. Nous voyons ici que si l’utilisateur entre trois codes PIN erronés, la fonction renvoie EXIT_FAILURE, ce qui a pour effet de déclencher une contre-mesure (par exemple un blocage de la carte). Dans ce cas précis, l’objectif de l’attaquant est de sortir avec la valeur EXIT_SUCCESS, ou encore de ne pas sortir avec la valeur EXIT_FAILURE qui peut entraîner le blocage de la carte. Pour ce faire, il peut par exemple injecter une faute consistant à forcer le résultat du test de la ligne 6 à FALSE afin de disposer de plus de trois essais. Dans la même idée, l’attaquant peut empêcher l’exécution de l’instruction triesLeft–– pour ne pas voir le nombre d’essais diminuer. C’est le principe d’injection de 3 Franck De Goër Injections de NOP sur cartes à puce faute : le comportement à l’exécution peut être modifié pour amener le système dans un état imprévu. L’injection de faute peut se faire de plusieurs manières, mais la principale utilisée sur carte à puce est l’attaque au laser : l’éclairage au laser d’un transistor l’amène régulièrement à introduire une erreur de valeur (chaque attaque de ce genre a un taux de réussite d’environ 20%). Il est donc possible de changer la valeur de tests, de modifier des instructions ou encore de modifier des adresses de saut. L’attaque qui nous intéresse particulièrement dans la suite est l’introduction de NOP à la place d’autres instructions, et notamment à la place de sauts, conditionnels ou non. L’introduction d’un NOP consiste à remplacer une instruction assembleur (par exemple BRA 0x5000 ou un JCC, dénotant un jump conditionnel générique 2 ) en instruction NOP qui est l’instruction null : le saut est donc remplacé par une instruction vide, et le programme continue donc sans avoir effectué le BRA. Dans la section qui suit, nous décrivons les approches existantes pour détecter des vulnérabilités aux injections de faute à différents niveaux (binaire, source, etc.). 2 Détections de vulnérabilité aux injections de faute : approches existantes Il existe plusieurs approches possibles dans le but de détecter des vulnérabilités aux injections de faute sur un exécutable. 2.1 Au niveau binaire : brute-tests La première solution (utilisée notamment par le CESTI) est une méthode d’injection à l’aveugle. À partir d’un fichier binaire, des mutants 3 sont produits, correspondant chacun à l’exécutable original dont un octet a été remplacé par 0x00 ou 0xFF (simulation d’une injection de faute, cf section 2.4). Chaque mutant est exécuté. La plupart du temps, la modification d’un octet amène à un exécutable qui ne s’exécute pas correctement, mais parfois le système est amené dans un état imprévu (par exemple EXIT_SUCCESS alors que le PIN n’a pas été rentré). Pour certifier un programme, toutes les injections de faute possibles sont testées et ne doivent pas aboutir à une vulnérabilité. L’exhaustivité de cette méthode repose sur l’hypothèse que l’attaquant n’est en mesure de faire qu’une unique injection de faute. Si l’on considère possible la double (voire triple) injection de faute, le nombre de cas à couvrir devient trop important. 2.2 Au niveau source : Lazart L’approche mise en place à Verimag n’a pas le même point de départ : à partir du graphe de flot 4 (CFG) d’un programme, le but est de déterminer en combien d’injections possibles l’attaquant peut arriver à une situation compromettant la sécurité du système. l’approche est découpée en trois grandes étapes (correspondant aux trois blocs de la figure 2). 2.2.1 Coloration du graphe de flot À partir du code dont on veut évaluer la robustesse, le graphe de flot est généré (à l’aide d’une librairie llvm). Ensuite, étant donné un noeud du graphe dit critique (noeud à atteindre ou à éviter 2. Par exemple JEQ ou JNE 3. Un mutant correspond à une version modifiée du binaire original 4. Un graphe de flot est un graphe décrivant les chemins possibles que peut prendre une exécution. Chaque noeud correspond à un bloc d’instructions séquentielles, et chaque arc correspond à un saut (conditionnel ou inconditionnel) 4 Franck De Goër Injections de NOP sur cartes à puce appli.ll CFG coloring mutation points mutation generation appli.ll attack objectives mutant.ll KLEE test directives attack paths inconclusive robustness "proof" Figure 2 – Schéma de l’approche Lazart pour l’attaquant, e.g. EXIT_SUCCESS ou EXIT_FAILURE) l’outil Lazart colorie le CFG, en mettant en évidence les noeuds vulnérables à une injection de faute. On fournit en entrée du programme de colorisation un noeud à atteindre (par exemple EXIT_SUCCESS). En sortie, les noeuds conduisant inévitablement vers le noeud à atteindre sont en vert, les noeuds ne pouvant plus conduire au but sont en rouge. Les noeuds pouvant amener soit dans le rouge soit dans le vert (selon le résultat des tests) sont en jaune, ou en orange s’ils ont un fils direct qui est rouge. La figure 3 illustre sur l’exemple du PIN la coloration d’un CFG. Dans ce cas, le but à atteindre est le noeud SUCCESS. On voit qu’une fois un noeud FAILURE atteint, il n’est plus possible d’atteindre le noeud SUCCESS, c’est pourquoi ces noeuds ainsi que leurs fils sont rouges. L’algorithme de coloration de graphe a été écrit et implémenté par J. Vivien [6]. entry: triesLeft < 0? T F bb1 FAILURE bb5: T F bb2: T SUCCESS F FAILURE bb4: bb7 return: CFG de la fonction 'verify' colorié (objectif : atteindre SUCCESS) Figure 3 – Exemple de coloration d’un CFG par Lazart 5 Franck De Goër 2.2.2 Injections de NOP sur cartes à puce Génération de mutants Une fois les noeuds vulnérables détectés, des mutants correspondant à des fautes injectées sont produits. En particulier, des compteurs sont introduits afin de compter le nombre de conditions forcées (et donc le nombre d’attaques à réaliser). Des paramètres sont également ajoutées, afin d’activer ou non chaque attaque. Ces mutants sont générés au niveau llvm 5 . Les mutants générés correspondent à des inversions de test, systématiques sur les noeuds oranges et non systématiques sur les noeuds jaunes. bb: T bb: T bb1: F bbTI: F bb2: bb1: (a) A mutation scheme (b) Mandatory mutation Figure 4 – Illustration des mutations sur le CFG - Source : [2] La figure 4(a) représente un test critique (si évalué à false, l’objectif ne sera plus atteignable). La figure 4(b) illustre un mutant modélisant une attaque non systématique : si le test est évalué à true, l’exécution se prolonge avec le noeud bb1, et l’attaquant n’a pas besoin d’intervenir pour éviter le rouge. En revanche, si le test est évalué à false, alors le compteur d’attaques est incrémenté et on poursuit l’exécution dans bb1 : cela modélise une inversion de test. La figure 5 montre au niveau C (afin d’être plus lisible) à quoi ressemble un mutant généré à partir du cfg figure 4(a). 1 2 3 4 5 6 7 8 /* Noeud bb */ if (triesLeft > 0) { /* bb1 */ inst; } else { /* bb2 */ return EXIT_FAILURE; } 1 2 3 4 5 6 7 8 (a) Code initial /* Noeud bb */ if (triesLeft > 0) { /* bb1 */ inst; } else { /* bb2 */ fault ++; } (b) Attaque non systématique Figure 5 – Illustration de la génération de mutants 2.2.3 Tests symboliques sur les mutants Ces mutants sont ensuite fournis à Klee qui détermine le nombre d’injections minimum amenant à atteindre l’état critique. Klee est un outil de génération de cas de tests symboliques par couverture de (k) chemins. En sortie, Klee fournit les entrées qui permettent d’activer les chemins faisables (c’est-à-dire les chemins qui amènent à une fin d’exécution et un respect des post-conditions. Ici par exemple, la valeur de retour doit être EXIT_SUCCESS. On peut aussi ajouter des conditions sur le 5. Le langage llvm est un langage intermédiaire entre le source et le binaire : il offre la modularité nécessaire à la création automatique de mutants, tout en gardant une syntaxe assez aisément manipulable 6 Franck De Goër Injections de NOP sur cartes à puce nombre d’attaques, pour ne retenir que les chemins ne faisant pas intervenir plus de deux attaques 6 .). Cette approche a l’avantage de détecter rapidement les vulnérabilités aux injections d’un programme, et ce quel que soit le nombre d’injections maximum considéré. Cependant, à l’heure actuelle, seules les attaques d’inversion de test sont prises en compte. 2.3 Sensibilité à l’introduction de NOP Nous proposons ici d’étendre l’approche Lazart aux attaques de type JUMP ← NOP 7 plus largement décrites section 3 (l’intérêt d’étudier ce type d’attaques est également décrit en section 3, ainsi que dans la thèse de Xavier Kauffman-Tourkestansky [7] section 3.4.2). Notre travail consiste à produire l’ensemble des graphes de flot modifiés correspondant à des injections de NOP. Par la suite, Lazart nous fournira comme précédemment les noeuds vulnérables aux attaques, et Klee le nombre d’attzaques réalsables, et pour chacune le nombre minimal d’injections au laser nécessaires à leur réalisation. Nous présenterons en section 4 Noppy, une extension de Lazart traitant des injections de NOP. 2.4 Pertinence des NOP Les valeurs des fautes injectées dépendent beaucoup du composant cible, ainsi que du type de mémoire embarquée (EEPROM, ROM, RAM). Les fautes sont injectées par laser, en faisant augmenter la tension locale au niveau d’un transistor, ce qui donne les trois fautes injectables suivantes possibles : – 0x00 : sur une carte où le 0 est décidé lorsque la tension aux bornes d’un transistor est au-dessus d’un seuil critique – 0xFF : à l’inverse, possible sur les cartes où la valeur 1 est décidée lorsque la tension est au dessus d’un certain seul – 0xRand : parfois les valeurs stockées en mémoire sont des valeurs chiffrées, qui subissent des modifications lors de la lecture (déchiffrement). Dans ce cas, en injectant 0x00 ou 0xFF dans la mémoire, cela correspondra après déchiffrement à une valeur a priori aléatoire Dans la suite de ce papier, nous ne considérerons que les injections de 0x00 qui correspondent à l’instruction NOP sur la plupart des architectures embarquées (voir Table 1). Il est important de noter que cette approche ne correspond pas à toutes les cartes à puces : si l’instruction NOP n’est pas codée par 0x00 ou si la valeur 0x00 n’est pas une valeur injectable sur la carte en question, ce modèle n’est plus applicable. L’étude faite reste valide, mais devient caduque, puisque l’attaquant n’est pas en mesure d’introduire des NOP. 3 Attaques de type JUMP ← NOP Nous proposons dans cette section de présenter une étude détaillée des effets d’une attaque qui consiste à remplacer une instruction de type JUMP par une instruction NOP. Une telle attaque est relativement facile à mettre en oeuvre, étant donné que dans la plupart des langages assembleurs utilisés sur les systèmes embarqués, l’instruction NOP est codée par un ou plusieurs octets nuls (voir Table 1). 6. Par exemple klee_assume(fault <= 2); 7. Notation que nous utiliserons dans ce papier, désignant le remplacement d’un JUMP par un NOP 7 Franck De Goër Injections de NOP sur cartes à puce CPU Architecture Intel x86 Intel 8051 ARM MIPS PIC OPCODE 0x90 0x00 0x00000000 0x00000000 0b000000000000 SIZE (Byte) 1 1 4 4 1,5 Table 1 – Caractéristiques de l’instruction NOP en assembleur pour quelques architectures En pratique, la réalisation d’une attaque de cette forme correspond à la modification de l’instruction exécutée par le processeur au moment où celle-ci est lue en mémoire. Nous ne nous intéresserons qu’aux attaques qui amènent à une modification structurelle du graphe de flot (c’est-àdire l’ajout ou la suppression de noeuds ou d’arcs) : les attaques d’inversion de test par exemple ne seront pas détaillées pour ce type d’attaque : cf [2]). Ces attaques en effet ont pour effet de modifier le déroulement de l’exécution (et donc le chemin dans le CFG), mais pas la structure du graphe. Les structures de code sensibles aux attaques JUMP ← NOP sont les suivantes : – les boucles while et for : les deux types de boucles se traitent de la même manière, par conséquent nous ne nous intéresserons qu’aux boucles while – les structures conditionnelles if|elsif|else – les structures switch : si, sémantiquement, ces structures sont un cas particulier des structures conditionnelles, elles sont généralement compilées différement, il serait donc intéressant de faire une étude approfondie des switch. Ce ne sera néanmoins pas traité dans ce papier – les retours de fonction (détaillées ci-après) – les appels de fonction Nous avons effectué une étude systématique des effets du remplacement d’un JUMP par un NOP sur les structures conditionnelles if|elsif|else et while. Il est important de noter que les exemples qui suivent illustrent le principe d’une attaque sur un schéma de compilation particulier. En effet, si notre travaille se situe au niveau source, il est tout de même dépendant du placement des blocs d’instructions dans le fichier binaire afin de réaliser les attaques réelles. Par exemple, nous supposons que le bloc if se situe avant le bloc else dans le binaire (cf section 3.2). Nous avons utilisé le modèle le plus fréquent (celui utilisé notamment par gcc), mais l’approche est à adapter au schéma de compilation effectivement utilisé. 3.1 Boucle while La figure 6 fournit un exemple de boucle while générique en assembleur. Dans cet exemple, on peut effectuer une attaque de type JUMP ← NOP à deux endroits : – Ligne 7 : le remplacement de ce jump par une instruction NOP amène à la sortie de boucle. Dans le cas d’une boucle while censée s’exécuter n fois, l’attaquant peut donc la forcer à s’exécuter seulement k fois (avec k ∈ 1..n) (en imposant la sortie de boucle après k exécutions). – Ligne 3 : une attaque ici évite la sortie de boucle. Dans ce cas, on force l’exécution de la boucle un nombre de fois strictement plus grand que n, mais a priori indéterminé et potentiellement infini. On détaille en annexe B deux comportements différents obtenus dans ce cas, selon la condition de sortie de la boucle while (condition ligne 3 de type JNE, ou de type JEQ - cf B). 8 Franck De Goër 1 2 3 while: [...] JCC endwhile Injections de NOP sur cartes à puce ; test de la condition de boucle ; sortie de boucle 4 5 [...] ; instructions BRA while ; saut au debut de la boucle 6 7 8 9 10 endwhile: [...] Figure 6 – Exemple de boucle while en assembleur La seconde attaque décrite (ligne 3) ne modifie pas la structure du graphe de flot (mais seulement son parcours lors de l’exécution), elle ne sera donc pas retenue pour la suite. En revanche, la première (ligne 7) laisse la possibilité de sortir de la boucle sans retester la condition. Cela amène donc à la modification du graphe de flot comme illustré figure 7 (la ligne ajoutée est en ligne pointillée). entry entry while: while T T do F endwhile do return F endwhile return CFG d'une structure while CFG d'une structure while {NOP} Figure 7 – Exemple de modification d’un CFG par un NOP sur une structure while 3.2 Structure if|elsif|else La compilation d’une structure de type if|elsif|else peut être faite de plusieurs façons différentes. On se propose d’en traiter deux, qui sont les plus courantes. On notera que les deux versions sont identiquement vulnérables aux attaques JUMP ← NOP. Schéma de compilation no 1. Au début de chaque bloc, la condition d’exécution du bloc est testée. Si elle n’est pas vérifiée, l’exécution saute au bloc suivant. Ce schéma de compilation est illustré par la figure 8. Ici, l’attaquant peut obtenir deux types de résultat en changeant un saut en NOP : 9 Franck De Goër 1 Injections de NOP sur cartes à puce if: [...] JNE elsif 2 3 ; Test de la condition 1 ; Si la condition n’est pas verifiee, on saute au second bloc 4 5 6 7 8 9 [...] BRA endif elsif: [...] JNE else ; Instructions du if ; sortie de la structure ; Test de la condition 2 ; Si la condition n’est pas verifiee, on saute au bloc else 10 11 12 13 14 15 16 [...] BRA endif else: [...] endif: [...] ; Instructions du elsif ; sortie de la structure ; Instructions du else Figure 8 – Exemple de structure if|elsif|else en assembleur – Lignes 3,9 : une attaque sur l’une de ces deux lignes a pour effet de forcer (partiellement) l’évaluation de la condition. À la ligne 3 par exemple, on empêche quoi qu’il arrive d’exécuter le bloc if. Ce type d’attaque n’est pas l’objet de cette étude, puisqu’elle ne modifie pas le graphe de flot mais constitue une inversion partielle de test (se référer à [2] pour les inversions de test) – Lignes 6,12 : si l’un de ces branchements est remplacé par NOP, cela permet l’exécution de deux blocs d’instructions consécutifs : on oblige potentiellement l’exécution du bloc suivant (de manière certaine si le bloc suivant est le bloc else, sous réserve de l’évaluation de la condition suivante sinon) Schéma de compilation no 2. Les tests sont effectués dès le début, et l’exécution saute directement au bloc à exécuter 8 . Ce schéma est illustré par la figure 9. De même que précédemment, une attaque sur un des sauts suivant l’évaluation d’une condition revient à forcer partiellement le test, et ne modifie pas le flot de contrôle. En revanche, une attaque sur un saut BRA endif force, de façon inconditionnelle cette fois-ci (à l’inverse du cas 1), l’exécution de deux blocs consécutifs. Nous venons de voir qu’il est possible, grâce à une injection de NOP, d’exécuter deux blocs consécutifs 9 . La figure 10 illustre l’impact d’une telle attaque sur le CFG. 3.3 Retours de fonction Au niveau C, les retours de fonction ne paraissent pas constituer une cible pour les attaques NOP. Cependant, si nous analysons au niveau assembleur (code assembleur généré par gcc fourni figure 11), nous pouvons constater que l’instruction return -1 en C est traduite en plusieurs instructions assembleur, et se fait en plusieurs étapes : 1. Chargement de la valeur de retour (-1 soit 0xffff) dans le registre ax 2. jmp à la fin de la fonction (ici @4F7) 3. récupération de l’adresse de retour depuis la pile vers bp 8. Cette méthode, notamment utilisée par le compilateur Deca de l’équipe GL-24, présente l’avantage de n’effectuer qu’un unique saut pour atteindre le bloc d’instructions à exécuter, quel qu’il soit. 9. Comprendre par consécutif : consécutif dans le code binaire. L’ordre des blocs dépend du compilateur. 10 Franck De Goër tests: [...] JEQ if [...] JEQ elsif BRA else if: [...] BRA endif elsif: [...] BRA endif else: [...] endif: [...] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Injections de NOP sur cartes à puce ; ; ; ; ; Test de la condition 1 Si la condition est verifiee, on saute au bloc if Test de la condition 2 Si la condition est verifiee, on saute au bloc elsif Sinon saut au bloc else ; Instructions du if ; sortie de la structure ; Instructions du elsif ; sortie de la structure ; Instructions du else Figure 9 – Autre schéma de structure if|elsif|else en assembleur 4. ret (correspond à un saut à l’adresse contenue dans bp) Il est important de noter que si plusieurs instructions return apparaissent à différents endroits d’une fonction au niveau C, elles correspondront toutes à un saut à l’adresse de fin de fonction (ici @4F7) au niveau assembleur. Il est donc tout à fait possible de remplacer l’instruction jmp short 0x4f7 par l’instruction nop pour empêcher le retour de fonction, et prolonger l’exécution. Au niveau du CFG, cela correspond à ajouter un arc comme illustré figure 12. Dans la section suivante, nous présentons notre proposition d’implémentation pour traiter avec l’approche Lazart des attaques JUMP ← NOP décrites précédemment. 4 4.1 Implémentation et résultats Noppy Noppy est un outil que nous avons développé. Il permet, à partir d’un CFG, de rajouter l’ensemble des chemins possibles par une injection de NOP. Noppy s’appuie sur le travail réalisé en section 3 pour rajouter des chemins sur les structures conditionnelles (uniquement if|else à ce jour), en prenant comme hypothèse de schéma de compilation celui utilisé en section 3.2 (c’est-à-dire que le bloc if se trouve avant le bloc else dans le code binaire). À titre d’exemple, Noppy transforme le CFG de gauche de la figure 10 en le CFG de droite. 4.1.1 Inclusion dans l’approche Lazart Il est important de rappeler que Noppy a pour objectif d’enrichir l’approche Lazart décrite section 2.2, et illustrée figure 2. En effet, l’ajout des chemins correspondant aux NOP dans un CFG ne devient intéressant que si l’on utilise ce nouveau CFG pour déterminer des attaques possibles. Noppy s’insère donc dans l’approche Lazart. L’intervention de Noppy se fait avant la coloration du graphe de flot et la génération de mutants - cf figure 13. Avant de colorier le graphe, les chemins correspondant à l’attaque de type JUMP ← NOP sont ajoutés. Pour l’outil de coloration, les noeuds modifiés (ceux avec un arc sortant ajouté par Noppy) sont vus comme des noeuds de test, sur lesquels il est possible de forcer le résultat (suivant le modèle de l’approche, à savoir les inversions de test). Noppy conserve une trace des arcs qu’il a ajoutés (sous la forme de couples (N1, N2)), pour la fournir ensuite à l’outil de 11 Franck De Goër Injections de NOP sur cartes à puce if: T inst: if: F T elsif: T F inst: elsif: F int: T else: int: int: F else: int: return: return: CFG d'une structure if|elsif|else CFG d'une structure if|elsif|else {NOP} Figure 10 – Exemple de modification du CFG par attaque NOP dans une structure if|elsif|else 1 2 3 4 5 6 7 000004E4 000004E7 000004E8 000004E9 ........ 000004F7 000004F8 B8FFFF FF FF EB0C .... 5D C3 mov ax,0xffff db 0xff db 0xff jmp short 0x4f7 [...] pop bp ret Figure 11 – Exemple de code assembleur correspondant à un retour de fonction généré par gcc génération des mutants. Ce dernier, pour chaque noeud à muter, va d’abord vérifier s’il est le premier noeud d’un des couples fournis par Noppy. Si c’est le cas, la mutation est effectuée comme décrit dans la figure 14. Sinon, la génération de mutants est la même que celle décrite en section 2.2.2. L’ensemble de la suite de l’approche est conservée, à savoir la génération des mutants et la couverture symbolique des chemins (via Klee). De cette manière, il est désormais possible de déterminer le nombre d’attaques minimum nécessaire pour atteindre un noeud ciblé en considérant les NOP. Un exemple est développé section 4.2. 4.1.2 Algorithme de base d’ajout d’arcs En premier lieu, nous décrivons l’algorithme de rajout de chemins dans un CFG qui correspondent à une attaque par NOP sur une structure if|elsif|else : G = (V,E): graphe de flot Pour tout N in V tel que N a au moins deux fils faire: /* Traitement de deux fils consécutifs */ Pour i allant de 1 à (N.nombreDeFils() - 1) faire : 12 Franck De Goër Injections de NOP sur cartes à puce entry: entry: T T F bb1 return -1 bb1 while T F bb2 while return -1 F return 0 T F bb2 bb5 return 0 bb5 return return CFG d'une fonction avec return CFG d'une fonction avec return {NOP} Figure 12 – Exemple de mutation de CFG d’une fonction contenant un retour appli.ll 1111 0000 0000 1111 0000 1111 Noppy CFG coloring mutation points attack objectives mutant generation appli.ll mutant.ll symbolic test case generator attack paths inconclusive robustness "proof" Figure 13 – Inclusion de Noppy dans l’approche Lazart /* Détermination du point de conjonction */ PointDeRencontre = TrouverPointdeRencontre(N.fils[i], N.fils[i+1]); si (PointDeRencontre == N) : /* Si le point de rencontre est le point de disjonction alors on est sur une structure de boucle */ N1 = NoeudPere(N.fils[i], N); N2 = N.fils[i+1]; /* On prend le dernier noeud du premier bloc */ N1 = PointDeRencontre.pereGauche(); /* Le premier noeud du second bloc */ N2 = N.fils[i+1]; /* Et on les relie */ E.ajouterArc(N1,N2); fin faire fin faire L’idée de l’algorithme est très simple : après chaque disjonction sur le CFG (e.g. if|else ou encore while|endwhile), il faut ajouter un chemin entre la fin du premier bloc et le début du 13 Franck De Goër Injections de NOP sur cartes à puce entry T F if else 1 2 3 endif 4 5 return 6 CFG d'une structure if|else 7 /* Entry */ if (...) { inst; } else { inst; } return; (1a) CFG du code initial (1b) Code initial 1 2 3 4 5 6 entry: T 7 F 8 bb: bb1: 9 10 11 bb2: 12 13 return: CFG d'une structure if|else mutée (NOP) 14 15 (2a) CFG du code muté bool nop_attack; /* Entry */ if (...) { inst; if (nop_attack) { /* if attack is performed */ /* incrementation of nb of faults */ fault++; /* execution of else */ goto elseLabel; } } else { elseLabel: inst; } (2b) Code muté : attaque systématique Figure 14 – Illustration de la génération de mutants pour un NOP second (comme détaillé en section 3). Spécification des fonctions utilisées : – TrouverPointdeRencontre(N1, N2) : retourne le premier noeud commun aux fils de N1 et N2 (requiert : un tel noeud existe). Ici, c’est le cas puisque N1 et N2 opnt au moins le noeud return comme fils commun. – NoeudPere(N1, N2) : retourne le premier noeud N parmi les fils de N1 tel que N2 soit un fils de N ; c’est à dire le père de N2 qui soit un descendant de N1. – ajouterArc(N1,N2) : ajoute un arc orienté allant du noeud N1 à N2 4.2 Sur un exemple : verify Détaillons la démarche Lazart dotée de Noppy sur l’exemple présenté en section 2. Le code C est rappelé Figure 15. L’objectif de cet exemple est de montrer que la prise en compte de NOP amène à la détection d’attaques qui ne sont pas réalisables si l’on ne prend en compte que les inversions de test. 14 Franck De Goër 4.2.1 Injections de NOP sur cartes à puce Mutation et coloration du CFG La première étape est la génération du graphe de flot (la figure 16 partie gauche). Ensuite, Noppy se charge de rajouter les arcs correspondant aux NOP possibles. La mutation du graphe de flot est illustrée figure 16 (partie droite). La figure 17 présente la coloration de deux graphes de flot guidée par l’objectif d’atteindre le noeud SUCCESS : à gauche la coloration du CFG non muté par Noppy, à gauche celle du CFG muté par Noppy. (Rappel des codes de couleur fournis section 2.2.1 : [vert] Noeud amenant de façon certaine à l’objectif [jaune|orange] Noeud pouvant conduire ou non à l’objectif, selon le résultat de tests [rouge] Noeud ne pouvant plus amener à l’objectif.) Note : la flèche allant du noeud FAILURE au noeud bb7 correspond au saut à la fin (unique) de la fonction dans le code binaire, à l’endroit de l’instruction de retour. On s’aperçoit ici que la coloration n’est pas la même selon si l’on considère les NOP ou pas. Dans la version non mutée, les noeuds FAILURE sont rouges par exemple, alors qu’ils sont orange dans la version mutée. En effet, si elles sont prises en compte, les attaques NOP permettent d’atteindre le noeud SUCCESS après avoir atteint un noeud FAILURE, en remplaçant le saut à la fin de la fonction par un NOP. 4.2.2 Production de codes mutants et analyse dynamique sur le CFG modifié La figure 15(b) fournit le code du mutant généré à partir du CFG modifié par Noppy (le mutant est fourni en C pour être plus lisible, mais il est en réalité produit au format llvm). Dans ce mutant n’apparaissent que les mutations correspondant à des NOP. Les inversions de test ne sont pas montrées. Nous donnons en annexe C le code C du driver de test. Nous pouvons reconnaître les trois NOP possibles dans les trois lignes klee_make_symbolic(&activNOPi, sizeof(int), "activNOPi");. Voici les attaques trouvées par Klee en au plus deux fautes injectées : – Attaque 1 : Un seul passage dans la boucle : 1. NOP sur la sortie du noeud FAILURE suivant bb2 (buffer[i] != pin[i]) 2. NOP sur la sortie du noeud bb4 pour quitter la boucle immédiatement Total : 2 fautes – Attaque 2 : Un seul passage dans la boucle : 1. Inversion de test sur bb2 pour aller vers bb4 2. NOP sur la sortie du noeud bb4 pour sortir immédiatement Total : 2 fautes – Attaque 3 : Aucun passage dans la boucle : 1. Inversion du test d’entrée de boucle sur bb5 pour ne pas l’exécuter et sortir vers SUCCESS Total : 1 faute L’attaque 3 ne fait pas intervenir d’injection de NOP, elle est donc contenue dans l’approche Lazart initiale (celle qui ne prend en compte que les inversions de test). L’attaque 1 fait intervenir deux attaques de type NOP, mais ces deux injections de NOP sont équivalentes à l’inversion des tests aux lignes 14 puis 12 (au second passage dans la boucle). L’attaque 2, quant à elle, est 15 Franck De Goër Injections de NOP sur cartes à puce intéressante, car elle combine les injections de NOP et les inversions de test. Il est à noter que les préconditions de l’analyseur choisies dans le cadre de ces tests imposent que tous les chiffres du code pin saisi soient erronés. Avec ces conditions de test, on trouve donc une nouvelle attaque qui lie les inversions de test et les injections de NOP que l’on n’a pas en ne traitant que les inversions de test. D’autre part, on peut modifier les préconditions. Par exemple, si l’on considère que l’attaquant peut connaître le premier chiffre du code PIN, alors il est possible de passer l’authentification avec succès en forçant la sortie de boucle à la fin de la comparaison : bb4 → SUCCESS). Comme l’attaquant dispose de trois tentatives pour trouver le code Pin, la probabilité de succès avec cette attaque devient 0.3 * 0.20 = 0.06 (probabilité de trouver le premier chiffre en trois essais multiplié par la probabilité de réussite de l’attaque), ce qui est bien mieux que la probabilité de 0.0003 (trois essais pour 10000 PIN possibles) pour un attaquant dépourvu de laser. 16 Franck De Goër Injections de NOP sur cartes à puce int verify(char buffer[], int ofs, int len) { int i; /* No comparison if PIN is blocked */ if(triesLeft < 0) { // FAILURE if (nop_attack_1) { fault++; goto bb1; } return EXIT_FAILURE; } 1 2 3 4 5 6 // bb1 bb1: /* Main Comparison */ // bb5 for (i = 0; i < len; i++) { // bb2 if(buffer[ofs + i] != pin[i]) { triesLeft--; authenticated = 0; // FAILURE if (nop_attack_2) { fault++; goto bb4; } return EXIT_FAILURE; } // bb4 if (nop_attack_3[i]) { fault++; goto SUCCESS; } } int verify(char buffer[], int ofs, int len) { int i; /* No comparison if PIN is blocked */ if(triesLeft < 0) // FAILURE return EXIT_FAILURE; 7 // bb1 8 9 /* Main Comparison */ // bb5 for (i = 0; i < len; i++) { // bb2 if(buffer[ofs + i] != pin[i]) { triesLeft--; authenticated = 0; // FAILURE return EXIT_FAILURE; } // bb4 } 10 11 12 13 14 15 16 17 18 19 20 21 /* Comparison is successful */ // SUCCESS SUCCESS: triesLeft = maxTries; authenticated = 1; return EXIT_SUCCESS; 22 /* Comparison is successful */ // SUCCESS triesLeft = maxTries; authenticated = 1; return EXIT_SUCCESS; 23 24 25 26 27 28 } } (a) Code initial de verify (b) Mutant correspondant aux injections de NOP Figure 15 – Code de vérification du code PIN et mutation 17 Franck De Goër Injections de NOP sur cartes à puce entry: entry: triesLeft < 0? T triesLeft < 0? F T F bb1 FAILURE bb1 bb5: T FAILURE T bb2: T bb5: F SUCCESS bb2: F FAILURE F T bb4: SUCCESS F FAILURE bb7 bb4: bb7 return return CFG for 'verify' function CFG for 'verify' function after Noppy Figure 16 – Graphe de flot de verify muté par Noppy entry: entry: triesLeft < 0? T triesLeft < 0? F T bb1 FAILURE bb1 bb5: T FAILURE T SUCCESS F bb2: F FAILURE bb5: F bb2: T F T bb4: FAILURE bb7 SUCCESS F bb4: bb7 return: return: CFG de la fonction 'verify' colorié (objectif : atteindre SUCCESS) CFG for 'verify' function - Coloration after Noppy Figure 17 – Graphe de flot de verify 18 Franck De Goër Injections de NOP sur cartes à puce Conclusion Nous avons dans ce papier tenté de fournir un apport à la détection automatique de vulnérabilités de code aux attaques par injection de faute. Nous nous sommes intéressés spécialement aux attaques consistant à remplacer un JUMP par un NOP. Après une étude détaillée des impacts de ce type d’attaque sur le CFG, nous avons proposé une implémentation permettant d’inclure ces attaques à l’approche Lazart développée à Verimag. L’algorithme proposé permet de traiter les structures de type if|elsif|else ainsi que les structures while, à supposer que l’on sache détecter les boucles dans un programme (c’est là un problème classique de compilation et d’optimisation de code que nous n’avons pas traité). L’inclusion dans Lazart se fait bien, puisque nous avons détecté des attaques faisant intervenir à la fois des inversions de test et des injections de NOP. L’implémentation détaillée dans ce papier ne prend cependant pas en compte les appels de fonctions mentionnés en section 3, ni les structures particulières que sont les switch. Il serait également intéressant de pouvoir moduler l’implémentation en fonction du schéma de compilation effectif utilisé. Enfin, dans la section 4.2, nous nous essayons à des comparaisons entre les différentes attaques trouvées ("L’attaque 1 fait intervenir deux attaques de type NOP, mais ces deux injections de NOP sont équivalentes à l’inversion des tests aux lignes 14 puis 12 (au second passage dans la boucle)."). Cependant, la comparaison entre deux attaques reste à formaliser (doit-on considérer les instructions exécutées ? Le parcours dans le CFG ? Le résultat en sortie ?). La majorité du travail présenté dans ce papier a été effectué dans les locaux de Verimag, en collaboration avec Marie-Laure Potet. Le contenu de ce papier est étroitement lié au travail de Maxime Puys (TER - UJF), qui développe l’automatisation de la génération des mutants en sortie de la coloration. Par ailleurs, ce papier s’appuie sur les travaux de Jonathan Vivien, à qui nous devons le module de coloration du CFG [6]. Enfin, de précieuses informations sur la pratique réelle des attaques laser ont été fournies par Jessy Clediere, du CESTI. 19 Franck De Goër Injections de NOP sur cartes à puce Références [1] H. Bar-El, H. Choukri, D. Naccache, M. Tunstall et C. Whelan, « The sorcerer’s apprentice guide to fault attacks », Proceedings of the IEEE, vol. 94, no. 2, p. 370–382, 2006. [2] M.-L. Potet, L. Mounier et J. Vivien, « A static approach for evaluating the robustness of secured codes against fault injection by test inverting », 2013. [3] A. Pellegrini, V. Bertacco et T. Austin, « Fault-based attack of rsa authentication », in Proceedings of the Conference on Design, Automation and Test in Europe, p. 855–860, European Design and Automation Association, 2010. [4] A. Barenghi, L. Breveglieri, I. Koren, G. Pelosi et F. Regazzoni, « Countermeasures against fault attacks on software implemented aes : effectiveness and cost », in Proceedings of the 5th Workshop on Embedded Systems Security, p. 7, ACM, 2010. [5] M. Christofi, Preuves de sécurité outillées d’implémentations cryptographiques. Thèse doctorat, Université de Versailles Saint-Quentin-En-Yvelines, 2013. [6] J. Vivien, « Evaluation of code vulnerabilities against fault attacks on smartcards », 2012. [7] X. Kauffmann-Tourkestansky, Analyses sécuritaires de code de carte à puce sous attaques physiques simulées. Thèse doctorat, Université d’Orléans, 2012. [8] Application of Attack Potential to Smartcards. Joint Interpretation Library, 2009. [9] P. Berthome, K. Heydemann, X. Kauffmann-Tourkestansky et J.-F. Lalande, « High level model of control flow attacks for smart card functional security », in Availability, Reliability and Security (ARES), 2012 Seventh International Conference on, p. 224–229, IEEE, 2012. 20 Franck De Goër A Injections de NOP sur cartes à puce Efficacité des détections au niveau source et au niveau binaire Nous nous proposons ici de résumer les résultats du papier de Berthomé, Heydemann, KauffmanTourkestansky et Lalande [9], comparant l’efficacité des analyses de vulnérabilités aux injections de faute au niveau binaire et au niveau source. Leur expérimentation a été effectuée sur le logiciel bzip2. À partir d’un fichier source d’environ 500K, a été produit un fichier compressé de référence avec bzip2. Puis ce même fichier a été compressé avec un bzip2 sur lequel a été simulé une attaque laser, soit au niveau source, soit au niveau ASM. Le tableau 2 récapitule leurs résultats. La deuxième ligne donne le nombre total d’attaques effectuées par chacun des deux modèles. La ligne "Nb of Bads" donne le nombre de simulations aboutissant à une compression (pas d’erreur à l’exécution) qui diffère de la compression de référence. Parmi ces compressions corrompues, la ligne "Nb of Empty file" donne le nombre de fichiers vides et la ligne "Uniq Bads" le nombre de fichiers compressés différents obtenu. Statistics Code Size Nb of attacks Simulation time Nb of Bads Nb of Enmpty file Uniq Bads ASM Coverage ASM 26103 3531954 2d18h 273129 103952 2326 100% C 8643 117802 8h 14050 6514 1245 21% Table 2 – Statistics for simulated attacks on bzip2 [Source : [9]] Parmi les 1245 fichiers compressés trouvés par l’analyse au niveau C, seulement 503 sont des attaques trouvées également au niveau ASM. Cela signifie d’une part que l’analyse au niveau source n’a trouvé que 21% des attaques trouvées par une analyse au niveau binaire, mais d’autre part que cette analyse est plus efficace, puisqu’elle dure 8 fois moins de temps qu’une analyse complète du binaire. Il est donc intéressant de travailler au niveau source en terme d’efficacité, mais il faut encore améliorer cette approche afin de la rendre le plus exhaustive possible. B Deux conséquences differentes à une attaque de type NOP sur une boucle while Voici deux cas de boucles while donnant des comportements différents si soumis à une unique attaque sur le test de condition : Cas particulier 1. Une attaque de type JUMP ← NOP à la ligne 3 amène la boucle à s’exécuter une fois de plus que prévu (donc 11 fois). Cas particulier 2. Dans ce cas-ci, la même attaque à la ligne 3 amène la boucle à s’exécuter infiniement (ou au moins le nombre de fois qu’il faut pour que i fasse un tour de l’ensemble des INTEGER). 21 Franck De Goër STORE $1, i while: CMP i, 10 JGE endwhile [...] ADD $1, i BRA while 1 2 3 4 5 6 7 Injections de NOP sur cartes à puce ; initialisation de i ; ; ; ; ; comparaison du compteur sortie de boucle instructions incrementation du compteur saut au debut de la boucle 8 endwhile: [...] 9 10 Figure 18 – Cas particulier [1] STORE $1, i while: CMP i, 10 JNE endwhile [...] ADD $1, i BRA while 1 2 3 4 5 6 7 ; initialisation de i ; ; ; ; ; comparaison du compteur sortie de boucle <- Difference avec le premier exemple instructions incrementation du compteur saut au debut de la boucle 8 endwhile: [...] 9 10 Figure 19 – Cas particulier [2] C 1 2 Mutant de verify pour analyse dynamique #include <stdio.h> #include <stdlib.h> 3 4 5 #define SIZE_OF_PIN 4 #define MAX_TRIES 3 6 7 8 9 10 typedef unsigned char BYTE; BYTE triesLeft = MAX_TRIES; BYTE authenticated; BYTE pin[SIZE_OF_PIN] = {1, 2, 3, 4} ; 11 12 13 14 15 16 17 18 19 20 21 int int int int int int int int int int fault; activ1; activ2; activ3; activ4; activ5; activ6; activNOP1; activNOP2; activNOP3; 22 23 24 void verify_precond(char buffer[SIZE_OF_PIN]) { int i; 25 26 klee_make_symbolic(&activ1, sizeof(int), "activ1"); 22 Franck De Goër klee_make_symbolic(&activ2, klee_make_symbolic(&activ3, klee_make_symbolic(&activ4, klee_make_symbolic(&activ5, klee_make_symbolic(&activ6, 27 28 29 30 31 Injections de NOP sur cartes à puce sizeof(int), sizeof(int), sizeof(int), sizeof(int), sizeof(int), "activ2"); "activ3"); "activ4"); "activ5"); "activ6"); 32 klee_make_symbolic(&activNOP1, sizeof(int), "activNOP1"); klee_make_symbolic(&activNOP2, sizeof(int), "activNOP2"); klee_make_symbolic(&activNOP3, sizeof(int), "activNOP3"); 33 34 35 36 klee_make_symbolic(buffer, sizeof(char)*SIZE_OF_PIN, "buffer"); for (i=0 ; i<SIZE_OF_PIN ; i++) { klee_assume(buffer[i] != pin[i]); } 37 38 39 40 41 } 42 43 44 45 void verify_postcond() { klee_assume(fault <= 2); } 46 47 48 int verify(BYTE buffer[SIZE_OF_PIN]) { int i = 0; 49 if (triesLeft == 0) { return 0; } 50 51 52 53 for(i=0 ; i<4 ; i++) { if(buffer[i] != pin[i]) { triesLeft--; authenticated = 0; return 0; } } 54 55 56 57 58 59 60 61 triesLeft = MAX_TRIES; authenticated = 1; 62 63 64 return EXIT_SUCCESS; 65 66 } 67 68 69 70 int main(int argc, char *argv[]) { BYTE buffer[SIZE_OF_PIN]; 71 verify_precond(buffer); verify(buffer); verify_postcond(); 72 73 74 75 return EXIT_SUCCESS; 76 77 } —————————————————————————————- 23