Buffer overflow é uma das vulnerabilidades mais clássicas e perigosas em programação C. Ela ocorre quando um programa escreve dados além dos limites de um buffer (área de memória alocada para armazenar dados), corrompendo memória adjacente. Isso pode causar falhas, comportamento imprevisível ou até permitir que um atacante execute código arbitrário. Nesta aula, vamos entender profundamente o que é buffer overflow, identificar funções inseguras que facilitam esse problema, aprender alternativas seguras e explorar mitigações que podem ser aplicadas no desenvolvimento e na compilação.

Dominar esse tema é essencial para qualquer programador C, pois muitas vulnerabilidades históricas e exploits famosos (como o Morris worm) exploraram exatamente esse tipo de falha. Ao final, você terá conhecimento prático para escrever código mais seguro e entender como proteger seus programas.

O que é buffer overflow

Buffer overflow (estouro de buffer) acontece quando um programa copia ou escreve dados em um espaço de memória (buffer) sem verificar se o tamanho dos dados é maior que o espaço alocado. Em C, isso é particularmente comum porque a linguagem não faz verificação automática de limites em operações de memória. O resultado é que os dados excedentes invadem regiões vizinhas da memória, que podem conter outras variáveis, endereços de retorno de funções ou até mesmo dados de outras partes do programa.

Para entender, imagine um buffer de 8 bytes. Se você tentar armazenar 20 bytes, os 12 bytes extras serão escritos em áreas adjacentes. Em um ataque, um invasor pode preencher o buffer com dados maliciosos e sobrescrever o endereço de retorno de uma função, fazendo o programa desviar para um código arbitrário. Isso é conhecido como smashing the stack (destruição da pilha), porque o buffer frequentemente reside na pilha de execução.

Considere o exemplo clássico:

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

void vulnerable(char *input) {
    char buffer[10];
    strcpy(buffer, input); // Sem verificação de tamanho!
    printf("Buffer: %s\n", buffer);
}

int main(int argc, char **argv) {
    if (argc > 1)
        vulnerable(argv[1]);
    return 0;
}

Se você executar esse programa com um argumento longo (por exemplo, mais de 10 caracteres), o strcpy copiará tudo, estourando o buffer. Dependendo do sistema, isso pode causar um segmentation fault ou, em cenários controlados, permitir execução de código. Esse é o princípio básico do buffer overflow.

Funções perigosas (gets, strcpy)

Algumas funções da biblioteca padrão C são notoriamente inseguras porque não permitem especificar o tamanho máximo de dados a serem copiados ou lidos. Elas são a causa raiz de muitos buffer overflows. Vamos analisar as principais:

  • gets(): Lê uma linha da entrada padrão sem limitar o número de caracteres lidos. É tão perigosa que foi removida da biblioteca padrão no padrão C11. Ela não recebe o tamanho do buffer, tornando impossível evitar estouro.
  • strcpy(): Copia uma string de origem para um destino sem verificar se o destino tem espaço suficiente. Se a origem for maior que o destino, ocorre overflow.
  • strcat(): Concatena strings sem verificar o espaço disponível no destino, podendo escrever além do fim.
  • sprintf(): Formata uma string em um buffer sem especificar o tamanho máximo de saída, permitindo overflow se a string formatada for maior que o buffer.
  • scanf() com %s: Lê uma string da entrada padrão sem limitar o tamanho, similar a gets.

Exemplo de uso inseguro de gets:

#include <stdio.h>

int main() {
    char buffer[10];
    printf("Digite algo: ");
    gets(buffer); // PERIGOSO!
    printf("Você digitou: %s\n", buffer);
    return 0;
}

Esse código compila com avisos (em alguns compiladores), mas se o usuário digitar mais de 9 caracteres, o buffer estoura. Em C11, gets foi removido e o compilador pode emitir erro. No entanto, muitos sistemas legados ainda usam essas funções, e é crucial reconhecê-las para substituí-las.

Alternativas seguras

Para evitar buffer overflow, a abordagem recomendada é usar funções que permitem especificar o tamanho máximo de dados. A biblioteca padrão C oferece alternativas seguras, e também existem extensões comuns (como as da Microsoft) que adicionam sufixo _s.

  • fgets(buffer, size, stdin) substitui gets. Ela lê no máximo size-1 caracteres e inclui o caractere nulo. Exemplo: fgets(buffer, sizeof(buffer), stdin);.
  • strncpy(dest, src, n) copia no máximo n caracteres. Porém, cuidado: se a origem for menor que n, ela preenche o resto com zeros; se for maior, não adiciona o nulo. É importante garantir a terminação manualmente.
  • strncat(dest, src, n) concatena no máximo n caracteres, mas sempre adiciona o nulo no final, desde que o destino tenha espaço suficiente.
  • snprintf(dest, size, format, ...) é a versão segura de sprintf; ela limita a saída a size-1 caracteres e adiciona o nulo.
  • scanf com especificador de largura: por exemplo, %9s lê no máximo 9 caracteres (mais o nulo).

Exemplo de uso seguro:

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

int main() {
    char buffer[10];
    printf("Digite algo: ");
    fgets(buffer, sizeof(buffer), stdin);
    // Remove o newline se presente
    buffer[strcspn(buffer, "\n")] = '\0';
    printf("Você digitou: %s\n", buffer);
    return 0;
}

No caso de strcpy, podemos usar strncpy ou, melhor, usar uma função que garanta terminação, como snprintf para copiar strings:

char dest[10];
const char *src = "Uma string muito longa que não cabe";
snprintf(dest, sizeof(dest), "%s", src);
// dest agora contém "Uma strin" (9 caracteres + nulo)

Além disso, em C padrão, não há uma função oficial strcpy_s, mas muitos compiladores oferecem strcpy_s como extensão. Se você estiver em um ambiente que suporta, pode usá-la, mas sempre verifique a documentação.

Mitigações

Além de usar funções seguras, existem várias técnicas e ferramentas para mitigar buffer overflow. Elas podem ser aplicadas no código, na compilação ou no sistema operacional.

Boas práticas de programação

  • Valide sempre o tamanho dos dados de entrada antes de copiá-los.
  • Use funções seguras como fgets, snprintf, strncpy com cuidado.
  • Prefira strings dinâmicas (como char * alocadas com malloc) e redimensione conforme necessário, mas isso também exige cuidado.
  • Evite usar strcpy, strcat, gets, sprintf sem limite.

Mitigações em nível de compilação

Compiladores modernos oferecem proteções que ajudam a detectar ou dificultar exploração de buffer overflow:

  • Stack Canaries (ProPolice): O compilador insere um valor sentinela (canário) antes do endereço de retorno na pilha. Se o buffer estourar, o canário é corrompido e o programa aborta antes do retorno. No GCC, use -fstack-protector ou -fstack-protector-strong.
  • ASLR (Address Space Layout Randomization): Aleatoriza endereços de memória, dificultando que um atacante adivinhe o endereço exato do código malicioso. Isso é feito pelo sistema operacional, não pelo compilador.
  • NX (No-Execute): Marca regiões de memória como não executáveis, impedindo que código injetado na pilha seja executado. Também é uma proteção do sistema operacional.
  • Fortify Source: O GCC com -D_FORTIFY_SOURCE=2 adiciona verificações em tempo de execução para algumas funções. Requer otimização (-O).

Exemplo de compilação com proteções no GCC:

gcc -o programa programa.c -fstack-protector-strong -D_FORTIFY_SOURCE=2

Ferramentas de análise estática e dinâmica

  • Valgrind: Detecta erros de memória em tempo de execução, incluindo estouros de buffer.
  • AddressSanitizer (ASan): Compilador com -fsanitize=address que detecta buffer overflow e outros erros de memória.
  • Analisadores estáticos como cppcheck ou clang-tidy podem encontrar usos inseguros.

Exemplo de uso do AddressSanitizer:

gcc -fsanitize=address -g programa.c -o programa
./programa

Se houver overflow, ele relatará o problema com detalhes.

Boas práticas e observações finais

Escrever código C seguro exige disciplina e consciência constante sobre o gerenciamento de memória. Sempre que você copiar dados, pergunte-se: "qual é o tamanho máximo que pode ser copiado?" e "o destino tem espaço suficiente?". Use funções que limitem o tamanho e verifique o retorno delas. Além disso, mantenha-se atualizado sobre as vulnerabilidades da linguagem e as ferramentas disponíveis.

Lembre-se de que mitigação não é segurança absoluta: mesmo com canários e ASLR, ataques sofisticados podem contornar. A melhor defesa é evitar a vulnerabilidade em primeiro lugar, usando boas práticas de codificação e revisão de código.

Exercícios

  1. Identifique a vulnerabilidade no código abaixo e explique o que pode acontecer se o usuário digitar uma string de 20 caracteres.
    #include <stdio.h>
    int main() {
        char nome[10];
        printf("Digite seu nome: ");
        gets(nome);
        printf("Olá, %s\n", nome);
        return 0;
    }
    

    ✓ Resposta: O código usa gets(), que não limita o tamanho da entrada. Se o usuário digitar 20 caracteres, eles serão escritos em nome[10], estourando o buffer e corrompendo a memória adjacente, possivelmente causando um crash ou permitindo execução de código malicioso.
  2. Reescreva o código da questão 1 usando uma função segura para ler a entrada, garantindo que o buffer não seja estourado.

    ✓ Resposta: Substitua gets(nome) por fgets(nome, sizeof(nome), stdin). Exemplo completo:
    #include <stdio.h>
    #include <string.h>
    int main() {
        char nome[10];
        printf("Digite seu nome: ");
        fgets(nome, sizeof(nome), stdin);
        nome[strcspn(nome, "\n")] = '\0';
        printf("Olá, %s\n", nome);
        return 0;
    }
    
  3. Explique a diferença entre strcpy e strncpy e mostre um exemplo onde strncpy pode não adicionar o caractere nulo, causando problemas.

    ✓ Resposta: strcpy copia a string até o nulo, sem limite; strncpy copia no máximo n caracteres. Se a origem tiver mais de n caracteres, strncpy não adiciona o nulo, deixando o destino sem terminação. Exemplo:
    char dest[5];
    strncpy(dest, "abcdefgh", sizeof(dest));
    // dest contém 'a','b','c','d','e' e não tem '\0' no final
    printf("%s", dest); // comportamento indefinido
    
  4. Qual é o papel do stack canary na mitigação de buffer overflow? Explique como ele funciona e como ativá-lo no GCC.

    ✓ Resposta: O stack canary é um valor aleatório colocado entre o buffer e o endereço de retorno. Se um buffer overflow sobrescrever o canário, o valor muda e o programa aborta antes de retornar, impedindo a exploração. No GCC, use as flags -fstack-protector ou -fstack-protector-strong.
  5. Escreva um programa seguro que leia um nome do usuário (máximo 20 caracteres) e o imprima, usando fgets e snprintf para construir uma mensagem de boas-vindas sem risco de overflow.

    ✓ Resposta:
    #include <stdio.h>
    #include <string.h>
    int main() {
        char nome[21];
        char mensagem[50];
        printf("Digite seu nome (máx 20): ");
        fgets(nome, sizeof(nome), stdin);
        nome[strcspn(nome, "\n")] = '\0';
        snprintf(mensagem, sizeof(mensagem), "Olá, %s!", nome);
        printf("%s\n", mensagem);
        return 0;
    }
    

Referências