Security hardening: PIE, RELRO, stack canaries
1. Introdução à Segurança de Binários em C
A linguagem C oferece controle direto sobre memória, o que a torna poderosa, mas também vulnerável a ataques clássicos como buffer overflow, retorno a libc (ret2libc) e programação orientada a retorno (ROP). Sem proteções adequadas, um invasor pode sobrescrever endereços de retorno, desviar fluxo de execução e executar código arbitrário.
Modernamente, compiladores como GCC e Clang incorporam técnicas de hardening que dificultam esses ataques. As três principais são:
- PIE (Position Independent Executable): randomização do endereço base do binário
- RELRO (Relocation Read-Only): proteção da tabela GOT contra sobrescrita
- Stack Canaries: detecção de estouro de buffer na pilha
Essas proteções atuam em momentos diferentes: PIE e RELRO são configuradas em tempo de compilação/linking, enquanto stack canaries são inseridas pelo compilador em cada função vulnerável.
2. PIE (Position Independent Executable)
Tradicionalmente, executáveis eram carregados em endereços fixos (ex: 0x400000 no Linux). Isso permitia que um invasor previsse endereços de funções da libc ou gadgets ROP. Com PIE habilitado, o binário é carregado em um endereço aleatório a cada execução, graças à randomização do espaço de endereçamento (ASLR).
Para habilitar no GCC:
gcc -fPIE -pie -o programa programa.c
Verificação com checksec (parte do pwntools ou pacote checksec):
checksec --file=./programa
Saída esperada:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
PIE dificulta ataques que dependem de endereços absolutos, como ret2libc (onde o invasor precisa do endereço de system()). A limitação é um pequeno overhead de desempenho (tipicamente <5%) e incompatibilidade com bibliotecas estáticas que não suportam PIC.
3. RELRO (Relocation Read-Only)
3.1 Partial RELRO vs. Full RELRO
A Global Offset Table (GOT) armazena endereços de funções de bibliotecas compartilhadas. Em um ataque clássico, o invasor sobrescreve uma entrada da GOT para redirecionar a chamada para código malicioso.
- Partial RELRO (
-Wl,-z,relro): torna a seção.got.pltsomente leitura após a resolução das funções, mas a GOT principal (GOT) ainda é gravável. - Full RELRO (
-Wl,-z,relro -Wl,-z,now): resolve todas as funções no carregamento e torna toda a GOT somente leitura.
Comando para Full RELRO:
gcc -Wl,-z,relro,-z,now -o programa programa.c
3.2 Como RELRO impede overwrite de GOT
Sem RELRO, um buffer overflow poderia sobrescrever a entrada da GOT para printf() com o endereço de system(), fazendo com que printf("/bin/sh") execute uma shell. Com Full RELRO, a GOT é mapeada como somente leitura, e qualquer tentativa de escrita gera uma falha de segmentação.
3.3 Verificação e diagnóstico
readelf -l ./programa | grep GNU_RELRO
Se presente, indica RELRO ativo. checksec mostra o nível:
checksec --file=./programa
# RELRO: Full RELRO
4. Stack Canaries (Canários de Pilha)
4.1 Funcionamento
Um stack canary é um valor aleatório inserido pelo compilador entre os buffers locais e o endereço de retorno no stack frame. Antes do retorno da função, o canário é verificado; se foi alterado (indicando overflow), o programa aborta com "stack smashing detected".
4.2 Ativação
GCC oferece três níveis:
-fstack-protector: protege funções com buffers locais >= 8 bytes-fstack-protector-strong: protege funções com qualquer buffer local, arrays ou chamadas a alloca()-fstack-protector-all: protege todas as funções
gcc -fstack-protector-strong -o programa programa.c
4.3 Exemplo prático
Programa vulnerável sem canário:
#include <stdio.h>
#include <string.h>
void vulneravel() {
char buffer[8];
gets(buffer); // entrada sem limite
printf("Buffer: %s\n", buffer);
}
int main() {
vulneravel();
return 0;
}
Compilado com gcc -fno-stack-protector -o vuln vuln.c, um input de 20 caracteres 'A' sobrescreve o endereço de retorno. Com -fstack-protector-strong, o mesmo input gera:
*** stack smashing detected ***: terminated
Aborted (core dumped)
4.4 Limitações
Canários podem ser bypassados se o invasor:
- Descobrir o valor do canário via vazamento de informação (info leak)
- Sobrescrever o canário e o endereço de retorno em uma única operação (raro)
- Atacar a thread-local storage (TLS) onde o canário é armazenado
5. Configuração Combinada e Boas Práticas
Flags recomendadas para hardening máximo:
gcc -O2 -fPIE -pie -Wl,-z,relro,-z,now -fstack-protector-strong -o programa programa.c
Exemplo de Makefile:
CFLAGS = -O2 -fPIE -fstack-protector-strong
LDFLAGS = -pie -Wl,-z,relro,-z,now
programa: programa.o
gcc $(LDFLAGS) -o $@ $^
programa.o: programa.c
gcc $(CFLAGS) -c -o $@ $<
Verificação final:
checksec --file=./programa
# RELRO: Full RELRO
# Stack: Canary found
# PIE: PIE enabled
6. Casos de Estudo e Exemplos de Código
Exemplo 1: Programa sem proteções
// sem_protecao.c
#include <stdio.h>
#include <string.h>
void alvo() {
char buf[16];
strcpy(buf, "AAAAAAAABBBBBBBBCCCCCCCC"); // overflow intencional
}
int main() {
alvo();
printf("Execução normal\n");
return 0;
}
Compilação: gcc -fno-stack-protector -no-pie -o sem_protecao sem_protecao.c
O binário tem endereços fixos e sem canário. Um invasor pode calcular o offset exato para sobrescrever o retorno.
Exemplo 2: Mesmo programa com hardening
// com_protecao.c
#include <stdio.h>
#include <string.h>
void alvo() {
char buf[16];
strcpy(buf, "AAAAAAAABBBBBBBBCCCCCCCC");
}
int main() {
alvo();
printf("Execução normal\n");
return 0;
}
Compilação: gcc -fstack-protector-strong -fPIE -pie -Wl,-z,relro,-z,now -o com_protecao com_protecao.c
Ao executar, o programa aborta imediatamente:
*** stack smashing detected ***: terminated
Aborted (core dumped)
Diferenças no assembly (com objdump -d):
- Sem proteção: função
alvonão tem referência a canário - Com proteção:
mov %fs:0x28,%rax(carrega canário da TLS) exor %fs:0x28,%rax(verificação antes do retorno)
7. Monitoramento e Verificação em Produção
Ferramentas essenciais:
- checksec: verifica PIE, RELRO, Stack Canary, NX
- hardening-check (Debian): relatório detalhado de proteções
- pwntools (Python): biblioteca para análise e exploração, inclui
checksec
Integração com CI/CD (exemplo com GitHub Actions):
- name: Verificar hardening
run: |
checksec --file=./build/programa
hardening-check ./build/programa
Logs de falha: quando um canário é violado, o kernel envia SIGABRT e o glibc imprime:
*** stack smashing detected ***: <nome_do_processo> terminated
Isso indica que a proteção funcionou, mas também que uma vulnerabilidade existe.
8. Conclusão e Próximos Passos
PIE, RELRO e stack canaries formam uma defesa em camadas que torna a exploração de vulnerabilidades em C significativamente mais difícil. PIE randomiza endereços, RELRO protege a GOT, e canários detectam estouros de pilha. Nenhuma técnica é infalível isoladamente, mas combinadas elevam substancialmente o custo para um atacante.
Para aprofundamento, explore:
- Cryptografia e OpenSSL: como proteger dados sensíveis
- IPC (comunicação entre processos): hardening de pipes e sockets
- systemd: sandboxing e isolamento de serviços
Consulte as man pages do GCC (man gcc) e a documentação do glibc para flags adicionais como -D_FORTIFY_SOURCE=2.
Referências
- GCC -fstack-protector documentation — Documentação oficial do GCC sobre opções de proteção de pilha
- Position Independent Executables (PIE) - Red Hat — Artigo técnico da Red Hat sobre PIE e ASLR
- RELRO: Relocation Read-Only - Oracle Developer Studio — Explicação detalhada sobre RELRO parcial e completo
- checksec: Tool for checking binary security properties — Repositório oficial do checksec com documentação de uso
- Stack Smashing Detected - glibc manual — Seção da documentação do glibc sobre detecção de stack smashing
- Hardening with GCC and Clang - OWASP — Guia do OWASP sobre hardening de binários C/C++
- pwntools: CTF library for exploit development — Biblioteca Python que inclui checksec e análise de binários