Publicidade

Manipulação Segura de Strings em C

Compare funções perigosas com suas substitutas seguras. Buffer overflows são uma das principais vulnerabilidades de segurança há décadas.

Por que é perigoso (buffer overflow)

Um buffer overflow acontece quando o código escreve além do fim de um buffer. Pode travar o programa, corromper dados próximos na pilha ou permitir que um atacante execute código arbitrário.
#include <string.h>

int main(void) {
    char buf[8];                      // só 8 bytes
    strcpy(buf, "This is too long!"); // escreve 18 bytes → overflow
    return 0;
}
O resultado: um quadro de pilha corrompido, levando a segmentation fault ou — pior — execução de código arbitrário. As funções clássicas da biblioteca do C não verificam tamanho, então quem chama precisa garantir a segurança.

Quatro regras para memorizar

  1. Sempre passe o tamanho do buffer explicitamente (via sizeof ou uma constante).
  2. Prefira as variantes com sufixo n ou com limite (strncpy, snprintf, fgets, strtol).
  3. Verifique o valor de retorno para detectar truncamento ou erros.
  4. Garanta que o resultado termine em NUL.

strcpy → strncpy / snprintf

❌ strcpy (inseguro)

char dst[8];
strcpy(dst, src);
// sem verificação de tamanho → overflow se src >= 8 bytes

✅ strncpy + NUL explícito

char dst[8];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';
// sempre termine explicitamente
Pegadinha do strncpy: Quando ele escreve n bytes, não adiciona o terminador '\0'. Você precisa adicioná-lo manualmente.

snprintf é o mais limpo

char dst[8];
int n = snprintf(dst, sizeof(dst), "%s", src);
// snprintf sempre termina em NUL (quando size > 0)
// retorno n é "caracteres que tentaria escrever"; houve truncamento se n >= sizeof(dst)
if (n >= (int)sizeof(dst)) {
    fprintf(stderr, "warning: truncated\n");
}
Ordem de preferência: snprintf > strncpy+NUL >> strcpy. Nunca use strcpy em código novo.

sprintf → snprintf

❌ sprintf

char buf[32];
sprintf(buf, "Hello, %s!", name);
// name longo faz buf estourar

✅ snprintf

char buf[32];
snprintf(buf, sizeof(buf), "Hello, %s!", name);

Detectando truncamento pelo valor de retorno

int n = snprintf(buf, sizeof(buf), fmt, ...);
if (n < 0)                 { /* erro de codificação */ }
else if (n >= sizeof(buf)) { /* truncamento */ }
else                       { /* OK: n bytes escritos */ }

gets → fgets

gets não tem parâmetro de tamanho e foi removido no C11. Sempre use fgets.

❌ gets (removido no C11)

char line[64];
gets(line);   // Errado: sem limite de tamanho

✅ fgets

char line[64];
if (fgets(line, sizeof(line), stdin) == NULL) {
    // EOF ou erro
}

Removendo a quebra de linha

char line[64];
if (fgets(line, sizeof(line), stdin)) {
    size_t len = strlen(line);
    if (len > 0 && line[len - 1] == '\n') {
        line[len - 1] = '\0';
    }
}
Atenção: Se a linha de entrada for maior que 64 bytes, o fgets lê apenas um prefixo, então line[len-1] não será '\n'. Decida se vai fazer um loop para ler o resto ou reportar erro.

atoi → strtol

atoi não consegue reportar erros e seu comportamento em overflow é indefinido. O strtol permite detectar corretamente "conversão falhou" e "fora de intervalo."

❌ atoi

int n = atoi(s);
// s="abc" também retorna 0 → impossível diferenciar de um erro
// s="99999999999" produz comportamento indefinido

✅ strtol

#include <stdlib.h>
#include <errno.h>

errno = 0;
char *end;
long v = strtol(s, &end, 10);

if (end == s)             { /* nenhum dígito consumido */ }
else if (*end != '\0')   { /* lixo no final */ }
else if (errno == ERANGE) { /* fora de intervalo */ }
else                      { /* sucesso: valor é v */ }

Wrapper prático

#include <errno.h>
#include <limits.h>
#include <stdlib.h>

int parse_int(const char *s, int *out) {
    errno = 0;
    char *end;
    long v = strtol(s, &end, 10);
    if (end == s || *end != '\0') return -1;
    if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return -1;
    *out = (int)v;
    return 0;
}
Veja também: strtod para doubles e strtoul para unsigned long seguem o mesmo padrão.

Resumo rápido

EviteUse no lugarObservações
strcpysnprintf / strncpy+NULNunca use. Sem verificação de tamanho
strcatstrncat / snprintfMesmo problema do strcpy
sprintfsnprintfDetecta truncamento pelo valor de retorno
getsfgetsgets foi removido no C11
atoi / atofstrtol / strtodSuporta detecção correta de erros
scanf("%s", ...)fgets + sscanf, ou uma largura como %31s%s sem limite é inseguro

Flags de compilação recomendadas

gcc -Wall -Wextra -Wformat-security -Wstack-protector \
    -fstack-protector-strong -D_FORTIFY_SOURCE=2 -O2 prog.c
-D_FORTIFY_SOURCE=2 adiciona verificações em tempo de execução via glibc, e -fstack-protector-strong aborta o programa em caso de corrupção de pilha.
Análise estática: Rode cppcheck, clang-tidy ou clang --analyze para pegar bugs de buffer antes de ir para produção.

Desafios

Desafio 1: Sinta o overflow do strcpy
Use strcpy para copiar uma string de 18 caracteres para um buffer de 8 bytes. Compile com -fstack-protector-strong e observe a mensagem stack smashing detected.
Desafio 2: Reescreva com snprintf
Escreva uma função que monte "Hello, [nome]! You are [idade]." usando strcpy/strcat, depois novamente usando snprintf. Compare como cada uma lida com entradas longas.
Desafio 3: Parser de inteiros mais seguro
Estenda parse_int: escreva uma versão estrita que rejeita "123abc" e uma versão tolerante que aceita "3.14" e mantém a parte inteira. Adicione 10 casos de teste.
Desafio 4: Leitor de linha seguro
Implemente char *read_line(FILE *fp, char *buf, size_t size);. Se a entrada exceder size, descarte o resto para que a próxima leitura comece limpa. Retorne buf, ou NULL em EOF.

Teste de Revisão

Confira seu entendimento desta aula.

Q1. O que você deve usar no lugar de strcpy?

Variantes com limite de tamanho, como strncpy / snprintf / strlcpy
memcpy_safe
copy_string

strcpy não verifica o tamanho do buffer, então é uma causa clássica de buffer overflows. Use variantes com limite de tamanho ou snprintf.

Q2. Qual a principal causa de um buffer overflow?

Escrever além do tamanho alocado
Velocidade de escrita lenta
Colisão de nomes de variáveis

Escrever além da região alocada é comportamento indefinido e pode causar segfaults ou abrir portas para vulnerabilidades de execução de código arbitrário.

Q3. Qual função usar para saída formatada em um buffer de tamanho fixo?

snprintf
sprintf
printf

sprintf não aceita argumento de tamanho e é perigoso. snprintf recebe o tamanho do buffer, tornando-o seguro de usar.