C4 — Sécurité applicativeDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 3 · Cybersécurité

Cours 4Sécurité applicative

Le bloc le plus employable : exploiter puis corriger les vulnérabilités du web et du logiciel natif.

2 chapitres · 14 h de travail estimé

  1. 1. Vulnérabilités web8 h
  2. 2. Vulnérabilités logicielles6 h

Chapitre 1 · 8 h

Vulnérabilités web

Le bloc le plus employable : OWASP Top 10, injection SQL, XSS, CSRF, contrôle d'accès défaillant, désérialisation, SSRF, en-têtes et CSP, sécurité des API et des JWT.

C'est le chapitre le plus long du cours, et le plus employable. La quasi-totalité des vulnérabilités qu'un diplômé de licence rencontrera dans un premier emploi sont ici, parce que c'est ici qu'est la surface d'attaque : une application web est exposée à Internet en permanence, elle accepte des entrées de n'importe qui, et le chapitre 6 a montré que le pare-feu ne comprend pas ce qui circule dans le flux HTTP qu'il autorise.

Une idée gouverne tout ce qui suit, et vaut la peine d'être posée avant les techniques :

Presque toutes les vulnérabilités web sont une variante d'une même faute — mélanger les données de l'utilisateur avec le code ou la logique de l'application, et faire confiance à ce qui vient du client.

Vous l'avez déjà croisée : le SUID qui fait confiance au PATH (chapitre 4), l'ARP qui croit la dernière réponse (chapitre 5). Sur le web, elle prend une dizaine de formes, que l'OWASP Top 10 classe et hiérarchise.

L'OWASP Top 10

L'OWASP Top 10 est le référentiel de fait des risques applicatifs web, révisé tous les trois ou quatre ans à partir de données réelles. Il ne faut pas l'apprendre par cœur, mais en comprendre la structure : il classe par risque, pas par difficulté technique, exactement au sens du chapitre 1.

Le fait le plus instructif est ce qui figure en tête : depuis plusieurs éditions, la première place n'est pas une faille exotique mais le contrôle d'accès défaillant — la confusion entre authentification et autorisation du chapitre 3. La deuxième grande famille reste les injections, dont l'injection SQL est l'archétype. Ce chapitre traite les plus structurantes ; le principe compte plus que l'exhaustivité.

L'injection SQL

C'est la vulnérabilité fondatrice, et le TP central de ce cours. Elle naît quand une application construit une requête SQL en collant l'entrée de l'utilisateur dans la chaîne :

SELECT * FROM users WHERE login = '<saisie>' AND mdp = '<saisie>'

Si l'attaquant saisit comme login admin' --, la requête devient :

SELECT * FROM users WHERE login = 'admin' --' AND mdp = '...'

Le -- commente la fin : la vérification du mot de passe disparaît, et l'attaquant se connecte en admin sans le connaître. La cause profonde est que le moteur ne fait aucune différence entre le code que le développeur a écrit et les données que l'utilisateur a fournies. Tout le reste — extraction de la base entière par UNION, injection à l'aveugle déduite des temps de réponse, exécution de commandes système — n'est qu'une élaboration de cette faute unique.

La correction n'est ni l'échappement, ni une liste de mots interdits. C'est la requête préparée (requête paramétrée) : le SQL est envoyé avec des marqueurs ?, et les valeurs sont transmises séparément. Le moteur compile la structure d'abord ; les données arrivent ensuite et ne peuvent plus en changer le sens.

SELECT * FROM users WHERE login = ? AND mdp = ?      -- structure["admin' --", "peu importe"]                          -- données, à part

Désormais admin' -- est cherché littéralement comme login : aucun compte ne porte ce nom, l'attaque échoue. C'est la seule correction correcte, et elle se généralise à toute injection — commandes système, LDAP, NoSQL : ne jamais construire une commande par concaténation ; séparer le code des données, toujours.

L'exercice de ce chapitre est exactement ce fil : vous montez l'attaque, puis vous écrivez le correctif. Faites-le maintenant si vous préférez, il éclaire tout le reste.

Quiz · 1 question

Pour corriger une injection SQL, un développeur remplace les apostrophes de l'entrée par des doubles apostrophes et interdit les mots « UNION » et « SELECT ». Est-ce suffisant ?

  • Oui : l'apostrophe échappée neutralise la sortie de chaîne, et les mots-clés dangereux sont bloquéséchappement + liste noire
  • Non : l'échappement manuel et les listes noires se contournent ; seule la requête préparée sépare le code des donnéesséparer structure et données
  • Oui, à condition d'ajouter aussi le mot « DROP » à la liste interditecompléter la liste

Réponse : L'échappement manuel oublie toujours un cas — champs numériques sans apostrophes, encodages alternatifs, autres contextes — et une liste noire de mots-clés se contourne indéfiniment (commentaires intercalés, casse, encodage). Ces approches traitent les symptômes. La requête préparée traite la cause : elle transmet la structure et les données par des canaux distincts, si bien qu'aucune valeur ne peut être interprétée comme du SQL. C'est la seule correction robuste, et elle vaut pour toutes les injections.

Le XSS : injection, côté navigateur

Le cross-site scripting est la même faute, déplacée : au lieu d'injecter du SQL dans la base, on injecte du JavaScript dans la page d'une autre victime. Le code s'exécute alors dans son navigateur, avec ses droits — et peut voler son cookie de session (chapitre 3), agir en son nom, défigurer la page, ou l'hameçonner.

Trois formes, selon le trajet du code injecté :

TypeOù est stockée la chargePortée
Stockédans la base (commentaire, profil)frappe tout visiteur de la page — le plus grave
Réfléchidans l'URL, renvoyée telle quellefrappe la victime d'un lien piégé
DOMjamais côté serveur ; le JS de la page l'insèreinvisible aux défenses serveur

La correction a, ici aussi, un principe unique : l'encodage de sortie contextuel. Toute donnée insérée dans une page est encodée selon le contexte où elle apparaît — corps HTML, attribut, URL, bloc JavaScript n'ont pas les mêmes règles. En pratique, on s'appuie sur un moteur de gabarits qui échappe par défaut (et l'on se méfie des échappatoires du type dangerouslySetInnerHTML ou innerHTML). Deux renforts : le cookie de session en HttpOnly, qui le rend inaccessible au JavaScript volé, et la politique de sécurité de contenu (CSP), traitée plus bas.

CSRF : abuser d'une session valide

La falsification de requête intersite exploite le fait que le navigateur joint automatiquement les cookies à chaque requête vers un domaine. Un site malveillant fait émettre, depuis le navigateur d'une victime déjà connectée à sa banque, une requête de virement — et la banque la traite, puisque le cookie de session valide l'accompagne.

Noter la différence avec le XSS, qu'on confond souvent : le XSS exécute du code sur le site vulnérable ; le CSRF ne fait qu'émettre une requête en abusant d'une session existante, sans lire la réponse. Les défenses :

  • un jeton anti-CSRF imprévisible, exigé pour toute action modifiante et absent d'un site tiers ;
  • l'attribut de cookie SameSite, qui empêche l'envoi automatique du cookie depuis un autre site — devenu la première ligne de défense ;
  • pour les API, ne pas dépendre d'une authentification par cookie ambiant.

Le contrôle d'accès défaillant

Première place de l'OWASP Top 10, et matérialisation exacte de la distinction du chapitre 3 : authentifié n'est pas autorisé. Deux formes reviennent constamment :

  • La référence directe non contrôlée (IDOR). L'URL …/facture?id=1043 affiche votre facture ; vous changez 1043 en 1044 et vous voyez celle d'un autre client. Le serveur a vérifié que vous étiez connecté, jamais que cette facture était la vôtre.
  • L'élévation verticale. Un utilisateur ordinaire appelle directement …/admin/users : si le serveur se contente de cacher le lien dans l'interface sans vérifier le rôle côté serveur, l'action réussit.

La correction tient en une règle : vérifier l'autorisation côté serveur, à chaque requête, sur chaque objet, sur le principe du refus par défaut. Ce qui est fait côté client — masquer un bouton, désactiver un champ — est du confort, jamais de la sécurité : le client est entre les mains de l'attaquant.

Quiz · 1 question

Une application affiche les documents à l'URL /document?id=N. Le serveur vérifie que l'utilisateur est connecté, puis renvoie le document N. Un utilisateur incrémente N et lit les documents d'autres comptes. Quelle est la nature de la faille et sa correction ?

  • Une injection : il faut échapper le paramètre N avant la requêteéchappement d'entrée
  • Un contrôle d'accès défaillant (IDOR) : le serveur authentifie mais ne vérifie pas que le document appartient à l'utilisateurauthentifié ≠ autorisé
  • Un XSS : il faut encoder N avant de l'afficherencodage de sortie

Réponse : La requête est bien formée et N est un entier légitime : il n'y a ni injection ni XSS. La faute est une confusion entre authentification et autorisation — le serveur vérifie QUI vous êtes mais pas ce à quoi vous avez DROIT. C'est l'IDOR, en tête de l'OWASP Top 10. La correction est une vérification d'appartenance côté serveur à chaque requête : le document N est-il bien rattaché au compte appelant ? Masquer les liens ne suffit jamais, puisque l'URL se devine.

Désérialisation, SSRF, et les autres

Trois familles à connaître, chacune une variante du même excès de confiance envers l'entrée.

La désérialisation non sécurisée reconstruit un objet à partir de données reçues. Si le format permet d'instancier des types arbitraires ou de déclencher du code à la reconstruction, un flux forgé mène à l'exécution de code à distance. La règle : ne jamais désérialiser une donnée non fiable dans un format qui autorise le code ; préférer des formats de données purs (JSON avec un schéma strict) et vérifier l'intégrité.

Le SSRF (server-side request forgery) trompe le serveur pour qu'il émette lui-même une requête vers une cible que l'attaquant choisit — typiquement un service interne inaccessible de l'extérieur, ou le service de métadonnées d'un hébergeur cloud, qui délivre des identifiants. Le serveur, digne de confiance sur le réseau interne (chapitre 6), devient le relais de l'attaquant. La défense : liste blanche stricte des destinations autorisées, jamais une liste noire, et segmentation.

L'injection de commandes système est l'injection SQL transposée au shell : une entrée collée dans une commande passée à l'interpréteur. Même cause, même correction — appeler le programme avec ses arguments séparés, jamais via une chaîne de shell concaténée.

En-têtes de sécurité et CSP

Le navigateur applique des défenses que le serveur active par des en-têtes HTTP. Elles ne corrigent pas les failles à la source, mais forment une couche de défense en profondeur, à coût quasi nul.

En-têteRôle
Strict-Transport-Security (HSTS)force HTTPS, neutralise le déclassement (chapitre 5)
Content-Security-Policy (CSP)restreint d'où le code et les ressources peuvent être chargés
X-Content-Type-Options: nosniffempêche le navigateur de deviner un type MIME
X-Frame-Options / frame-ancestorsinterdit l'inclusion en cadre (clickjacking)
attributs de cookieHttpOnly, Secure, SameSite

La CSP mérite une mention particulière : bien réglée, elle atténue le XSS en interdisant l'exécution de scripts en ligne et en limitant les sources de code autorisées. Un script injecté malgré tout ne s'exécute pas s'il ne vient pas d'une source déclarée. Ce n'est pas un substitut à l'encodage de sortie — c'est un filet de sécurité pour le jour où l'encodage a été oublié quelque part. C'est l'esprit même de la défense en profondeur : supposer que la première barrière cédera.

Sécurité des API et des JWT

Les applications modernes exposent des API, qui héritent de tout ce qui précède — injection, contrôle d'accès, SSRF — et ajoutent leurs propres pièges : contrôle d'accès au niveau de chaque objet et de chaque fonction (l'IDOR est la faille reine des API), absence de limitation de débit, exposition excessive de données « que le client filtrera » — alors que le client est justement l'attaquant.

Les JWT (chapitre 3) concentrent les erreurs, et elles sont spécifiques :

  • accepter l'algorithme none, ou laisser le jeton dicter son propre algorithme de vérification — le serveur doit imposer l'algorithme attendu ;
  • ne pas vérifier la signature du tout — le jeton n'est alors qu'un cookie que l'attaquant réécrit à sa guise, role: admin compris ;
  • oublier la révocation : un JWT volé reste valide jusqu'à expiration (la limite déjà vue au chapitre 3).

La règle générale : un JWT est un jeton signé, pas chiffré — son contenu est lisible par quiconque. On n'y met donc jamais de secret, et l'on vérifie la signature et l'algorithme, systématiquement, avec une bibliothèque éprouvée (la règle du chapitre 2 : ne jamais implémenter la crypto soi-même).

À vous

Le TP central du cours. Une page de connexion construit sa requête SQL par concaténation.

D'abord, attaquez : connectez-vous en administrateur sans connaître son mot de passe — par la tautologie, puis par le commentaire. Ensuite, corrigez : réécrivez la fonction avec une requête préparée, et vérifiez que la même attaque échoue désormais. Notez au passage ce qui ne corrige pas l'injection — c'est aussi instructif que ce qui la corrige.

Exercice de code

Une page de connexion construit sa requête SQL par concaténation. Connectez-vous en admin sans son mot de passe (deux méthodes), puis réécrivez la fonction avec une requête préparée pour que l'attaque échoue. Notez au passage ce qui NE corrige pas l'injection.

Point de départ

// Un formulaire de connexion. La requête est construite en COLLANT
// l'entrée de l'utilisateur dans la chaîne SQL — l'erreur d'origine de
// l'injection, et elle reste la faille n°1 du web.

const UTILISATEURS = [
  { id: 1, login: "amina",  mdp: "s3cr3t",   role: "user"  },
  { id: 2, login: "admin",  mdp: "xK9#mP2!", role: "admin" },
];

// Un mini-moteur SQL : il ne comprend que ce dont on a besoin ici.
// Ce qui compte : il ne fait AUCUNE différence entre le code que le
// développeur a écrit et les données que l'utilisateur a fournies. C'est
// TOUTE la vulnérabilité.
function executer(sql) {
  console.log("SQL exécuté : " + sql);
  const m = sql.match(/WHERE login = '(.*)' AND mdp = '(.*)'/);
  if (!m) return [];
  let [, condLogin, condMdp] = [m[0], m[1], m[2]];
  return UTILISATEURS.filter(u => {
    const okLogin = estVrai(condLogin, u.login);
    const okMdp   = estVrai(condMdp, u.mdp, true);
    return okLogin && okMdp;
  });
}
// estVrai gère le littéral normal ET la tautologie ' OR '1'='1 injectée.
function estVrai(cond, valeur, finDeChaine) {
  if (cond.includes("OR '1'='1")) return true;         // injection réussie
  if (finDeChaine && cond.includes("--")) return true; // commentaire : mdp ignoré
  return cond === valeur;
}

// ── La fonction vulnérable ──────────────────────────────────────────────────
function connexionVulnerable(login, mdp) {
  const sql = "SELECT * FROM users WHERE login = '" + login + "' AND mdp = '" + mdp + "'";
  const r = executer(sql);
  console.log(r.length ? "  -> connecté en tant que " + r[0].login + " (" + r[0].role + ")" : "  -> refusé");
  console.log("");
}

// ── ATTAQUE ─────────────────────────────────────────────────────────────────
console.log("=== Attaque ===");
connexionVulnerable("amina", "s3cr3t");                 // cas normal
connexionVulnerable("admin", "mauvais");                // refusé, normal
// À VOUS : trouvez une entrée de login (ou de mdp) qui connecte en ADMIN
// SANS connaître son mot de passe. Deux classiques : la tautologie, et le
// commentaire qui fait disparaître la vérification du mot de passe.
connexionVulnerable("???", "???");

// ── CORRECTION — à vous ─────────────────────────────────────────────────────
// Réécrivez la connexion avec une REQUÊTE PRÉPARÉE simulée : le SQL contient
// des paramètres (?), et les valeurs sont passées À PART, jamais collées.
console.log("=== Correctif ===");
function requetePreparee(sql, params) {
  // Le moteur reçoit la structure ET les données séparément : une valeur
  // n'est JAMAIS interprétée comme du SQL.
  const trouve = UTILISATEURS.filter(u => u.login === params[0] && u.mdp === params[1]);
  console.log("préparé : " + sql + "  params=" + JSON.stringify(params));
  return trouve;
}

function connexionSure(login, mdp) {
  // à compléter en utilisant requetePreparee(...)
}

connexionSure("amina", "s3cr3t");
connexionSure("admin' --", "peu importe");   // l'attaque doit désormais ÉCHOUER

Solution

// ── ATTAQUE ─────────────────────────────────────────────────────────────────
// Deux entrées qui connectent en admin sans le mot de passe :
connexionVulnerable("admin' --", "peu importe");
//   SQL : ... WHERE login = 'admin' --' AND mdp = '...'
//   Le -- commente la fin : la vérification du mot de passe DISPARAÎT.
connexionVulnerable("x' OR '1'='1", "x' OR '1'='1");
//   La condition devient toujours vraie : le premier utilisateur est renvoyé.
//
// Aucun mot de passe n'a été deviné. On n'a pas cassé l'authentification :
// on a réécrit la REQUÊTE, parce que l'entrée est traitée comme du code.

// ── CORRECTION ──────────────────────────────────────────────────────────────
function connexionSure(login, mdp) {
  const sql = "SELECT * FROM users WHERE login = ? AND mdp = ?";
  const r = requetePreparee(sql, [login, mdp]);
  console.log(r.length ? "  -> connecté : " + r[0].login : "  -> refusé");
  console.log("");
}

connexionSure("amina", "s3cr3t");           // -> connecté
connexionSure("admin' --", "peu importe");  // -> refusé : "admin' --" est
                                            //    cherché LITTÉRALEMENT comme
                                            //    login. Aucun compte ne
                                            //    s'appelle ainsi.
//
// La requête préparée sépare la STRUCTURE (le SQL, avec ses ?) des DONNÉES
// (le tableau de paramètres). Le moteur compile la structure d'abord ; les
// valeurs arrivent ensuite et ne peuvent plus changer le sens de la requête.
// C'est la SEULE correction correcte de l'injection SQL.
//
// Ce qui NE corrige PAS l'injection, et qu'on voit trop souvent :
//   - « échapper les apostrophes » à la main : on en oublie toujours un cas
//     (encodages, guillemets, requêtes numériques sans quotes) ;
//   - une liste noire de mots comme SELECT ou UNION : contournable à l'infini ;
//   - un pare-feu applicatif : utile en défense en profondeur, mais il filtre
//     des motifs, il ne rend pas la requête sûre.
//
// La règle générale, valable pour TOUTE injection (SQL, commandes, LDAP,
// NoSQL) : ne jamais construire une commande par concaténation d'entrée.
// Séparer le code des données, toujours.

Ce que la suite en fait

Ce chapitre a traité les vulnérabilités des applications qui s'exécutent dans un environnement géré — un interpréteur, un moteur de base, un navigateur. Le chapitre 8 descend d'un cran, vers le logiciel natif, où l'application gère elle-même sa mémoire : là, la même faute — faire confiance à une entrée non validée — ne renvoie plus une facture qui n'est pas la vôtre, elle écrase la pile et détourne l'exécution.

Vous retrouverez le principe directeur intact — séparer le code des données, valider toute entrée — et la même logique de défense en couches, l'encodage et la CSP d'ici trouvant leur écho dans l'ASLR et le DEP de là.

À retenir

Flashcards · 4 cartes

Quelle faute unique sous-tend presque toutes les vulnérabilités web, et quelle en est la conséquence pratique ?
Mélanger les DONNÉES de l'utilisateur avec le CODE ou la logique de l'application, et faire confiance à ce qui vient du client. Conséquence : la correction consiste presque toujours à SÉPARER le code des données (requête préparée contre l'injection SQL, encodage de sortie contre le XSS, arguments séparés contre l'injection de commandes) et à valider toute entrée côté serveur.
Pourquoi la requête préparée corrige-t-elle l'injection SQL là où l'échappement et les listes noires échouent ?
Parce qu'elle transmet la STRUCTURE (le SQL avec ses marqueurs ?) et les DONNÉES (les valeurs) par deux canaux distincts : le moteur compile la structure d'abord, et aucune valeur ne peut ensuite changer le sens de la requête. L'échappement manuel oublie toujours un cas et les listes noires se contournent indéfiniment — ils traitent les symptômes, la requête préparée traite la cause.
Qu'est-ce que l'IDOR, pourquoi est-il en tête de l'OWASP Top 10, et comment le corrige-t-on ?
Une référence directe non contrôlée : on change un identifiant dans l'URL (…/facture?id=1044) et on accède à l'objet d'un autre. Il est en tête parce qu'il incarne la confusion authentification/autorisation, la plus répandue. Correction : vérifier l'autorisation CÔTÉ SERVEUR, à chaque requête, sur chaque objet (cet objet appartient-il à l'appelant ?). Masquer un lien côté client n'est jamais une protection.
Quelles sont les erreurs classiques sur les JWT, et la règle qui les évite ?
Accepter l'algorithme « none » ou laisser le jeton choisir son algorithme de vérification ; ne pas vérifier la signature (le jeton devient réécrivable, role:admin compris) ; oublier qu'un JWT volé reste valide jusqu'à expiration. Règle : un JWT est SIGNÉ, pas chiffré (contenu lisible, donc aucun secret dedans) ; on impose l'algorithme attendu et on vérifie la signature systématiquement, avec une bibliothèque éprouvée.

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 :

  • 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 · 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.