Segurança em C: buffer overflow e como prevenir

1. Introdução ao Buffer Overflow em C

Buffer overflow é uma das vulnerabilidades mais antigas e perigosas em sistemas computacionais. Ocorre quando um programa tenta armazenar mais dados em um buffer (área de memória temporária) do que ele foi projetado para conter, resultando na sobrescrita de áreas adjacentes de memória.

A linguagem C é particularmente vulnerável a esse tipo de ataque por dois motivos fundamentais: não realiza verificação automática de limites (bounds checking) e permite acesso direto à memória através de ponteiros. Diferente de linguagens como Java ou Python, C confia que o programador gerenciará corretamente o tamanho dos buffers.

#include <stdio.h>

int main() {
    char buffer[10];
    printf("Digite seu nome: ");
    gets(buffer);  // FUNÇÃO PERIGOSA - sem limite de tamanho
    printf("Olá, %s\n", buffer);
    return 0;
}

Se o usuário digitar mais de 9 caracteres (lembre-se do terminador nulo), o programa sobrescreverá memória além do buffer, potencialmente corrompendo dados ou permitindo execução de código arbitrário.

2. Funções Perigosas da Biblioteca Padrão

A biblioteca padrão do C contém diversas funções historicamente problemáticas. Conhecer essas funções e suas alternativas seguras é o primeiro passo para escrever código robusto.

Funções de alto risco:

// VULNERÁVEL
char destino[50];
char origem[] = "Texto muito longo que pode causar estouro";
strcpy(destino, origem);    // Sem verificação de tamanho
strcat(destino, origem);    // Concatena sem verificação
sprintf(buffer, "%s", origem);  // Formata sem limite

// SEGURO
strncpy(destino, origem, sizeof(destino) - 1);
destino[sizeof(destino) - 1] = '\0';  // Garante terminação

strncat(destino, origem, sizeof(destino) - strlen(destino) - 1);

snprintf(buffer, sizeof(buffer), "%s", origem);  // Limite explícito

O scanf() também merece atenção especial:

// VULNERÁVEL
char nome[20];
scanf("%s", nome);  // Sem limite de tamanho

// SEGURO
scanf("%19s", nome);  // Limita a 19 caracteres + terminador
fgets(nome, sizeof(nome), stdin);  // Alternativa mais segura

3. Técnicas de Prevenção em Tempo de Compilação

Compiladores modernos oferecem mecanismos de proteção que podem ser ativados durante a compilação. Essas flags não resolvem todos os problemas, mas adicionam camadas extras de segurança.

// Compilação com proteções básicas
gcc -fstack-protector -D_FORTIFY_SOURCE=2 -O2 programa.c -o programa

// Compilação com warnings máximos
gcc -Wall -Wextra -Wformat-security -Werror programa.c

// Compilação com sanitizers (para desenvolvimento/testes)
gcc -fsanitize=address -fsanitize=undefined -g programa.c -o programa
  • -fstack-protector: insere canários de pilha para detectar estouros
  • -D_FORTIFY_SOURCE=2: substitui funções perigosas por versões mais seguras
  • -fsanitize=address: detecta erros de memória em tempo de execução

4. Funções Seguras e Boas Práticas de Codificação

A prevenção mais eficaz começa com a adoção de funções que limitam explicitamente o tamanho dos buffers e verificam erros.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void processar_entrada() {
    char buffer[100];

    // fgets() com limite explícito
    if (fgets(buffer, sizeof(buffer), stdin) != NULL) {
        // Remove o newline se presente
        buffer[strcspn(buffer, "\n")] = '\0';

        // Processa o buffer com segurança
        char resultado[200];
        snprintf(resultado, sizeof(resultado), "Processado: %s", buffer);
        printf("%s\n", resultado);
    }
}

// Alocação dinâmica segura
char* criar_buffer_seguro(size_t tamanho) {
    char* buffer = (char*)calloc(tamanho, sizeof(char));
    if (buffer == NULL) {
        fprintf(stderr, "Erro de alocação de memória\n");
        exit(EXIT_FAILURE);
    }
    return buffer;
}

5. Proteção em Nível de Sistema Operacional

O sistema operacional também oferece mecanismos de proteção contra buffer overflow. Embora não substituam boas práticas de codificação, eles dificultam a exploração de vulnerabilidades.

ASLR (Address Space Layout Randomization)

Randomiza endereços de memória, dificultando que atacantes prevejam onde o código malicioso será carregado.

NX/DEP (No-Execute / Data Execution Prevention)

Marca regiões de memória como não executáveis, impedindo que dados sejam executados como código.

Stack Canaries

Valores sentinela colocados entre buffers e dados de controle na pilha. Se sobrescritos, o programa é encerrado antes que ocorra dano.

// Exemplo de como canários protegem (simplificado)
void funcao_vulneravel() {
    char buffer[10];
    int canario = 0xDEADBEEF;  // Valor sentinela

    // Se buffer overflow ocorrer, canario será modificado
    gets(buffer);  // Perigoso!

    if (canario != 0xDEADBEEF) {
        // Detectou overflow - aborta
        abort();
    }
}

6. Ferramentas de Análise Estática e Dinâmica

Ferramentas especializadas podem detectar vulnerabilidades antes que cheguem à produção.

Análise Estática (sem executar o código)

# Usando cppcheck
cppcheck --enable=all --suppress=missingIncludeSystem programa.c

# Usando flawfinder
flawfinder programa.c

# Usando Clang Static Analyzer
clang --analyze programa.c

Análise Dinâmica (durante execução)

# Com Valgrind
gcc -g programa.c -o programa
valgrind --tool=memcheck ./programa

# Com AddressSanitizer (já incluso no GCC/Clang)
gcc -fsanitize=address -g programa.c -o programa
./programa

Exemplo prático de detecção:

#include <string.h>
#include <stdio.h>

int main() {
    char buffer[5];
    char dados[] = "Esta string é muito longa para o buffer";

    // strcpy() sem limites - detectado por sanitizers
    strcpy(buffer, dados);

    printf("Buffer: %s\n", buffer);
    return 0;
}

Compilando com -fsanitize=address, a execução produzirá um relatório detalhado do overflow ocorrido.

7. Conclusão e Checklist de Segurança

Buffer overflow continua sendo uma ameaça real em sistemas C, especialmente em código legado ou desenvolvido sem atenção adequada à segurança. As técnicas modernas de proteção, combinadas com boas práticas de programação, podem reduzir drasticamente os riscos.

Checklist para Revisão de Código C Seguro:

  • [ ] Todas as funções de manipulação de strings usam limites explícitos?
  • [ ] gets() foi substituído por fgets()?
  • [ ] strcpy()/strcat() foram substituídos por strncpy()/strncat() ou snprintf()?
  • [ ] sprintf() foi substituído por snprintf()?
  • [ ] O código compila com -Wall -Wextra -Werror sem warnings?
  • [ ] Foi testado com -fsanitize=address?
  • [ ] Alocações dinâmicas verificam retorno NULL?
  • [ ] Entradas do usuário são validadas antes do processamento?
  • [ ] O código foi analisado com ferramentas de análise estática?
  • [ ] FORTIFY_SOURCE está ativado na compilação?

Lembre-se: segurança não é um recurso que se adiciona no final do desenvolvimento, mas uma consideração contínua durante todo o ciclo de vida do software.

Referências