• Aucun résultat trouvé

3.2 Implémentation du mécanisme d’injection

4.1.1 Scénarios d’exécution

4.1.1.3 Cas problématiques

En écrivant les scénarios, aussi bien pour le serveur que pour le client, nous avons remarqué que certains points d’injection étaient très difficiles voir impossibles à at-teindre durant l’exécution du programme. Nous avons alors identifié deux catégories d’injections qui posent ce problème d’atteignabilité. Tout d’abord, celui-ci se pose pour les mécanismes d’injection qui se trouvent dans des blocs d’instructions dédiés au traitement des erreurs liées à l’environnement d’exécution. La figure 4.2 illustre cela dans le cadre d’une tâche prise en charge par un fil d’exécution dédié. Le code original est situé dans la colonne de gauche. On peut voir que le programmeur a pris soin de vérifier que la création à la ligne 02 d’un nouveau fil d’exécution s’est bien déroulée correctement. Cette vérification a lieu à la ligne 07 (la valeur retournée par l’appel de fonction ne doit pas valoir −1). En cas d’erreur, le programme fait appel à la fonction exit_error qui arrête proprement l’exécution du programme après avoir enregistré l’erreur dans le fichier journal.

00. int ret; 01. 02. ret = pthread_create(handle 03. &thr_attr, 04. htloop, 05. NULL); 06. 07. if (ret == -1){ 08. 09. 10. 11. exit_error("pthread_create"); 12. } 00. int ret; 01. 02. ret = pthread_create(handle 03. &thr_attr, 04. htloop, 05. (void *)0); 06. 07. if (ret == -1){ 08. 09. inject(0x0000000b, 1, 10. &ret, sizeof(int)); 11. exit_error("pthread_create"); 12. }

Figure 4.2 – Exemple d’erreur difficile à reproduire

Ce même code, une fois instrumenté par notre outil, est situé dans la colonne de droite. On peut voir qu’un mécanisme d’injection a été placé à la ligne 09 juste

avant l’appel à la fonction exit_error. En effet, la valeur −1 fait partie des valeurs que peut retourner un appel à la fonction pthread_create. L’analyse de valeur a donc considéré que ce point particulier du programme n’est pas du code mort et qu’il est parfaitement atteignable durant l’exécution du processus. De plus, on trouve parmi les dépendances de cet appel la variable entière ret, dont la valeur participe aux décisions qui permettent d’atteindre ce point particulier du programme. On peut donc bien ajouter un mécanisme d’injection à cet endroit.

Cependant, cette injection est particulièrement difficile à atteindre. En effet, pour y arriver, le scénario de tests doit pouvoir mettre le système dans un état qui ferait échouer la création du nouveau fil d’exécution (par exemple en saturant la mémoire de la machine afin de faire échouer les allocations mémoire effectuées par la fonction pthread_create). De plus, cet état ne doit être atteint que pour cet appel de fonction précis. L’atteindre plus tôt c’est prendre le risque que l’exécution du programme échoue avant d’avoir atteint le point d’injection désiré. Une première solution serait de modifier l’analyse de valeur pour qu’elle suppose que la fonction pthread_create n’échoue jamais. Si cette solution marche pour l’exemple de la figure 4.2, elle n’est pas satisfaisante dans le cas général.

En effet, cela suppose d’une part que tout code de gestion d’erreur suite à un appel de fonction dont on décide qu’il ne peut pas échouer sera considéré comme du code mort. Pourtant, on peut envisager qu’une injection fasse échouer un de ces appels. Supposons par exemple que le cas de la figure 4.2 s’inscrivent dans une fonction de plus grande taille. Si dans ce cas un mécanisme d’injection en amont dans le code venait à corrompre l’un des paramètres de l’appel à la fonction pthread_create ligne 02, ce dernier pourrait échouer. Si l’injection n’est pas détectée immédiatement, on peut envisager qu’une assertion située dans le code de gestion d’erreur la détecte par propagation de l’erreur. Mais si ce code est considéré comme mort par l’analyse de valeur, alors aucun ensemble de variation ne sera calculé et donc il ne pourra pas y avoir d’assertion.

D’autre part, cette solution n’est envisageable dans le cas de la figure 4.2 que parce que la fonction concernée, pthread_create, est une fonction standard du sys-tème. En effet, pour ce type d’appel de fonction, l’analyse de valeur a connaissance de l’ensemble de valeurs qui peut être retourné grâce à l’écriture d’un stub corre-spondant pour chaque fonction (voir section 2.4.2.1). Il suffit alors de modifier en conséquence le stub des fonctions concernées pour que l’analyse de valeur considère que ces dernières n’échouent jamais. Cependant, cette situation peut également se produire pour des fonctions dont le code est fourni à l’analyse de valeur, notamment à cause des sur-approximations effectuées par l’interprétation abstraite. Cette solu-tion n’est donc bien pas envisageable dans le cas général, ce qui la rend inutilisable dans le cadre d’une approche automatisée.

Notons toutefois que dans le cas d’un code de gestion d’erreur suite à un appel à une fonction standard, nous avons envisagé l’utilisation d’une bibliothèque de fonctions personnalisée. Celle-ci serait chargée à l’exécution via le mécanisme de

LD_PRELOAD et aurait pour but d’intercepter les appels de fonction qui nous intéressent afin de les faire échouer à un moment choisi par le moniteur d’exécution. Nous avons cependant choisi de ne pas complexifier l’écriture de nos scénarios avec un tel mécanisme. En effet, ce type d’appel à la fonction d’injection représente un très faible nombre du total des points d’injection. Nous avons d’ailleurs pu atteindre dans tous les cas des taux de couverture des injections supérieurs à 80% sans avoir recours à ce stratagème.

Abordons maintenant l’autre type d’injections que nous avons identifié comme posant un problème d’atteignabilité. Il s’agit des mécanismes d’injection qui se trou-vent dans des blocs d’instructions qui ne seront jamais exécutés à cause d’une faute de conception. La figure 4.3 illustre cela par un exemple extrait du serveur web ihttpd [IHTTP]. Ce serveur web n’implémente que les requêtes de type GET et POST. D’après la norme HTTP 1.0, il est censé renvoyer une erreur 501 si le type de requête demandée n’est pas implémentée. Dans le code original qui se trouve en haut dans la colonne de gauche, on peut voir que le développeur s’est trompé dans la vérification de cette condition. Une même chaîne de caractères ne pouvant à la fois être égale à "GET" et "POST", le code qui retourne la valeur 501 ne sera donc jamais atteint.

00. if (strcasecmp(mt, "GET") == 0 01. && strcasecmp(mt, "POST") == 0){ 02.

03. logErr("%s unsupported", mt); 04. return 501; /*unimplemented*/ 05. }

00. if (strcasecmp(mt, "GET") != 0 01. && strcasecmp(mt, "POST") != 0){ 02. 03. logErr("%s unsupported", mt); 04. return 501; /*unimplemented*/ 05. } 00. int tmp_0; 01. int tmp_1; 02. 03. no_inject(); 04. tmp_0 = strcasecmp(mt, "GET"); 05. 06. if (tmp_0 == 0){ 07. 08. inject(0x00000c07, 1, 09. &(tmp_0), sizeof(int)); 10. tmp_1 = strcmp(mt, "POST"); 11. 12. if (tmp_1 == 0){ 13. 14. inject(0x00000c0a, 2, 15. &(tmp_0), sizeof(int), 16. &(tmp_1), sizeof(int)); 17. logErr("%s unsupported", mt); 18. 19. __retres = 501; 20. goto return_label; 21. } 22. }

Ceci n’empêche nullement le serveur web de fonctionner correctement. En ef-fet, d’autres vérifications, qui se trouvent plus loin dans le code du programme, compensent cette faute de conception. Cependant, l’analyse de valeur, même en lui fournissant une implémentation de la fonction strcmp et non un stub, n’est pas ca-pable de détecter que ce bloc de code est une portion de code mort. Le code, une fois instrumenté par notre outil, se trouve dans la colonne de droite de la figure 4.3. On peut voir qu’une injection a pu être ajoutée juste avant l’appel à la fonction logErr à la ligne 17 (dans le code original, cet appel se trouve à la ligne 03). Cette injection se trouvant dans la portion de code mort, il n’est pas possible pour un scénario d’exécution de l’atteindre.

Pour contourner le problème, nous pourrions corriger cette faute en modifiant le code source du programme de manière à obtenir ce que l’on voit en bas de la colonne de gauche de la figure 4.3. Toutefois, rappelons que le but de nos campagnes de tests est d’évaluer un système de détection d’intrusion qui fonctionne au niveau applicatif et ce malgré les vulnérabilités présentes dans les programmes. Nous faisons donc le choix, dans ce type de cas, de ne pas corriger les fautes de conception afin que le contexte d’évaluation de notre approche pour la détection soit la plus proche possible d’une situation réelle.