7.7 KiB
Relro
{{#include ../../banners/hacktricks-training.md}}
Relro
RELRO signifie Relocation Read-Only et c'est une atténuation mise en œuvre par le linker (ld
) qui rend un sous-ensemble des segments de données ELF lecture seule après que toutes les relocations ont été appliquées. L'objectif est d'empêcher un attaquant de remplacer des entrées dans la GOT (Global Offset Table) ou d'autres tables liées aux relocations qui sont déréférencées pendant l'exécution du programme (par exemple, __fini_array
).
Les linkers modernes mettent en œuvre RELRO en réorganisant la GOT (et quelques autres sections) afin qu'elles se trouvent avant le .bss et – surtout – en créant un segment PT_GNU_RELRO
dédié qui est remappé R–X
juste après que le chargeur dynamique ait terminé d'appliquer les relocations. Par conséquent, les débordements de tampon typiques dans le .bss ne peuvent plus atteindre la GOT et les primitives d'écriture arbitraire ne peuvent pas être utilisées pour écraser des pointeurs de fonction qui se trouvent à l'intérieur d'une page protégée par RELRO.
Il existe deux niveaux de protection que le linker peut émettre :
Partial RELRO
- Produit avec le drapeau
-Wl,-z,relro
(ou juste-z relro
lors de l'invocation directe deld
). - Seule la partie non-PLT de la GOT (la partie utilisée pour les relocations de données) est placée dans le segment en lecture seule. Les sections qui doivent être modifiées à l'exécution – surtout .got.plt qui prend en charge lazy binding – restent modifiables.
- À cause de cela, une primitive d'écriture arbitraire peut toujours rediriger le flux d'exécution en écrasant une entrée PLT (ou en effectuant ret2dlresolve).
- L'impact sur les performances est négligeable et donc presque toutes les distributions expédient des paquets avec au moins Partial RELRO depuis des années (c'est le défaut de GCC/Binutils depuis 2016).
Full RELRO
- Produit avec les deux drapeaux
-Wl,-z,relro,-z,now
(a.k.a.-z relro -z now
).-z now
force le chargeur dynamique à résoudre tous les symboles à l'avance (liaison impatiente) afin que .got.plt n'ait jamais besoin d'être écrit à nouveau et puisse être mappé en toute sécurité en lecture seule. - L'ensemble de la GOT, .got.plt, .fini_array, .init_array, .preinit_array et quelques tables internes supplémentaires de glibc se retrouvent à l'intérieur d'un segment
PT_GNU_RELRO
en lecture seule. - Ajoute un surcoût de démarrage mesurable (toutes les relocations dynamiques sont traitées au lancement) mais aucun surcoût à l'exécution.
Depuis 2023, plusieurs distributions grand public ont commencé à compiler la tool-chain système (et la plupart des paquets) avec Full RELRO par défaut – par exemple Debian 12 “bookworm” (dpkg-buildflags 13.0.0) et Fedora 35+. En tant que pentester, vous devez donc vous attendre à rencontrer des binaires où chaque entrée GOT est en lecture seule.
Comment vérifier le statut RELRO d'un binaire
$ checksec --file ./vuln
[*] '/tmp/vuln'
Arch: amd64-64-little
RELRO: Full
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
checksec
(fait partie de pwntools et de nombreuses distributions) analyse les en-têtes ELF
et affiche le niveau de protection. Si vous ne pouvez pas utiliser checksec
, comptez sur readelf
:
# Partial RELRO → PT_GNU_RELRO is present but BIND_NOW is *absent*
$ readelf -l ./vuln | grep -E "GNU_RELRO|BIND_NOW"
GNU_RELRO 0x0000000000600e20 0x0000000000600e20
# Full RELRO → PT_GNU_RELRO *and* the DF_BIND_NOW flag
$ readelf -d ./vuln | grep BIND_NOW
0x0000000000000010 (FLAGS) FLAGS: BIND_NOW
Si le binaire est en cours d'exécution (par exemple, un helper set-uid root), vous pouvez toujours inspecter l'exécutable via /proc/$PID/exe
:
readelf -l /proc/$(pgrep helper)/exe | grep GNU_RELRO
Activer RELRO lors de la compilation de votre propre code
# GCC example – create a PIE with Full RELRO and other common hardenings
$ gcc -fPIE -pie -z relro -z now -Wl,--as-needed -D_FORTIFY_SOURCE=2 main.c -o secure
-z relro -z now
fonctionne pour GCC/clang (passé après -Wl,
) et ld directement. Lorsque vous utilisez CMake 3.18+, vous pouvez demander Full RELRO avec le préréglage intégré :
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) # LTO
set(CMAKE_ENABLE_EXPORTS OFF)
set(CMAKE_BUILD_RPATH_USE_ORIGIN ON)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-z,relro,-z,now")
Techniques de contournement
Niveau RELRO | Primitive typique | Techniques d'exploitation possibles |
---|---|---|
Aucun / Partiel | Écriture arbitraire | 1. Écraser l'entrée .got.plt et pivoter l'exécution. 2. ret2dlresolve – créer un faux Elf64_Rela & Elf64_Sym dans un segment écrivable et appeler _dl_runtime_resolve .3. Écraser les pointeurs de fonction dans .fini_array / liste atexit(). |
Complet | GOT en lecture seule | 1. Rechercher d'autres pointeurs de code écrivable (vtables C++, __malloc_hook < glibc 2.34, __free_hook , rappels dans des sections .data personnalisées, pages JIT).2. Abuser des primitives de lecture relative pour divulguer libc et effectuer SROP/ROP dans libc. 3. Injecter un objet partagé malveillant via DT_RPATH/ LD_PRELOAD (si l'environnement est contrôlé par l'attaquant) ou ld_audit .4. Exploiter format-string ou écriture partielle de pointeur pour détourner le flux de contrôle sans toucher le GOT. |
💡 Même avec un RELRO Complet, le GOT des bibliothèques partagées chargées (par exemple, libc elle-même) est seulement Partiel RELRO car ces objets sont déjà mappés lorsque le chargeur applique les relocations. Si vous obtenez une primitive d'écriture arbitraire qui peut cibler les pages d'un autre objet partagé, vous pouvez toujours pivoter l'exécution en écrasant les entrées GOT de libc ou la pile
__rtld_global
, une technique régulièrement exploitée dans les défis CTF modernes.
Exemple de contournement dans le monde réel (CTF 2024 – pwn.college “enlightened”)
Le défi était livré avec un RELRO Complet. L'exploitation a utilisé un off-by-one pour corrompre la taille d'un morceau de tas, a divulgué libc avec tcache poisoning
, et enfin a écrasé __free_hook
(en dehors du segment RELRO) avec un one-gadget pour obtenir l'exécution de code. Aucune écriture dans le GOT n'était requise.
Recherches récentes & vulnérabilités (2022-2025)
- glibc 2.40 déprécie
__malloc_hook
/__free_hook
(2025) – La plupart des exploits de tas modernes qui abusaient de ces symboles doivent maintenant pivoter vers des vecteurs alternatifs tels quertld_global._dl_load_jump
ou les tables d'exceptions C++. Étant donné que les hooks vivent en dehors du RELRO, leur suppression augmente la difficulté des contournements de RELRO Complet. - Correction “max-page-size” de Binutils 2.41 (2024) – Un bug permettait aux derniers octets du segment RELRO de partager une page avec des données écrivables sur certaines constructions ARM64, laissant un petit écart RELRO qui pouvait être écrit après
mprotect
. En amont,PT_GNU_RELRO
est maintenant aligné sur les limites de page, éliminant ce cas particulier.
Références
- Documentation de Binutils –
-z relro
,-z now
etPT_GNU_RELRO
- “RELRO – Complet, Partiel et Techniques de Contournement” – article de blog @ wolfslittlered 2023
{{#include ../../banners/hacktricks-training.md}}