Détournement de bibliothèque partagée Linux et ld.so
Section intitulée « Détournement de bibliothèque partagée Linux et ld.so »Le chargeur dynamique Linux résout les dépendances d’un binaire au lancement, dans un ordre précis et documenté. Qui écrit dans l’un des répertoires consultés avant le bon obtient l’exécution de code dans le processus cible. C’est le pendant Linux du DLL hijacking, avec ses propres règles de priorité et son propre garde-fou.
Un programme a besoin de briques toutes faites rangées quelque part sur la machine. Avant de démarrer, il part chercher ces briques en suivant toujours la même liste d’endroits. Celui qui glisse une fausse brique dans un endroit regardé trop tôt fait exécuter ce qu’il veut. C’est discret, et ça vaut le détour.
→ Lire l’explication complète de la marmotte
L’ordre de recherche de ld.so, pierre angulaire du détournement
Section intitulée « L’ordre de recherche de ld.so, pierre angulaire du détournement »Le chargeur dynamique (ld.so, ou ld-linux.so selon l’architecture) résout chaque bibliothèque partagée (fichier .so, pour shared object) dont un exécutable ELF (Executable and Linkable Format) dépend. Il consulte des emplacements dans un ordre figé, et s’arrête au premier fichier qui porte le bon nom (SONAME). Tout le détournement tient dans cette règle : le premier trouvé gagne, peu importe qu’il soit légitime.
L’ordre, pour un binaire ordinaire, est le suivant : d’abord les bibliothèques préchargées via LD_PRELOAD, puis le DT_RPATH du binaire (hérité, et prioritaire sur la variable d’environnement, ce qui en fait le piège le plus ancien), puis LD_LIBRARY_PATH, puis le DT_RUNPATH, puis le cache /etc/ld.so.cache généré par ldconfig, et enfin les répertoires par défaut /lib et /usr/lib (et leurs variantes multiarch).
Ce schéma dit l’essentiel : chaque niveau qui contient un répertoire inscriptible par un attaquant, et placé avant celui où réside la vraie bibliothèque, ouvre un détournement. Le point qui surprend le plus : si un binaire porte un DT_RPATH, celui-ci l’emporte sur LD_LIBRARY_PATH, alors que DT_RUNPATH, lui, passe après. D’où la règle, vieille comme les distributions, d’abandonner RPATH au profit de RUNPATH.
LD_PRELOAD, RPATH, RUNPATH et le rôle de $ORIGIN
Section intitulée « LD_PRELOAD, RPATH, RUNPATH et le rôle de $ORIGIN »LD_PRELOAD force le chargement d’un .so avant toute autre dépendance : ses symboles masquent ceux des bibliothèques suivantes. C’est l’outil légitime du traçage et du débogage, et l’instrument classique du détournement quand un processus l’hérite de son environnement. Cet angle précis, l’injection via l’environnement d’un processus enfant, est traité ailleurs : voir la pollution de variables d’environnement. Ici le sujet est la résolution par nom, pas la variable qui la court-circuite.
DT_RPATH et DT_RUNPATH sont deux entrées inscrites dans l’en-tête ELF à la compilation (dans la section dynamique). Elles listent des répertoires de recherche propres au binaire. La différence fonctionnelle tient en une ligne : RPATH est consulté avant LD_LIBRARY_PATH, RUNPATH après. RPATH est déprécié depuis des années, mais il traîne dans des binaires recompilés à la hâte, des éditeurs de liens mal configurés et des SDK tiers qui n’ont jamais lu le changelog.
Le jeton $ORIGIN est l’autre pièce clé. Dans un RPATH ou un RUNPATH, il est remplacé au chargement par le répertoire du binaire exécuté. Il sert à rendre une application relogeable ($ORIGIN/../lib), ce qui est commode pour les paquets autonomes. Le danger : si le binaire vit dans un répertoire inscriptible, ou s’il peut être copié dans un tel répertoire par un attaquant, $ORIGIN pointe désormais vers un terrain qu’il contrôle. Un RUNPATH à base de $ORIGIN n’est sûr que si l’emplacement du binaire ne l’est pas moins que /usr/lib.
Le mode secure-execution neutralise LD_PRELOAD sur les binaires privilégiés
Section intitulée « Le mode secure-execution neutralise LD_PRELOAD sur les binaires privilégiés »Le garde-fou du noyau et de la glibc s’appelle le mode secure-execution. Il s’active automatiquement quand un binaire tourne avec une identité qui n’est pas celle de l’appelant : setuid/setgid (bit SUID, pour Set User ID), capabilities de fichier, ou tout écart entre UID réel et effectif. Dans ce mode, ld.so ignore purement et simplement LD_PRELOAD, LD_LIBRARY_PATH et plusieurs autres variables sensibles, et neutralise un RPATH/RUNPATH qui contiendrait $ORIGIN ou un chemin relatif.
Autrement dit, le vecteur par variable d’environnement est mort pour les binaires privilégiés depuis longtemps. Qui espère détourner un SUID via LD_PRELOAD perd son temps : ce chemin a été refermé il y a une vingtaine d’années. L’élévation de privilège via SUID passe aujourd’hui par d’autres défauts, détaillés dans l’exploitation des binaires SUID.
Ce que secure-execution ne couvre pas, et c’est capital : il ne protège que les binaires privilégiés. Un service applicatif lancé sous son propre compte, sans SUID ni capability, ne bénéficie d’aucune de ces protections. Son RUNPATH relatif, son $ORIGIN dans un répertoire inscriptible et son LD_LIBRARY_PATH restent pleinement exploitables. La protection suit le privilège, pas la sensibilité réelle du service, qui peut être énorme sans être root.
Exemple : auditer les RPATH et RUNPATH d’un binaire
Section intitulée « Exemple : auditer les RPATH et RUNPATH d’un binaire »L’audit consiste à lire la section dynamique de l’ELF et à repérer tout chemin relatif, tout $ORIGIN suspect, et la présence de RPATH là où RUNPATH devrait régner.
# Lire les entrées de recherche de bibliothèques d'un binairereadelf -d /opt/app/bin/monservice | grep -E 'RPATH|RUNPATH'# Sortie typique à proscrire : (RPATH) Library rpath: [.:lib:$ORIGIN/../lib]# -> RPATH présent (déprécié), chemin relatif "." et "lib" (inscriptibles selon cwd)
# Détecter en masse tout RPATH résiduel sur un arbre de binairesfind /opt /usr/local -type f -executable -exec sh -c \ 'readelf -d "$1" 2>/dev/null | grep -q RPATH && echo "RPATH: $1"' _ {} \;
# Vérifier ce que le chargeur résoudra réellement, sans exécuterldd /opt/app/bin/monservice# Chaque "=> /chemin/trouvé" montre le .so retenu ; un chemin inattendu = détournement potentielLa règle de lecture : un RUNPATH doit être absolu (/opt/app/lib), sans composant inscriptible par un compte non privilégié, et sans $ORIGIN quand le binaire peut vivre ailleurs que dans un répertoire verrouillé. Un . ou un répertoire relatif dans la liste est une faille ouverte, pas une coquetterie de build. ldd reste à manier avec prudence sur un binaire non fiable : sur certaines versions, il peut déclencher l’interpréteur, donc à réserver aux binaires qu’on contrôle.
Durcir le chargeur et le cache ld.so
Section intitulée « Durcir le chargeur et le cache ld.so »Les contre-mesures se classent par ordre d’efficacité. D’abord, bannir RPATH de toute la chaîne de build et n’émettre que du RUNPATH absolu : sur la plupart des éditeurs de liens, -Wl,--disable-new-dtags produit encore du RPATH, c’est l’option à ne jamais laisser traîner, tandis que --enable-new-dtags donne du RUNPATH. Ensuite, proscrire $ORIGIN dès que le binaire n’est pas installé dans un chemin dont les permissions valent celles de /usr/lib (racine propriétaire, non inscriptible par les autres).
Le cache /etc/ld.so.cache, régénéré par ldconfig, doit refléter uniquement les répertoires de /etc/ld.so.conf et /etc/ld.so.conf.d/. Un répertoire inscriptible ajouté à cette configuration, c’est un détournement pour tous les binaires de la machine d’un coup : la surface maximale. Le fichier de config et les répertoires qu’il déclare doivent appartenir à root et refuser l’écriture au reste du monde. Toute entrée pointant vers /tmp, un home ou un montage partagé est une anomalie à traiter comme un incident.
Cette topologie montre la seule chaîne défendable : le binaire sort du build avec un RUNPATH absolu figé, il est empaqueté et signé, et l’hôte de production n’installe que des paquets signés. La signature de paquets ferme la porte à la substitution de .so au repos, que le détournement par ordre de recherche contourne précisément quand rien ne vérifie l’origine des fichiers. Pour les services conteneurisés, l’image doit être construite avec les mêmes garanties et des répertoires de bibliothèques non inscriptibles par l’utilisateur du conteneur ; le voisinage Windows de ce mécanisme est décrit dans le DLL hijacking.
Côté détection continue : un audit périodique des RPATH/RUNPATH relatifs via readelf -d sur les binaires déployés, la surveillance de l’intégrité de /etc/ld.so.conf* et de /etc/ld.so.cache, et le contrôle des permissions des répertoires listés dans RUNPATH. Un .so apparu dans un répertoire applicatif hors paquet doit déclencher une alerte, pas une régénération silencieuse du cache.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans RUNPATH absolu ni abandon de RPATH, un compte non privilégié qui écrit dans un répertoire consulté avant la vraie bibliothèque fait charger son .so dans le processus cible, donc exécute du code avec les droits de ce processus. Sans contrôle de $ORIGIN, un binaire recopié dans un répertoire inscriptible résout ses dépendances sur un terrain contrôlé par l’attaquant. Sans cache ld.so verrouillé, un seul répertoire inscriptible ajouté à la configuration détourne toutes les exécutions de la machine. Sans audit des entrées dynamiques ni paquets signés, la substitution de bibliothèque passe inaperçue : aucune trace, aucune alerte, et la persistance s’installe à chaque lancement du service.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Un programme est rarement complet à lui seul : il utilise des morceaux de code déjà écrits, rangés dans des fichiers séparés sur la machine. Au moment où le programme démarre, un composant du système part chercher ces fichiers. Il suit une liste d’endroits, toujours dans le même ordre, et prend le premier fichier qui porte le bon nom, sans vérifier s’il est authentique. C’est là tout le problème : si quelqu’un a le droit d’écrire dans un endroit regardé tôt dans la liste, il y dépose un faux fichier, et le programme charge ce faux fichier à la place du vrai. Le code de l’attaquant tourne alors avec les droits du programme visé. Certains programmes transportent eux-mêmes une liste d’endroits à regarder, inscrite au moment de leur fabrication ; si cette liste contient des emplacements relatifs ou modifiables, elle devient une porte d’entrée. Le système protège quand même les programmes puissants, ceux qui changent d’identité pour obtenir plus de droits : pour eux, il ignore les raccourcis qu’un utilisateur pourrait fournir. Mais cette protection ne couvre que ces programmes-là ; un service ordinaire, même très sensible, n’en profite pas. Les bonnes défenses sont donc : des listes d’endroits fixes, absolues, non modifiables, pas de raccourci relatif, un fichier de configuration verrouillé, et des fichiers de code signés pour prouver leur origine.
À lire aussi
- Dirty COW CVE-2016-5195 : course dans le copy-on-write LinuxDirty COW (CVE-2016-5195), condition de course dans le copy-on-write du noyau Linux permettant d'écrire dans un fichier en lecture seule et de passer root, avec sa cause, sa chronologie et ses contre-mesures.
- Linux shell, permissions et processus, le modèle de contrôle d'accèsComment un système Linux décide qui a le droit de faire quoi, du shell qui lance les processus aux identités, permissions et capabilities que ces processus portent.
- Exploiter une race condition TOCTOU sur fichier privilégiéDémonstration d'une attaque TOCTOU sur une opération de fichier privilégiée, avec mécanisme, reproduction contrôlée et contre-mesures.
- Écrire des scripts Bash système fiables et sûrs en productionBash et scripting système, ou comment écrire des scripts d'administration qui tiennent en production sans devenir une faille (mode strict, guillemets, injection, secrets, verrous, environnement d'exécution).
Pages voisines
La citent
Détournement de bibliothèque partagée Linux et ld.so
Elle cite
- Lien dans les deux sens :Élévation de privilège via un binaire SUID root mal configuré
- Lien dans les deux sens :Pollution de variable d'environnement héritée dans un conteneur
- Lien dans les deux sens :DLL hijacking Windows et ordre de recherche des bibliothèques
- Lien dans les deux sens :Détecter la persistance d’un attaquant après incident
- Écrire un Dockerfile durci avec condensé, multi-étapes et non root
- Signer et vérifier les artefacts de build
: les deux pages se citent l’une l’autre.
