Pourquoi bannir gets() de ses programmes C ?

On l’a dit, on l’a répété, on le clamera encore, si possible haut et fort : un programme C digne d’un minimum de respect n’utilise pas la fonction gets().

On lit souvent qu’elle n’est pas sécurisée, qu’on risque les buffers overflow, que c’est très dangereux. C’est d’ailleurs clairement précisé dans les pages de manuel, à la section Bogues. Remettons un ou deux trucs à leur place :

  • gets() expose à des buffers overflows et ce n’est pas bien !
  • L’injection de code est possible à cause des buffers overflows, même là où on ne s’y attend pas.
  • Il est peu probable que votre programme du dimanche ou même de la semaine se fasse pirater à coup d’injection de code.
  • Votre programme risque surtout de faire n’importe quoi et ce de manière plus ou moins aléatoire.

Je me souviens en école d’ingénieurs de mon ami Albin qui me disait « gets() c’est mal, ça fait des buffers overflows et c’est dangereux pour la sécurité du programme ». Je le regardais dubitatif sans franchement comprendre les impacts ou même ce qu’était un buffer overflow… Il faut dire que c’était un développeur déjà chevronné alors que je débutais la programmation. Pour moi, les variables étaient comme une sorte de nébuleuse placée en mémoire telles les étoiles dans le ciel. Comme beaucoup de gens débutant le C ou la programmation « bas niveau », l’organisation en mémoire des données était encore très floue pour moi.

Le but de cet article est juste de donner un exemple simple illustrant que gets(), c’est mal !

Voici un joli bout de code :

#include
#include

int main(void)
{
    struct
    {
        char chaine[4] ;
        int entier;
    } demo ;

    demo.chaine[0] = 'a';
    demo.chaine[1] = 'b';
    demo.chaine[2] = 'c';
    demo.chaine[3] = '\0';
    demo.entier = 42;

    printf("'%s' %d\n", demo.chaine, demo.entier);

    printf("Saisir une chaine : \n");
    gets(demo.chaine);

    printf("'%s' %d\n", demo.chaine, demo.entier);

    return demo.entier;
}

On y déclare une structure à la forme certes un peu bizarre, on demande à l’utilisateur la chaine à mettre dans la structure, on affiche la chaine. Essayez juste de taper « bonjour<return> » et admirez.

$ ./a.out
'abc' 42
Saisir une chaine :
warning: this program uses gets(), which is unsafe.
bonjour
'bonjour' 7501167

Oui, c’est vraiment ce que m’affiche ma console ^^

En fait, j’ai un peu raté ma démonstration… Je voulais vous montrer que le code compile sans erreur ni warning (même avec gcc -Wall -Wextra), que vous pouvez entrer une chaine bien trop longue pour le tableau et que votre programme fait un buffer overflow sans crier gare. C’est en tout cas ce qui se passe sous Windows avec MinGW, environnement où j’avais écrit ce code. C’est différent sous Mac OS X avec gcc, environnement depuis lequel j’écris cet article et où j’ai retesté le code. Le message est équivoque et confirme la légende sur la sécurité de gets().

Analysons le résultat en faisant fi de ce message qui ne s’affiche pas sur toutes les plateformes. Au début, le champ entier vaut bien 42 mais quand gets() essaye de copier "bonjour" dans chaine, il écrit quelques cases mémoire trop loin et écrase la valeur contenue dans entier, qui est placé immédiatement après chaine en mémoire. Je sais qu’un int fait 32 octets sur mon architecture donc je peux en déduire une représentation des cases mémoire de ma structure. En voici un magnifique schéma dessiné avec mon doigt sur mon iPad :

struct-gets

"bonjour" n’est pas une chaine totalement prise au hasard : en comptant le caractère de fin de chaine, elle fait 8 octets et remplit donc toute la structure. Avec quelques décalages et additions, on devrait donc retrouver la nouvelle valeur d’entier à partir du contenu qu’on a copié dans chaine. On va rajouter quelques lignes à la fin de notre programme, histoire que ça calcule tout seul pour nous :

    char *entier = (char*) &demo.entier;

    int somme = (entier[3] << 24) +
                (entier[2] << 16) +
                (entier[1] <<  8) +
                 entier[0];

    printf("Octets de 'demo.entier' :'%c' '%c' '%c' '%c'\n",
            entier[3], entier[2], entier[1], entier[0]);

    printf("Somme des octets = %d vs 'demo.entier' = %d\n",
            somme, demo.entier);

On compile et on exécute :

./a.out
'abc' 42
Saisir une chaine :
warning: this program uses gets(), which is unsafe.
bonjour
'bonjour' 7501167
Octets de 'demo.entier' :'' 'r' 'u' 'o'
Somme des octets = 7501167 vs 'demo.entier' = 7501167

Tout ceci me parait clair 🙂

La ficelle est ici un peu grosse mais imaginez maintenant un programme où vous avez un tableau de taille 30 et demandez son prénom à l’utilisateur. La plupart des utilisateurs entreront une chaine qui ne dépassera pas du tableau donc aucune conséquence. Et puis un jour, un gus écrira n’importe quoi comme « edouard-maximilien le beau gosse » et là bim ! La chaine fait 33 octets, ça écrase ce qui est après en mémoire. Les conséquences sont plus ou moins tragiques selon les programmes et selon la taille du dépassement…

  • Si la structure contient le titre d’une fenêtre et sa couleur RGB, l’apparence de l’IHM est modifiée mais ce n’est pas bien grave.
  • Si elle contient un message à envoyer sur un port série et le baudrate, la transmission sera totalement incorrecte.
  • Il est possible d’aller écrire l’adresse d’une fonction dans un pointeur de fonction, ce pointeur n’appellera alors plus la bonne fonction (on se rapproche un peu de l’injection de code…), sans que cela ne plante forcément, et le comportement du programme sera vraiment incompréhensible.
  • Vous pouvez aussi avoir des effets non visibles directement, comme des écrasements de compteurs de boucles.
  • Le dépassement va au-delà de la zone réservée pour votre programme et une erreur de segmentation se produit.

Le plus tragique sera probablement de reproduire le bug et de trouver d’où vient le bug puisque cela ne se produit pas tout le temps et que l’effet peut se faire sentir longtemps après l’utilisation du gets().

Conclusion : il ne faut jamais utiliser gets() ; il faut lui préférer la fonction fgets() qui permet de spécifier la taille des données à lire et accorder cette taille avec celle du tableau.

Publicités

4 Réponses

  1. Yoann

    Super article! 🙂

    J’ajouterai scanf(« %s ») qui a exactement le même problème.

    J’ai utilisé des Canaries (http://en.wikipedia.org/wiki/Buffer_overflow_protection#Canaries) pour protéger mon code d’une librairie tierce pas hyper bien codé ^-^

    Big up à Albin

    J'aime

    31 mai 2013 à 9:00

    • Merci Yoann.

      Ton histoire de canari ressemble à une sentinelle améliorée. Le canari se met « après » le buffer alors que la sentinelle termine le buffer, non ? As-tu des exemples de codes à portée de souris ?

      Ca se passe bien ton « nouveau » taff sinon ?

      J'aime

      4 juin 2013 à 9:12

      • Pour ce qui est de scanf(%s), tu n’es pas le premier à me le dire et tu as tout à fait raison. Si je n’en n’ai pas parlé dans cet article, c’est parce qu’il ne faudrait pas utiliser scanf() tout court pour les entrées clavier.

        Je l’ai exclu de fait de la discussion mais tu fais bien de le préciser : c’est mal aussi XD

        J'aime

        4 juin 2013 à 9:19

  2. Yoann

    C’est un peu le même concept que la sentinelle mais le canary peut-être redondant… Il est juste là pour vérifier que la sentinelle a fait son boulot ^-^

    Exemple de canari https://gist.github.com/anonymous/a7dd93829526bd9e78ef (c’est plus ou moins ce que j’avais fait pour tester la fiabilité d’une lib). Au final j’avais bien fait parce qu’il fallait au moins 4 fois plus de mémoire à la lib que prévu. 🙂

    Et à part à avoir à utiliser des libs pourraves, ça se passe super bien. 😀 et toi?

    J'aime

    4 juin 2013 à 9:39

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s