Skip to main content

Une introduction conviviale à la science obscure des vulnérabilités C/C++

Écrit par:
Aviad Hahami
Aviad Hahami
wordpress-sync/feature-c-vulnerabilities-orange

15 avril 2022

0 minutes de lecture

Lumos !

Alors que Synk annonce sa prise en charge des dépendances non gérées (principalement des bibliothèques C/C++), nous avons pensé qu’il serait bénéfique de présenter à notre communauté « non-C » certains dangers courants et à haut risque qui se cachent dans le monde du C. Il s’agit d’un « guide pour débutants » sur les vulnérabilités des langages C et C++ abordant leurs formes, les problèmes qu’elles peuvent causer et la manière de les corriger.

Les principales failles et où les trouver

Le C et le C++ (dans le reste de cet article, nous dirons « le C » pour faire référence aux deux) sont considérés comme des langages de programmation de bas niveau. Lorsque l’on compare les langages de bas niveau aux langages de haut niveau (JS, Python, etc.), on constate que le processus de gestion de la mémoire de la machine constitue la principale différence. Alors que les langages de haut niveau gèrent l’allocation, la consommation et la libération de la mémoire, le langage C transfère la responsabilité de la gestion de la mémoire au développeur. Cela permet aux développeurs d’être précis dans l’optimisation des performances et de la mise en œuvre de leurs routines, mais peut également introduire diverses problématiques spécifiques à ce domaine de langages.

Débordements de tampon (CWE-121) et écriture hors limites (CWE-787)

Les débordements de tampon sont probablement les vulnérabilités liées à la mémoire les plus connues. Si l’exploitation des débordements de tampon peut être compliquée, la vulnérabilité elle-même est simple : vous dépassez la mémoire tampon qui vous a été allouée. Les « débordements de tampon » font généralement référence à l’exploitation de la vulnérabilité elle-même. Les « débordements de tampon basés sur la pile » (stack-based buffer overflow) et les « écriture hors limites » (out-of-bounds write) correspondant pour l’essentiel à la même faiblesse, nous les aborderons ensemble.

L’exploitation peut s’avérer difficile car il n’est pas toujours possible d’écrire du code dans la stack (ou le tas), même en cas de débordement. Si, au fil du temps, des tentatives ont été faites pour renforcer et atténuer ce problème, peu de mécanismes de défense ont été introduits. Ces mécanismes de défense dépendent de nombreux facteurs et peuvent prendre en compte votre système d’exploitation, les fonctionnalités du noyau, le compilateur et d’autres éléments de défense de votre code. Il existe des mécanismes tels que l’ASLR (randomisation de la disposition de l’espace d’adressage), les canaris de pile et la DEP (prévention de l’exécution des données), pour n’en citer que quelques-uns. Tous ces mécanismes visent à prévenir les bugs de corruption de la mémoire tels que les débordements de mémoire tampon. Pendant l’exécution, l’échec de l’un de ces mécanismes entraîne un arrêt d’exécution du système d’exploitation et le lancement d’un SEGFAULT, rendant l’ensemble du processus d’exploitation plus complexe.

Voici un exemple illustrant ce type de vulnérabilité et son exploitation. Considérons le programme C suivant :

1#include <stdlib.h>
2#include <unistd.h>
3#include <stdio.h>
4
5int main(int argc, char **argv)
6  volatile int modified;
7  char buffer[64];
8
9  modified = 0;
10  gets(buffer);
11
12  if(modified != 0) {
13    printf("you have changed the 'modified' variable\n");
14  } else {
15    printf("Try again?\n");
16  }
17}

Exemple fourni par 0xRick,comme vu sur son blog.

En regardant le code (même sans connaître le C), on peut dire que buffer est alloué comme un tampon de 64 caractères et que modified est un entier. Bien.

Nous voyons également à la ligne 9 que modified est fixé à une valeur de 0, et à la ligne 12, nous vérifions si la valeur est 0 ou non. Si vous ne l’avez pas encore deviné, notre but est de la rendre différente de zéro. À la ligne 10, nous utilisons la fonction gets pour lire la variable buffer à partir de stdin(=entrée de l’utilisateur). Pour ceux qui ne sont pas familiers de gets : elle lit depuis stdin dans un tampon donné jusqu’à ce qu’un caractère de nouvelle ligne (\\n) soit lu.

Puisque nous savons (parce que vous avez regardé la vidéo YouTube ci-dessus) que la pile se développe dans une adresse inférieure, et qu’en raison de la structure de données de la pile, modified se trouve « sous » buffer dans la mémoire, si nous écrivons plus de 64 caractères dans buffer, nous commencerons à remplacer la valeur de modified ! C’est la magie du débordement de tampon !

Cet exemple spécifique n’est pas très dangereux (puisqu’il s’agit d’une démo). Mais imaginez ce qui pourrait se passer si vous remplaciez la valeur d’une variable de mot de passe, ou de l’URL ciblée par la machine. La solution d’atténuation est simple : il est recommandé d’utiliser la fonction fgets, qui vérifie également la longueur de l’entrée et pas seulement l’existence du « caractère de fin de séquence ».

Utilisation après libération (Use after free) (CWE-416)

Les vulnérabilités de type « Utilisation après libération » se décrivent bien d’elles-mêmes : elles se produisent lorsque vous utilisez une référence de variable après qu’elle ait été libérée. Cette vulnérabilité résulte d’une erreur de gestion de la mémoire relative au flux logiciel, lorsque la variable a été utilisée aprèsavoir été libérée, avec pour résultat une action inattendue ou un effet résiduel imprévu sur l’application.

Pour voir la vulnérabilité et son exploitation en pratique, je vous recommande la vidéo LiveOverflow sur la façon dont il exploite une vulnérabilité UAF dans un challenge.

Débordement d’entier par le haut/par le bas (Integer overflow/underflow) (CWE-190 et CWE-191)

Les débordements d’entier par le haut et par le bas sont deux types similaires de bugs (et plus tard de vulnérabilités) qui se produisent en raison des représentations de nombres dans les ordinateurs.

Je ne m’étendrai pas sur les types de variables et sur la manière dont les nombres sont représentés dans les ordinateurs, mais je mentionnerai qu’il existe deux méthodes principales de représentation des nombres : signed et unsigned. Si les variables signed peuvent représenter des nombres négatifs et positifs, les variables unsigned stockent uniquement des nombres positifs.

Un débordement d’entier par le haut signifie que la valeur que nous avons demandée à la machine de stocker est plus grande que la valeur maximale pouvant être stockée. Un débordement d’entier par le bas signifie que nous avons demandé à la machine de stocker une valeur plus petite que la valeur minimale (par exemple, demander à un nombre entier non signé de stocker un nombre négatif).

Dans les deux cas, le résultat sera similaire. La valeur sera « enveloppée » (c’est-à-dire qu’elle commencera au début ou à la fin de la plage stockable) et changera donc de valeur. En cas de débordement par le haut, la valeur enveloppée commencera à 0, tandis qu’en cas de débordement par le bas, elle commencera à la valeur maximale stockable (c’est-à-dire que l’entier non signé de 8 bits sera enveloppé à 256 \[décimal]).

Ce diagramme illustre les types de variables et les valeurs qu’elles peuvent contenir :

wordpress-sync/blog-cworld-variabletypes

Un exemple d’exploitation d’un débordement d’entier par le haut conduisant à un débordement de tampon a été détecté sur OpenSSH v3.3 (CVE-2002-0639).

Considérons l’extrait suivant :

1nresp = packet_get_int();
2if (nresp > 0) {
3 response = xmalloc(nresp*sizeof(char*));
4 for (i = 0; i < nresp; i++)
5  response[i] = packet_get_string(NULL);
6}

Supposons que nresp soit 1073741824 et que sizeof(char\*) soit 4 (qui est la taille classique d’un pointeur), nresp*sizeof(char*) entraîne un débordement par le haut (parce que la valeur sera enveloppée et aboutira à 0). Par conséquent, xmalloc() reçoit et alloue un tampon de 0 octet. La boucle suivante provoque un débordement de tampon du tas car nous écrivons dans un emplacement de mémoire non alloué, qui peut, à son tour, être utilisé par un attaquant pour exécuter un code arbitraire.

Déréférencement de pointeur nul (CWE-467)

Le déréférencement consiste à effectuer une action sur une valeur à une adresse donnée. Pour mieux expliquer cette vulnérabilité, prenons un exemple :

1#include <stddef.h>
2
3void main(){
4        int *x = NULL;
5        *x = 1;
6}

Selon la norme C, l’exécution du code ci-dessus peut entraîner un « comportement non défini (undefined) ». Cependant, la plupart des implémentations seront en panique avec SEGFAULT, qui signifie que le logiciel a tenté d’accéder à une zone restreinte de la mémoire (ce qui constitue une violation de l’accès à la mémoire). Le système d’exploitation interrompt alors l’exécution de votre logiciel.

Lecture hors limites (CWE-125)

Une lecture hors limites se produit lorsque « vous accédez en lecture en dehors de l’emplacement/du tampon désigné ». Une telle vulnérabilité peut aboutir à un crash du système (dans le meilleur des cas) ou à la divulgation d’informations provenant de votre application (c’est-à-dire les mots de passe d’autres utilisateurs), ce qui n’est pas une bonne chose.

Pour illustrer une telle vulnérabilité, voici un extrait de l’application PureFTPd. Considérons la ligne 17. Si la longueur de s1 est supérieure à la longueur de s2, alors, puisque la ligne 8 itère sur la longueur de s1 -, les informations auxquelles nous accéderons à la ligne 10 dépasseront les limites de s2. Ceci aboutira à une lecture hors limites.

1int pure_memcmp(const void *const b1_, const void *const b2_, size_t len)
2  {
3   const unsigned char *b1 = (const unsigned char *) b1_;
4   const unsigned char *b2 = (const unsigned char *) b2_;
5   size_t i;
6   unsigned char d = (unsigned char) 0 U;
7   for (i = 0 U; i < len; i++)
8   {
9     d |= b1[i] ^ b2[i];
10   }
11   return (int)((1 &((d - 1) >> 8)) - 1);
12}
13
14int pure_strcmp(const char *const s1, const char *const s2)
15{
16  return pure_memcmp(s1, s2, strlen(s1) + 1 U);
17}

Ce bug a été classé CVE-2020-9365 ; vous pouvez lire le rapport.

Conclusion et prochaines étapes

Nous espérons que vous avez maintenant une (bonne) idée de la nature des vulnérabilités C/C++, d’où elles se trouvent habituellement, et de la forme qu’elles peuvent prendre. Bien que certains de ces exploits puissent sembler complexes de prime abord, une compréhension plus approfondie de ceux-ci affinera votre compréhension globale des rouages profonds du logiciel et vous aidera à éviter et prévenir les bugs critiques.

Comme l’illustre l’explication sur le débordement d’entier ci-dessus, de telles vulnérabilités peuvent être enchaînées les unes aux autres, créant ainsi une chaîne de faiblesse susceptible d’être exploitée de manière malveillante.

Maintenant que nous avons lancé C/C++ pour Snyk Open Source, nous partagerons d’autre contenu sur des thématiques expliquant comment détecter, exploiter et corriger les vulnérabilités C et C++.