• Aucun résultat trouvé

2.3 Un modèle orienté autour des variables

2.3.1 Présentation du modèle sur un exemple

Le très déployé serveur SSH libre OpenSSH [OSSH] fut sujet en 2001 (versions 2.2 ou antérieures) à une vulnérabilité de type dépassement de capacité d’un entier. En effet, dans une fonction utilisée par ce dernier pour calculer une somme de contrôle, un entier d’une taille de 2 octets reçoit comme valeur le résultat d’un calcul utilisant des données envoyées par le client et qui peut atteindre une taille de 4 octets [Zal01]. Le code de la figure 2.3 (colonne de gauche) est inspiré de cette version particulière de OpenSSH et reproduit la structure de base de la partie du véritable code qui nous intéresse. Nous utiliserons dans la suite de cette section cet exemple pour présenter et illustrer notre approche.

2.3.1.1 Description de la vulnérabilité

Le code de la figure 2.3 reproduit la dernière partie de la phase d’authentification du server SSH. Lorsqu’une demande d’authentification est reçue par le programme, celui-ci charge en mémoire la structure de données associée à l’utilisateur corre-spondant à la demande d’authentification. C’est cette structure qui contient entre autres son mot de passe et son uid. Si le mot de passe de l’utilisateur est vide, alors l’authentification est immédiatement validée par le programme (l’exécution du code passe directement la ligne 22).

00. void do_authentication() 01.{

02. int ok = 0; ...

03. if(!strcmp(pwd, ""))

04. /* users with no password */ 05. else 06. /* do_authloop(); */ 07. while(ok != 1) { 08. 09. type = packet_read(buf); 10. 11. switch (type) { ... 12. case CMSG_AUTH_PWD: 13. 14. 15. ok = auth_pwd(pwd, buf); 16. break; ... 17. } 18. } 19. 20. 21. 22. do_authenticated(user); 23. } 00. void do_authentication() 01. { 02. int ok = 0; ... 03. if(pwd[0] != ’\0’)

04. /* users with no password */ 05. else 06. /* do_authloop(); */ 07. while(ok != 1) { 08. 09. type = packet_read(buf); 10. 11. switch (type) { ... 12. case CMSG_AUTH_PWD: 13. 14. assert(pwd[0] != ’\0’); 15. ok = auth_pwd(pwd, buf); 16. break; ... 17. } 18. } 19. 20. assert(ok==0 OR (ok==1 21. AND type==CMSG_AUTH_PWD)); 22. do_authenticated(user); 23. }

Figure2.3 – Exemple d’attaques contre les données de calcul inspiré de la version vulnérable de OpenSSH

Dans le cas contraire, le serveur va exécuter en boucle des instructions qui vont recevoir d’autres informations de la part de l’utilisateur distant. Ce qui nous intéresse dans cet exemple, c’est le cas où le serveur reçoit une demande d’authentification par mot de passe (il existe d’autres méthodes d’authentification, par clé RSA par exemple). Si le mot de passe fourni par l’utilisateur qui demande à s’authentifier est correct, alors l’exécution de la boucle se termine (et le programme exécute main-tenant le code à partir de la ligne 22).

Il faut savoir que, durant cette phase d’authentification, la fonction responsable de la réception des données envoyées par l’utilisateur distant (appelée à la ligne 09 (packet_read)) fait appel à la fonction de calcul de somme de contrôle alors que cette dernière présente une vulnérabilité. Dans cette fonction vulnérable, la

don-née concerdon-née par le dépassement d’entier est utilisée pour calculer un masque qui est lui-même utilisé pour empêcher une opération d’écriture de sortir des limites d’un tableau. Un utilisateur malveillant peut dès lors utiliser cette faute dans le pro-gramme pour modifier de manière non prévue le masque afin de pouvoir modifier des données à n’importe quel emplacement mémoire. Pekka et Kalle [PK02] fournissent une analyse détaillée de cette vulnérabilité.

2.3.1.2 Les scénarios d’attaque

Cette vulnérabilité peut par exemple être utilisée durant l’exécution de la boucle do_authloop de manière à forcer la variable pwd à présenter comme valeur une chaîne de caractère vide. Ainsi, un utilisateur malveillant aura la possibilité de s’au-thentifier sur le système avec en se faisant passer pour l’utilisateur de son choix (par exemple, root) sans avoir à fournir le bon mot de passe. Une explication détaillée d’une telle exploitation de la faille a été réalisée par Starzetz [Sta01]. Notons que le nom d’utilisateur utilisé pour réaliser l’attaque doit nécessairement correspondre à un compte existant sur le système ciblé. En effet, un nom d’utilisateur inexistant ne permet pas d’atteindre cette portion de code.

Une autre manière permettant de forcer la réussite de la phase d’authentification est de forcer la valeur de la variable de boucle ok. Il s’agit de la variable qui permet de quitter la boucle d’authentification. Celle-ci ne peut normalement être égale à 1 que si l’authentification de l’utilisateur est réussie. Pour contourner cela, il faut que l’utilisateur malveillant envoie des informations dont le traitement ne modifie pas cette variable (c’est-à-dire tout sauf une demande d’authentification, on peut par exemple demander un nouvel échange de clés de chiffrement). Il est alors possible de forcer la valeur de ok à 1 en reposant sur la même vulnérabilité que celle utilisée précédemment. À nouveau, on peut ainsi s’authentifier sur le système avec n’importe quel compte connu.

2.3.1.3 La détection de l’intrusion

Cependant, dans les deux cas l’exploitation de la vulnérabilité met l’espace mé-moire du processus dans un état incohérent. En effet, dans le cas de l’attaque qui modifie la variable pwd, si le mot de passe de l’utilisateur concerné était effective-ment vide en premier lieu, alors jamais le serveur n’aurait dû exécuter la boucle d’authentification. Autrement dit, si le flot d’exécution atteint cette boucle d’au-thentification, alors le mot de passe ne peut pas être une chaîne de caractères vide. Si on vérifie cette contrainte avant l’utilisation de la variable pwd alors cette pre-mière attaque est détectée. Notons que la vérification est placée juste avant l’appel de fonction concernée. Ceci nous permet d’être sûr que si attaque il y a, alors celle-ci ne peut avoir lieu après la vérification (on suppose que le code qui effectue la vérification n’est pas attaquable).

Dans le cas de l’attaque qui modifie la variable de boucle ok, celle-ci ne peut valoir 1 que si le dernier message reçu est une demande d’authentification. Si on vérifie cette contrainte avant l’appel à la fonction qui autorise l’accès au système (do_authenticated), alors cette seconde attaque est aussi détectée. À nouveau et pour les mêmes raisons, la vérification est placée juste avant l’appel de fonction concernée.

L’objectif de notre approche pour la détection est donc de déduire du code source ce type de contraintes puis de les traduire sous forme d’assertions exécutables à insérer dans ce code source. La figure 2.3 (colonne de droite) présente les assertions nécessaires à la détection de ces attaques dans le cas de notre exemple. Dans le cas de la première attaque, l’invariant à vérifier est le suivant : pwd[0]! = \0. Dans le cas de la seconde attaque, l’invariant à vérifier est le suivant : ok == 0 OR (ok == 1 AN D type == CM SG_AU T H_P W D). Le modèle de détection que nous proposons dans ces travaux de thèse repose sur le calcul automatisé de ce type de contraintes.

2.3.1.4 Formalisation du modèle

Les données de calcul sont utilisées par le programme pour contenir la valeur des variables présentes dans son code source. Nous définissons l’état interne d’un pro-gramme comme l’ensemble des éléments mémoire utilisés pour contenir ces variables. Ces dernières peuvent être liées entre elles (par exemple, deux variables particulières doivent être égales), ou posséder des propriétés qui leur sont propres (par exemple, une variable ne peut prendre qu’un nombre restreint de valeurs). Ces propriétés sont des invariants et constituent la base de notre modèle. Notre objectif est de compléter les mécanismes de détection efficaces vis-à-vis des attaques contre les données de con-trôle (basés sur la vérification des séquences d’appels système) en se concentrant sur l’exactitude des données de calcul. Notre approche consiste donc en la recherche d’invariants au sein d’un programme afin de détecter des corruptions de variables induites par une anomalie.

Cependant, contrairement aux autres méthodes et ce dans un souci de perfor-mance, nous ne voulons pas vérifier de telles propriétés en tout point du programme et pour l’ensemble des variables. Ce que nous voulons, c’est seulement les vérifier sur les chemins menant aux appels système et seulement pour le sous-ensemble de variables dont dépendent ces appels. Cet ensemble constitue en effet l’ensemble des cibles potentielles pour un attaquant. Un appel système dépend de deux types de variables : les variables utilisées pour calculer les arguments qui lui sont passés en paramètre et les variables utilisées pour atteindre cet appel (par exemple, les variables utilisées lors d’une évaluation conditionnelle sur un chemin menant à cet appel).

Nous pouvons donc définir pour un appel système donné ASison modèle de com-portement par le triplet (ASi, Vi, Ci) où Vi est l’ensemble des variables dont dépend

l’appel système et Ci l’ensemble des contraintes que ces dernières doivent respecter. Nous définissons alors le modèle de comportement normal du programme MCN comme l’ensemble des ASi , soit MCN = {∀i, (ASi, Vi, Ci)}. Une attaque contre les données de calcul consiste alors à modifier la valeur d’une ou plusieurs variables appartenant à Vi. Une ou plusieurs des contraintes appartenant à Ci peuvent alors être violées. Un programme est donc considéré dans un état correct tant que toutes ses contraintes sont vérifiées durant son exécution. Une intrusion est détectée dès lors qu’au moins une seule des contraintes devient fausse.

Pour pouvoir construire de manière automatique un tel modèle, deux problèmes doivent être résolus par des techniques d’analyse statique : comment déterminer les variables sur lesquelles doivent porter les contrôles et comment déterminer les contraintes que doivent vérifier ces contrôles. Nous répondons à ces questions dans les deux sections suivantes.