Rechercher
Contactez-nous Suivez-nous sur Twitter En francais English Language
 

De la Théorie à la pratique











Abonnez-vous gratuitement à notre NEWSLETTER

Newsletter FR

Newsletter EN

Vulnérabilités

Se désabonner

Jane Goh, Coverity : Pourquoi gérer les problèmes de sécurité au plus tôt dans le cycle de développement

octobre 2012 par Jane Goh, Product Marketing Manager chez Coverity

En limitant le nombre de primitives dans le code, les développeurs peuvent rendre bien plus difficile l’exploitation des logiciels par les pirates, augmentant ainsi le coût de cette activité et réduisant donc sa probabilité.

Les logiciels sont essentiels, dans la vie courante comme dans des secteurs clés tels que la défense ou l’aéronautique. Devenus des éléments critiques de nombreux systèmes, ils doivent être fiables et sûrs et il faut savoir que les problèmes de sécurité peuvent être tout aussi dangereux que les problèmes fonctionnels.

La plupart des techniques qui visent à réduire les défaillances fonctionnelles et les problèmes de qualité peuvent également réduire les vulnérabilités des logiciels. En matière de développement de logiciels, les problèmes de sécurité doivent être traités comme des défauts et intégrés dans le cadre du processus de développement. De fait, la distinction entre la sécurité et la qualité peut parfois être subtile, en effet, une erreur système aujourd’hui risque d’être exploitée par un pirate demain.

Les défauts sont essentiellement l’exploitation potentielle de primitives1, qui peuvent être alignées de façon créative par des pirates pour monter une attaque. En en éliminant le plus possible, les développeurs rendront donc bien plus difficile l’exploitation du logiciel. L’exemple suivant illustre comment on peut enchaîner plusieurs primitives pour exécuter du code à distance.

Exemple d’une attaque basée sur plusieurs primitives

Supposons l’existence d’une vulnérabilité dans le code hébergé sur un serveur distant. Alors qu’il suffit d’en identifier la cause pour remédier à la faille, le succès de son exploitation dépend de nombreuses conditions préexistantes. Dans le cadre de cet exemple, nous supposons qu’un pirate vise l’exécution de code à distance (Remote Code Execution ou RCE), c’est-à-dire à faire tourner le programme qu’il veut sur la machine distante. Bien que le déclenchement de la faille de sécurité soit nécessaire pour réussir la RCE, mais le but final nécessite en fait de nombreuses petites étapes, que nous appelons des primitives d’exploitation. En enchaînant ces primitives, le pirate peut créer une attaque qui fonctionne de manière fiable et préserve la stabilité du système après avoir été lancée.

Dans notre exemple, l’attaquant utilise quatre primitives différentes, mais il n’est pas limité à celles-ci. Il commence par une primitive de soft leak2, qui utilise une fonctionnalité normale du programme pour manipuler la mémoire de l’application attaquée, sans répercussion sur sa stabilité ni sa sécurité. Les primitives de ce type sont les plus courantes car elles s’appuient sur des fonctionnalités prévues et voulues du programme. Par exemple, un serveur est conçu pour accepter des requêtes d’un client et le client envoie des informations qui sont conservées jusqu’à la fin de sa session. Un pirate peut faire certaines suppositions sur l’agencement mémoire d’une application donnée, à partir de ses fonctionnalités et en imaginant comment fonctionnent ces requêtes et ces sessions.

La primitive suivante sera un hard leak2. Ce hard leak, ou fuite de ressource, est assez bien connu des développeurs C/C++. Il survient lorsque le programmeur oublie de libérer la mémoire allouée dynamiquement lors de l’exécution du programme. La plupart des programmeurs considèrent que c’est un problème de qualité, qui se traduira -au pire- par une surconsommation de mémoire. Cependant, pour le pirate talentueux, c’est une excellente occasion de garantir la stabilité de son attaque. Il peut en effet s’assurer que certaines parties de la mémoire ne seront plus jamais réutilisées durant le cycle de vie d’un processus, en se les appropriant de manière permanente.

La troisième primitive est un integer overflow ou dépassement d’entier. Si un calcul tente de stocker un nombre plus grand que peut coder un entier, le « supplément » est perdu. Cette perte de données en trop est parfois appelée rebouclage ou débordement (wrap) d’entier. Par exemple, un entier non signé sur 32 bits est limité à une certaine valeur positive maximale. En ajoutant 1 à cette valeur, l’entier revient à zéro (UINT_MAX + 1 == 0). Un exemple courant d’un tel phénomène est le compteur kilométrique d’un véhicule, qui revient à zéro après avoir atteint 999999. En utilisant cet entier « en débordement », l’attaquant peut tromper une routine d’allocation et allouer moins de mémoire que prévu.

La dernière primitive sera le classique dépassement buffer ou buffer overflow. C’est le problème le plus courant susceptible d’entraîner des failles de sécurité dans les programmes en C/C++. Un dépassement de tampon survient lorsqu’un programme écrit au-delà de la taille d’un tampon, modifiant ainsi la mémoire adjacente. Dans certains cas, ceci permet au pirate de modifier le contenu de la pile ou du tas, afin de détourner le fonctionnement normal du système et, à terme, de prendre le contrôle à partir du programme.

Utilisation de primitives dans la RCE

Maintenant que nous avons présenté les types de primitives utilisées, voyons comment le pirate de notre exemple s’en sert pour arriver à exécuter du code distant. D’abord, il utilise les fonctionnalités du programme pour envoyer des requêtes parfaitement légitimes, qui allouent de nombreux segments de mémoire, en fonction de la taille de ce qu’il envoie. Cette action peut sembler bénigne, mais elle est vitale pour l’organisation déterministe de la pile : il s’agit de mettre la mémoire d’une application dans un état bien connu, obligatoire pour exploiter les dépassements de tampons visant la pile. Ensuite, le pirate fait en sorte qu’une partie de la mémoire ne soit plus jamais libérée après avoir été allouée. Il utilise les fuites de ressources de l’application pour disposer d’une mémoire qui persiste toute la vie du processus, s’assurant d’une plus grande stabilité après l’attaque. Le dépassement d’entier déclenché conduit à un dépassement de tas sous-alloué, qu’il est facile de déborder. Ceci entraîne un décalage entre la taille réelle du tampon alloué et le nombre d’éléments de données qu’il est supposé pouvoir contenir. Le pirate peut alors utiliser un dépassement de tampon pour écraser la mémoire adjacente. À titre d’analogie, supposons que l’on soit incapable de reconnaître la dernière ligne d’une feuille de papier réglé. On continuera à écrire des phrases, y compris sur le bureau et même sur sa belle chemise toute neuve. En écrasant ainsi la mémoire adjacente, le pirate peut remplacer des informations importantes par des données qu’il contrôle.

Le fait d’enchaîner des primitives, même relativement bénignes, permet de mieux contrôler l’attaque et les fonctionnalités qui en résultent (figure 1). Si notre pirate n’avait pas pu créer des fuites de ressources au sein de l’application, il lui aurait fallu trouver une autre méthode pour s’assurer que cette mémoire ne sera pas libérée à la fin de sa session, ou bien il aurait fini par comprendre qu’il était impossible d’éviter un plantage du programme. Et en l’absence de débordement d’entier, il n’aurait même pas pu commencer à monter son attaque.

Figure 1 | Un attaquant enchaîne des primitives pour lancer l’exécution d’un code arbitraire. En limitant le nombre de primitives dans le code, les développeurs peuvent rendre bien plus difficiles l’exploitation des logiciels par les pirates, augmentant le coût de cette activité et réduisant sa probabilité. La liaison entre l’exploitation des primitives et les vulnérabilités peut être directe ou indirecte. Certains types de primitives, comme les dépassements de tampons, peuvent conduire à divers types de vulnérabilités selon les compétences, la créativité et la détermination de l’attaquant. Ce qui est indiscutable, c’est que plus il dispose de primitives, plus il lui sera facile de s’appuyer sur des vulnérabilités plus graves et de construire des attaques plus dommageables. Par conséquent, le fait de découvrir et d’éliminer un grand nombre de ces primitives, très tôt au cours du développement, peut contribuer largement à réduire les risques de vulnérabilité et les coûts de maintenance durant la vie de l’application.

Une approche pratique pour sécuriser le développement

Développer un logiciel fiable et sûr est un challenge pour les équipes informatiques, alors que l’intégration précoce des tests de sécurité n’est pas encore largement adoptée au processus de développement. Les développeurs souhaitent évidemment concevoir des programmes sûrs mais ils sont focalisés sur les nouvelles fonctions et fonctionnalités et sont souvent soumis à de très fortes pressions pour tenir les délais. Et, outre le manque d’incitation financière pour renforcer la sécurité, les développeurs ne sont généralement pas formés pour être des experts en sécurité. De plus, les cursus informatiques forment de bons programmeurs, avant des experts en sécurité. Par conséquent, les développeurs aujourd’hui n’ont pas souvent conscience des très nombreuses façons dont ils peuvent introduire des vulnérabilités dans leurs programmes et ils n’ont pas les moyens de les corriger lorsqu’elles sont identifiées.

Par ailleurs, les solutions de test de développement doivent être conçues du point de vue du développeur, ce qui implique d’éliminer les principaux problèmes qui ont éloigné les développeurs des outils classiques d’évaluation de la sécurité : difficultés d’utilisation et taux élevé de faux positifs. Les chefs de projet qui veulent intégrer les tests de la sécurité à leurs processus doivent rechercher des outils de test automatisés, capables de :

• Présenter des défauts clairement expliqués, avec le minimum de « bruit de fond ». Les développeurs n’ont pas de temps à perdre à trier des résultats intempestifs ou à chercher à reproduire des défauts qui n’existent pas. Ils ont besoin de défauts faciles à comprendre, avec le minimum de faux positifs.

• Détecter les défauts très tôt et souvent, au fur et à mesure de l’écriture du code. C’est une lourde tâche que de déterminer la cause exacte d’un défaut et sa correction peut imposer d’importantes modifications de l’architecture. Le fait de découvrir les défauts critiques le plus tôt possible permet d’anticiper la charge de travail et l’impact sur la date de publication, ce qui réduit le coût du projet.

• Proposer des conseils exploitables et justes sur la façon de corriger les défauts. En général, les solutions d’évaluation de la sécurité fournissent des conseils de correction des défauts qui ne sont pas personnalisés pour le framework utilisé par le logiciel, le langage ou les bibliothèques. Les développeurs ont du mal à traduire ces conseils généraux en un correctif efficace, ce qui conduit souvent à une correction inadéquate ou incomplète, nécessitant de recommencer.

Les défauts font partie intégrante du développement de logiciel. Même s’il est extrêmement difficile d’éliminer totalement l’introduction de vulnérabilités lors de la programmation, les développeurs disposent maintenant de solutions et de processus pour faciliter la découverte et la correction de ces défauts, aussi rapidement et efficacement que possible.


Références :

1. 1999. M. Bishop, “Vulnerabilities Analysis”, Proceedings of the Second International Symposium on Recent Advances in Intrusion Detection, pp. 125–136 (sept. 1999).




Voir les articles précédents

    

Voir les articles suivants