• Aucun résultat trouvé

1 Introduction aux modèles d’attaque du flot de contrôle

1 Introduction aux modèles d’attaque du flot de contrôle

Le flot de contrôle est influencé par la nature des instructions exécutées : soit les instructions se font en séquence, soit une instruction est une rupture de cette séquence. Lorsque le processeur rencontre une instruc-tion de type call qui indique l’adresse de l’instrucinstruc-tion suivante, il transfert le contrôle à la porinstruc-tion de code qui correspond à l’appel. Lorsque la procédure se termine, le processeur redonne le contrôle à l’instruction qui a suivi le call. Ce mécanisme nécessite de sauver l’adresse de retour, l’état des registres vivants, de connaître la localisation des fonctions et de traiter le passage des paramètres. Suivant le matériel et les implémentations, la gestion de ces données varie, notamment leur emplacement sur la pile.

Dans la littérature sur les attaques contre le flot de contrôle [82,126], il est classique de considérer les attaques influant la pile pour les programmes natifs. Par exemple, dans une frame de la pile, un calcul de pointeur simple à partir d’une variable locale peut permettre d’aller modifier l’adresse de retour de la fonction associée à cette frame. Un autre exemple d’attaque est le débordement de pile qui peut permettre d’aller écrire dans les variables de la section BSS du segment de données (en bout de pile) où l’on peut par exemple écraser des variables globales. On peut tenter des attaques similaires pour du bytecode interprété par une machine virtuelle mais bien souvent, des vérifications sont faites à l’exécution, ce qui rend ces attaques plus difficiles à mettre en œuvre [107].

Ainsi, pour décrire précisément des attaques contre le contrôle de flot, il faut considérer le programme à un niveau plus bas que son code source. Il est nécessaire de disséquer le fonctionnement de l’interface binaire-programme, la gestion de la pile et le mapping mémoire (notamment la gestion de la pile lors des appels de fonc-tions). Ces éléments sont spécifiques à chaque architecture et différents suivant que l’on considère un programme natif ou interprété. Dès lors, il est difficile de modéliser ceux-ci, notamment pour la gestion de la mémoire. Dans la suite, nous décrivons les éléments communs à des programmes pour caractériser le flot de contrôle et nous introduisons un modèle général d’attaque de ce flot de contrôle, quelle que soit l’architecture considérée. Ces éléments permettront ensuite d’élaborer des attaques pour deux types de systèmes distincts en section2, les cartes à puce et les téléphones mobiles.

1.1 Niveaux de représentation d’un modèle d’attaque

Une attaque contre un programme peut être considéré à différents niveaux de représentation de ce pro-gramme ou du matériel sur lequel il s’exécute [171] :

— Niveau matériel : des composants spécifiques du matériel sont ciblés et peuvent être altérés ; il peut s’agir de bus de transmission, de zone mémoire ou de composants spécialisés comme l’horloge ou l’alimentation ; — Niveau assembleur : des registres ou des cases mémoire peuvent être altérés ;

— Niveau bytecode : dans le cas de l’utilisation d’une machine virtuelle, à ce niveau, le code ou les variables peuvent être altérés ;

— Niveau du code source : à ce niveau, les attaques peuvent être conceptualisées et prises en compte mais l’attaquant dispose rarement de la capacité à modifier le code source puisqu’il attaque un composant cible où le code compilé s’y exécute.

Pour chacun de ces niveaux, les entités à considérer sont différentes. Partons d’un exemple simple ou l’on appelle une fonction vérifiant l’égalité entre deux codes pin. Comme le montre les trois listings ci-dessous (à gauche le cas natif, à droite le cas d’une machine virtuelle), les entités du programme à considérer sont très différentes : CALL check_pin MOV R0 , # pin MOV A , R7 MOV @R0, A MOV R7 , # pin RET Assembleur pin = check_pin (1234,0000) ; returnpin; Code source i n v o k e s t a t i c #3 i s t o r e _ 0 i l o a d _ 0 i r e t u r n Bytecode

Il est alors clair que le modèle d’attaque est plus facile à exprimer si l’on considère le code au plus près de sa nature lors de son exécution, en l’occurrence l’assembleur pour du code natif ou du bytecode pour du code interprété. Cependant, il est bien évident qu’un attaque décrite à un tel niveau de détail du code sera plus complexe à comprendre d’un point de vue de l’expert en sécurité, surtout s’il veut faire un lien avec le source de l’application.

Pour pouvoir décrire une attaque, il faut arriver à caractériser ce que l’attaquant est en capacité de réaliser sur le programme, en l’exprimant sur un des niveaux de description du programme vus précédemment. La difficulté réside dans le fait que les moyens dont dispose l’attaquant dépendent du système cible et de la nature physique

1.2 Temporalité de l’attaque

de l’attaque, et qu’il y a donc de très grandes variations suivant les hypothèses considérées. Par exemple, pour un code malveillant ayant été introduit à l’intérieur d’un binaire compilé, celui-ci dispose de beaucoup plus de privilèges pour modifier son code ou les variables du programme. A l’inverse, dans le cas d’attaques physiques ciblant des carte à puce, l’attaquant ne maîtrise pas forcément les modifications qu’il opère sur les valeurs des variables ou les instructions du programme [171]. Cependant, quelles que soient les hypothèses, on peut syn-thétiser ces niveaux de précision dans un ensemble de grandes classes générales d’effets d’attaque permettant de caractériser le pouvoir de l’attaquant [174,171] :

— Bit flip/set : l’attaquant peut effectuer une modification au bit près ;

— Word randomize/set : l’attaquant peut effectuer une modification sur un mot, en général un octet ; — Variable ou code du programme : à ce niveau de précision, on cherche à se rendre indépendant de

l’archi-tecture cible et la plage de modification est considérée très grande.

Cette classification est issue des modèles utilisés dans les travaux de tolérance aux fautes, comme le précise

Abadi et al. [82]. Malheureusement, ceux-ci notent que ces classes sont peu adaptées quand on considère la

sécurité vis-à-vis d’un attaquant. Notamment le modèle bit flip représente une erreur qui survient à cause d’une défaillance matérielle et les contremesures de la littérature utilisent par exemple des codes correcteurs d’erreur pour vérifier l’intégrité en sortie d’un basic block [151,157]. Si les garanties dépendent de probabilités, elles ne conviennent plus pour un modèle d’attaquant qui cherche à corrompre de manière délibérée un programme en utilisant éventuellement des vulnérabilités.

Ces différents niveaux de précision de l’attaque sont liés à l’ensemble des éléments qui influent sur le contrôle de flot. Il peut s’agir d’attaquer le program counter [108], un élément d’une frame de la pile [97], une variable globale dans une section de données [126], une variable locale de la RAM [120], un registre qui influe sur un test conditionnel [106], une instruction (opcode et/ou opérande) du code exécuté [172], etc. Pour tous ces éléments, on peut considérer une attaque influant sur un bit, un octet ou bien directement sur l’ensemble des octets constituant la valeur de l’élément.

On peut ajouter un raffinement si l’on distingue le cas où l’attaquant contrôle la valeur de la modification ou si la modification prend une valeur aléatoire. Si l’on considère par exemple une attaque permettant d’écraser un mot avec une valeur contrôlée (e.g. 0F0H) on peut par exemple écraser la variable pin de l’exemple précédent, c’est-à-dire une partie des opérandes pour le code assembleur mais un opcode pour du bytecode :

CALL check_pin MOV R0 , # pin MOV A , #0F0H / / i n s t e a d of R7 MOV @R0, A MOV R7 , # pin RET Assembleur pin = check_pin (1234,0000) ; pin = 0x0F0; returnpin; Code source i n v o k e s t a t i c #3 i s t o r e _ 0 i l o a d _ 1 / / i n s t e a d of iload_0 i r e t u r n Bytecode

L’écrasement d’un opcode par une valeur fixe est un exemple classique de la littérature [163,149,136] : elle consiste à remplacer une opération par un NOP (qui peut être une valeur particulière au niveau binaire, par exemple des 0 ou des 1). Au niveau d’un langage interprété, la machine virtuelle peut se rendre compte d’un problème de consistance de la pile, comme dans l’exemple ci-dessous qui illustre l’effet d’un NOP sur les trois niveaux de représentation : NOP MOV R0 , # pin MOV A , R7 MOV @R0, A MOV R7 , # pin RET Assembleur returnpin; Code source i n v o k e s t a t i c #3 i s t o r e _ 0 nop / / i n s t e a d of iload_0 i r e t u r n / / oops ! Bytecode

Cependant, cela suppose que le système considéré dispose d’un vérificateur de bytecode, ce qui peut ne pas être le cas. Pour des programmes natifs, il n’y a en général pas de vérification de ce type, et le remplacement d’un opcode par un NOP permet de réaliser le saut d’une instruction, réalisant une attaque contre le flot de contrôle.

1.2 Temporalité de l’attaque

Suivant la manière dont l’attaquant influe sur le programme ou les données de celui-ci, l’attaque n’aura pas la même portée temporelle.

Si l’attaquant modifie les opcodes ou opérandes du programme dans le conteneur matériel qui les stocke (Flash, ROM, RAM, cache) [172], l’attaque sera persistante temporellement. A chaque fois que le processeur exé-cutera cette partie du code en le chargeant depuis le conteneur matériel, alors les opcodes ou opérandes modifiés

1.3 Modèles d’attaque du flot de contrôle par l’attaquant seront exécutés. Ce type d’attaque est une attaque dite “permanente”. Elle persiste pendant toute la durée de l’exécution du programme (sauf pour une attaque de cache ou l’attaque persiste sur une certaine durée) et s’apparente plus au chargement d’un programme malveillant qu’à la modification lors de l’exécution d’un code sain.

Inversement, l’attaque peut produire des effets qui ne sont pas persistants temporellement. On parle alors d’attaque “transiente” [90,166] pour signifier que l’effet de bord sur le programme obtenu par l’attaquant ne persiste pas au-delà de l’instant où l’attaque est perpétrée. Si, par exemple, l’attaque s’appuie sur la modification des instructions qui circulent sur le bus de chargement du programme avant leur exécution sur le processeur, alors une attaque pendant un premier chargement d’une portion de code n’aura pas d’influence sur le prochain chargement de cette même portion de code. De même, une frame comportant des variables locales modifiées n’aura pas de conséquence sur cette même frame pour un prochain appel de fonction. Ainsi, de nombreux cas où l’attaque est dynamique, c’est-à-dire réalisée au moment de l’exécution du programme sera transiente si elle touche des ressources du programme qui sont elles-mêmes temporaires (frame, registre, bus de donnée, etc.).

1.3 Modèles d’attaque du flot de contrôle

Nous proposons dans cette section trois modèles d’attaque du flot de contrôle [103,3]. La particularité de ces modèles est de corrompre le flot normal du programme tout en préservant les données manipulées par le programme. Ces modèles peuvent paraître équivalents, mais nous montrons par la suite qu’ils ne permettent pas de représenter les mêmes ensembles d’attaques réelles.

Modèle d’attaque du flot de contrôle au niveau C Au niveau source, nous avons proposé de représenter

une telle attaque par l’insertion d’une instruction goto label; au sein du code source comme montré en listing3.1. Lorsque l’attaque n’est pas permanente sur le chemin d’exécution, on implémente une variante qui permet de déclencher l’attaque au moment voulu, comme montré en listing3.2.

intf () { ... gotolabel ; ... ... label : ... }

Listing 3.1 – Attaque permanente en C

int attack_trigger = false ; /∗ is the attack on? ∗/ inttime_is_reached = false ; /∗ synchronization condition ∗/ inttime_is_over = false ; /∗ stop condition ∗/

intf () { ...

if (! attack_trigger && time_is_reached && !time_is_over) {

/∗ perform the attack ∗/ attack_trigger = true ; gotolabel ; } ... label : }

Listing 3.2 – Attaque transiente en C

Un tel modèle d’attaque n’a de sens que pour des sauts intra fonction (et non inter fonctions). Toutefois, il est réaliste car un saut en dehors d’une fonction met en général le programme dans un état instable car le contenu de la pile est inconsistant.

Modèle d’attaque du flot de contrôle au niveau assembleur Au niveau assembleur, une démarche similaire

permet d’implanter des sauts inconditionnels à l’aide de l’opcode jmp. Lorsque l’attaque est transiente, le code se complexifie. Il est donné en annexe1: il consiste à sauter sur une portion de code .hack qui va sauvegarder l’état des registres, évaluer si l’attaque doit être déclenchée, puis soit revenir en séquence dans le code initial, soit sauter à la destination de l’attaque .desthack.

. file "main.c" . text ... jmp .desthack ... ... .desthack: ... }

Listing 3.3 – Attaque permanente en assembleur ..

.debuthack:

/∗ ∗ Saut vers .hack pour evaluer le declenchement ∗ ∗/ jmp .hack . finhack : ... ... /∗ ∗ Destination de l ’ attaque ∗ ∗/ .desthack: