7.4 KiB
Raw Blame History

Relro

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

Relro

RELRO significa Relocation Read-Only e é uma mitigação implementada pelo linker (ld) que torna um subconjunto dos segmentos de dados do ELF somente leitura após todas as realocações terem sido aplicadas. O objetivo é impedir que um atacante sobrescreva entradas na GOT (Tabela de Deslocamento Global) ou outras tabelas relacionadas a realocações que são desreferenciadas durante a execução do programa (por exemplo, __fini_array).

Linkers modernos implementam RELRO reordenando a GOT (e algumas outras seções) para que elas fiquem antes da .bss e mais importante criando um segmento dedicado PT_GNU_RELRO que é remapeado RX logo após o carregador dinâmico terminar de aplicar as realocações. Consequentemente, estouros de buffer típicos na .bss não podem mais alcançar a GOT e primitivas de escrita arbitrária não podem ser usadas para sobrescrever ponteiros de função que estão dentro de uma página protegida por RELRO.

Existem dois níveis de proteção que o linker pode emitir:

Partial RELRO

  • Produzido com a flag -Wl,-z,relro (ou apenas -z relro ao invocar ld diretamente).
  • Apenas a parte não-PLT da GOT (a parte usada para realocações de dados) é colocada no segmento somente leitura. Seções que precisam ser modificadas em tempo de execução mais importante .got.plt que suporta lazy binding permanecem graváveis.
  • Por causa disso, uma primitiva de escrita arbitrária ainda pode redirecionar o fluxo de execução sobrescrevendo uma entrada PLT (ou realizando ret2dlresolve).
  • O impacto no desempenho é negligenciável e, portanto, quase todas as distribuições têm enviado pacotes com pelo menos Partial RELRO há anos (é o padrão do GCC/Binutils desde 2016).

Full RELRO

  • Produzido com ambas as flags -Wl,-z,relro,-z,now (também conhecido como -z relro -z now). -z now força o carregador dinâmico a resolver todos os símbolos antecipadamente (binding ansioso) para que .got.plt nunca precise ser escrita novamente e possa ser mapeada com segurança como somente leitura.
  • A GOT inteira, .got.plt, .fini_array, .init_array, .preinit_array e algumas tabelas internas adicionais da glibc acabam dentro de um segmento PT_GNU_RELRO somente leitura.
  • Adiciona uma sobrecarga de inicialização mensurável (todas as realocações dinâmicas são processadas na inicialização), mas sem sobrecarga em tempo de execução.

Desde 2023, várias distribuições populares mudaram para compilar a ferramenta de sistema (e a maioria dos pacotes) com Full RELRO por padrão por exemplo, Debian 12 “bookworm” (dpkg-buildflags 13.0.0) e Fedora 35+. Como um pentester, você deve, portanto, esperar encontrar binários onde cada entrada da GOT é somente leitura.


Como verificar o status RELRO de um binário

$ checksec --file ./vuln
[*] '/tmp/vuln'
Arch:     amd64-64-little
RELRO:    Full
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

checksec (parte do pwntools e muitas distribuições) analisa os cabeçalhos ELF e imprime o nível de proteção. Se você não puder usar checksec, confie em 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

Se o binário estiver em execução (por exemplo, um helper set-uid root), você ainda pode inspecionar o executável via /proc/$PID/exe:

readelf -l /proc/$(pgrep helper)/exe | grep GNU_RELRO

Habilitando RELRO ao compilar seu próprio código

# 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 funciona para ambos GCC/clang (passado após -Wl,) e ld diretamente. Ao usar CMake 3.18+ você pode solicitar Full RELRO com o preset embutido:

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")

Técnicas de Bypass

Nível RELRO Primitiva típica Técnicas de exploração possíveis
Nenhum / Parcial Escrita arbitrária 1. Sobrescrever a entrada .got.plt e mudar a execução.
2. ret2dlresolve criar Elf64_Rela & Elf64_Sym falsos em um segmento gravável e chamar _dl_runtime_resolve.
3. Sobrescrever ponteiros de função na lista .fini_array / atexit().
Completo GOT é somente leitura 1. Procurar outros ponteiros de código graváveis (vtables C++, __malloc_hook < glibc 2.34, __free_hook, callbacks em seções .data personalizadas, páginas JIT).
2. Abusar de primitivas de leitura relativa para vazar libc e realizar SROP/ROP em libc.
3. Injetar um objeto compartilhado malicioso via DT_RPATH/LD_PRELOAD (se o ambiente for controlado pelo atacante) ou ld_audit.
4. Explorar format-string ou sobrescrita parcial de ponteiro para desviar o fluxo de controle sem tocar na GOT.

💡 Mesmo com Full RELRO, a GOT de bibliotecas compartilhadas carregadas (por exemplo, a própria libc) é apenas RELRO Parcial porque esses objetos já estão mapeados quando o carregador aplica relocations. Se você ganhar uma primitiva de escrita arbitrária que pode direcionar as páginas de outro objeto compartilhado, ainda pode mudar a execução sobrescrevendo as entradas da GOT da libc ou a pilha __rtld_global, uma técnica frequentemente explorada em desafios modernos de CTF.

Exemplo de bypass no mundo real (2024 CTF pwn.college “enlightened”)

O desafio foi enviado com Full RELRO. O exploit usou um off-by-one para corromper o tamanho de um chunk de heap, vazou libc com tcache poisoning e finalmente sobrescreveu __free_hook (fora do segmento RELRO) com um one-gadget para obter execução de código. Nenhuma escrita na GOT foi necessária.


Pesquisas recentes & vulnerabilidades (2022-2025)

  • glibc 2.40 descontinuou __malloc_hook / __free_hook (2025) A maioria dos exploits modernos de heap que abusaram desses símbolos agora deve mudar para vetores alternativos, como rtld_global._dl_load_jump ou tabelas de exceção C++. Como os hooks vivem fora do RELRO, sua remoção aumenta a dificuldade de bypasses de Full-RELRO.
  • Correção “max-page-size” do Binutils 2.41 (2024) Um bug permitiu que os últimos bytes do segmento RELRO compartilhassem uma página com dados graváveis em algumas compilações ARM64, deixando um pequeno gap RELRO que poderia ser escrito após mprotect. O upstream agora alinha PT_GNU_RELRO aos limites de página, eliminando esse caso extremo.

Referências

  • Documentação do Binutils -z relro, -z now e PT_GNU_RELRO
  • “RELRO Full, Partial and Bypass Techniques” post no blog @ wolfslittlered 2023

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