Vulnérabilités logiciellesDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Cybersécurité · C4 Sécurité applicative · Chapitre 2 · 6 h

Vulnérabilités logicielles

Dépassement de tampon et de pile, chaînes de format, ASLR, DEP et canaris, dépassement d'entier, conditions de course, sécurité de la chaîne d'approvisionnement logicielle.

Le chapitre 7 traitait des applications qui s'exécutent dans un environnement géré : un interpréteur, un moteur de base, un navigateur qui tiennent la mémoire à votre place. Ce chapitre descend d'un cran, vers le logiciel natif — C, C++ — où le programme gère lui-même sa mémoire. La faute reste la même — faire confiance à une entrée non validée — mais la conséquence change de nature : elle ne renvoie plus un objet qui n'est pas le vôtre, elle écrase la mémoire et détourne l'exécution.

C'est la classe de vulnérabilités la plus ancienne du domaine, et elle n'a pas disparu : les défauts de sécurité mémoire représentent encore, d'après les données de Microsoft et de Google, autour de 70 % des vulnérabilités graves de leurs bases de code natif. Le ver Morris du chapitre 1 exploitait déjà un dépassement de tampon en 1988.

Le dépassement de tampon sur la pile

À l'appel d'une fonction, le programme réserve sur la pile un espace pour ses variables locales, et y trouve aussi l'adresse de retour — l'endroit où l'exécution reprendra une fois la fonction terminée. Cette proximité est le cœur du problème.

Si le programme copie une entrée dans un tampon local sans vérifier sa taille, une entrée trop longue déborde et écrase ce qui suit — y compris l'adresse de retour. En choisissant les octets qui débordent, l'attaquant remplace cette adresse par une valeur de son choix : à la fin de la fonction, le processeur « revient » là où l'attaquant l'a décidé.

En C, strcpy, gets, sprintf font exactement cette copie non bornée. Rien dans le langage ne vérifie les bornes d'un tableau : le débordement n'enfreint aucune règle, il exploite une liberté délibérée du langage. C'est ce que l'exercice de ce chapitre vous fera provoquer, voir, puis corriger.

L'exploitation a évolué avec les défenses. À l'origine, on injectait le code (shellcode) dans le tampon lui-même et l'on y sautait. Quand la pile est devenue non exécutable (voir plus bas), on est passé au retour vers du code existantreturn-to-libc, puis la programmation orientée retour (ROP), qui recompose un comportement malveillant à partir de fragments de code légitime déjà présents. Vous n'avez pas à monter une chaîne ROP en L3 ; vous devez comprendre pourquoi elle est apparue : chaque protection déplace l'attaque, elle ne l'élimine pas.

La correction, et les protections qui rattrapent

La correction est simple et elle est unique : ne jamais copier plus que la taille de la destination. On bannit les fonctions non bornées au profit de leurs variantes vérifiées (snprintf, strncpy utilisée correctement), on valide toute longueur venue de l'extérieur, et — la tendance de fond de l'industrie — on choisit quand c'est possible un langage à vérification de bornes (Rust, Go, Java), qui rend cette classe entière de bugs impossible par construction.

Mais le code natif hérité est immense, et l'oubli est humain. Le système ajoute donc trois protections qui ne corrigent aucun bug mais renchérissent l'exploitation — de la défense en profondeur, dans l'esprit exact de la CSP du chapitre 7 :

ProtectionCe qu'elle faitCe qu'elle ne fait pas
Canari de pilevaleur secrète entre le tampon et l'adresse de retour, vérifiée avant le retour ; un débordement l'écrase et le programme s'arrêten'empêche pas le débordement, seulement son exploitation par la pile
DEP / NXmarque la pile et le tas non exécutables : le shellcode injecté ne s'exécute pasn'arrête pas le ROP, qui réutilise du code déjà exécutable
ASLRrandomise les adresses (pile, tas, bibliothèques) à chaque exécution : l'attaquant ne sait plus où sautertombe s'il existe une fuite d'adresse qui révèle la disposition mémoire

Le tableau se lit dans les deux sens. Ces trois protections combinées rendent un dépassement classique très difficile à exploiter aujourd'hui — c'est un vrai progrès. Et chacune a une parade connue, ce qui rappelle qu'aucune ne remplace la correction du code. La sécurité mémoire se gagne d'abord à l'écriture, la défense en profondeur ne fait que rattraper les oublis.

Quiz · 1 question

Une équipe active canari de pile, DEP et ASLR, et en conclut qu'elle peut conserver ses appels à strcpy « puisque l'exploitation est devenue très difficile ». Où est l'erreur de raisonnement ?

  • Aucune erreur : ces trois protections combinées rendent l'exploitation impossibleprotection totale
  • Ces protections renchérissent l'exploitation sans corriger le bug ; chacune a une parade (ROP contre DEP, fuite d'adresse contre ASLR), et le débordement reste une faille à corrigerrenchérir n'est pas corriger
  • L'erreur est d'avoir activé ASLR, qui ralentit le programme sans bénéficecoût d'ASLR

Réponse : Les protections mémoire augmentent le coût de l'exploitation, elles ne suppriment pas la vulnérabilité. Le ROP contourne le DEP en réutilisant du code exécutable existant ; une fuite d'adresse défait l'ASLR en révélant la disposition mémoire ; un canari ne protège que certains scénarios. Conserver strcpy, c'est parier que toutes les couches tiendront simultanément et pour toujours — un pari perdant. La correction (copie bornée, ou langage sûr) supprime la classe de bugs ; les protections ne font que rattraper les oublis.

Les chaînes de format

Une faille plus subtile, née d'un raccourci d'écriture. En C, printf(entree) — au lieu de printf("%s", entree) — passe l'entrée de l'utilisateur comme chaîne de format. Or les spécificateurs de format sont un petit langage : %x lit la pile, %s déréférence un pointeur, et %n écrit en mémoire le nombre d'octets déjà affichés. Une entrée bien construite lit ou écrit donc des adresses arbitraires — encore une fois parce que la donnée a été traitée comme du code, la faute directrice du bloc IV.

La correction est triviale et absolue : la chaîne de format est toujours une constante du programme, jamais une entrée. printf("%s", entree), jamais printf(entree).

Le dépassement d'entier

Les entiers machine ont une taille fixe ; les dépasser produit un résultat qui « boucle ». Le danger surgit quand ce résultat sert à dimensionner une allocation ou à borner une copie :

taille = n_elements * taille_element;   // déborde → petit nombretampon = allouer(taille);               // trop petitcopier(tampon, source, n_elements * taille_element);  // écrit hors limites

Le dépassement d'entier ne fait pas de dégât par lui-même ; il désactive la vérification de taille qui devait protéger d'un dépassement de tampon. C'est un amplificateur, et c'est pourquoi on le trouve à l'origine de vulnérabilités graves dans les décodeurs d'images et de médias. Correction : vérifier les bornes avant le calcul, utiliser des fonctions d'allocation qui détectent le débordement, et se méfier des conversions signé/non signé.

Les conditions de course

Une condition de course apparaît quand le résultat dépend de l'ordre d'exécution de deux opérations concurrentes. Le cas de sécurité classique est le TOCTOU (time-of-check to time-of-use) : le programme vérifie une condition, puis agit en la supposant toujours vraie — mais un attaquant modifie l'état entre les deux.

si (acces_autorise("/tmp/f")) {   // vérification    // <-- l'attaquant remplace ici /tmp/f par un lien vers /etc/shadow    ouvrir("/tmp/f");             // utilisation : ouvre en réalité /etc/shadow}

La fenêtre est minuscule, mais un attaquant la déclenche des milliers de fois jusqu'à tomber dedans. Les corrections : opérations atomiques (agir sur un descripteur déjà ouvert plutôt que sur un nom de chemin revérifié), verrous appropriés, et suppression du décalage entre le contrôle et l'usage. C'est une faille de logique concurrente, pas de mémoire — d'où sa place à part.

La chaîne d'approvisionnement logicielle

Une application moderne, c'est un peu de votre code et beaucoup de code d'autrui : dépendances, bibliothèques, images de base, outils de construction. Chacune est une entrée dans votre système, et donc une part de votre surface d'attaque (chapitre 1) — que vous n'avez pas écrite et souvent pas lue.

Les scénarios ne sont plus théoriques :

Les défenses relèvent autant de l'outillage que de la discipline : verrouiller les versions (fichiers de verrou, empreintes), analyser les dépendances en continu pour les vulnérabilités connues (SCA), tenir un inventaire — le SBOM, nomenclature logicielle, qui répond à la seule question qui compte le jour d'une alerte : « utilisons-nous ce composant, et où ? » —, et réduire le nombre de dépendances, car la première façon de sécuriser une dépendance reste de ne pas l'ajouter. On retrouvera le SBOM au chapitre 10 comme instrument de gouvernance.

Quiz · 1 question

Une faille critique est annoncée dans une bibliothèque de journalisation très répandue. Une entreprise doit savoir en urgence si elle est concernée et où. Quel dispositif, préparé à l'avance, répond le plus directement à cette question ?

  • Un pare-feu applicatif, qui bloquera les tentatives d'exploitationblocage réseau
  • Un inventaire des composants logiciels (SBOM), qui recense les dépendances utilisées et leurs emplacementsinventaire des dépendances
  • Une sauvegarde récente de tous les serveurssauvegarde

Réponse : La question posée est « utilisons-nous ce composant, et où ? ». Seul un inventaire des composants — le SBOM — y répond directement et vite. Le pare-feu applicatif peut atténuer l'exploitation en attendant le correctif, mais il ne dit pas où la bibliothèque est déployée ; la sauvegarde sert à la restauration, pas à l'identification. Log4Shell a montré que les organisations dépourvues d'inventaire ont passé des semaines à simplement chercher où elles étaient vulnérables — le temps que d'autres exploitaient la faille.

À vous

Le dépassement de tampon se comprend en le déclenchant. L'exercice simule une pile où une copie sans borne écrit un tampon de 8 octets placé juste sous l'adresse de retour.

D'abord, attaquez : fabriquez une entrée qui écrase l'adresse de retour et détourne l'exécution. Ensuite, corrigez : écrivez la copie bornée qui l'empêche. Enfin, nommez les trois protections système — canari, DEP, ASLR — et dites précisément ce que chacune fait et ne fait pas. C'est cette dernière nuance qui sépare celui qui récite les sigles de celui qui comprend la défense en profondeur.

Exercice de code

Une copie sans borne écrit un tampon de 8 octets juste sous l'adresse de retour. Fabriquez une entrée qui détourne l'exécution, puis écrivez la copie bornée qui l'empêche. Nommez enfin les trois protections système (canari, DEP, ASLR) et dites ce que chacune fait — et ne fait pas.

Point de départ

// Une pile, de haut en bas des adresses. Une fonction y réserve un tampon de
// 8 octets, JUSTE SOUS l'adresse de retour — l'adresse où le programme
// reprendra à la fin de la fonction. En C, un tableau qui déborde vers le
// haut écrit PAR-DESSUS cette adresse.

function nouvellePile() {
  return {
    tampon: Array(8).fill("."),      // 8 octets pour la saisie
    sauvegarde: "@retour_normal",    // adresse de retour, juste au-dessus
  };
}

// strcpy() en C : copie SANS vérifier la taille de la destination. C'est la
// faille. Chaque octet au-delà de 8 déborde sur 'sauvegarde'.
function copieVulnerable(pile, entree) {
  for (let i = 0; i < entree.length; i++) {
    if (i < 8) pile.tampon[i] = entree[i];
    else pile.sauvegarde = "@" + entree.slice(8);   // débordement !
  }
}

function retour(pile) {
  console.log("  tampon      = [" + pile.tampon.join("") + "]");
  console.log("  adr. retour = " + pile.sauvegarde);
  if (pile.sauvegarde !== "@retour_normal") {
    console.log("  >>> EXÉCUTION DÉTOURNÉE vers " + pile.sauvegarde + " <<<");
  } else {
    console.log("  retour normal.");
  }
  console.log("");
}

// ── Cas normal ──────────────────────────────────────────────────────────────
console.log("Entrée courte :");
let p = nouvellePile();
copieVulnerable(p, "salut");
retour(p);

// ── ATTAQUE — à vous ──────────────────────────────────────────────────────
// Fabriquez une entrée qui écrase l'adresse de retour et la remplace par
// "shellcode". (Indice : 8 octets de remplissage, puis l'adresse voulue.)
console.log("Attaque :");
p = nouvellePile();
const charge = ""; // à compléter
copieVulnerable(p, charge);
retour(p);

// ── CORRECTION — à vous ────────────────────────────────────────────────────
// Réécrivez la copie pour qu'elle NE PUISSE PAS déborder, quelle que soit
// l'entrée.
function copieSure(pile, entree) {
  // à compléter
}

console.log("Copie bornée, même attaque :");
p = nouvellePile();
copieSure(p, charge);
retour(p);

Solution

// ── ATTAQUE ─────────────────────────────────────────────────────────────────
// 8 octets pour remplir le tampon, puis l'adresse qu'on veut faire exécuter.
const charge = "AAAAAAAA" + "shellcode";
copieVulnerable(p, charge);
// tampon = [AAAAAAAA], adr. retour = @shellcode : à la fin de la fonction, le
// processeur "revient" à l'adresse choisie par l'attaquant. En C réel, cette
// adresse pointerait vers du code injecté dans le tampon, ou vers une
// fonction existante (return-to-libc / ROP). L'exécution est détournée sans
// qu'aucune règle du langage n'ait été violée : C ne vérifie pas les bornes.

// ── CORRECTION ──────────────────────────────────────────────────────────────
function copieSure(pile, entree) {
  // On ne copie JAMAIS plus que la taille de la destination.
  for (let i = 0; i < entree.length && i < pile.tampon.length; i++) {
    pile.tampon[i] = entree[i];
  }
  // 'sauvegarde' n'est jamais touchée : l'adresse de retour reste intacte.
}
// En C : bannir strcpy/gets/sprintf ; employer les versions bornées
// (strncpy, snprintf) ou, mieux, un langage à vérification de bornes.
//
// ── Les protections du système, pour le jour où on oublie ──────────────────
// La copie bornée corrige LA faille. Mais le code hérité est immense, et les
// systèmes ajoutent des couches qui rendent l'exploitation plus dure quand
// une faille subsiste :
//
//   - Canari : une valeur secrète placée entre le tampon et l'adresse de
//     retour, vérifiée avant le retour. Un débordement l'écrase → le
//     programme s'arrête au lieu de sauter.
//   - DEP/NX : la pile est marquée NON EXÉCUTABLE. Le shellcode injecté dans
//     le tampon ne peut plus s'exécuter (d'où l'évolution vers ROP).
//   - ASLR : les adresses (pile, bibliothèques) sont randomisées à chaque
//     exécution. L'attaquant ne sait plus OÙ sauter — "@shellcode" n'a plus
//     d'adresse fixe.
//
// Aucune de ces protections ne CORRIGE le bug : elles rehaussent le coût.
// C'est de la défense en profondeur, exactement comme la CSP au chapitre 7 :
// on suppose qu'une faille passera, et on fait en sorte qu'elle ne suffise
// pas à elle seule.

Ce que la suite en fait

Les blocs I à IV ont montré comment les systèmes tombent — par le réseau, par le système, par l'application, par la mémoire. Le bloc V change de posture : il suppose que, malgré tout, une attaque a réussi.

Le chapitre 9 traite ce moment — détecter, contenir, éradiquer, analyser, apprendre — et s'appuiera directement sur ce que vous avez vu casser : reconstituer une intrusion, c'est retrouver dans les journaux la chaîne d'attaque du chapitre 1, phase par phase. Le chapitre 10 remettra enfin l'ensemble en ordre de risque et de gouvernance — l'étape qui n'avait de sens qu'après avoir exploité, de vos mains, une injection SQL et un dépassement de tampon.

À retenir

Flashcards · 4 cartes

Comment un dépassement de tampon sur la pile détourne-t-il l'exécution, et quelle est sa correction ?
Le tampon local voisine l'adresse de retour sur la pile ; une copie non bornée déborde et écrase cette adresse, que l'attaquant remplace par une valeur choisie — à la fin de la fonction, le processeur y saute. Correction : ne jamais copier plus que la taille de la destination (fonctions bornées, validation des longueurs), et de préférence un langage à vérification de bornes qui rend la classe entière impossible.
Que font — et ne font pas — le canari, le DEP/NX et l'ASLR ?
Canari : valeur secrète devant l'adresse de retour, un débordement l'écrase et le programme s'arrête (n'empêche pas le débordement). DEP/NX : pile non exécutable, le shellcode injecté ne tourne pas (n'arrête pas le ROP). ASLR : adresses randomisées, l'attaquant ne sait plus où sauter (tombe sur une fuite d'adresse). Aucune ne CORRIGE le bug : elles renchérissent l'exploitation — de la défense en profondeur.
Pourquoi un dépassement d'entier est-il dangereux alors qu'il ne corrompt rien directement ?
Parce qu'il DÉSACTIVE une vérification de taille : un calcul de taille qui « boucle » produit un petit nombre, on alloue trop peu, puis on copie la vraie quantité — et l'on déborde. C'est un amplificateur qui transforme une multiplication anodine en dépassement de tampon. Correction : vérifier les bornes AVANT le calcul et se méfier des conversions signé/non signé.
Quels sont les trois scénarios de risque de la chaîne d'approvisionnement logicielle, et le dispositif clé pour y répondre ?
Dépendance vulnérable (Log4Shell), dépendance compromise par un attaquant (SolarWinds), et paquet malveillant au nom trompeur (typosquattage, confusion de dépendances). Dispositif clé : l'inventaire des composants (SBOM), qui répond à « utilisons-nous ce composant, et où ? » ; complété par le verrouillage des versions, l'analyse continue des dépendances, et la réduction de leur nombre.