Aller au contenu

Dirty COW CVE-2016-5195 : course dans le copy-on-write Linux

Dirty COW CVE-2016-5195 : course dans le copy-on-write Linux

Section intitulée « Dirty COW CVE-2016-5195 : course dans le copy-on-write Linux »

Dirty COW (CVE-2016-5195) est une élévation de privilège locale du noyau Linux, corrigée en urgence en octobre 2016 après avoir été trouvée exploitée dans la nature, alors qu’elle traînait là depuis 2007. Le défaut vit dans le traitement du copy-on-write : une course interne au noyau laisse un utilisateur sans privilège modifier un fichier qu’il n’a le droit que de lire.

→ Lire l’explication complète de la marmotte

Copy-on-write, cache de pages et condition de course

Section intitulée « Copy-on-write, cache de pages et condition de course »

Le copy-on-write (COW) est une optimisation du gestionnaire de mémoire. Quand un processus projette un fichier en mémoire de façon privée, le noyau ne recopie rien tout de suite : il laisse plusieurs utilisateurs partager la même page physique, qui provient du cache de pages (page cache), la copie en mémoire du contenu du fichier. La copie réelle n’intervient qu’au moment où l’un d’eux tente d’écrire. Tant qu’on ne fait que lire, une seule page sert tout le monde ; l’écriture déclenche la duplication, et chacun repart avec sa version.

Le défaut de CVE-2016-5195 se situe dans l’enchaînement de deux opérations distinctes côté noyau : constater qu’une écriture doit porter sur une copie privée, puis réaliser effectivement cette écriture. Entre ces deux instants, l’état sur lequel la première opération s’est appuyée peut changer. C’est une condition de course classée CWE-362, nichée dans mm/gup.c, le code qui va chercher les pages d’un processus quand le noyau doit y lire ou y écrire à sa place (get_user_pages). Le motif est celui d’une vérification séparée de l’action qu’elle autorise : entre le moment où le noyau décide et le moment où il agit, la décision n’est plus valable.

Le déroulé tient en peu d’acteurs : deux fils d’exécution du même processus, le noyau, et le cache de pages qui porte le contenu du fichier. Le schéma montre où la décision du noyau cesse d’être vraie.

Schéma de séquence entre cinq colonnes : Fil A, Fil B, le noyau, la copie privée et le cache de pages. Étape 1, Fil A demande au noyau une écriture par /proc/self/mem dans la projection privée d'un fichier en lecture seule. Étape 2, le noyau applique le copy-on-write et duplique la page vers la copie privée. Étape 3, le noyau retire l'intention d'écriture sur lui-même et recommence la recherche de la page. Étape 4, Fil B appelle madvise MADV_DONTNEED et la copie privée est jetée. Étape 5, la nouvelle recherche du noyau retombe sur la page du fichier d'origine dans le cache de pages. Étape 6, en cuivre et en pointillé, l'écriture modifie le fichier d'origine si le fil B est passé entre les deux.

Le résultat, quand la fenêtre se referme du mauvais côté, est que l’écriture atterrit sur la page du cache correspondant au fichier d’origine au lieu de la copie privée. Les permissions du fichier n’ont pas bougé, et pourtant son contenu a changé : c’est la version en mémoire que le système sert aux autres processus, et la page, marquée modifiée (le « dirty » du surnom), peut ensuite être réécrite sur le disque. Appliqué à /etc/passwd ou à un binaire SUID (Set User ID, un exécutable qui s’exécute avec les droits de son propriétaire), le passage à root est immédiat. Voir à ce sujet l’élévation de privilège via SUID.

Précision qui change tout pour la modélisation de menace : c’est une faille locale. Il faut déjà un compte sur la machine ou du code qui y tourne. Elle ne transforme pas un inconnu d’Internet en root ; elle transforme un utilisateur restreint, un compte de service, ou un processus déjà compromis par une autre brèche, en administrateur complet. Autrement dit, le maillon d’une chaîne, pas le point d’entrée.

Onze ans de latence : chronologie de CVE-2016-5195

Section intitulée « Onze ans de latence : chronologie de CVE-2016-5195 »

La faille est datée du noyau 2.6.22, en 2007. Le plus savoureux figure dans le message du correctif : Linus Torvalds avait déjà tenté de corriger ce même bogue onze ans plus tôt, mal, dans un commit annulé ensuite parce qu’il cassait l’architecture s390. La course était alors jugée « purement théorique ». Elle l’est restée jusqu’à ce que les gestionnaires de mémoire deviennent franchement plus parallèles, rendant la fenêtre nettement plus facile à viser. Le théorique a une fâcheuse habitude de patienter.

Phil Oester ne l’a pas trouvée en relisant du code par oisiveté. Il l’a repérée en analysant une capture réseau d’une attaque réelle contre un serveur qu’il administrait : la faille était exploitée avant d’être connue. Le correctif le crédite, avec un humour sec qui tranche dans la prose kernel, sous la mention « Reported-and-tested-by: Phil “not Paul” Oester ».

La séquence de divulgation s’est jouée en quelques jours.

Chronologie de CVE-2016-5195 en sept étapes. En 2007, le noyau 2.6.22 introduit la course dans le COW. Le 18 octobre 2016, Linus Torvalds publie le commit FOLL_COW, qui retire les jeux FOLL_WRITE de __get_user_pages. Les 19 et 20 octobre 2016, la divulgation publique touche les distributions Red Hat, Debian, Ubuntu et SUSE, qui publient dans la foulée des correctifs d'urgence. Ces étapes forment la séquence de divulgation, qui a duré quelques jours. En novembre 2016, Android reçoit un correctif partiel (Nexus, Pixel, Samsung), puis le bulletin Android du 5 décembre 2016 apporte le correctif complet. Le 3 mars 2022, la CISA inscrit la faille au catalogue KEV.

Ce qu’il faut lire dans cette chronologie : le correctif technique (un drapeau interne FOLL_COW validé par le bit « dirty » de l’entrée de table des pages) est arrivé vite, mais la propagation réelle s’étale sur des années. CISA (Cybersecurity and Infrastructure Security Agency) ne l’a inscrite au catalogue KEV (Known Exploited Vulnerabilities, vulnérabilités exploitées) que le 3 mars 2022, soit plus de cinq ans après le correctif. Un correctif publié n’est pas un parc corrigé, et le décalage se compte ici en demi-décennie.

Serveurs, conteneurs et Android : la surface réelle

Section intitulée « Serveurs, conteneurs et Android : la surface réelle »

Dirty COW ne connaît pas de frontière de distribution : tout ce qui embarque un noyau Linux vulnérable est concerné, serveurs, postes, et téléphones Android, où la faille a directement alimenté des outils d’enracinement. Deux points méritent d’être martelés pour l’architecture.

D’abord les conteneurs. Un conteneur n’est pas une machine virtuelle : il partage le noyau de l’hôte. L’isolation par espaces de noms et cgroups découpe la vue d’un processus, elle ne lui donne pas son propre noyau. Une faille du noyau de l’hôte est donc, par construction, une faille de tous les conteneurs qui tournent dessus. Compter sur « c’est dans un conteneur » pour contenir une élévation de privilège noyau est une erreur de modèle, pas une approximation tolérable. Le sujet est développé dans la page sur l’isolation des conteneurs par espaces de noms et cgroups.

Ensuite les parcs dormants. Les serveurs sous contrat de support reçoivent leurs correctifs ; les équipements embarqués, les appliances, les téléphones sortis du support et les images de base jamais reconstruites gardent le noyau de leur jour de fabrication. Android illustre le problème jusqu’à la caricature : correctif partiel en novembre 2016, complet seulement au niveau de correctif 2016-12-05, et une fragmentation telle qu’une fraction du parc n’a jamais vu passer la mise à jour.

La famille ne s’est d’ailleurs pas arrêtée là. « Huge Dirty COW » (CVE-2017-1000405, fin 2017) a montré que le correctif de 2016 restait incomplet pour les pages énormes transparentes (Transparent Huge Pages). Puis « Dirty Pipe » (CVE-2022-0847, 2022), mécanisme tout différent (des drapeaux de tampon de tube laissés non initialisés) pour presque le même effet : écrire dans le cache de pages d’un fichier en lecture seule et passer root, cette fois sans que la page soit marquée modifiée, donc sans trace sur le disque après un redémarrage. Inscrite au KEV le 25 avril 2022. La leçon architecturale n’est pas liée à un bogue précis mais au motif : la frontière entre « lecture seule » et « modifiable » repose sur une chaîne de code noyau dont un seul maillon fautif suffit à annuler la permission.

Exemple : contrôle d’intégrité et inventaire de noyaux

Section intitulée « Exemple : contrôle d’intégrité et inventaire de noyaux »

Il n’y a pas de configuration à activer contre Dirty COW : la seule vraie réponse est le noyau corrigé. Côté défense, deux gestes concrets tiennent : vérifier que le noyau réellement en service correspond au correctif, et surveiller les fichiers sensibles que ce type de faille vise. Le schéma se lit de haut en bas, machine par machine.

Arbre de décision qui se lit de haut en bas, pour chaque machine. On lit d'abord le noyau en service avec uname -r, et non la version du paquet installé. Si la version est vulnérable et que le paquet corrigé est déjà installé : redémarrage ou correctif à chaud. Si elle est vulnérable et que le paquet est absent : mise à jour du noyau puis redémarrage. Si la version est corrigée : contrôle d'intégrité par empreintes de référence de /etc/passwd, /etc/shadow et des binaires SUID recensés, avec une alerte quand un fichier est modifié hors paquet et hors changement tracé.

Le piège à comprendre avant tout : uname -r affiche le noyau chargé, pas celui posé sur le disque. Installer le paquet corrigé ne corrige rien tant que la machine n’a pas redémarré sur ce noyau (ou reçu un correctif à chaud via kpatch, Livepatch ou kGraft). Une machine qui tourne depuis des mois peut porter un paquet à jour et exécuter encore le noyau vulnérable. Comparer la sortie de uname -r au paquet installé est le seul contrôle qui ne ment pas.

Pour la détection, l’angle n’est pas de « voir » la course, qui ne laisse aucune trace réseau ni aucune entrée de journal, mais de constater son effet : un binaire SUID ou un fichier d’authentification modifié sans paquet associé ni changement enregistré. Un contrôle d’intégrité de fichiers, comparant une empreinte de référence aux fichiers sensibles, lève cette alerte. Les mitigations partielles de l’époque, comme le script SystemTap diffusé par Red Hat, étaient fragiles et ne remplaçaient pas le correctif ; elles achetaient du temps, pas de la sûreté.

Sans noyau corrigé puis redémarré, toute présence sur la machine, compte restreint, compte de service, processus déjà compromis par une autre brèche, dispose d’un chemin fiable vers root. L’écriture illégitime ne passe par aucun droit d’accès au fichier et ne génère aucun flux réseau : sans contrôle d’intégrité sur les binaires SUID et les fichiers d’authentification, la compromission ne laisse quasiment rien à voir. Un parc qui installe le paquet sans redémarrer croit être protégé tout en exécutant le noyau fautif, écart que seul un contrôle du noyau en service révèle. Les conteneurs n’offrent aucun rempart, puisqu’ils partagent le noyau de l’hôte : une seule machine non corrigée expose tout ce qui tourne dessus.

Le système Linux garde en mémoire une copie du contenu des fichiers pour aller plus vite. Quand un programme ouvre un fichier qu’il a le droit de lire mais pas de modifier, Linux lui laisse partager cette copie commune tant qu’il se contente de lire. S’il veut écrire, le système lui fabrique une copie à part, bien à lui, pour qu’il ne touche jamais l’original. C’est une bonne idée, et ça marche presque toujours.

Le problème est que le système fait ça en deux temps : d’abord il prépare la copie privée, ensuite il y écrit. Entre ces deux instants, un autre fil du même programme peut faire disparaître la copie privée au bon moment. L’écriture, qui croyait viser la copie, retombe alors sur la version commune du fichier, celle que tout le système utilise. La version en mémoire du fichier est modifiée, c’est elle que tout le système lit, et elle peut ensuite être recopiée sur le disque. En visant le fichier qui liste les comptes ou un programme à privilèges, un utilisateur ordinaire se fabrique un accès administrateur complet.

Pour que ça marche, il faut déjà être sur la machine. La seule vraie défense est de remplacer le morceau de système fautif par sa version corrigée, puis de redémarrer : tant que l’ancien tourne encore en mémoire, rien n’est réglé, même si la mise à jour est installée.

À lire aussi

Pages voisines