Il existe une distinction fonctionnelle entre le linker (éditeur de liens) et le loader (chargeur), bien que leurs tâches se chevauchent, notamment en ce qui concerne la résolution des adresses et la relocation. Le terme de loader dynamique fait généralement référence au lieur d’exécution (runtime linker), qui est un type de chargeur spécialisé dans la gestion des bibliothèques partagées.
1. Le linker (éditeur de liens)
Le linker (éditeur de liens) est un programme qui combine diverses parties de code et de données pour former un unique exécutable qui peut être chargé en mémoire.
Le processus de linking peut s’effectuer à différents moments : à la compilation, au chargement (par les chargeurs), ou à l’exécution (par des programmes d’application).
Les tâches principales du linker sont :
- Résolution des symboles (symbol resolution) : un programme est souvent composé de multiples sous-programmes qui se référencent mutuellement via des symboles. Le travail du linker est de résoudre ces références en localisant le symbole et en corrigeant le code objet de l’appelant.
- Relocation : les compilateurs et assembleurs génèrent initialement le code objet avec une adresse de départ de zéro. La relocation est le processus d’assignation d’adresses de chargement aux différentes parties du programme, en fusionnant toutes les sections de même type en une seule section. Le code et les sections de données sont ajustés pour pointer vers les adresses d’exécution correctes. Une fois tous les symboles résolus, le linker modifie chaque référence de symbole dans les sections de code et de données pour qu’elles pointent vers les adresses de chargement correctes.
2. Le loader (chargeur)
Le loader (chargeur) effectue le chargement du programme (program loading). Cela consiste à copier l’image du programme du disque dur vers la mémoire principale afin de le mettre dans un état prêt à l’exécution. Dans certains cas, cela peut également impliquer l’allocation d’espace de stockage ou le mappage d’adresses virtuelles vers des pages disque.
Sous Linux, le shell (interprète de commandes) invoque la fonction du chargeur (souvent execve) pour copier le code et les données du fichier exécutable en mémoire, puis transférer le contrôle à la première instruction du programme.
3. Le loader dynamique (lieur d’exécution) et la différence
Le terme loader dynamique (ou lieur d’exécution ou runtime linker) désigne le programme chargé d’effectuer la liaison (linking) des bibliothèques partagées au moment de l’exécution (ou du chargement).
Pour la majorité des programmes qui sont liés dynamiquement, le noyau vérifie la présence d’une entrée PT_INTERP dans l’en-tête du programme ELF. Cette entrée identifie le lieur d’exécution nécessaire pour assembler le programme complet (par exemple /lib64/ld-linux-x86-64.so.2).
Le processus est le suivant :
- Le gestionnaire ELF du noyau charge l’interpréteur ELF (le lieur d’exécution) en mémoire.
- Le chargeur passe le contrôle au dynamic linker (lieur dynamique).
- Le lieur dynamique mappe les bibliothèques partagées requises dans l’espace d’adressage du programme.
- Il effectue la relocation du texte et des données de ces bibliothèques partagées.
- Il résout toute référence de symbole dans l’exécutable principal qui dépend de ces bibliothèques partagées.
- Enfin, il transfère le contrôle à l’application.
En résumé, la différence principale réside dans le moment de l’opération : le linker (statique) effectue la résolution des symboles et la relocation avant l’exécution (au moment de la compilation / édition des liens), créant un exécutable complet. Le loader dynamique (lieur d’exécution) est un composant qui effectue ces tâches (chargement des bibliothèques, résolution des symboles) pendant le chargement ou l’exécution du programme, pour gérer les dépendances dynamiques.
4. Rôle dans la résolution d’adresses GOT/PLT (exploitation binaire)
Dans le contexte des binaires liés dynamiquement, le lieur dynamique joue un rôle crucial dans la résolution des adresses en utilisant la Global Offset Table (GOT) et la Procedure Linkage Table (PLT).
Fonctionnement du GOT/PLT et du lieur dynamique
- Appel de fonction : lorsqu’une fonction d’une bibliothèque partagée est appelée pour la première fois, le code effectue l’appel via la PLT (située dans la section
.plt). - Redirection initiale : la PLT contient des instructions qui pointent vers la GOT (située dans la section
.got.plt). Initialement, l’entrée GOT pour cette fonction pointe en fait vers l’entrée de la PLT elle-même. - Appel du lieur dynamique : lorsque le code est exécuté pour la première fois, il passe par la PLT, qui déclenche un appel au lieur dynamique.
- Résolution : le lieur dynamique prend le relais, trouve l’emplacement réel de la fonction dans la bibliothèque partagée en mémoire.
- Mise à jour du GOT : l’emplacement réel de la fonction trouvée est ensuite écrit dans la GOT.
- Appels ultérieurs : les appels suivants à cette fonction utilisent directement le pointeur mis à jour dans la GOT, qui pointe désormais vers la fonction dans la bibliothèque partagée. Ce mécanisme est appelé lazy binding (liaison paresseuse), car il économise des cycles CPU en ne résolvant les fonctions que lorsqu’elles sont utilisées.
Implications pour l’exploitation et la mitigation
Le rôle du lieur dynamique et le mécanisme de liaison paresseuse ont des implications directes pour la sécurité et le reverse engineering :
-
Vulnérabilité (avant RELRO) : étant donné que la GOT est mise à jour dynamiquement par le lieur, elle doit être inscriptible (writable).
-
La GOT résidant à un emplacement statique prédéfini en mémoire, si un programme contient une vulnérabilité permettant à un attaquant d’écrire à une adresse contrôlée (par exemple un dépassement d’entier), il peut écraser une entrée de la GOT. En remplaçant le pointeur de fonction légitime dans la GOT par l’adresse d’un shellcode injecté, un attaquant peut obtenir l’exécution de code arbitraire.
-
Mitigation (RELRO) : la technique de durcissement RELRO (Relocation Read-Only) a été développée pour contrer cette vulnérabilité.
- RELRO force le linker à résoudre toutes les fonctions liées dynamiquement au début de l’exécution (en utilisant l’option
-z,nowlors de la compilation). - Une fois la résolution initiale effectuée, la GOT est marquée en lecture seule.
- La version complète de RELRO (full RELRO) rend l’intégralité de la GOT (
.gotet.got.plt) en lecture seule, empêchant ainsi l’écrasement des entrées de la GOT pour détourner l’exécution du programme. Si un attaquant tente d’écraser la GOT, l’application se bloque avecSIGSEGV.
- RELRO force le linker à résoudre toutes les fonctions liées dynamiquement au début de l’exécution (en utilisant l’option
En synthèse, le lieur dynamique est l’acteur qui effectue la résolution d’adresses nécessaire à la liaison des bibliothèques, en écrivant les adresses cibles dans la GOT, faisant ainsi du GOT un point d’intérêt majeur pour l’exploitation binaire. RELRO force le lieur (statique, lors de la compilation ou du chargement initial) à effectuer son travail plus tôt afin de sécuriser ce tableau avant l’exécution du programme principal.
Analogie : le linker statique est comme un architecte qui planifie où chaque pièce du bâtiment ira avant même le début de la construction. Le loader est l’équipe de construction qui assemble les murs sur le site. Le loader dynamique (lieur d’exécution) est le chef de chantier qui, au fur et à mesure que les occupants arrivent (les appels de fonctions), trouve les adresses des services externes (bibliothèques partagées) et met à jour un panneau d’affichage central (le GOT) pour que tout le monde sache où aller immédiatement la prochaine fois, au lieu de chercher à nouveau l’adresse.