cursus.

Cours 4 · Sécurité applicativeLeçon 2 sur 2

Vulnérabilités logicielles

6 h de lecture9 sections Version PDF

À la fin de cette leçon, vous saurez

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 · vérifiez votre compréhension Sans réponse

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 ?

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 :

  • la dépendance vulnérable : une faille dans une bibliothèque très répandue vous expose sans que votre code ait changé — Log4Shell (2021) a touché d'innombrables applications par une seule bibliothèque de journalisation ;
  • la dépendance compromise : un attaquant prend la main sur un paquet légitime et y glisse du code malveillant, qui se propage à tous ceux qui le mettent à jour — l'affaire SolarWinds (2020) en est le cas majeur ;
  • la confusion de dépendances et le typosquattage : un paquet malveillant au nom proche d'un paquet réel, publié pour être installé par erreur.

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 · vérifiez votre compréhension Sans réponse

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 ?

À 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 · JavaScript · à vous de jouer

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.

En attente
// 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);

Console de sortie
Le résultat s'affiche dans la console

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 · 1 / 4Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 9 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.