Survivre à la déferlante de LPE Linux : la défense en profondeur n'est pas morte !
Maintenant que l'IA aide à la recherche de vulnérabilités et au patch diffing de kernel, faisant sauter les embargos de responsible disclosure, la période est pour le moins intense pour les défenseurs. Chaque veille de week-end semble apporter son lot de nouvelles vulnérabilités critiques sous Linux, avec des exploits complets rendus publics plusieurs jours avant que les correctifs ne soient réellement disponibles.
Pourtant, la majorité des chaînes d'attaque récemment publiées peuvent être neutralisées grâce à des techniques éprouvées de durcissement (hardening) Linux. De quoi donner un peu de répit aux équipes de défense pour patcher sereinement, pendant que les attaquants N-day testent leur tout nouveau ./sploit
Passons en revue quelques-unes de ces contre-mesures !
Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus
Durcissement des permissions sur les binaires setuid
Quand les CVE de Crackarmor sont tombées un vendredi matin, nous avons dévoré l'excellent rapport de Qualys décrivant les multiples vulnérabilités découvertes dans le module de sécurité Linux (LSM) AppArmor. Nous avons rapidement réalisé deux choses :
- C'était du sérieux : en tant qu'aficionados de Debian et d'AppArmor, nous devions patcher bon nombre de nos propres systèmes, mais le noyau corrigé n'était pas encore disponible dans les dépôts Debian.
- Nous avions déjà une mesure d'atténuation (mitigation) en place pour le principal scénario d'élévation de privilèges (LPE) décrit. Ce dernier utilisait 'su' (puis 'sudo' dans un second scénario) pour déclencher le désormais célèbre « problème du député confus » (confused deputy problem) et injecter du contenu arbitraire dans les pseudo-fichiers de /sys/kernel/apparmor/ .
Sur une installation par défaut d'un système Debian ou Ubuntu, les permissions sur les binaires su & sudo ressemblent à ceci :
$ ls -l $(which su{,do})
-rwsr-xr-x 1 root root /usr/bin/su
-rwsr-xr-x 1 root root /usr/bin/sudo
Ce qui signifie que n'importe quel utilisateur du système peut tenter d'invoquer su ou sudo.
Quand on dit n'importe quel utilisateur, cela inclut des comptes de services comme ntpd, dnmasq, avahi, saned ou même l'utilisateur "nobody", c'est-à-dire ceux qui font tourner des processus exposés sur le réseau.
Puisque tout le monde peut exécuter le binaire setuid, la sécurité repose alors uniquement sur la logique interne du programme et sur la solidité de ses fichiers de configuration. Le moindre défaut de configuration ou la plus petite faille logique dans ces programmes ultra-sensibles peut alors mener à une LPE. Et historiquement il y a déjà eu bon nombre de CVE concernant ces outils.
Cerise sur le gâteau (ou plutôt le contraire) avec Crackarmor : même pas besoin d'une configuration ou d'un binaire su vulnérable, puisque la chaîne d'exploitation utilise su uniquement pour devenir... soi-même (en saisissant son propre mot de passe).
Sur nos systèmes durcis, les permissions POSIX de ces binaires ressemblent plutôt à ça :
$ ls -l $(which su{,do})
-rwsr-x--- 1 root admins /usr/bin/su
-rwsr-x--- 1 root admins /usr/bin/sudo
Grâce à cela, si votre utilisateur local n'est pas déjà root ou membre du groupe admins (un groupe personnalisé non standard créé par nos soins), vous ne pourrez PAS DU TOUT exécuter les binaires su ou sudo. Mieux encore, vous ne pourrez même pas ouvrir un descripteur de fichier (filesystem handle) dessus, ce qui s'avère particulièrement utile pour compliquer les attaques basées sur le cache du système de fichiers, comme la famille des failles « Dirty » (Dirty COW, Dirty Pipe, etc.).
Cela réduit considérablement l'impact de Crackarmor : il faut d'abord être un utilisateur un minimum privilégié ou de confiance pour tenter d'obtenir une LPE via le chemin décrit par Qualys (et à ce stade, il y a de fortes chances que vous puissiez déjà devenir root légitimement).
En bonus : même si la configuration de sudo était totalement permissive (ALL : ALL NOPASSWD: ALL), elle n'est pas exploitable si vous ne pouvez même pas lancer sudo.
Évidemment, il ne s'agit que d'une mesure d'atténuation : si vous ne patchez pas le noyau (ou ne redémarrez pas ensuite :)), la stack AppArmor reste vulnérable. Il reste par exemple possible que d'autres binaires setuid présents sur le système soient détournés à la place de su pour déclencher ce problème de « député confus » (bien que nos chercheurs en sécurité n'en aient pas trouvé d'autres — il faut dire que nous avons tendance à restreindre la quasi-totalité des binaires setuid sur nos systèmes).
Mais c'est précisément tout l'intérêt du concept de « défense en profondeur » : faire gagner du temps aux défenseurs dans la guerre asymétrique qui les oppose aux attaquants, qu'il s'agisse de hackers armés de 0-days, de cybercriminels exploitant des 1-days, ou même de sympathiques pentesters.
Note : le PoC public de Copy Fail (CVE-2026-31431) utilisait lui aussi su pour son élévation de privilèges, et se retrouve donc neutralisé par ce même durcissement.
Implémentation de ce durcissement
Si vous souhaitez reproduire cette approche, attention : un simple chmod/chgrp sur les binaires ne tiendra pas longtemps. À la prochaine mise à jour de ces paquets (lors d'un correctif de sécurité, par exemple), le gestionnaire de paquets de Debian (dpkg) réappliquera les permissions par défaut (issues du paquet). Votre ligne de défense se retrouverait instantanément balayée. De toute façon, modifier directement des fichiers fournis par des paquets est une mauvaise pratique, et des outils comme debsums (qui vérifient l'intégrité des paquets installés) ne manqueraient pas de lever des alertes.
La méthode officielle pour modifier de manière permanente les ACL locales d'un fichier packagé au format dpkg est d'utiliser dpkg-statoverride comme suit :
dpkg-statoverride --update --add root admins 4750 /usr/bin/su{,do}
C'est une commande très utile à connaître lorsque l'on durcit un OS basé sur Debian ou Ubuntu, et elle s'intègre très facilement dans vos playbooks (Ansible ou autres).
Sa man page résume d'ailleurs parfaitement la situation : les binaires setuid sont des cibles de choix pour un durcissement d'ACL. Supprimez leur bit setuid si vous pouvez vous en passer (comme c'est le cas pour ping depuis Debian 13) ou restreignez leur exécution à un groupe spécifique.
Durcissement du chargement des modules kernel
Par défaut, Linux embarque des centaines de modules noyau « optionnels » et se fait un plaisir de les charger dynamiquement dès qu'un programme effectue un appel système (syscall) qui le requiert.
C'est très pratique pour l'utilisateur, mais aussi particulièrement dangereux. On trouve régulièrement des vulnérabilités critiques dans des modules de niche, moins connus (et donc beaucoup moins audités).
De plus, avec l'avènement des user namespaces non privilégiés maintenant activés par défaut sur les distributions majeures, il est devenu facile pour un utilisateur lambda de créer un namespace au sein duquel il peut invoquer des syscalls privilégiés. Cela lui donne accès à des chemins entiers de code noyau potentiellement vulnérables qui n'étaient auparavant accessibles qu'au « vrai » utilisateur root.
Et parfois, même pas besoin de cela, si les vulnérabilités se cachent dans des portions de code directement appelables par des processus non privilégiés.
Dans la dernière fournée d'exploits LPE (Local Privilege Escalation) sur le noyau Linux, beaucoup s'appuient sur des syscalls vers des modules noyau vulnérables dont vous n'avez probablement pas vraiment besoin sur vos machines :
- Copy Fail crée des sockets
AF_ALGpour exploiteralgif_aead. Ce module permet aux programmes de déléguer certaines opérations de chiffrement au noyau, mais il semble que la plupart des logiciels qui l'utilisent retombent très bien sur d'autres mécanismes en cas d'absence. - Dirty Frag s'appuie sur des vulnérabilités trouvées dans les modules
esp4etesp6(qui implémentent le protocole IPsec). À moins que votre système ne serve de passerelle VPN IPsec, vous n'avez aucune raison de les charger. Une autre vulnérabilité (utilisée lorsque la création de user namespaces non privilégiés est bloquée) réside dans les socketsAF_RXRPCfournies par le modulerxrpc(utilisé pour l'« Andrew File System (AFS) », une sorte d'alternative distribuée à NFS, très probablement absente de la majorité des infrastructures, bien qu'Ubuntu semble charger ce module par défaut).
Les mesures d'atténuation officielles suggèrent toutes de bloquer individuellement les modules noyau affectés, ce qui fonctionne.
Mais si vous voulez avoir un coup d'avance sur les prochaines vagues d'attaques, il est préférable d'adopter une approche holistique et minimaliste basée sur une liste blanche (allowlist) : n'autoriser le noyau à charger que les modules spécifiques dont votre système a un besoin légitime et avéré.
C'est toutefois plus facile à dire qu'à faire.
Une première méthode consiste à compiler des noyaux personnalisés, en n'activant dans la configuration que les modules strictement nécessaires et en désactivant le chargement à la volée des autres (en remplaçant tous les 'm' par des 'n'). C'est une approche ultra-robuste, mais pas à la portée de tout le monde : vous abandonnez les noyaux précompilés fournis par votre distribution, ce qui signifie que vous devez gérer vous-même les problèmes de compilation et l'application des patchs de sécurité. De plus, chaque typologie de serveur nécessiterait un build de noyau différent (ex : certains avec IPsec, d'autres sans...).
Une approche beaucoup plus légère pour mettre en place cet allow-listing consiste à utiliser la sysctl /proc/sys/kernel/modules_disabled .
Dès que l'utilisateur root écrit la valeur 1 dans ce paramètre de configuration, le noyau verrouille la liste des modules actuellement chargés. Toute nouvelle requête de modprobe pour charger un nouveau module sera rejetée.
Attention : la seule façon d'autoriser à nouveau le chargement de nouveau modules sera de redémarrer complètement la machine. Une fois ce verrouillage activé, il est impossible de le désactiver "à chaud".
Implémentation de ce durcissement
L'idée est donc de laisser le système démarrer normalement et charger tous les modules réellement nécessaires au lancement des différents services, puis, à un moment donné, de tout verrouiller par un simple
echo 1 > /proc/sys/kernel/modules_disabled
Cela peut se faire via une simple unit systemd exécutée en fin de chaîne de démarrage, ou même déclenchée par un timer systemd après quelques minutes d'uptime.
[Unit]
Description=Kernel modules final lockdown
After=default.target # adjust if needed
[Service]
RemainAfterExit=yes
ExecStart=/bin/sh -c 'echo 1 > /proc/sys/kernel/modules_disabled'
[Install]
WantedBy=multi-user.target
Ce n'est qu'un brouillon, n'oubliez pas d'utiliser shh pour durcir la configuration de l'unit ;)
Le piège, évidemment, c'est que tout programme s'appuyant sur un module qui n'était pas chargé au moment du verrouillage plantera plus ou moins salement. De plus, lorsque son appel à modprobe échouera, le code d'erreur renvoyé ressemblera généralement à "file not found" (et non à un "permission denied"), ce qui peut induire en erreur lors du débuggage.
Ce n'est donc pas une solution magique sans effort (easy-mode) : attendez-vous à quelques tâtonnements, et prévoyez éventuellement de charger préemptivement certains modules utiles dès le démarrage en les listant dans /etc/modules-load.d/.
Quoi qu'il en soit, si vous y parvenez, félicitations : vous venez de passer d'une configuration de chargement des modules noyau en mode « autorisé par défaut » à un mode « bloqué par défaut ». De quoi faire gagner un temps précieux à votre Blue Team face aux futurs exploits, le tout sans avoir à recompiler vos propres noyaux.
Vers une approche plus fine : l'avenir avec eBPF ?
Une approche plus souple et plus granulaire pourrait s'inspirer de ce que Cloudflare a décrit dans son article de blog concernant leur gestion de Copy Fail. Ils ont utilisé leurs outils eBPF internes pour intercepter (hook) les syscalls socket_bind et n'autoriser la création de sockets AF_ALG qu'aux programmes qui l'avaient déjà fait auparavant.
Pour élargir cette méthode, on pourrait imaginer écrire un hook LSM eBPF pour les fonctions noyau __request_module() et call_modprobe(). Cela permettrait d'exécuter une logique de contrôle (comme la vérification d'une liste blanche de modules et/ou de processus appelants) pour autoriser ou bloquer à la volée l'insertion d'un nouveau module.
Le sujet fait déjà l'objet de discussions sur les forums et les mailing lists de sécurité Linux, il y a donc fort à parier qu'une approche unifiée et standardisée émerge prochainement.
Envie d'en apprendre plus sur les techniques de hardening Linux ?
Notre équipe infra organise périodiquement une formation de 4 jours sur ce sujet
Synacktiv recrute !
Si cet article vous a plu et que vous avez envie de rejoindre l'équipe chez nous qui essaie de trouver les durcissements avec le bon équilibre entre "gêner les attaquants" et "ne pas perturber la prod", jetez un coup d'oeil à notre offre d'emploi !