Engenharia Estrutural Civil &
Como escrever código C seguro para evitar excessos de buffer
Table of Contents
Os excessos de buffer continuam a ser uma das vulnerabilidades de segurança mais persistentes e perigosas na programação C. Apesar de estarem bem documentados há décadas, eles continuam a causar problemas graves, como corrupção de dados, falhas no sistema e execução de código remoto. Escrever código C seguro requer uma compreensão profunda de como os excessos de buffer ocorrem e uma abordagem disciplinada para evitá-los. Este artigo fornece um guia abrangente para escrever código C robusto, resistente ao transbordamento, cobrindo conceitos fundamentais, funções seguras, técnicas de validação, proteções de compiladores e medidas defensivas modernas.
Entender os Sobrefluxos de Tampões
Um excesso de buffer acontece quando um programa escreve mais dados para um bloco contíguo de memória (um buffer) do que o buffer foi alocado para manter. Uma vez que os buffers residem na memória de pilha ou pilha, excedendo os seus limites, sobrepõe- se às localizações de memória adjacentes. Esta corrupção pode alterar o estado do programa, introduzir um comportamento imprevisível ou ser explorado por um atacante para injectar e executar código arbitrário.
As consequências dependem do que é substituído. Sobrescrever um endereço de retorno na pilha pode redirecionar a execução para o código controlado pelo atacante. Sobrescrever ponteiros pode levar a erros de memória arbitrários. Mesmo falhas simples podem ser alavancadas para ataques de negação de serviço. Entender a mecânica é o primeiro passo para a prevenção.
Sobrefluxos baseados em pilhas
As variáveis locais, incluindo buffers declarados dentro das funções, são armazenadas na pilha. A pilha também contém o endereço de retorno, ponteiros de quadros salvos e outros dados de controle. Quando um buffer linear como é invadido, os dados são derramados no endereço de retorno e além. Explorações clássicas como o worm Morris (1988) usaram o buffer stack overflow para obter acesso não autorizado.
Sobrefluxos de peso
Os buffers alocados dinamicamente (via , , etc.] residem no heap. Os excessos aqui podem corromper os metadados usados pelo alocador, levando a falhas ou exploração através de ataques de pulverização de heap ou de uso livre. Os excessos de peso são mais difíceis de explorar, mas igualmente perigosos.
Funções Vulneráveis Comuns e Suas Alternativas Seguras
A biblioteca padrão C fornece várias funções que não executam a verificação de limites. Usar essas funções é a causa mais comum de transbordamentos de buffer. Substituindo-as com contrapartes mais seguras é uma prática melhor fundamental.
Copiar e Concatenação de Textos
- — Inseguro: copia até um terminador nulo, sem limite de comprimento.
Alternativa segura: — copia no máximo n caracteres; note que não termina nulo se a fonte for maior do que n, então sempre manualmente nulo-terminado. - Melhor ainda: — disponível em BSD e muitos sistemas Linux; sempre termina nulo e retorna o comprimento da string fonte para detecção de truncamento.
- — Inseguro: concatena sem limites.
Alternativa segura: — Anexa no máximo n caracteres e sempre nulo-terminados.
Saída e Entrada Formatadas
- — Inseguro: escreve saída formatada para um buffer sem verificação de tamanho.
Alternativa segura: — limita a saída a caracteres tamanho-1 mais o terminal nulo. - — Risco semelhante; utilizar em vez disso.
- — Extremamente perigoso; removido da norma C11. Use em vez disso.
- — Sem verificação dos limites. Use ou com o especificador de largura de campo.
Copiar e Mover Memória
- — Salvo apenas se n for verificado para não exceder o tamanho do tampão de destino.
Alternativa segura: (assuntos sobrepostos) e garantir sempre n ≤ tamanho do desdobrável. - Algumas plataformas fornecem do anexo K (opcional em C11), mas a adoção é limitada.
Gerenciamento de Validação e Tamanho
Mesmo com funções seguras, você deve validar comprimentos de entrada, garantir tamanhos de buffer adequados e lidar com truncamento potencial graciosamente.
Verificar os Comprimentos de Entrada
Antes de copiar ou processar a entrada externa (input do usuário, dados da rede, conteúdo do arquivo), determinar o seu comprimento máximo aceitável e rejeitar ou truncar dados que o excedam. Por exemplo:
#define MAX_INPUT 255
char buffer[MAX_INPUT + 1]; // +1 for null
if (strlen(user_input) > MAX_INPUT) {
// Handle error: reject or truncate
fputs("Input too long", stderr);
return -1;
}
strncpy(buffer, user_input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
Usar buffers de tamanho fixo com limites conhecidos
Sempre que possível, defina buffers com um tamanho constante e execute-o em todo o código. Evite arrays de comprimento variável (VLAs) que podem causar transbordamento de pilha se grandes tamanhos são fornecidos. Em vez disso, aloque dinamicamente com verificações de tamanho explícitas.
Lidar com a Truncação Explicitamente
Funções como e podem truncar dados. Esteja ciente do valor de retorno para detectar truncamento e decidir se os dados truncados são aceitáveis ou se um erro deve ser levantado. Ignorar truncamento pode deixar buffers em um estado inesperado.
Bandeiras de segurança do compilador e proteções de tempo de execução
Os compiladores modernos oferecem sinalizadores que adicionam detecção e mitigação de sobrecarga de buffer sem alterações de código. Habilite-os em seu sistema de compilação.
- / — Insere canários de pilha (valores aleatórios) antes de endereços de retorno. Se um buffer overwrites o canário antes de modificar o endereço de retorno, o programa aborta antes que o explore termine.
- — Substitui chamadas para funções inseguras como e com versões verificadas que abortam se o buffer de destino for muito pequeno. Requer ou otimização superior.
- — Avisa sobre vulnerabilidades de texto de formato que podem levar a transbordamentos de buffer ou vazamentos de informação.
- — Código de instrumentos AddressSanitizer (ASan) para detectar transbordamentos de buffer, uso livre e outros erros de memória em tempo de execução. Atrasa a execução, mas é inestimável para testes.
- ] — Evita otimizar os controlos de excesso (utilizar com precaução).
Proteçãos do Sistema Operacional
Os canários Stack são apenas uma camada. Tecnologias de mitigação de exploração em SOs modernos incluem:
- Prevenção de Execução de Dados (DEP) / NX bit — Marca pilha e pilha como não executável, impedindo a execução do código de shell.
- Address Space Layout Randomization (ASLR) — Randomiza endereços de memória (stack, heap, shared libraries) para tornar mais difícil prever alvos.
- Relocalização apenas para leitura (RELRO) — Protege GOT (Quadro de Offset Global) da substituição.
Habilitar essas proteções (geralmente padrão) eleva a barra para exploração, mas não substitui codificação segura.
Auditorias de Código e Análise Estática
Revisão humana combinada com análise estática automatizada pode capturar problemas de sobrecarga de buffer precocemente. Integre-os em seu fluxo de trabalho de desenvolvimento.
- Revisão manual de código — Procure usos de funções inseguras, verificações de tamanho em falta e loops que não sejam limites de buffer.
- Ferramentas de análise estática — Ferramentas como , , , e detectar potenciais transbordamentos, utilização de funções perigosas e erros off-by-one. Podem ser executados em gasodutos CI.
- Fuzzing — Use libFuzzer, AFL ou outros fuzzers para testar automaticamente o manuseio de entrada com dados inesperados que podem desencadear o transbordamento.
Exemplos práticos de código seguro
Cópia segura de texto com verificação de limites
#include <stdio.h>
#include <string.h>
int safe_string_copy(char *dest, size_t dest_size, const char *src) {
if (!dest || !src || dest_size == 0) {
return -1; // Invalid parameters
}
size_t src_len = strlen(src);
if (src_len >= dest_size) {
// Source too large; truncation or error
// Option: copy what fits and null-terminate
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
return 1; // Truncation occurred
}
strncpy(dest, src, dest_size);
// strncpy fills remaining with null, so dest_size fits; no need to null-terminate if src shorter
return 0; // Success, no truncation
}
Manuseamento Inteiro Seguro para Tamanhos de Tampão
O excesso de buffer também pode resultar de transbordamentos inteiros quando os tamanhos de computação. Verifique sempre a aritmética antes da alocação.
#include <stdlib.h>
#include <limits.h>
#include <errno.h>
void *safe_malloc_array(size_t nmemb, size_t size) {
if (nmemb == 0 || size == 0) {
return NULL; // Or handle zero-size allocation
}
if (nmemb > SIZE_MAX / size) {
// Integer overflow would occur
errno = ENOMEM;
return NULL;
}
return malloc(nmemb * size);
}
Usando snprintf para strings formatados
char log_message[256];
int ret = snprintf(log_message, sizeof(log_message),
"User %s logged in from %s", username, ip_address);
if (ret < 0) {
// Output error
} else if ((size_t)ret >= sizeof(log_message)) {
// Truncation occurred; handle if needed
}
Melhores Práticas Adicionais
- Iniciar buffers — Sempre buffers de inicialização zero para evitar vazamentos de memória não inicializada.
- Evite a recursão com profundidade não ligada — Os excessos de pilha podem ocorrer a partir de recursão profunda; use a iteração ou a profundidade limite.
- Use ] Qualificador — Ajuda o compilador a otimizar e pode pegar problemas de apelido, embora não diretamente evitando transbordamentos.
- Preferência ]-correcção — Previne a modificação acidental de cadeias de entrada e impõe a intenção.
- Implementar o tratamento de erros — Não ignorar os valores de retorno de funções como , , , etc.
Recursos para uma aprendizagem mais aprofundada
- Sei CERT C Coding Standard — Regras abrangentes para a codificação segura de C.
- CWE-120: Buffer Copy without Checking Size of Input — Classificação do MITRE de fraquezas de excesso de buffer.
- OWASP Buffer Overflow — Orientações práticas do Projeto de Segurança de Aplicações da Web Aberta.
- GNU C Library Manual: String and Array Utilities — Documentação para funções seguras de string.
- Endereço Sanitizer — Um detector rápido de erros de memória.
Conclusão
Prevenir o excesso de buffer em C não é opcional; é uma responsabilidade fundamental de qualquer desenvolvedor que trabalhe com a linguagem. Ao compreender os mecanismos de transbordamento, substituir funções perigosas por alternativas mais seguras, validar rigorosamente entradas e tamanhos, permitir proteções de compiladores e empregar análises e testes estáticos, você pode reduzir drasticamente o risco dessas vulnerabilidades. Nenhuma técnica única é suficiente; a defesa em profundidade — combinando disciplina de codificação, bandeiras de compilador, mitigação de sistemas operacionais e testes completos — fornece a proteção mais forte. Com estas práticas, você pode escrever código C que seja poderoso e seguro, capaz de rodar com segurança em sistemas críticos. Lembre- se: a codificação segura é uma prática contínua, não uma correção única. Mantenha- se informado sobre novas vulnerabilidades e revise continuamente e melhore seu código.