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.plt somente 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 alvo não tem referência a canário
  • Com proteção: mov %fs:0x28,%rax (carrega canário da TLS) e xor %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