Sus aux saturations de tampon !

Michael Howard
Microsoft Corporation
Mis à jour le 28 mai 2002

Quand David LeBlanc et moi-même avons défini la table des matières de notre livre intitulé Writing Secure Code Lien non MSDN France, il allait sans dire que nous devions nous pencher sur la question de la saturation du tampon. En effet, les développeurs font trop souvent des erreurs de code qui entraînent des saturations de tampon exploitables. Dans cet article, je m'intéresserai à ce qui rend le problème si grave, aux raisons de son existence et à la façon de le résoudre.

Les raisons de l'existence des saturations de tampon

Il y a saturation du tampon lorsque plusieurs événements sont réunis :

  • utilisation d'un langage sans sécurité de type comme C/C++ ;
  • accès au tampon ou copie du tampon non sécurisés ;
  • le compilateur place les tampons à côté ou près de structures de données critiques dans la mémoire.

Étudions chacun de ces événements en détail.

Les saturations de tampon tiennent avant tout à l'utilisation de C et de C++, car ces langages ne contrôlent pas les limites des tableaux et ne vérifient pas la sécurité de type. C/C++ permet au développeur de créer des programmes qui s'exécutent très près du métal, ce qui offre un accès direct à la mémoire et aux registres de la machine. Cela se traduit dans les performances ; il est difficile de créer une application qui s'exécute aussi vite qu'une application en C/C++ bien écrite. Les saturations de tampon peuvent se produire avec d'autres langages mais cela est rare. Et si un tel bogue existe, ce n'est généralement pas la faute du développeur mais plutôt de l'environnement d'exécution.

Ensuite, si l'application prend des données provenant d'un utilisateur (ou d'un attaquant) et les copie sur un tampon maintenu par l'application sans se préoccuper de la taille du tampon de destination, le tampon risque de saturer. En d'autres termes, le code N octets mais copie plus de N octets dans le tampon alloué. Imaginez que vous avez un verre d'une contenance de 25 centilitres et que vous y versiez 33 centilitres d'eau. Ou irons les 8 centilitres de trop ? Ils déborderont partout !

Enfin et surtout, les tampons sont souvent placés par le compilateur près de structures de données " intéressantes ". Par exemple, dans le cas d'une fonction qui a un tampon sur la pile, l'adresse de retour de la fonction est placée dans la mémoire après le tampon. Par conséquent, si l'attaquant peut saturer le tampon, il peut remplacer l'adresse de retour de la fonction de sorte qu'à son retour, la fonction aille à l'adresse spécifiée par l'attaquant. Il existe d'autres structures de données intéressantes : les vtables C++, les adresses du gestionnaire des exceptions, les pointeurs de fonctions, etc.

Ok, je parle trop, alors passons à un exemple.

Qu'est-ce qui ne va pas dans ce code ?

void CopyData(char *szData) {
 char cDest[32];
 strcpy(cDest,szData);

 // utiliser cDest
 ...
} 

Curieusement, ce code n'est pas forcément problématique ! Tout dépend de comment CopyData() est appelé. Par exemple, le code suivant est sûr :

char *szNames[] = {"Michael","Cheryl","Blake"};
CopyData(szName[1]);

Ce code est sûr car les noms sont codés en dur et que l'on sait que chaque chaîne ne dépasse pas 32 caractères. Par conséquent, l'appel vers strcpy est toujours sûr. Toutefois, si l'argument unique pour CopyData(), szData provient d'une source non digne de confiance comme un socket ou un fichier, alors strcpy copiera les données jusqu'à ce qu'il rencontre un caractère null, et si les données dépassent 32 caractères, le tampon cDest sera saturé et toutes les données dépassant la capacité de mémoire du tampon seront corrompues. Malheureusement, dans ce cas, l'information corrompue est l'adresse de retour de CopyData(), ce qui veut dire que quand CopyData() se terminera, l'exécution se poursuivra à l'endroit imposé par l'attaquant. Dommage !

D'autres structures de données sont aussi sensibles. Imaginez que la vtable d'une classe C++ soit corrompue dans le code :

void CopyData(char *szData) {
 char cDest[32];
 CFoo foo;
 strcpy(cDest,szData);

 foo.Init();
}

Cet exemple part du principe que la classe CFoo a des méthodes virtuelles ainsi qu'une vtable ou une liste d'adresses pour les méthodes de classes communes à toutes les classes C++. Si la vtable est endommagée lors du remplacement du tampon cDest, alors toute méthode virtuelle de la classe, Init() dans cet exemple, pourra appeler une adresse imposée par l'attaquant à la place de Init(). D'ailleurs, ne croyez pas que votre code est sûr si aucune méthode C++ n'est appelée ; sachez qu'il existe une méthode qui est toujours appelée : le destructeur virtuel de la classe ! C'est évident, le jour où vous verrez une classe qui n'effectue pas d'appels de méthodes, demandez-vous pourquoi cette classe existe.

Saturations de tampon

Passons maintenant à un aspect un peu plus positif : comment éliminer et éviter les saturations de tampon dans votre code ?

Migration vers du code managé

En février et mars 2002, nous avons organisé le Microsoft Windows® Security Push. Pendant ce temps, mon groupe a formé plus de 8 500 personnes sur ce qu'il faut savoir pour concevoir, écrire, tester et documenter des fonctionnalités sécurisées. Nous avons notamment conseillé à tous les concepteurs de trouver comment faire migrer certaines applications et outils du code natif Win32® C++ vers du code managé. Nous avons fait cela pour plusieurs motifs mais la raison principale était de diminuer les problèmes de saturation du tampon. En code managé, il est beaucoup plus difficile de créer du code qui contienne une saturation du tampon car le code que vous écrivez n'a pas d'accès direct aux pointeurs, aux registres des machines ni à la mémoire. Vous devez prévoir ou au moins envisager de faire migrer certaines applications et outils vers du code managé. Par exemple, un outil de gestion est le type même d'élément qu'il faut faire migrer. Bien sûr, vous devez rester réaliste ; vous n'allez pas en une seule soirée faire migrer toutes vos applications de C++ vers C# ou autre langage managé.

Les règles d'or à respecter

Lorsque vous écrivez du code C et C++, vous devez faire attention à la façon dont vous gérez les données provenant des utilisateurs. Suivez ces règles si vous avez une fonction qui utilise un tampon provenant d'une source non digne de confiance :

  • Exiger que le code transmette la longueur du tampon.
  • Sonder la mémoire
  • Adopter une stratégie défensive

Voyons ces trois points en détail.

Exiger que le code transmette la longueur du tampon

Vous aurez un bogue si vous avez un appel de fonction doté d'une signature du type suivant :

void Function(char *szName) {
 char szBuff[MAX_NAME];
 // Copier et utiliser szName
 strcpy(szBuff,szName);
}

Le problème avec ce code, c'est que la fonction n'a aucune idée de la longueur de szName, ce qui signifie que vous ne pouvez pas copier les données en toute sécurité. La fonction devrait prendre la taille de szName :

void Function(char *szName, DWORD cbName) {
 char szBuff[MAX_NAME];
 // Copier et utiliser szName
 if (cbName < MAX_NAME)
 strncpy(szBuff,szName,MAX_NAME-1);
}

Toutefois, ne faites pas aveuglément confiance à cbName. L'attaquant peut très bien définir le nom et la taille du tampon, alors pensez à vérifier !

Sonder la mémoire.

Comment savez-vous que szName et cbName sont valides ? Faites-vous confiance à l'utilisateur et à la validité des valeurs qu'il vous donne ? En général, la réponse est non. Il existe une façon simple de confirmer que la taille du tampon est valide : sonder la mémoire. L'extrait de code suivant vous montre comment effectuer cette tâche dans une version déboguée de votre code :

void Function(char *szName, DWORD cbName) {
 char szBuff[MAX_NAME];
 
#ifdef _DEBUG

 // Sonder
 memset(szBuff, 0x42, cbName);
#endif

 // Copier et utiliser szName
 if (cbName < MAX_NAME)
 strncpy(szBuff,szName,MAX_NAME-1);
}

Ce code tentera d'écrire la valeur 0x42 dans le tampon de destination. Vous vous demandez probablement pourquoi faire ça plutôt que de se contenter de copier le tampon. En écrivant une valeur fixe et connue à la fin du tampon de destination, vous pouvez obliger le code à échouer si le tampon source est trop volumineux. Il peut aussi trouver les bogues de développement plus tôt dans le processus de développement. Il vaut mieux échouer plutôt que d'exécuter la charge utile nuisible de l'attaquant ; c'est la raison pour laquelle il ne faut pas copier le tampon de l'attaquant.

Remarque : N'effectuez cette opération que sur les versions de débogage, pour identifier les saturations de tampon pendant le test.
Adopter une stratégie défensive.

En toute honnêteté, le sondage est utile mais il ne vous met pas à l'abri des attaques. La seule façon de vraiment garantir la sécurité est de coder de façon défensive. Vous remarquerez que le code est déjà défensif. Il vérifie que les données qui entrent dans la fonction ne sont pas plus volumineuses que le tampon interne, szBuff. Néanmoins, certaines fonctions peuvent présenter de sérieux problèmes de sécurité en cas de mauvaise utilisation lors de la manipulation ou du copiage de données peu fiables. Le problème essentiel ici, c'est le manque de fiabilité des données. Lorsque vous révisez votre code à la recherche des bogues de saturation du tampon, vous devez suivre le flux des données au travers du code et remettre en question les hypothèses concernant ces données. C'est incroyable le nombre de bogues que vous trouverez lorsque vous réaliserez que certaines hypothèses sont incorrectes.

Vous devez vérifier un certain nombre de fonctions, notamment les fonctions classiques comme strcpy, strcat, gets, etc. Mais n'omettez pas les fameuses n-versions sécurisées de strcpy et strcat (strncpy et strncat). Ces fonctions sont sensées être plus sécurisées et sûres à utiliser car elles permettent au développeur de limiter le volume de données copiées dans le tampon de destination. Cependant, les développeurs se trompent une fois de plus ! Jetez un œil sur le code ci-dessous. Vous voyez le problème ?

#define SIZE(b) (sizeof(b))
char buff[128];
strncpy(buff,szSomeData,SIZE(buff));
strncat(buff,szMoreData,SIZE(buff));
strncat(buff,szEvenMoreData,SIZE(buff));

Si vous voulez un indice, regardez les derniers arguments de chaque fonction de traitement de la chaîne. Vous donnez votre langue au chat ? Avant de vous donner la réponse, sachez que je remarque souvent en plaisantant que si vous interdisiez les fonctions de traitement des chaînes " non dignes de confiance " et mandatiez les n-versions plus sures, il vous faudrait passer le reste de votre vie à résoudre les nouveaux bogues que vous auriez introduits. Pourquoi ? Je vais vous le dire. Premièrement, le dernier argument n'est pas la taille totale du tampon de destination. Il donne l'espace restant dans le tampon, et chaque fois que le code fait des ajouts dans le tampon, le tampon se réduit. Le deuxième problème, c'est que la plupart des gens, même lorsqu'ils transmettent la taille du tampon, commettent souvent une légère erreur. Faut-il inclure le null de fin dans le calcul de la taille de la chaîne ? Lorsque je pose la question à mon auditoire, les avis sont partagés à parts égales. La moitié de la salle pense que vous devez tenir compte du null de fin lorsque vous calculez la taille du tampon, et l'autre moitié pense que non. Troisièmement, dans certains cas, la n-version ne termine pas par un null la chaîne produite ; pensez donc à bien lire la documentation.

Si vous écrivez du code C++, envisagez d'utiliser ATL, STL, MFC ou vos classes de traitement des chaînes favorites pour gérer les chaînes plutôt que de gérer directement les octets. Le seul inconvénient possible est une éventuelle baisse des performances, mais en général, l'utilisation de ces classes permet d'obtenir un code plus robuste et plus facile à maintenir.

Compiler avec /GS

Cette nouvelle option de compilation de Visual C++® .NET insère des valeurs dans certains frames de pile de fonctions pour aider à réduire l'éventuelle vulnérabilité aux saturations de tampon sur pile. N'oubliez pas que cette option n'assainit pas votre code et n'élimine pas non plus les bogues. Elle sert simplement d'anti-retour pour tenter d'éviter que certains types de saturation de tampon ne deviennent exploitables et ne permettent à un attaquant d'injecter et d'exécuter du code dans votre processus. C'est comme une petite police d'assurance. Remarquez que pour les nouveaux projets natifs Win32 C++ créés à partir de l'Assistant Création d'applications Win32, cette option est activée dans la configuration par défaut. En outre, Windows Server 2003 est compilé à l'aide de cette option. Pour plus d'informations, veuillez consulter l'article de Brandon Bray, Contrôles approfondis de la sécurité du compilateur.

Trouver la vulnérabilité

Je veux procéder à un empaquetage avec un code qui présente au moins une faille de sécurité. La voyez-vous ? Je vous donnerai la réponse dans mon prochain article !

WCHAR g_wszComputerName[INTERNET_MAX_HOST_NAME_LENGTH + 1];

// Obtenir le nom du serveur et le convertir en chaîne Unicode.
BOOL GetServerName (EXTENSION_CONTROL_BLOCK *pECB) {
 DWORD dwSize = sizeof(g_wszComputerName);
 char szComputerName[INTERNET_MAX_HOST_NAME_LENGTH + 1];

 if (pECB->GetServerVariable (pECB->ConnID,
 "SERVER_NAME",
 szComputerName,
 &dwSize)) {
 // reste de l'extrait de code



Michael Howard est responsable des programmes de sécurité dans le groupe Secure Windows Initiative de Microsoft et coauteur du livre intitulé Writing Secure Code Site en anglais. Son principal objectif dans la vie et de faire en sorte que rien ne soit conçu, créé, testé ni documenté sans un système de sécurité. Sa maxime est " Les facilités des uns sont les exploits des autres ".



Dernière mise à jour le lundi 8 juillet 2002



Pour en savoir plus
Afficher: