Peu de mécanismes sont aussi centraux, en exploitation binaire, que la paire GOT/PLT : c’est elle qui décide comment un binaire dynamique appelle le monde extérieur, et c’est elle que RELRO vient précisément verrouiller.

Le rôle de la GOT et de la PLT

La GOT (Global Offset Table, en .got et .got.plt) est une table utilisée par le loader dynamique pour résoudre, à l’exécution, les adresses réelles des fonctions et variables externes — notamment pour du code indépendant de sa position (PIC/PIE). Quand un binaire appelle une fonction d’une bibliothèque partagée (printf, system), le code n’y encode jamais une adresse absolue — inconnue à la compilation — mais passe par un emplacement de la GOT, que le loader dynamique remplit à l’exécution avec l’adresse effective. La GOT agit ainsi comme une indirection, un mécanisme de relocalisation dynamique, qui rend possibles la modularité et le PIE.

La PLT (Procedure Linkage Table, en .plt) est un trampoline : elle sert de pont vers la GOT pour les appels externes, en particulier lors du premier appel à une fonction. Au premier appel de foo(), la PLT contient un stub qui invoque le loader dynamique ; celui-ci résout l’adresse réelle, la place dans le bon emplacement de la GOT, puis redirige l’appel. Les appels suivants passent directement par la GOT via la PLT, sans repasser par la résolution. C’est le lazy binding : ne lier une fonction que lorsqu’elle sert, pour gagner du temps au démarrage.

RELRO : verrouiller la GOT

RELRO (Relocation Read-Only) est une protection runtime appliquée aux sections de relocation — GOT en tête — pour rendre certaines zones mémoire non modifiables après résolution des symboles. Elle existe en deux niveaux.

En partial RELRO, mode par défaut le plus courant, seule la partie « non-PLT » de la GOT est réorganisée en mémoire, souvent placée avant la BSS pour réduire le risque d’écrasement par débordement de variable globale. Mais .got.plt — la partie que le lazy binding doit pouvoir écrire — reste modifiable : le GOT overwrite demeure possible tant que le binaire est chargé et que la PLT n’a pas fini de résoudre les symboles utiles. Partial RELRO n’offre donc qu’une protection faible.

En full RELRO (options -z now et -z relro à la compilation), tous les symboles dynamiques sont résolus au lancement, avant main() — remplissant intégralement la GOT — puis celle-ci est marquée en lecture seule. Toute tentative d’y écrire provoque un SIGSEGV. Le GOT overwrite devient alors impossible ; le compromis est un léger surcoût au démarrage, le temps de résoudre toutes les liaisons d’un coup plutôt qu’à la demande.

Vérifier la présence de RELRO

readelf -l <binaire> | grep GNU_RELRO révèle une entrée PT_GNU_RELRO — signe d’au moins une RELRO partielle. Pour confirmer le full, chercher en plus le drapeau BIND_NOW (DF_BIND_NOW) dans la section dynamique. Une inspection sous GDB, ou de /proc/<pid>/maps, confirme si la zone GOT/PLT est en r-x ou r-w une fois le programme chargé.

Pourquoi cela conditionne la stratégie d’exploitation

Dans un contexte d’exploitation binaire, la capacité à écraser une entrée GOT est un vecteur d’attaque classique : rediriger puts@GOT vers system, ou vers un gadget ROP, suffit à obtenir l’exécution de code arbitraire dès lors qu’on dispose d’une écriture arbitraire. Si le binaire est compilé en full RELRO, ce vecteur est fermé — il faut viser autre chose : ROP direct, ret2libc sans passer par un GOT overwrite, ou une vulnérabilité d’une autre nature.

RELRO n’est cependant pas une protection universelle : même en full RELRO, un débordement dans .text, un use-after-free ou toute corruption qui ne passe pas par la GOT échappe entièrement à sa portée. Sur des binaires statiquement liés ou fortement optimisés, GOT et PLT peuvent d’ailleurs ne pas exister du tout — ce qui change complètement le modèle d’attaque. Vérifier la présence et le niveau de RELRO est donc, avant toute stratégie fondée sur la GOT, un préalable incontournable.