Ce sprint reprend le fonctionnement de RELRO, GOT et PLT dans une vision claire et opérationnelle pour l’exploitation binaire.
Rôle de la GOT et de la PLT dans un binaire ELF
GOT (Global Offset Table)
- La GOT est une table (souvent en
.got+.got.plt) utilisée par le loader dynamique / linker pour résoudre les adresses réelles des fonctions et variables externes (dynamiques) au moment de l’exécution, notamment pour du code position-indépendant (PIC/PIE)1. - Concrètement, quand un binaire appelle une fonction d’une bibliothèque partagée (e.g.
printf,system), le code ne hard-code pas directement une adresse absolue (inconnue à la compilation). À la place, il passe par un « slot » dans la GOT, que le dynamic linker remplit à l’exécution avec l’adresse effective de la fonction1. - Ainsi, la GOT agit comme une indirection — un mécanisme de « relocation » dynamique — pour permettre la modularité, le chargement d’éléments dynamiques et le PIE.
PLT (Procedure Linkage Table)
- La PLT est une zone de code (dans
.plt) qui sert de trampoline : c’est le « pont » vers la GOT pour les appels de fonctions externes — en particulier les premières fois que la fonction est appelée (lazy binding)2. - Schéma classique : lors du premier appel à
foo()→ la PLT contient un stub qui appelle le dynamic linker, ce dernier remplit le bon slot dans la GOT (relocation), puis redirige l’appel vers la vraie adresse. Les appels suivants defoo()passent directement via la GOT (via la PLT / GOT indirection). - Cela permet de ne pas lier (résoudre) toutes les fonctions externes dès le lancement (gain de temps), mais seulement quand elles sont utilisées — c’est le mécanisme lazy binding.
En résumé : la PLT fournit un trampoline + lazy binding, la GOT fournit l’indirection + le stockage des adresses réelles. La GOT et la PLT collaborent pour le linking dynamique.
Ce que RELRO change / protège
RELRO (Relocation Read-Only) est une protection runtime appliquée aux sections de relocation (notamment la GOT/PLT) pour rendre certaines parties mémoire non modifiables après résolution des symboles3.
Il existe deux modes principaux : partial RELRO et full RELRO4.
Partial RELRO
- C’est le mode le plus courant / par défaut4.
- Avec partial RELRO : la partie « non-PLT » de la GOT peut être rendue en lecture seule, ou du moins la disposition mémoire est organisée pour que la GOT soit « avant » la BSS, ce qui réduit certains risques d’écrasement via des débordements sur des variables globales5.
- Cependant la partie
.got.pltreste modifiable (puisque le lazy binding nécessite que le dynamic linker puisse écrire dedans) — donc des attaques de type GOT overwrite restent possibles tant que le binaire reste chargé et que la PLT n’a pas résolu tous les symboles utiles6. - Pour un attaquant exploitant une vulnérabilité d’écriture arbitraire, partial RELRO offre donc un faible niveau de protection : la GOT « statique » est un peu mieux protégée, mais la surface d’attaque (PLT/GOT.PLT) reste.
Full RELRO
- Avec full RELRO, le binaire (ou la bibliothèque) est lié de façon à résoudre tous les symboles dynamiques au lancement (option
-z now+-z relro), ce qui remplit tous les slots de la GOT/PLT avant d’exécutermain(), puis rend la GOT/PLT non modifiable (lecture seule)7. - Résultat : plus de possibilité de GOT overwrite à runtime — c’est un durcissement réel contre les attaques reposant sur la corruption des entrées GOT (redirection des fonctions, ROP via PLT/GOT)3.
- Le compromis : le temps de démarrage du programme peut augmenter (puisque toutes les liaisons dynamiques doivent être résolues avant l’entrée dans
main)3.
En pratique, lorsque full RELRO est actif, toute tentative d’écrire dans la GOT provoquera un segfault / crash (ou un comportement bloqué), ce qui empêche les attaques GOT overwrite3.
Pourquoi c’est important en exploitation / reverse / shellcoding
- Dans un contexte crackme / pwn / exploitation, la capacité à écraser des entrées GOT est un vecteur d’attaque classique — c’est un moyen de rediriger des appels de fonctions (par exemple faire pointer
puts@GOTverssystem, ou vers un gadget ROP) pour exécuter du code arbitraire. Si la GOT/PLT est modifiable, l’attaquant peut corrompre des pointeurs de fonction. - Si le binaire est build avec full RELRO, ce vecteur est fermé — on doit viser autre chose : ret2libc sans overwrite GOT, ROP direct, ou exploitation d’un bug différent.
- Ça change le « modèle mental » en reverse : il faut vérifier si RELRO est activé (et quel mode) avant de compter sur des techniques de GOT overwrite.
Comment inspecter si un binaire a RELRO (partial / full)
Avec des outils comme readelf, objdump, checksec :
readelf -l <binaire> | grep GNU_RELRO— si l’on voitPT_GNU_RELRO, c’est un signe que RELRO (au moins partiel) est inscrit5.- Pour full RELRO, on cherche en plus un drapeau indiquant la résolution bind now, comme
BIND_NOW/DT_FLAGS = DF_BIND_NOWdans la section dynamique5. - On peut aussi inspecter la protection mémoire après chargement (via GDB,
/proc/.../maps) pour voir si la section GOT/PLT est en r-x (read/execute) ou r-w (read/write).
Limites de RELRO — ce qu’il ne protège pas / ce qu’on peut encore faire
- Même avec full RELRO, certaines techniques d’attaque peuvent survivre — par exemple des attaques « non-invasives » via le loader / dynamic linker, ou des retours à des fonctions déjà résolues. Un article (How the ELF Ruined Christmas) montre qu’il existe des faiblesses dans le modèle dynamique ELF permettant de contourner partiellement RELRO8.
- RELRO ne protège que contre l’overwrite de la GOT/PLT — si l’on a un overflow de buffer dans le segment code, ou un use-after-free, ou un autre type de bug, la protection ne s’applique pas.
- Dans des binaires statiquement liés (ou fortement optimisés), la GOT/PLT peut ne pas exister, ou être moins pertinente — ce qui modifie complètement le scénario d’attaque.
Conclusion
- Toujours vérifier (avec
readelf,checksec) si RELRO est activé, et s’il s’agit de partial ou full — ça change beaucoup la surface d’attaque. - Si full RELRO est actif, ne pas partir du principe qu’un GOT-overwrite est possible : il faudra envisager d’autres techniques (ROP non-GOT, ret2libc par gadgets non modifiables, syscalls direct).
- Comprendre le rôle de la GOT + PLT + dynamic linker, c’est essentiel : c’est un bon point d’entrée pour analyser un binaire dynamique, identifier les zones sensibles, et modéliser les vecteurs d’attaque ou de contournement.
- Dans un contexte crackme / étude / shellcoding, savoir repérer ces protections permet de s’adapter, d’anticiper les défis, et de développer des méthodologies robustes, même sur binaires « hard-enforced ».