Blog

Como Desenvolver Aplicações Mais Seguras com C

Um guia prático de desenvolvimento seguro em C: previna buffer overflows, use-after-free e integer overflows com funções seguras, hardening do compilador e ferramentas.

Bruno Baldo·14 de set. de 2026·7 min de leitura·Revisado por Rainforest Technologies

Desenvolvimento seguro em C tem menos a ver com truques engenhosos e mais com respeito por uma linguagem que confia inteiramente em você. C dá controle direto sobre a memória e, com ele, responsabilidade direta por cada byte. Não há verificação de limites em tempo de execução, não há coletor de lixo e não há exceções para capturar um ponteiro ruim antes que ele cause estragos. É justamente esse poder que faz o C ainda rodar sistemas operacionais, dispositivos embarcados e serviços críticos em desempenho décadas depois de ter sido projetado, e também é por isso que a segurança em C continua sendo um dos problemas mais difíceis da área. A boa notícia é que a segurança de memória em C é alcançável com hábitos disciplinados, as flags corretas de compilador e ferramentas automatizadas no seu pipeline.

Por que C é propenso a vulnerabilidades de segurança

C não rastreia o tamanho dos seus buffers nem o tempo de vida das suas alocações. Quando você escreve além do fim de um array, libera um ponteiro duas vezes ou lê memória que nunca foi inicializada, o programa muitas vezes continua rodando, silenciosamente corrompido, até que um atacante aprenda a direcionar essa corrupção. Como o C fica tão próximo do hardware, um único bug de memória pode se traduzir em execução remota de código, vazamento de informações ou uma falha que derruba um serviço. Entender os modos de falha comuns é o primeiro passo rumo à programação segura em C.

Os bugs de segurança de memória que mais importam

Buffer overflows acontecem quando você escreve mais dados em um buffer do que ele comporta, sobrescrevendo a memória adjacente, o endereço de retorno da pilha ou os metadados do heap. Um overflow de pilha sobre um endereço de retorno é um caminho clássico para o sequestro do fluxo de controle.

Use-after-free ocorre quando você continua usando um ponteiro depois que a memória que ele referencia foi liberada. O alocador pode entregar essa memória a outra parte do programa, então uma leitura vaza dados não relacionados e uma escrita os corrompe. Definir ponteiros como NULL imediatamente após o free() transforma muitos desses casos em falhas limpas, em vez de bugs exploráveis.

Double free libera a mesma alocação duas vezes, corrompendo a contabilidade do alocador de maneiras que vêm sendo transformadas em armas há anos. A propriedade única de cada alocação é a cura.

Integer overflow é sutil e perigoso. Um cálculo de tamanho como malloc(count * size) pode dar a volta (wrap around) para um valor minúsculo quando count * size excede a largura do tipo, então você aloca alguns bytes e em seguida escreve centenas. Valide a aritmética antes que ela alimente uma alocação ou uma cópia, e prefira size_t para tamanhos.

Vulnerabilidades de format string aparecem quando a entrada do usuário chega ao argumento de formato de uma função como printf. Escrever printf(user_input) permite que um atacante leia a pilha com %x ou escreva na memória com %n. Use sempre uma format string fixa: printf("%s", user_input).

Erros de off-by-one são os primos discretos dos buffer overflows, geralmente por esquecer o terminador \0 que uma string em C precisa, ou por um <= onde cabia um <. Eles corrompem exatamente um byte, o que muitas vezes é o suficiente.

Variáveis não inicializadas e valores de retorno não verificados completam a lista. Ler uma variável local não inicializada expõe conteúdo obsoleto da pilha, e ignorar o retorno de malloc, read ou snprintf significa agir sobre dados que nunca foram realmente escritos.

Aposente as funções perigosas da libc

Algumas funções padrão não podem ser usadas com segurança porque não têm ideia de quão grande é o destino. gets é a pior de todas e foi removida do padrão C; nunca a use. strcpy, strcat e sprintf escrevem alegremente além do fim de um buffer de destino, e scanf("%s", ...) lê entrada sem limites. Recorra a alternativas com limites: use snprintf no lugar de sprintf, use strncpy ou strlcpy (com um tamanho explícito e um terminador garantido) em vez de strcpy, e sempre acompanhe uma cópia de uma verificação explícita de limites. Ao converter números, prefira strtol a atoi para poder detectar erros.

Práticas seguras que valem a pena

Valide a entrada na fronteira. Trate cada byte vindo de um socket de rede, arquivo, variável de ambiente ou linha de comando como hostil até prova em contrário. Verifique tamanhos e faixas de valores antes de copiar ou fazer contas com dados não confiáveis.

Gerencie a memória deliberadamente. Dê a cada alocação um único dono, claro, e um único ponto de liberação. Case cada malloc com exatamente um free, anule o ponteiro em seguida e mantenha alocação e desalocação na mesma camada do seu código, para que a propriedade seja óbvia para quem revisa.

Transforme o compilador em um aliado. Compile com -Wall -Wextra -Werror para que os warnings se tornem falhas que você não pode ignorar. Adicione -D_FORTIFY_SOURCE=2 para obter verificações em tempo de execução em chamadas comuns da libc, habilite o stack protector com -fstack-protector-strong e distribua executáveis independentes de posição com RELRO completo (-fPIE -pie -Wl,-z,relro,-z,now). Durante os testes, compile com o AddressSanitizer (-fsanitize=address) e o UndefinedBehaviorSanitizer (-fsanitize=undefined) para capturar overflows, use-after-free e comportamento indefinido no exato momento em que acontecem.

Analise e faça fuzzing. A análise estática lê seu código sem executá-lo e sinaliza classes inteiras de bugs de memória antes que cheguem à produção. O fuzzing alimenta seu programa com entrada malformada em grande volume para expor falhas nos exatos caminhos não confiáveis que os atacantes sondam. Juntos, eles cobrem um terreno que a revisão manual e os testes unitários baseados em exemplos não alcançam.

Respeite suas dependências. Bibliotecas C "vendored" e código de terceiros carregam suas próprias vulnerabilidades. Um CVE conhecido em uma biblioteca que você copiou para a sua árvore de código três anos atrás ainda é a sua exposição hoje, então mantenha um inventário e fique atento a advisories.

Esses hábitos reforçam a disciplina mais ampla abordada no nosso guia de desenvolvimento seguro de software, e mapeiam diretamente para os riscos rastreados no OWASP Top 10 (2025), onde injeção e design inseguro continuam em primeiro plano.

Coloque as ferramentas no seu pipeline

Boas intenções não escalam; a automação escala. Integre o static application security testing (SAST) à integração contínua para que cada commit seja escaneado em busca de padrões de segurança de memória e de injeção assim que chega. Adicione software composition analysis (SCA) para sinalizar versões vulneráveis das bibliotecas C das quais você depende, e rode secret scanning para que credenciais e chaves nunca cheguem a um repositório. Se você está avaliando onde cada tipo de verificação se encaixa, nossa análise de SAST vs DAST explica o que cada técnica enxerga e onde ela se encaixa no ciclo de vida.

Um checklist rápido de C seguro

  • Nada de gets, strcpy, strcat, sprintf ou scanf sem limites; use equivalentes com limites.
  • Toda escrita em buffer é precedida por uma verificação explícita de tamanho.
  • Todo malloc tem um dono, um free e a anulação do ponteiro em seguida.
  • A aritmética de tamanhos é validada contra overflow antes da alocação ou da cópia.
  • A entrada do usuário nunca vira uma format string.
  • As builds usam flags de hardening; os testes rodam sob ASan/UBSan.
  • SAST, SCA e secret scanning barram o pipeline.

Como a Rainforest ajuda

A Rainforest reúne application security testing e software composition analysis para que as equipes de C possam capturar problemas de segurança de memória e dependências vulneráveis sem sair do seu fluxo de trabalho. Ela se conecta ao seu pipeline de CI/CD, escaneia código e dependências à medida que mudam e aponta a localização exata e o impacto de cada achado, de modo que a remediação seja rápida em vez de investigativa. Você pode explorar a plataforma de application security testing e seus recursos de software composition analysis e, quando estiver pronto para vê-la contra a sua própria base de código, agende uma demonstração.

Escrever C mais seguro é um hábito, não um esforço heroico. Aposente as funções perigosas, faça o hardening das suas builds, valide cada entrada e deixe as ferramentas automatizadas vigiarem os caminhos que os humanos deixam passar. Faça isso de forma consistente e o poder bruto da linguagem se torna uma vantagem, em vez de um passivo.

Perguntas frequentes

Por que C é propenso a vulnerabilidades de segurança?

C não realiza verificação automática de limites nem gerenciamento de memória. Ele confia no desenvolvedor para dimensionar buffers, rastrear o tempo de vida das alocações e validar a entrada, então erros como overflows e use-after-free passam despercebidos em tempo de execução e se tornam exploráveis.

Quais são as funções mais perigosas de C?

gets (removida do padrão), strcpy, strcat, sprintf e scanf com %s são as suspeitas de sempre, porque escrevem em um destino sem conhecer seu tamanho. Substitua-as por snprintf, strncpy ou strlcpy e cópias com verificação de tamanho.

O que é um buffer overflow?

Um buffer overflow escreve mais dados em um buffer do que ele comporta, sobrescrevendo a memória adjacente. Quando essa memória inclui um endereço de retorno ou um ponteiro de função, um atacante pode redirecionar a execução e, potencialmente, rodar código arbitrário.

Como torno o código C mais seguro?

Valide toda entrada não confiável, gerencie a memória com propriedade única, evite as funções inseguras da libc e compile com flags de hardening como -Wall -Wextra -Werror, -D_FORTIFY_SOURCE=2 e o stack protector. Teste sob o AddressSanitizer e o UndefinedBehaviorSanitizer.

Como escaneio código C em busca de vulnerabilidades?

Use static application security testing (SAST) para encontrar bugs de segurança de memória e de injeção, software composition analysis (SCA) para capturar dependências vulneráveis e fuzzing para estressar os caminhos de entrada não confiável. Rodar isso no CI a cada commit impede que os problemas cheguem à produção.

Bruno Baldo

Escrito por

Bruno Baldo

CMO

Um pouco de marketing e um pouco de curiosidade e temos a receita pra criar um apaixonado por cyber!

Continue lendo