Aller au contenu

Intégrité du flot de contrôle : CFI, CET et PAC/BTI

L'intégrité du flot de contrôle sur les arêtes avant et arrière, de Clang CFI à Intel CET et l'authentification de pointeur Arm, et ses limites.

Publié le 8 min de lecture

Une fois la mémoire non exécutable devenue universelle, les attaquants ont cessé d'injecter du code pour se mettre à le réutiliser : rediriger un appel indirect ou un retour vers des instructions existantes qui font quelque chose d'utile pour eux. Les techniques de cette famille, la plus connue étant la programmation orientée retour, sont la raison d'être de l'intégrité du flot de contrôle (CFI). La CFI fait vérifier au programme, à l'exécution, que chaque transfert de contrôle indirect aboutit à un endroit où le code d'origine pouvait légitimement aller. Ce guide explique les principaux schémas de CFI sous Linux, Windows et Arm, comment les activer, et ce qu'ils ne couvrent pas. Il s'appuie sur les mitigations classiques présentées dans drapeaux de durcissement des binaires.

Arêtes avant et arêtes arrière

Le flot de contrôle d'un programme comporte deux types de transferts indirects :

  arête avant                          arête arrière
  (appel / saut indirect)              (retour)

  appelant                             appelé
    |  call *%rax  ---------->  f()      |  ret  ---------->  caller+N
    |                                    |
    +-- où %rax peut-il pointer ?        +-- l'adresse de retour est-elle
                                             celle empilée par le call ?
  • Les arêtes avant sont les appels via pointeurs de fonction, les appels virtuels C++ et les sauts indirects. La question est : « cette cible est-elle une destination légitime pour ce site d'appel ? »
  • Les arêtes arrière sont les retours. La question est : « est-ce l'adresse que le call correspondant a empilée ? »

Des mécanismes différents protègent chaque arête, et un déploiement complet a besoin des deux.

SchémaArêteGranularitéPlateformeApplication
Clang CFI (-fsanitize=cfi)AvantBasée sur le typeToute, nécessite LTOLogicielle
Microsoft CFG (/guard:cf)AvantÀ gros grain (cibles d'appel valides)WindowsLogicielle + OS
Intel IBT (CET)AvantÀ gros grain (zones ENDBR)x86-64Matérielle
Arm BTIAvantÀ gros grain (zones BTI)AArch64Matérielle
Pile fantôme Intel (CET)ArrièreExactex86-64Matérielle
Arm PAC (signature de l'adresse de retour)ArrièreCryptographiqueAArch64Matérielle
Clang ShadowCallStackArrièreExacteAArch64 (et RISC-V)Logicielle, registre réservé

CFI d'arête avant

Clang CFI

La famille -fsanitize=cfi de Clang vérifie, à chaque appel indirect, que le type de la fonction cible correspond au type attendu au site d'appel. Un appel virtuel sur un Widget* ne peut atterrir que sur une méthode de Widget ; un appel via int (*)(const char *) ne peut atteindre que des fonctions ayant cette signature. Comme le compilateur doit voir tout le programme pour construire ces ensembles de types, la CFI nécessite l'optimisation à l'édition de liens et une visibilité cachée :

clang++ -O2 -flto -fvisibility=hidden -fsanitize=cfi \
        -fno-sanitize-trap=cfi -fsanitize-recover=cfi \
        -o app app.cpp        # diagnostic mode, for testing

clang++ -O2 -flto -fvisibility=hidden -fsanitize=cfi -o app app.cpp  # trapping mode, for production

Le mode diagnostic affiche le site d'appel en infraction, ce qui est utile lors de la première activation de la CFI sur une grande base de code : les casts entre types de fonction non apparentés, un idiome C courant, apparaissent comme des infractions et doivent être corrigés. Chromium est livré avec Clang CFI activé sur plusieurs plateformes, ce qui démontre que la CFI d'arête avant à grain fin est praticable à grande échelle.

Microsoft Control Flow Guard

CFG (/guard:cf à la compilation et à l'édition de liens) enregistre chaque adresse qui est une cible d'appel indirect valide dans un bitmap maintenu par l'OS. Avant chaque appel indirect, une vérification confirme que la cible est dans l'ensemble. C'est à gros grain : n'importe quelle entrée de fonction valide passe. Microsoft a depuis expérimenté une extension consciente des types (XFG) pour une granularité plus fine. Sous Windows, CFG est largement déployé sur les binaires système.

Intel IBT et Arm BTI

Les deux sont des schémas matériels à zones d'atterrissage. Le compilateur insère une instruction marqueur à chaque emplacement pouvant légitimement être atteint par un branchement indirect : ENDBR64 sur x86-64, BTI sur AArch64. Lorsque l'application effective est activée, un appel ou saut indirect qui atterrit ailleurs déclenche une faute. Ils sont à gros grain comme CFG, mais n'ont presque aucun coût à l'exécution et ne nécessitent aucune analyse du programme entier.

# x86-64: emit ENDBR landing pads and shadow-stack compatibility markers
gcc -O2 -fcf-protection=full -o app app.c

# AArch64: return-address signing (PAC) + BTI landing pads
gcc -O2 -mbranch-protection=standard -o app app.c

Vous pouvez confirmer que les marqueurs sont présents dans les propriétés ELF :

readelf -nW ./app | grep -A2 'GNU_PROPERTY'
      Properties: x86 feature: IBT, SHSTK

Sur AArch64, la même note rapporte AArch64 feature: BTI, PAC. Si un seul objet lié dans le binaire, y compris une bibliothèque statique ou un fichier assembleur écrit à la main, ne possède pas la propriété, l'éditeur de liens l'abandonne pour tout le binaire. C'est la raison la plus fréquente pour laquelle CET ou BTI se retrouve silencieusement désactivé.

CFI d'arête arrière

Piles fantômes (Intel CET)

Une pile fantôme est une seconde pile, protégée par le matériel et l'OS, qui ne contient que des adresses de retour. Chaque call empile l'adresse de retour sur les deux piles ; chaque ret compare les deux et fait fauter en cas de divergence (une exception de protection du contrôle, délivrée sous forme de SIGSEGV sous Linux). Les écritures mémoire normales ne peuvent pas modifier les pages de la pile fantôme, de sorte qu'un débordement de tampon de pile qui écrase l'adresse de retour est attrapé au retour suivant, même s'il a sauté par-dessus le canari de pile.

L'application effective requiert toute la chaîne :

  1. Un CPU doté de CET (Intel depuis Tiger Lake, AMD depuis Zen 3).
  2. La prise en charge par l'OS. Windows active la protection de pile appliquée par le matériel pour les binaires liés avec /CETCOMPAT. Sous Linux, la prise en charge de la pile fantôme en espace utilisateur a été fusionnée dans le noyau 6.6 pour x86-64.
  3. Une bibliothèque C qui l'active pour les binaires compatibles. Dans glibc, cela se contrôle à la compilation et via le réglage glibc.cpu.x86_shstk.
  4. Chaque objet chargé marqué compatible, comme montré ci-dessus.

Voir l'entrée de glossaire pile fantôme pour une courte définition.

Authentification de pointeur (Arm PAC)

Sur Armv8.3-A et ultérieur, l'authentification de pointeur stocke une courte signature cryptographique dans les bits de poids fort inutilisés d'un pointeur. Avec -mbranch-protection=standard (ou pac-ret), les fonctions signent l'adresse de retour dans le registre de lien à l'entrée (PACIASP) et la vérifient avant de retourner (AUTIASP). Une adresse de retour modifiée échoue à l'authentification et fait fauter lorsqu'elle est utilisée. Les plateformes Apple utilisent PAC intensivement (l'ABI arm64e), et Linux le prend en charge pour l'espace utilisateur sur le matériel capable.

ShadowCallStack

Le -fsanitize=shadow-call-stack de Clang est une pile fantôme logicielle pour AArch64 qui réserve le registre x18 pour pointer vers une pile séparée d'adresses de retour. Android l'utilise dans certaines parties de la plateforme et du noyau. Elle dépend du maintien secret de l'emplacement de la pile fantôme, ce qui la rend plus faible que l'application matérielle mais disponible sur des processeurs sans PAC.

Matériel connexe : le marquage mémoire

La Memory Tagging Extension d'Arm (MTE, Armv8.5-A) n'est pas de la CFI, mais elle mérite d'être connue à ses côtés. Elle assigne une étiquette de 4 bits à chaque granule de 16 octets de mémoire et à chaque pointeur ; une divergence entre l'étiquette du pointeur et celle de la mémoire est détectée à l'accès. Cela attrape de manière probabiliste de nombreux débordements de tas et bugs use-after-free, à la source plutôt qu'au moment du détournement du flot de contrôle. Android prend en charge MTE sur les appareils dont les SoC l'implémentent, et c'est la base matérielle d'une vérification de type HWASan avec une surcharge très faible.

Ce que la CFI n'arrête pas

BrèchePourquoi elle subsiste
Cibles valides mais faussesLes schémas à gros grain autorisent toute entrée de fonction ; les schémas basés sur le type autorisent toute fonction ayant la même signature
Attaques par données seulesCorrompre un drapeau, une longueur ou une vérification de permission change le comportement sans transfert de contrôle illégal
Fuites d'informationLes lectures hors limites ne sont pas des événements de flot de contrôle
JIT et code généré dynamiquementNécessite ses propres schémas d'intégrité
Composants incompatiblesUne seule bibliothèque non marquée peut désactiver l'application effective pour tout le processus

La CFI est donc une couche qui s'ajoute à la prévention et à la détection des bugs, pas un substitut. La combinaison qui fonctionne est : du code sûr pour la mémoire là où c'est possible, des sanitizers et du fuzzing pour trouver ce qui reste, un durcissement classique pour rendre l'exploitation coûteuse, et la CFI pour supprimer les moyens les plus faciles de détourner le flot de contrôle.

Checklist de déploiement

  • Construisez le code x86-64 avec -fcf-protection=full et le code AArch64 avec -mbranch-protection=standard ; vérifiez les notes de propriété ELF en CI.
  • Auditez l'assembleur écrit à la main et les bibliothèques statiques précompilées, qui manquent couramment des marqueurs CET/BTI.
  • Sous Windows, liez avec /guard:cf et /CETCOMPAT.
  • Pour les services C++ où vous contrôlez tout le build, évaluez Clang CFI avec LTO d'abord en mode diagnostic, puis en mode trapping.
  • Testez sur du matériel et des noyaux où l'application effective est réellement activée, pas seulement là où les marqueurs sont présents.

Guides associés