Skip to main content

Introduction décomplexée aux arts obscurs des vulnérabilités en C/C++

Écrit par
Headshot of Aviad Hahami

Aviad Hahami

feature vulnerabilities orange

15 avril 2022

0 minutes de lecture

Lumos !

Alors que Snyk annonce la prise en charge des dépendances non gérées (principalement des bibliothèques C/C++), nous avons pensé qu’il serait utile de présenter à notre communauté non-C quelques dangers courants et à haut risque qui se cachent dans l’univers du C (vous voyez ?). Considérez ceci comme un « guide du débutant » sur les vulnérabilités en C et C++ : à quoi elles ressemblent, quels problèmes elles peuvent causer et comment les corriger.

Des vulnérabilités extraordinaires et où les trouver

Le C et le C++ (dans la suite de cet article, nous utiliserons « C » pour désigner les deux) sont considérés comme des langages de programmation de bas niveau. Par rapport aux langages de haut niveau (JS, Python, etc.), la principale différence tient à la gestion de la mémoire par la machine. Alors que les langages de haut niveau gèrent l’allocation, l’utilisation et la libération de la mémoire, en C, cette responsabilité incombe au développeur. Cela permet aux développeurs d’optimiser précisément les performances de leurs routines et leur implémentation, mais peut aussi introduire divers problèmes propres à cette catégorie de langages.

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

Les débordements de tampon sont probablement les vulnérabilités liées à la mémoire les plus tristement célèbres. Leur exploitation peut être complexe, mais la vulnérabilité elle-même est simple : vous dépassez la capacité du tampon qui vous a été alloué. En général, le terme « débordement de tampon » désigne l’exploitation proprement dite de la vulnérabilité, mais les « débordements de tampon sur la pile » et les « écritures hors limites » correspondent essentiellement à la même faiblesse (nous les aborderons donc ensemble).

L’exploitation peut être difficile, car vous ne pouvez pas toujours simplement « écrire du code » dans la pile (ou le tas), même si vous la faites déborder. Au fil du temps, des mesures ont été prises pour renforcer les protections et atténuer ce problème, et quelques mécanismes de défense ont été mis en place. Ces mécanismes dépendent de nombreux facteurs et peuvent prendre en compte votre système d’exploitation, les fonctionnalités du noyau, le compilateur, et bien d’autres éléments, afin de protéger votre code. Parmi ces mécanismes, citons l’ASLR (randomisation de l’espace d’adressage), les canaris de pile et la DEP (prévention de l’exécution des données). Ils visent tous à empêcher les bogues de corruption mémoire tels que les débordements de tampon. À l’exécution, si l’un de ces mécanismes échoue, le système d’exploitation interrompt l’exécution et déclenche une erreur de segmentation, ce qui rend l’exploitation moins directe.

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

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
  volatile int modified;
  char buffer[64];

  modified = 0;
  gets(buffer);

  if(modified != 0) {
    printf("you have changed the 'modified' variable\n");
  } else {
    printf("Try again?\n");
  }
}

Exemple reproduit avec l’aimable autorisation de 0xRick, comme on peut le voir sur son blog.

En examinant le code (même sans connaître le C), on peut voir que buffer est alloué avec une capacité de 64 caractères et que modified est un entier. Parfait.

On voit également à la ligne 9 que modified reçoit la valeur 0 et qu’à la ligne 12, nous vérifions si sa valeur est égale à 0. Si vous ne l’aviez pas encore deviné, notre objectif est de la rendre non nulle. ? À la ligne 10, nous utilisons la fonction gets pour lire depuis stdin (c’est-à-dire la saisie de l’utilisateur) et stocker le résultat dans la variable buffer. Pour celles et ceux qui ne connaissent pas gets : cette fonction lit depuis stdin dans un tampon donné jusqu’à la lecture d’un caractère de nouvelle ligne (\n).

Comme nous le savons (grâce à la vidéo YouTube liée plus haut) que la pile se développe vers les adresses inférieures et que, du fait de la structure de données de la pile, modified se trouve « sous » buffer en mémoire, si nous écrivons plus de 64 caractères dans buffer, nous commencerons à écraser la valeur de modified ! Et voilà le fameux débordement de tampon !

Cet exemple précis n’est pas très dangereux (il s’agit d’une démonstration). Mais imaginez ce qui pourrait se passer si vous écrasiez la valeur d’une variable de mot de passe ou l’URL ciblée par la machine. L’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 la présence du « caractère de fin de séquence ».

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

Les vulnérabilités d’utilisation après libération portent bien leur nom : elles surviennent lorsqu’une référence à une variable est utilisée après sa libération. Cette vulnérabilité résulte d’une erreur de gestion de la mémoire dans le flux d’exécution du logiciel : la variable est utilisée après avoir été libérée, ce qui entraîne une action inattendue ou un effet résiduel imprévu dans l’application.

Pour voir cette vulnérabilité et son exploitation en pratique, je vous recommande la vidéo de LiveOverflow où il exploite une vulnérabilité UAF dans le cadre d’un défi.

Débordement/sous-dépassement d’entier (CWE-190 et CWE-191)

Les débordements et sous-dépassements d’entiers sont deux types de bogues similaires (qui peuvent ensuite devenir des vulnérabilités), dus à la représentation des nombres dans les ordinateurs.

Sans entrer dans le détail des types de variables ni de la représentation exacte des nombres dans les ordinateurs, signalons qu’il existe deux principales méthodes de représentation des nombres : signée et non signée. Les variables signées peuvent représenter des nombres négatifs et positifs, tandis que les variables non signées ne stockent que des nombres positifs.

Un débordement d’entier signifie que la valeur que nous demandons à la machine de stocker est supérieure à la valeur maximale pouvant être stockée. Un sous-dépassement d’entier signifie que nous lui demandons de stocker une valeur inférieure à la valeur minimale (par exemple, demander à un entier non signé de stocker un nombre négatif).

Dans les deux cas, le résultat est similaire. La valeur « reboucle » (autrement dit, repart du début ou de la fin de la plage de valeurs stockables) et change donc. En cas de débordement, la valeur rebouclée repart de 0 ; en cas de sous-dépassement, elle repart de la valeur maximale stockable (ainsi, un entier non signé de 8 bits reboucle à 256 [en décimal]).

Ce schéma illustre les types de variables et les valeurs qu’ils peuvent contenir :

Schéma montrant six plages horizontales rouges de différentes longueurs sur une échelle verticale commune, chacune délimitée par des extrémités circulaires.

Un exemple d’exploitation d’un débordement d’entier ayant entraîné un débordement de tampon a été découvert dans OpenSSH v3.3 (CVE-2002-0639).

Considérez l’extrait suivant :

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

Supposons que nresp soit égal à 1073741824 et que sizeof(char*) soit égal à 4 (la taille habituelle d’un pointeur). Le résultat de nresp*sizeof(char*) déborde donc (la valeur reboucle et devient 0). xmalloc() reçoit alors un tampon de 0 octet et l’alloue. La boucle suivante provoque un débordement de tampon dans le tas, car nous écrivons dans une zone mémoire non allouée. Un attaquant peut ensuite potentiellement s’en servir pour exécuter du code arbitraire.

Déréférencement d’un pointeur nul (CWE-467)

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

#include <stddef.h>

void main(){
        int *x = NULL;
        *x = 1;
}

Selon la norme C, l’exécution du code ci-dessus peut entraîner un « comportement indéfini ». Cependant, la plupart des implémentations déclenchent une panique avec une erreur de segmentation (SEGFAULT), ce qui signifie que le logiciel a tenté d’accéder à une zone mémoire protégée (et a donc provoqué une violation d’accès mémoire). Le système d’exploitation met alors fin au logiciel.

Lecture hors limites (CWE-125)

Une lecture hors limites se produit lorsque « vous lisez en dehors de l’emplacement ou du tampon prévu ». Une telle vulnérabilité peut provoquer une panne du système (dans le meilleur des cas) ou divulguer des informations contenues dans votre application (par exemple, les mots de passe d’autres utilisateurs), ce qui est loin d’être idéal.

Prenons cet extrait de l’application PureFTPd comme exemple. Regardez la ligne 17. Si la longueur de s1 est supérieure à celle de s2, alors, puisque la ligne 8 parcourt la longueur de s1, les informations auxquelles nous accéderons à la ligne 10 dépasseront les limites de s2. Il en résultera une lecture hors limites.

int pure_memcmp(const void *const b1_, const void *const b2_, size_t len)
  {
   const unsigned char *b1 = (const unsigned char *) b1_;
   const unsigned char *b2 = (const unsigned char *) b2_;
   size_t i;
   unsigned char d = (unsigned char) 0 U;
   for (i = 0 U; i < len; i++)
   {
     d |= b1[i] ^ b2[i];
   }
   return (int)((1 &((d - 1) >> 8)) - 1);
}

int pure_strcmp(const char *const s1, const char *const s2)
{
  return pure_memcmp(s1, s2, strlen(s1) + 1 U);
}

Ce bogue a reçu l’identifiant CVE-2020-9365 ; vous pouvez lire le rapport.

Conclusion et prochaines étapes

Nous espérons qu’à ce stade, vous comprenez (dans les grandes lignes) à quoi ressemblent les vulnérabilités en C/C++, où elles se trouvent généralement et sous quelle forme elles se présentent. Même si certaines de ces exploitations peuvent sembler complexes au premier abord, mieux les comprendre vous permettra d’approfondir votre connaissance du fonctionnement interne des logiciels et pourra vous aider à éviter et à prévenir les bogues critiques.

Comme l’exemple du débordement d’entier ci-dessus le montre, ces vulnérabilités peuvent être chaînées entre elles, créant une chaîne fragile susceptible d’être exploitée à des fins malveillantes.

Maintenant que nous avons lancé la prise en charge du C/C++ dans Snyk Open Source, nous partagerons davantage de contenu pour vous montrer comment détecter, exploiter et corriger les vulnérabilités en C et C++.

Sécurisez vos dépendances open source

Les outils Snyk, conçus pour les développeurs, génèrent en un clic des pull requests correctives pour vos dépendances open source vulnérables et leurs dépendances transitives.