7.7 KiB
Raw Blame History

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é RX 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 de ld).
  • 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 que rtld_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 et PT_GNU_RELRO
  • “RELRO Complet, Partiel et Techniques de Contournement” article de blog @ wolfslittlered 2023

{{#include ../../banners/hacktricks-training.md}}