Aller au contenu

Canaris de pile, NX, ASLR/PIE, RELRO et FORTIFY_SOURCE

Ce que protège chaque mitigation classique, les drapeaux GCC et Clang qui l'activent, son coût, et comment vérifier un binaire avec checksec et readelf.

Publié le 7 min de lecture

Les chaînes d'outils modernes livrent un ensemble de mitigations qui transforment de nombreux bugs de sûreté mémoire de « l'attaquant exécute du code » en « le processus avorte ». Aucune ne corrige un bug, et chacune a des limites connues, mais ensemble elles augmentent considérablement le coût de l'exploitation. Ce guide couvre l'ensemble classique sous Linux (canaris de pile, mémoire non exécutable, ASLR avec PIE, RELRO, FORTIFY_SOURCE et protection contre le stack clash), présente les drapeaux pour GCC et Clang, et explique comment les vérifier sur un vrai binaire. La protection du flot de contrôle assistée par le matériel est traitée séparément dans intégrité du flot de contrôle.

La base de référence recommandée

Le Compiler Options Hardening Guide for C and C++ de l'OpenSSF est la meilleure référence actuelle. Une base de référence de production raisonnable pour GCC ou Clang sur Linux x86-64 ressemble à ceci :

CFLAGS="-O2 -g \
  -Wall -Wextra -Wformat=2 -Werror=format-security \
  -D_FORTIFY_SOURCE=3 \
  -D_GLIBCXX_ASSERTIONS \
  -fstack-protector-strong \
  -fstack-clash-protection \
  -fcf-protection=full \
  -fPIE \
  -ftrivial-auto-var-init=zero"

LDFLAGS="-pie -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack"

Sur AArch64, remplacez -fcf-protection=full par -mbranch-protection=standard. De nombreuses distributions injectent déjà la plupart de ces drapeaux via les valeurs par défaut de leur packaging, mais les logiciels construits hors distribution (dépendances vendorisées, images de conteneurs, artefacts de CI) les manquent fréquemment. Lorsque _FORTIFY_SOURCE est déjà défini par la chaîne d'outils, utilisez -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 pour éviter les avertissements de redéfinition.

Ce que fait chaque mitigation

MitigationDrapeau(x)Protège contreCoût typique
Canari de pile-fstack-protector-strongDébordements de pile linéaires atteignant des données sauvegardéesFaible (un chargement et une comparaison par fonction instrumentée)
NX / DEPPar défaut ; -Wl,-z,noexecstackExécution de données injectéesNul
PIE-fPIE -pieAdresses de code prévisibles dans le binaire principalFaible sur x86-64
ASLRNoyau (kernel.randomize_va_space=2)Adresses de pile, tas et bibliothèques codées en durNul
RELRO partiel-Wl,-z,relroÉcrasements de données de relocation hors .got.pltNul
RELRO complet-Wl,-z,relro -Wl,-z,nowÉcrasements de n'importe quelle entrée GOTTemps de démarrage si nombreux imports
FORTIFY_SOURCE-D_FORTIFY_SOURCE=3 (nécessite -O1+)Débordements dans memcpy, strcpy, sprintf et consorts aux tailles connuesFaible
Protection stack clash-fstack-clash-protectionGrands cadres ou alloca sautant par-dessus la page de gardeFaible
Init auto des variables-ftrivial-auto-var-init=zeroFuites et bugs dus à des variables de pile non initialiséesFaible à modéré
Assertions de la bibliothèque C++-D_GLIBCXX_ASSERTIONSoperator[] hors limites, itérateurs invalides dans libstdc++Faible

Canaris de pile

Le compilateur place une valeur aléatoire, lue depuis le stockage local au thread (fs:0x28 sur glibc x86-64), entre les tableaux locaux d'une fonction et son pointeur de cadre et son adresse de retour sauvegardés. L'épilogue la compare avant de retourner et appelle __stack_chk_fail, qui avorte avec *** stack smashing detected *** si elle a changé. L'octet de poids faible du canari glibc est nul, de sorte que les fonctions de chaîne qui s'arrêtent à un octet NUL ne peuvent pas le copier. Le stack protector réordonne aussi les variables locales pour que les tableaux se placent au-dessus des scalaires et des pointeurs. Voir l'entrée de glossaire canari de pile et le guide sur le débordement de pile pour le schéma du cadre.

Limites : les canaris ne détectent la corruption qu'au retour de fonction, uniquement pour les fonctions instrumentées, et pas pour les écritures qui sautent par-dessus le canari ou ciblent d'autres variables locales. Un bug de divulgation mémoire peut aussi laisser fuiter la valeur du canari.

NX / DEP

Les pages sont marquées non exécutables sauf si elles contiennent du code. L'en-tête de programme ELF GNU_STACK indique au noyau si la pile doit être exécutable ; il devrait afficher RW, jamais RWE. Une pile exécutable est généralement causée par un fichier assembleur dépourvu de section .note.GNU-stack, ce qui fait retomber l'éditeur de liens sur une pile exécutable ; les binutils récents avertissent à ce sujet.

Limites : NX arrête le code injecté, pas la réutilisation de code existant. C'est la brèche que traite l'intégrité du flot de contrôle.

ASLR et PIE

L'ASLR rend aléatoires les adresses de base de la pile, du tas, de la région mmap et du vDSO à chaque exécution. Sous Linux, kernel.randomize_va_space=2 active la randomisation complète, et c'est la valeur par défaut. PIE étend la randomisation à l'exécutable lui-même ; sans PIE, le binaire principal se charge toujours à la même adresse, ce qui laisse quantité de code prévisible. GCC construit désormais en PIE par défaut dans la plupart des distributions.

Limites : la randomisation est par processus, pas par requête. Un serveur qui fork sans que ses enfants ne fassent d'exec partage la disposition de son parent, et toute fuite d'information révélant une adresse révèle généralement toute la disposition du module.

RELRO

La liaison dynamique repose sur la Global Offset Table (GOT), une table d'adresses remplie par le chargeur. Les pointeurs de fonction inscriptibles utilisés à chaque appel de bibliothèque sont une cible évidente. Le RELRO partiel rend la plupart des données de relocation en lecture seule après le démarrage ; le RELRO complet (-z now) résout tous les symboles avec empressement afin que toute la GOT puisse être protégée. Voir l'entrée de glossaire RELRO.

FORTIFY_SOURCE

Avec _FORTIFY_SOURCE défini et l'optimisation activée, les en-têtes glibc redirigent les appels tels que memcpy, strcpy, snprintf et read vers des variantes vérifiantes chaque fois que le compilateur peut déterminer la taille de l'objet de destination. Le niveau 2 utilise les tailles connues à la compilation ; le niveau 3 (GCC 12+ et glibc 2.34+, ou Clang récent) utilise __builtin_dynamic_object_size pour couvrir les tailles connues seulement à l'exécution, ce qui augmente sensiblement la couverture. Les violations avortent avec *** buffer overflow detected ***. Le niveau 2 et au-delà rejettent aussi %n dans les chaînes de format inscriptibles.

Protection contre le stack clash

Une allocation de pile très grande, souvent issue d'un tableau de longueur variable ou d'alloca avec une taille influencée par l'attaquant, peut déplacer le pointeur de pile au-delà de la page de garde, directement dans un autre mapping. -fstack-clash-protection fait en sorte que le compilateur touche chaque page à mesure que la pile croît, de sorte que la page de garde soit toujours atteinte.

Vérifier un binaire

Ne supposez jamais qu'un binaire est durci parce que le système de build l'affirme. Vérifiez l'artefact.

checksec

checksec (le script autonome, ou l'implémentation livrée avec pwntools) résume les principales propriétés :

checksec --file=./server
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY  Fortified  Fortifiable
Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes      6          11

readelf, à la main

Chaque propriété peut être confirmée avec binutils :

# NX: GNU_STACK should be RW, not RWE
readelf -lW ./server | grep GNU_STACK

# RELRO: GNU_RELRO segment present; full RELRO also needs BIND_NOW
readelf -lW ./server | grep GNU_RELRO
readelf -dW ./server | grep -E 'BIND_NOW|FLAGS'

# PIE: type DYN plus the PIE flag
readelf -hW ./server | grep 'Type:'
readelf -dW ./server | grep FLAGS_1

# Canary and FORTIFY: look for the imported checking functions
readelf -sW --dyn-syms ./server | grep -E '__stack_chk_fail|_chk@'

La sortie attendue sur un binaire durci comprend GNU_STACK ... RW, un segment GNU_RELRO, FLAGS BIND_NOW (ou FLAGS_1 NOW PIE), Type: DYN, et des imports tels que __stack_chk_fail et __memcpy_chk.

Sous Debian et Ubuntu, hardening-check du paquet devscripts effectue des vérifications similaires. En CI, faites échouer le build si un artefact de release manque de PIE, de RELRO complet ou d'une pile non exécutable.

Équivalents Windows

Les mêmes idées existent dans la chaîne d'outils MSVC, sous des noms différents :

Linux / GCC / ClangWindows / MSVC
-fstack-protector-strong/GS (activé par défaut)
NX (GNU_STACK)/NXCOMPAT (DEP)
PIE + ASLR/DYNAMICBASE, /HIGHENTROPYVA
Clang CFI / -fcf-protection/guard:cf (Control Flow Guard), /CETCOMPAT (shadow stack)
-D_FORTIFY_SOURCEFonctions Secure CRT, /sdl

Des outils comme dumpbin /headers ou des modules PowerShell comme Get-PESecurity rapportent ces drapeaux pour les fichiers PE.

Pièges courants

  • Optimisation désactivée. _FORTIFY_SOURCE nécessite -O1 ou plus ; les builds de debug à -O0 le sautent silencieusement.
  • Liaison statique de vieilles dépendances. Une bibliothèque liée statiquement et construite sans durcissement introduit des fonctions non protégées dans un binaire durci.
  • Fichiers assembleur. Des sections .note.GNU-stack manquantes rendent exécutable la pile de tout le binaire.
  • Systèmes de build vendorisés. Les Makefile et CMakeLists.txt tiers écrasent souvent les CFLAGS. Vérifiez l'artefact final, pas la configuration.
  • Croire la checklist. Un binaire peut passer toutes les vérifications et rester exploitable via une faille logique ou une fuite d'information. Les mitigations achètent du temps et un coût ; elles n'achètent pas la correction.

Checklist

  • Adoptez la base de référence de durcissement de l'OpenSSF pour chaque build C et C++, y compris les outils internes et les conteneurs.
  • Vérifiez les artefacts de release avec checksec ou readelf en CI et faites échouer en cas de régression.
  • Gardez kernel.randomize_va_space=2 sur chaque hôte.
  • Ajoutez la protection du flot de contrôle (-fcf-protection=full ou -mbranch-protection=standard) comme couvert dans intégrité du flot de contrôle.
  • Continuez à traquer les bugs avec les sanitizers et le fuzzing : les mitigations sont la dernière couche, pas la première.

Guides associés

0x7000 · Techniques d'exploitation

ret2dlresolve : résoudre un symbole sans fuite

Sans fuite libc : forgez la relocation que lit l'éditeur de liens dynamique pour lui faire résoudre et appeler system. Et pourquoi Full RELRO y met fin.

0x7000 · Techniques d'exploitation

Détourner la GOT : rediriger un appel à la libc

La liaison paresseuse laisse la GOT inscriptible : une écriture arbitraire sur une entrée transforme puts() en system() — jusqu'à ce que Full RELRO la fige.