Les 5 principaux risques de sécurité en C++
Snyk Security Research Team
16 août 2022
0 minutes de lectureLe C++ offre de nombreuses fonctionnalités puissantes aux développeurs, ce qui explique pourquoi il est utilisé dans de nombreux secteurs et dans de nombreux systèmes essentiels. Mais contrairement à certains langages de plus haut niveau qui offrent un contrôle moins direct sur les ressources, le C++ présente divers risques de sécurité dont les développeurs doivent avoir pleinement conscience lorsqu’ils écrivent du code, afin d’éviter d’introduire des vulnérabilités dans leurs projets.
En tant que développeurs, nous concevons des applications en pensant à leurs utilisateurs finaux. Ceux-ci nous confient leurs données, leur temps et l’accès à leurs appareils. Il nous incombe de veiller à ce que notre application — et les données de nos utilisateurs — soient toujours en sécurité.
Cet article examine les cinq principaux risques de sécurité liés au C++ et propose des conseils pour les atténuer.
1. Dépassement de tampon
Le dépassement de tampon est l’un des risques de sécurité les plus courants en C++. Il est dû à l’absence de mécanismes intégrés de vérification des limites en C++, qui réduiraient le risque d’écrasement de la mémoire. Écrire en dehors de la mémoire qui nous est allouée peut faire planter le programme et corrompre des données. Cela peut même entraîner l’exécution de code malveillant. Le dépassement de tampon a ainsi été classé comme la faille logicielle la plus dangereuse dans l’enquête 2021 CWE Top 25 Most Dangerous Software Weaknesses.
La attaque par dépassement de tampon de 2019 contre la pile VOIP de l’application WhatsApp illustre les graves conséquences de cette vulnérabilité. Lors de cette attaque, l’assaillant a exploité une vulnérabilité de dépassement de tampon dans WhatsApp pour installer un logiciel espion sur les téléphones des utilisateurs ciblés.
Voyons à quoi ressemble concrètement une vulnérabilité de dépassement de tampon. Dans l’extrait de code suivant, nous récupérons la saisie de l’utilisateur avec la fonction gets et vérifions si elle a modifié la variable important_data.
Ce code peut sembler inoffensif, mais il est vulnérable à une attaque par dépassement de tampon si l’utilisateur saisit une chaîne plus longue que le tableau user_input. La pile croît vers les adresses inférieures, ce qui signifie que important_data se trouve sous user_input en mémoire.
Il existe plusieurs façons d’éviter ce problème, dont certaines dépendent des fonctionnalités du compilateur, du système d’exploitation et du noyau. Les principaux outils à notre disposition pour protéger nos données sont les canaris de pile, la randomisation de la disposition de l’espace d’adressage (ASLR) et la prévention de l’exécution des données (DEP).
Les canaris de pile ajoutent à la pile une nouvelle valeur secrète sélectionnée au hasard chaque fois qu’un programme démarre. Cette valeur est vérifiée avant le retour d’une fonction.
ASLR empêche un attaquant de connaître la disposition de la mémoire, ce qui complique les attaques par dépassement de tampon. S’il ignore où se trouvent les données en mémoire, il ne saura pas quel tampon attaquer.
DEP marque certaines zones de mémoire, comme la pile, comme non exécutables.
En plus d’utiliser les fonctionnalités du système d’exploitation et du compilateur, nous devons adopter de bonnes pratiques de codage, notamment la vérification des limites. Évitons les fonctions de bibliothèque standard vulnérables aux attaques par dépassement de tampon, comme get, strcpy, strcat, scanf et printf, car elles ne vérifient pas les limites. Remplaçons-les plutôt par des fonctions sécurisées équivalentes, comme fgets.
2. Dépassement et sous-dépassement d’entier
Il y a dépassement d’entier lorsque la valeur que nous essayons de stocker dans une variable entière dépasse la valeur maximale qu’elle peut contenir. Un sous-dépassement d’entier se produit lorsque cette valeur est inférieure à la valeur minimale que peut contenir la variable. Dans ce cas, la valeur revient à l’autre extrémité de la plage.
L’enquête 2021 CWE Top 25 Most Dangerous Software Weaknesses a classé cette faille au 12e rang des faiblesses logicielles les plus dangereuses. De plus, un dépassement ou un sous-dépassement d’entier peut entraîner une vulnérabilité de dépassement de tampon.
Le code suivant est un bogue réel découvert dans OpenSSH v3.3. Il illustre comment un bogue de dépassement d’entier peut provoquer une attaque par dépassement de tampon :
Ce code présente une vulnérabilité de dépassement d’entier. Même si le code vérifie que la valeur n’est pas égale à zéro, l’allocation mémoire peut être de taille nulle si l’entrée nresp est égale à 1073741824. La multiplication de cette valeur par quatre (la taille d’un pointeur de caractère) provoque un dépassement de la variable, et xmalloc(nresp*sizeof(char*)) alloue un tampon de taille 0.
Pour atténuer cette vulnérabilité, nous devons vérifier les plages de valeurs afin de contrôler la valeur zéro, la valeur minimale et la valeur maximale, et ainsi nous protéger contre le dépassement ou le retour à zéro de la valeur de la variable.
3. Initialisation des pointeurs
L’initialisation des pointeurs est essentielle. Un pointeur qui n’est pas initialisé comme pointeur vers un emplacement mémoire ou une fonction peut exposer de nombreuses données. Si un pointeur non initialisé pointe vers un emplacement mémoire, le programme peut lire ou écrire à un emplacement inattendu. S’il pointe vers une fonction, il peut entraîner l’exécution involontaire d’une fonction arbitraire.
Les pointeurs sont également vulnérables aux attaques par déréférencement de pointeur nul, qui peuvent nuire à la fiabilité des programmes. Ces attaques consistent à accéder à un pointeur initialisé à null, ce qui entraîne un comportement indéfini (UB) dans le programme. Dans la plupart des cas, celui-ci plante. Un attaquant peut en tirer parti en provoquant un déréférencement de pointeur nul. Une initialisation incorrecte du pointeur entraîne un comportement inattendu et imprévisible. Un attaquant peut contourner certains contrôles de sécurité ou révéler des informations de débogage qu’il pourra exploiter par la suite.
Voyons un exemple de déréférencement de pointeur dû à une initialisation incorrecte :
Comme nous le voyons, le pointeur n’est pas initialisé. Il reçoit donc une valeur aléatoire, et la vérification réussira puisqu’il peut s’agir d’une valeur non nulle.
Pour éviter les attaques par déréférencement de pointeur, nous ne devons pas nous fier uniquement aux vérifications ou à la gestion des exceptions. Envisageons de ne pas utiliser de pointeur lorsque nous pouvons nous en passer et d’utiliser plutôt des références. Si nous utilisons des pointeurs, remplaçons les pointeurs bruts par des pointeurs intelligents.
Une autre solution consiste à adopter la technique Resource Acquisition Is Initialization (RAII) dans notre implémentation, qui garantit qu’une ressource est disponible pour toute fonction qui y a accès.
4. Conversion de type incorrecte
La conversion de type incorrecte est une autre vulnérabilité courante. La plupart des problèmes liés aux conversions de type sont dus à la conversion d’un type signé en type non signé, qui se produit généralement lors d’appels de fonction où le type de paramètre transmis est incorrect. Une autre vulnérabilité courante survient lors de la conversion de types de données plus grands, comme double en float ou long int en int, ce qui entraîne une perte de données pendant la conversion implicite.
Voici un exemple simple de vulnérabilité due à une conversion de type incorrecte :
Il semble impossible d’atteindre l’instruction else, puisque la longueur de toute chaîne saisie sera supérieure à -1. Pourtant, ce code ne fonctionne pas et exécute toujours l’instruction else. Selon la norme C++ et le concept de promotion des entiers, lorsque deux valeurs de types de données différents sont comparées, leur représentation est modifiée.
Dans notre cas, la valeur de signed short int sera convertie vers le type plus grand unsigned int. La valeur -1 devient alors un entier non signé égal à 4294967295, ce qui fait passer l’exécution du programme dans l’instruction else.
Le même problème peut survenir si vous utilisez des entiers non signés pour soustraire deux valeurs saisies par l’utilisateur, en supposant que celui-ci ne saisira jamais d’abord la plus petite valeur. Le résultat de la soustraction peut alors être négatif.
Selon le guide de style C++ de Google, éviter les opérations mathématiques avec des entiers non signés permet d’atténuer la plupart des problèmes de conversion de type, sauf lorsqu’ils servent à représenter des champs de bits. Le guide recommande également d’éviter de mélanger les types signés et non signés, et d’utiliser des itérateurs et des conteneurs plutôt que des pointeurs et des tailles.
5. Vulnérabilité de chaîne de format
Une vulnérabilité de chaîne de format comporte deux éléments : la fonction de formatage et la chaîne de format. Avant d’examiner ces vulnérabilités, voyons le rôle de la fonction de formatage et de la chaîne de format.
La fonction de formatage convertit les variables du langage de programmation en un format lisible par un humain. printf et fprintf en sont des exemples.
La chaîne de format est l’argument de la fonction de formatage ; elle contient du texte et des paramètres de format. Prenons un exemple :
"This is a test text of number: %d ", 11"est la chaîne de format."%d"est le paramètre de la chaîne de format qui définit le format de conversion.
Les attaques par chaîne de format se produisent lorsque nous ne vérifions pas les paramètres transmis à la fonction de formatage. Imaginons, par exemple, que nous ayons implémenté une application comme celle-ci :
Ce code est vulnérable à une attaque par chaîne de format, car il ne vérifie pas la saisie de l’utilisateur. L’attaque consiste à transmettre un paramètre de chaîne de format et une entrée de ce type :
Cette entrée peut récupérer des données de la pile. Le programme produit alors un résultat semblable à celui-ci :
Ce résultat est dû à printf, qui traite %p comme une référence à un pointeur void et tente d’interpréter les adresses mémoire.
Pour éviter cette vulnérabilité, nous devons ajouter un argument de format à notre code, comme indiqué ci-dessous :
L’extrait de code ci-dessus est sûr, car il n’interprétera pas la chaîne. Par exemple, si nous compilons et exécutons le code, le résultat sera le suivant :
Le résultat se limite aux caractères transmis au programme, sans les interpréter comme une référence au pointeur void.
Une meilleure solution consiste à éviter autant que possible d’utiliser printf, sauf en cas d’obligation, et à le remplacer par std::format et std::vformat (introduits en C++ 20). Ces fonctions vérifient la correspondance entre l’entrée et les types à l’exécution ou à la compilation, et déclenchent une format_error en cas d’incompatibilité.
Réduisez vos risques de sécurité en C++
Dans cet article, nous avons passé en revue les cinq principaux risques de sécurité liés au C++. Nous avons vu comment un attaquant peut exploiter ces vulnérabilités pour accéder aux données des utilisateurs ou récupérer des informations depuis nos applications, ainsi que leur manifestation dans le code. Nous avons également présenté des stratégies pour éviter que ces vulnérabilités ne se transforment en failles de sécurité majeures.
Contrairement aux langages de plus haut niveau qui offrent un contrôle moins direct sur les ressources, le C++ présente diverses vulnérabilités dont les développeurs doivent avoir conscience lorsqu’ils écrivent du code, afin d’éviter de les introduire dans leurs projets. Les attaques peuvent prendre de nombreuses formes, et les acteurs malveillants ne cessent de perfectionner leurs stratégies. Pour en savoir plus sur les vulnérabilités en C++, consultez cet article de blog de l’équipe de recherche en sécurité de Snyk, ainsi que cet article sur les vulnérabilités de traversée de répertoires en C/C++.
En tant que développeurs, il nous incombe de mettre en œuvre des techniques de sécurité afin de garantir la fiabilité de nos logiciels et de les protéger contre les attaques susceptibles d’exposer les informations des utilisateurs. Avec les bonnes stratégies, toutes les vulnérabilités abordées dans cet article peuvent être évitées.
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.
