C Segurança (Buffer Overflow)
O C deixa você escrever direto na memória, byte a byte, e não confere se você está dentro dos limites. Se um array tem lugar para 8 letras e o programa escreve 12, o C escreve mesmo: passa por cima do que estava na memória ao lado. Isso é um estouro de buffer (buffer overflow), a falha mais famosa — e mais perigosa — da história do C. Ela derruba o programa, estraga dados sem avisar e é a porta por onde muitos ataques entram. Nesta aula você vai ver o estouro acontecer de verdade, entender por que ele passa calado, e aprender as funções e as opções de compilação que o pegam.
- O que é um estouro de buffer e por que o C não o impede sozinho.
- As funções perigosas (
gets,strcpy,strcat,sprintf,scanf("%s")) e as versões seguras (fgets,snprintf,%29s). - Um estouro silencioso que troca o valor de outra variável e libera um acesso que deveria ser proibido.
- Como ligar os dois fiscais e o que cada um mostra de verdade no laboratório: o AddressSanitizer (
-fsanitize=address) e o FORTIFY_SOURCE (-D_FORTIFY_SOURCE=2 -O2). - Por que um erro que explode no seu Linux pode passar despercebido no editor do guia.
Todas as saídas desta aula são reais, feitas com o gcc 15 na máquina fictícia da Ana, na pasta ~/c/seg. Esta é uma aula sobre defender o seu próprio programa dos seus próprios enganos: nada aqui serve para atacar o programa de outra pessoa.
A ideia: a prateleira que invade a vizinha
Pense num array como uma prateleira com um número fixo de espaços — digamos, 8. Você pode guardar coisas no espaço 0 até o espaço 7. Em quase toda linguagem, se você tentar pôr algo no espaço 8, a linguagem reclama na hora. No C, não: ele simplesmente coloca a coisa no lugar onde o espaço 8 estaria, que já é a prateleira do vizinho. O vizinho pode ser outra variável, um valor de controle do programa ou uma peça interna que faz o programa voltar das funções. Escrever ali é o estouro de buffer.
O nome vem do inglês buffer ("pulmão", um espaço reservado de memória) e overflow ("transbordar"): o dado transbordou do espaço reservado. Três coisas podem acontecer:
- O programa morre com
*** stack smashing detected ***— às vezes. É o melhor caso, porque você percebe. - O programa continua e entrega um resultado errado, porque estragou uma variável ao lado. Pior, porque parece que funcionou.
- Alguém de fora controla o estrago e faz o programa rodar o que quiser. É assim que muitos vírus e invasões começam.
A origem do transbordo é quase sempre a mesma: uma função que copia texto sem saber o tamanho do destino. Veja a lista das perigosas e o que usar no lugar:
| Perigosa (não sabe o limite) | Por quê | Versão segura |
|---|---|---|
gets(nome) | Lê uma linha sem limite nenhum. Foi removida do C no C11. | fgets(nome, sizeof nome, stdin) |
scanf("%s", nome) | Lê uma palavra sem limite. | scanf("%29s", nome) (um a menos que o array) |
strcpy(dest, origem) | Copia até o '\0' da origem, caiba ou não no destino. | snprintf(dest, sizeof dest, "%s", origem) |
strcat(dest, origem) | Gruda no fim, sem olhar o espaço que sobra. | snprintf montando o texto todo de uma vez |
sprintf(dest, ...) | Escreve formatado, sem limite de tamanho. | snprintf(dest, sizeof dest, ...) |
Repare no padrão: toda versão segura recebe o tamanho do destino. A regra de ouro desta aula é uma só: nunca copie texto para um array sem dizer à função quanto cabe nele.
1. O clássico: scanf("%s") sem limite
A Ana quer ler o nome de uma pessoa. O array tem 8 bytes. Veja o %s "pelado", sem número:
#include <stdio.h>
int main(void) {
char nome[8];
printf("Nome: ");
if (scanf("%s", nome) != 1) {
return 1;
}
printf("Olá, %s!\n", nome);
return 0;
}
Compila sem um aviso sequer. Com um nome curto, funciona. Com um nome de 11 letras num array de 8:
$ gcc -std=c23 -Wall -Wextra nome.c -o nome $ echo Ana | ./nome Nome: Olá, Ana! $ echo Maximiliano | ./nome Nome: *** stack smashing detected ***: terminated Abortado (imagem do núcleo gravada) ./nome $ echo $? 134
- nenhum aviso na compilação
- O compilador não tem como saber, enquanto compila, que a pessoa vai digitar 11 letras. O tamanho só existe na hora de rodar.
- *** stack smashing detected ***
- "Esmagamento da pilha detectado". O
scanfescreveuMaximiliano\0(12 bytes) num array de 8 e passou por cima de uma trava que o gcc coloca na pilha (o stack protector). No fim domain, o programa viu a trava quebrada e se abortou. - Abortado … 134
- O programa foi encerrado à força. O código 134 é o "abortado" do Linux (veja exit).
stack smashing é sorte, não garantia
A trava da pilha só é conferida quando a função termina, e só pega estouros que chegam até ela. Um estouro de 1 ou 2 bytes, ou que atinge só outra variável local, pode não disparar nada — e aí o programa segue com um dado estragado. Nunca conte com o stack smashing para descobrir os seus estouros; use os fiscais da seção 5.
O conserto: diga o tamanho ao scanf
O %s aceita um número antes do s: a quantidade máxima de caracteres que ele pode ler. Para um char nome[8], use %7s: 7 letras mais o '\0' do fim cabem exatos nos 8 bytes (veja Strings). O que passar disso fica na entrada, sem estourar nada:
$ echo Maximiliano | ./nome « agora com scanf("%7s", nome)
Nome: Olá, Maximil!
No editor do guia, o mesmo %7s corta o nome igualzinho (o resto, iano, fica na entrada):
$ clang -std=c23 -Wall -Wextra nome.c -o nome $ ./nome Nome: Maximiliano Olá, Maximil! [código de saída: 0]
Missão: este scanf lê o nome com %s sem limite — um estouro esperando para acontecer. O array nome tem 8 bytes. Troque o %s por %7s (7 letras + o '\0'). A entrada da missão é Ana, que cabe de sobra; a saída continua Olá, Ana!, mas agora o programa está seguro. (A missão confere o seu código: o estouro em si passa calado no editor.)
#include <stdio.h>
int main(void) {
char nome[8];
printf("Nome: ");
if (scanf("%s", nome) != 1) {
return 1;
}
printf("Olá, %s!\n", nome);
return 0;
}
#include <stdio.h>
int main(void) {
char nome[8];
printf("Nome: ");
if (scanf("%7s", nome) != 1) {
return 1;
}
printf("Olá, %s!\n", nome);
return 0;
}
2. O estouro silencioso que vira "administrador"
Até aqui o estouro derrubou o programa. Agora o caso que assusta de verdade: um estouro que não derruba nada e troca o valor de outra variável. A Ana escreveu um login de brincadeira. A struct guarda o nome (8 bytes) e um campo admin, que vale 0 para uma pessoa comum:
#include <stdio.h>
#include <string.h>
struct Usuario {
char nome[8];
int admin;
};
int main(void) {
struct Usuario u = {.nome = "", .admin = 0};
char entrada[100];
printf("Nome: ");
if (scanf("%99s", entrada) != 1) {
return 1;
}
strcpy(u.nome, entrada);
printf("Olá, %s!\n", u.nome);
if (u.admin) {
printf("Acesso de ADMINISTRADOR liberado (admin = %d).\n", u.admin);
} else {
printf("Acesso comum.\n");
}
return 0;
}
O scanf("%99s", entrada) está correto: lê com limite, para um array de 100. O erro está no strcpy(u.nome, entrada), que copia sem limite para o u.nome, que só tem 8 bytes. Veja o que acontece com um nome comprido:
$ gcc -std=c23 -Wall -Wextra login.c -o login $ echo Ana | ./login Nome: Olá, Ana! Acesso comum. $ echo Maximiliano | ./login Nome: Olá, Maximiliano! Acesso de ADMINISTRADOR liberado (admin = 7302753). $ echo $? 0
Ninguém mexeu em admin, e ele virou 7302753: deixou de ser zero e o acesso de administrador foi liberado. O que aconteceu? Na memória, o campo admin vem logo depois dos 8 bytes de nome. "Maximiliano" tem 11 letras + o '\0' = 12 bytes. Os 8 primeiros encheram nome; os 4 que sobraram (a, n, o e o '\0') foram escritos bem em cima de admin. O programa não morreu, não avisou, e passou a dar acesso que não devia. É o estouro de buffer na sua forma mais traiçoeira.
7302753?
Os quatro bytes que caíram sobre admin foram 'a' (97), 'n' (110), 'o' (111) e '\0' (0). Lidos como um int, do jeito do processador da Ana, dão 97 + 110×256 + 111×65536 = 7302753. Em outra máquina ou com outro nome, sairia outro número — o único certo é que não é mais zero.
E o AddressSanitizer, que pega estouros tão bem? Aqui ele fica calado: o estouro ficou dentro da mesma struct (de nome para admin), e para o ASan a struct inteira é uma região válida. Ele vigia as bordas de cada variável, não as fronteiras entre os campos de uma struct. Quem pega este caso é o outro fiscal, o FORTIFY (seção 5). No editor do guia, o estouro também passa calado e imprime o mesmo admin = 7302753.
Um programa que "roda e dá certo" não prova que está livre de estouros. O erro pode estar ali, mudando um valor vizinho, esperando o dia em que o nome é comprido demais. Por isso a defesa não é testar e torcer: é nunca copiar sem limite e rodar os fiscais.
3. strcpy e o "+1" esquecido
O strcpy ("string copy", copiar string) copia da origem até o '\0', caiba ou não no destino. Veja um programa que copia o que veio na linha de comando para um array de 8 bytes:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char item[8];
if (argc < 2) {
return 1;
}
strcpy(item, argv[1]);
printf("Item: %s\n", item);
return 0;
}
$ gcc -std=c23 -Wall -Wextra copia.c -o copia $ ./copia biscoito Item: biscoito $ ./copia "biscoito recheado" *** stack smashing detected ***: terminated Abortado (imagem do núcleo gravada) ./copia "biscoito recheado" $ echo $? 134
Repare numa armadilha fina: ./copia biscoito pareceu funcionar. Mas "biscoito" tem 8 letras; com o '\0', são 9 bytes num array de 8. O nono byte foi escrito fora do array, só que caiu num canto que não disparou a trava da pilha. É o pior caso: o programa escreveu onde não devia e tudo parece certo. Esse é o clássico erro do "+1" esquecido — o espaço do '\0' que ninguém somou.
O conserto é o snprintf ("string print, com N"), que escreve no máximo o tamanho do destino e sempre termina a string com '\0':
char item[8];
if (argc < 2) {
return 1;
}
snprintf(item, sizeof item, "%s", argv[1]); // nunca passa de 8 bytes
printf("Item: %s\n", item);
Agora ./copia "biscoito recheado" guarda só biscoit (7 letras + o '\0') e não estoura nada. Perder o fim do texto é chato, mas é muito melhor que estragar a memória. Em um programa de verdade, você daria ao array um tamanho folgado e ainda avisaria quando o texto não coube.
strncpy?
Existe um strncpy(dest, origem, n), mas ele é uma cilada: se a origem tem n ou mais letras, ele não coloca o '\0' no fim, e você fica com uma string sem terminador — outro problema. O snprintf sempre termina a string, por isso é o que o guia recomenda.
4. gets: a função banida
A função mais perigosa de todas foi a gets ("get string"), que lia uma linha inteira sem limite nenhum — não havia nem como dizer o tamanho. Ela foi tão fonte de desastres que o C a marcou como obsoleta no C99 e a removeu da linguagem no C11: é a única função da biblioteca padrão que já foi apagada. No gcc 15, usá-la nem compila em C23:
$ gcc -std=c23 -Wall -Wextra gets.c -o gets
gets.c: In function ‘main’:
gets.c:6:5: error: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
6 | gets(nome);
| ^~~~
| fgets
A aula Ler uma Linha (fgets) conta essa história em detalhe e mostra o fgets, a substituta segura que recebe o tamanho do array. Se você ver gets em um código ou livro antigo, troque por fgets na hora.
5. Os dois fiscais: ASan e FORTIFY
Você não vai achar os estouros lendo o código com os olhos — eles passam calados. Precisa de ferramentas que conferem enquanto o programa roda. Duas são fáceis de ligar, e cada uma pega casos diferentes.
AddressSanitizer: o relatório completo
O -fsanitize=address coloca um fiscal em cada acesso à memória (é a mesma ferramenta da aula Sanitizers, vista aqui pelo lado da segurança). Recompile o nome.c da seção 1 com ele e com -g (para ter arquivo e linha):
$ gcc -std=c23 -Wall -Wextra -g -fsanitize=address nome.c -o nome-asan
$ echo Maximiliano | ./nome-asan
Nome: =================================================================
==415616==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7bed3aaf0028 at pc 0x7fed3d0b05dc bp 0x7fff8af96370 sp 0x7fff8af95af8
WRITE of size 12 at 0x7bed3aaf0028 thread T0
#0 0x7fed3d0b05db in scanf_common ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_format.inc:346
#1 0x7fed3d0f0e4e in __isoc23_vscanf ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1586
#2 0x7fed3d0f1634 in __isoc23_scanf ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1616
#3 0x562116b362de in main /home/ana/c/seg/nome.c:6
«… a pilha da biblioteca e o mapa da memória …»
This frame has 1 object(s):
[32, 40) 'nome' (line 4) <== Memory access at offset 40 overflows this variable
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/ana/c/seg/nome.c:6 in main
==415616==ABORTING
- stack-buffer-overflow
- "Estouro de buffer na pilha": um array local foi escrito fora dos limites.
- WRITE of size 12
- Uma escrita de 12 bytes (as 11 letras de "Maximiliano" + o
'\0'). Quem escreveu foi oscanf. - #3 … in main /home/ana/c/seg/nome.c:6
- A primeira linha com um arquivo seu: o
scanfda linha 6. - [32, 40) 'nome' (line 4)
- Qual variável foi estourada: o array
nome, declarado na linha 4, de 8 bytes. - SUMMARY … nome.c:6
- O resumo em uma linha. Se for ler só uma, leia esta.
Lembre da seção 2: o ASan é ótimo para estourar os limites de uma variável, mas não pega o estouro de um campo de struct para o vizinho. Para esse, entra o segundo fiscal.
FORTIFY_SOURCE: a conferência barata do compilador
O -D_FORTIFY_SOURCE=2 pede ao compilador que troque funções como strcpy, memcpy e sprintf por versões que, quando sabem o tamanho do destino, conferem antes de copiar. É quase de graça (não deixa o programa mais lento como o ASan), mas só funciona com otimização ligada (-O2). Volte ao login da seção 2:
$ gcc -std=c23 -Wall -Wextra -O2 -D_FORTIFY_SOURCE=2 login.c -o login-fort $ echo Maximiliano | ./login-fort Nome: *** buffer overflow detected ***: terminated Abortado (imagem do núcleo gravada) ./login-fort $ echo $? 134
O estouro que o ASan deixou passar e que o programa normal nem percebeu foi pego aqui: *** buffer overflow detected *** ("estouro de buffer detectado"), e o programa parou com código 134, antes de liberar o acesso de administrador. O FORTIFY conhece o tamanho de u.nome (8 bytes, escrito no código) e sabe que o strcpy de 12 bytes não cabe.
-O2 já liga o FORTIFY sozinho
O Ubuntu 26.04 da Ana vem configurado para ativar o FORTIFY (no nível 3) assim que você compila com -O2, mesmo sem escrever -D_FORTIFY_SOURCE. Por isso, no laboratório, gcc -O2 login.c já aborta com buffer overflow detected. Em outras distribuições isso pode não vir ligado; escrever -D_FORTIFY_SOURCE=2 garante.
Mas o FORTIFY tem um limite: ele só confere as funções que recebem um destino de tamanho conhecido. O scanf("%s") da seção 1 não é uma dessas, então nem o FORTIFY o pega na hora da cópia — ali quem avisa é a trava da pilha (o stack smashing) ou o ASan. Por isso os dois fiscais se completam.
| Caso | Programa normal | -fsanitize=address | -O2 -D_FORTIFY_SOURCE=2 |
|---|---|---|---|
scanf("%s") estoura um array local (seção 1) | Às vezes stack smashing, 134 | Pega: relatório com a linha | Não pega na cópia (ainda pode dar stack smashing) |
strcpy de um campo para o vizinho na struct (seção 2) | Passa calado: dado estragado | Passa calado (é dentro da struct) | Pega: buffer overflow detected, 134 |
strcpy estoura um array local (seção 3) | Às vezes stack smashing, 134 | Pega: relatório com a linha | Pega: buffer overflow detected, 134 |
Qual usar: durante o desenvolvimento e nos testes, compile com -g -fsanitize=address; na versão que vai para os usuários, deixe -O2 -D_FORTIFY_SOURCE=2 (rápido e ainda protege). Os dois juntos cobrem quase tudo.
O editor compila o seu programa para WebAssembly, e nem a biblioteca do AddressSanitizer nem o FORTIFY existem lá. Pedir -fsanitize=address dá erro de ligação; -D_FORTIFY_SOURCE=2 é aceito mas não faz nada (o login continua imprimindo admin = 7302753). Por isso os estouros desta aula são mostrados no terminal do laboratório, e as missões conferem o seu código. Para rodar os fiscais de verdade, use o seu Linux (veja Sanitizers).
Missão final: a função guardar usa strcpy para copiar o nome digitado para u.nome, de 8 bytes — o estouro da seção 2. Troque o strcpy por snprintf(destino, tam, "%s", origem), que respeita o tamanho. A entrada é Maximiliano; com a cópia segura, o programa guarda só Maximil (7 letras + o '\0') e escreve Guardado: Maximil.
#include <stdio.h>
#include <string.h>
void guardar(char destino[], size_t tam, const char *origem) {
(void) tam;
strcpy(destino, origem);
}
int main(void) {
char nome[8];
char entrada[100];
printf("Nome: ");
if (scanf("%99s", entrada) != 1) {
return 1;
}
guardar(nome, sizeof nome, entrada);
printf("Guardado: %s\n", nome);
return 0;
}
#include <stdio.h>
#include <string.h>
void guardar(char destino[], size_t tam, const char *origem) {
snprintf(destino, tam, "%s", origem);
}
int main(void) {
char nome[8];
char entrada[100];
printf("Nome: ");
if (scanf("%99s", entrada) != 1) {
return 1;
}
guardar(nome, sizeof nome, entrada);
printf("Guardado: %s\n", nome);
return 0;
}
Quando dá errado
*** stack smashing detected ***: terminated
O que aconteceu: um array local foi escrito além do fim, e a trava da pilha percebeu no fim da função. Quase sempre é um scanf("%s"), strcpy ou strcat sem limite, ou um laço com <= no lugar de <.
Como resolver: recompile com -g -fsanitize=address para ver a linha exata, e troque a função perigosa pela versão com tamanho (%29s, snprintf).
*** buffer overflow detected ***: terminated
O que aconteceu: o FORTIFY (ligado por -O2 -D_FORTIFY_SOURCE=2, ou pelo -O2 sozinho no laboratório) pegou uma cópia que não cabia no destino. É uma mensagem diferente do stack smashing: esta vem do fiscal do compilador, não da trava da pilha.
Como resolver: veja qual strcpy/memcpy/sprintf copiou demais e passe o tamanho do destino.
$ echo Maximiliano | ./login Nome: Olá, Maximiliano! Acesso de ADMINISTRADOR liberado (admin = 7302753).
O que aconteceu: um estouro silencioso escreveu por cima da variável vizinha (seção 2). O ASan não pega quando é dentro de uma struct.
Como resolver: compile com -O2 -D_FORTIFY_SOURCE=2, que pega esse caso, e conserte a cópia sem limite.
gcc: error: implicit declaration of function ‘gets’
O que aconteceu: o código usa gets, que foi removida do C no C11 (seção 4).
Como resolver: troque por fgets(nome, sizeof nome, stdin) (veja fgets).
Referência rápida
| Item | O que é |
|---|---|
scanf("%29s", nome) | Lê uma palavra com limite: um a menos que o tamanho do array. |
snprintf(dest, sizeof dest, "%s", origem) | Copia/formata sem estourar e sempre termina com '\0'. |
fgets(linha, sizeof linha, stdin) | Lê uma linha inteira com limite; substitui o gets. |
gets, strcpy, strcat, sprintf, scanf("%s") | Sem limite: evite ou dê sempre o tamanho do destino. |
-g -fsanitize=address | Fiscal que pega estouros de array com a linha exata (no desenvolvimento). |
-O2 -D_FORTIFY_SOURCE=2 | Conferência barata do compilador; pega até estouro entre campos de struct (na versão final). |
*** stack smashing detected *** | A trava da pilha pegou um estouro de array local (134). |
*** buffer overflow detected *** | O FORTIFY pegou uma cópia grande demais (134). |
Exercícios
1. Ler uma palavra com segurança
O array é char nome[20]. Complete o formato do scanf para nunca passar do fim:
2. Copiar sem estourar
Complete a função que copia origem para destino respeitando o tamanho:
3. O fiscal da versão final
Complete as duas opções que ligam o FORTIFY (uma delas também liga a otimização):
4. Por que strcpy(destino, origem) é perigoso?
5. Um programa leu um nome comprido, não deu erro nenhum, mas uma variável vizinha mudou de valor. O que foi?
6. Qual fiscal pega o estouro de um campo de struct para o campo vizinho?
Resumo
- Um estouro de buffer é escrever além do fim de um array. O C não impede, e o erro pode derrubar o programa, estragar um dado vizinho em silêncio ou abrir a porta para um ataque.
- A causa é quase sempre copiar texto sem o tamanho do destino:
gets,strcpy,strcat,sprintf,scanf("%s"). - As versões seguras recebem o tamanho:
%29s,snprintf(dest, sizeof dest, ...),fgets. Ogetsfoi removido do C no C11. - Um programa que "roda e dá certo" não prova nada: o estouro pode estar lá, calado.
-g -fsanitize=addresspega estouros de array com a linha exata;-O2 -D_FORTIFY_SOURCE=2é barato e pega até estouro entre campos de struct. Use o ASan no desenvolvimento e o FORTIFY na versão final.- No editor do guia, os dois fiscais não funcionam; rode-os no seu Linux.
Próxima aula: Boas Práticas.