← Voltar para Insights Fundamentos · Segurança ofensiva

Buffer Overflow: de escrever fora do lugar ao controle total

É uma das falhas mais antigas da computação — e continua derrubando software em 2026. Este é um guia do zero: como a memória e a pilha funcionam, por que “estourar” um buffer vira execução de código, e como as proteções modernas transformaram um truque de uma linha numa corrida de obstáculos.

Aviso: este é um material didático. Todo experimento aqui descrito é feito em ambiente de laboratório controlado, com binários próprios e proteções propositalmente desligadas para fins de ensino. Explorar sistemas de terceiros sem autorização por escrito é crime.

1. Por que ainda falar disso

O buffer overflow é anterior à web, anterior ao Windows, quase tão antigo quanto o C. O ataque que ficou famoso com o worm Morris, em 1988, e virou catecismo com o artigo “Smashing the Stack for Fun and Profit”, em 1996, deveria estar aposentado. Não está.

Em 2026, uma IA de segurança da Anthropic encontrou, sozinha, um buffer overflow de 16 anos no FFmpeg — a biblioteca de vídeo que roda em praticamente todo lugar — num trecho de código que ferramentas de teste automatizado já haviam percorrido cinco milhões de vezes sem notar. Se a falha mais estudada da computação ainda se esconde por décadas no software que você usa todo dia, entender como ela funciona não é nostalgia: é alfabetização de segurança.

E há uma razão de fundo para ela não morrer: linguagens como C e C++ dão ao programador controle direto sobre a memória — e, com ele, a responsabilidade de nunca escrever um byte a mais. Levantamentos da Microsoft e da equipe do Chromium apontam que cerca de 70% das vulnerabilidades graves nessas bases de código nascem de erros de memória. O buffer overflow é o patriarca dessa família.

2. O mapa: memória, pilha e registradores

Para ver a falha, primeiro é preciso enxergar onde ela mora. Quando um programa roda, o sistema operacional lhe dá uma região de memória organizada em áreas. Uma delas é a pilha (stack) — e é ali que a mágica acontece.

A pilha é uma pilha de bandejas

A pilha funciona no princípio LIFO (last in, first out): o último a entrar é o primeiro a sair, como uma pilha de bandejas num restaurante. Cada vez que uma função é chamada, o programa empilha uma “bandeja” chamada stack frame, contendo as variáveis locais daquela função. Quando a função termina, a bandeja é retirada e o programa volta para quem a chamou.

Aqui está o detalhe que faz tudo: para o programa saber para onde voltar quando a função acabar, ele guarda, dentro dessa bandeja, o endereço de retorno — o ponto exato do código a retomar. Esse endereço fica salvo na pilha, encostado nas variáveis locais. Guarde essa palavra: endereço de retorno. É o alvo.

Os registradores que importam

O processador tem “gavetas” ultrarrápidas chamadas registradores. Num sistema x86-64 (64 bits) moderno, três interessam para nossa história:

Quando uma função retorna, a instrução ret faz uma coisa simples e fatal: pega o endereço de retorno salvo na pilha e o coloca dentro do RIP. Ou seja: o valor que está na pilha vira o próximo endereço executado. Se conseguirmos escrever nesse valor, dizemos ao processador exatamente para onde ir.

3. O bug: o que é “estourar” um buffer

Um buffer é só um espaço reservado para guardar dados — por exemplo, um vetor de 64 caracteres para receber um nome. O problema surge quando o programa copia dados para dentro do buffer sem checar se cabem. Se o buffer tem 64 bytes e o programa aceita 200, os 136 bytes excedentes não desaparecem: eles vazam para a memória vizinha. Isso é o overflow.

Veja um exemplo mínimo em C. A função lê uma entrada para um buffer local, usando gets() — uma função tão perigosa que foi removida do padrão da linguagem, justamente porque não tem como limitar o tamanho:

// vuln.c — exemplo de laboratório
#include <stdio.h>

void recebe_nome(void) {
    char buffer[64];        // espaço para 64 bytes
    printf("Digite seu nome: ");
    gets(buffer);            // ← lê SEM limite de tamanho: a falha
    printf("Ola, %s\n", buffer);
}

int main(void) { recebe_nome(); return 0; }

Enquanto o usuário digitar até 64 caracteres, tudo funciona. Mas o gets() não pergunta o tamanho: ele escreve o que vier. E a direção em que ele escreve é o que torna o ataque possível.

A geografia do estouro

Na pilha, os endereços da bandeja ficam organizados assim (endereços crescem para cima neste desenho): o buffer fica “embaixo”, e logo acima dele estão o RBP salvo e o endereço de retorno. Quando você escreve além do buffer, você escreve na direção desses valores:

Digitar 64 caracteres preenche o buffer. Digitar 72 preenche o buffer e os 8 bytes do RBP salvo. Digitar mais que 72 começa a sobrescrever o endereço de retorno. A partir desse ponto, não estamos mais corrompendo dados — estamos reescrevendo para onde o programa vai quando a função terminar.

4. Da falha ao controle: o endereço de retorno

Aqui a corrupção de memória vira sequestro de execução. Se sobrescrevemos o endereço de retorno com um valor à nossa escolha, então, no exato instante em que a função executa o seu ret, o processador salta para esse endereço. Deixamos de ser “quem digita um nome” para virar “quem decide a próxima instrução”.

O impacto disso não é abstrato. Controlar o fluxo de execução de um processo é, potencialmente, executar comandos com os privilégios daquele processo. Se o programa vulnerável roda como serviço de sistema, isso pode significar execução remota de código — a classe de falha mais grave que existe. Foi assim que worms se espalharam por milhões de máquinas e que roteadores, câmeras e servidores foram transformados em botnets.

Corromper memória é o meio. Controlar o RIP é o fim. Todo o resto da exploração de binário é sobre uma pergunta só: para onde apontar o ponteiro de instrução — e o que colocar lá para ele executar.

5. Achando o ponto exato (offset)

Para sobrescrever o endereço de retorno — e não o lixo ao redor — o atacante precisa saber a distância exata, em bytes, do início do buffer até ele. Isso se chama offset. No nosso exemplo é fácil na teoria (64 do buffer + 8 do RBP = 72), mas o compilador adiciona alinhamentos e o cálculo real quase nunca é redondo.

A técnica clássica é o padrão cíclico: enviar uma sequência não repetitiva (como Aa0Aa1Aa2…) grande o bastante para estourar. O programa quebra, e o valor que sobrou no RIP é um pedaço único daquela sequência — que revela, por engenharia reversa, a posição exata. Ferramentas de depuração como o GDB (com extensões como o PEDA/GEF) automatizam isso: você provoca o crash, lê o registrador, e a ferramenta te diz o offset em segundos.

Com o offset em mãos, o payload ganha uma forma previsível:

6. E agora, o que executar?

Controlado o ponteiro de instrução, falta responder: apontar para o quê? Historicamente, há duas escolas.

Escola 1: injetar o próprio código (shellcode)

A abordagem clássica é colocar, dentro do próprio payload, um pedacinho de código de máquina — o shellcode — que normalmente abre um shell (/bin/sh). Aponta-se o endereço de retorno de volta para a pilha, onde o shellcode foi parar, e ele executa.

Um refinamento célebre é o NOP sled: antes do shellcode, coloca-se uma sequência de instruções “não faça nada” (NOP). Como acertar o endereço exato do início do shellcode é difícil, basta o retorno cair em qualquer ponto da rampa de NOPs — o processador “escorrega” por eles até chegar ao código real. Aumenta muito a confiabilidade do exploit.

Escola 2: reaproveitar o código que já existe

Injetar shellcode só funciona se a pilha for executável — e, como veremos, a defesa moderna proíbe isso. A resposta dos atacantes foi elegante: se eu não posso trazer código novo, eu reutilizo o código que já está no programa. Essa é a base do ret2libc e do ROP (Return-Oriented Programming), que veremos a seguir.

7. As muralhas modernas — e como se escala cada uma

Se você acha que basta um gets() desavisado para tomar um servidor em 2026, a realidade é mais dura — para os dois lados. Ao longo de décadas, sistemas operacionais e compiladores empilharam proteções que transformaram o buffer overflow de “um truque” numa cadeia de bypasses. Entender cada muralha — e por que nenhuma sozinha é suficiente — é o coração da exploração moderna.

ProteçãoO que fazComo o atacante contorna
NX / DEP
No-eXecute
Marca a pilha como não executável. O shellcode injetado na pilha vira dados inertes — tentar executá-lo trava o programa. Reutilização de código: em vez de código novo, o atacante encadeia trechos que já existem no binário (ret2libc, ROP). Não executa nada “novo” — só rearranja o velho.
Stack Canary
Stack Protector
Coloca um valor aleatório (o “canário”) logo antes do endereço de retorno. Antes de retornar, a função confere se ele mudou; se mudou, aborta na hora. Vazar ou adivinhar: se houver um info-leak, lê-se o canário e recoloca-se o mesmo valor no payload. Em serviços que reiniciam por processo, dá até para descobri-lo byte a byte por força bruta.
ASLR
Address Space Layout Randomization
Randomiza, a cada execução, os endereços de bibliotecas, heap e pilha. O atacante não sabe mais “para onde” apontar, porque os endereços mudam sempre. Vazamento de endereço: basta vazar um endereço conhecido em tempo de execução. Como as distâncias internas são fixas, dele se recalcula a base de tudo (o resto é aritmética).
PIE
Position Independent Executable
Estende a randomização ao próprio binário, não só às bibliotecas. Nem os endereços do seu código são previsíveis. Mesma lógica: vaza-se um ponteiro do binário, subtrai-se o offset conhecido e obtém-se o endereço-base real. “Os endereços mudam; as distâncias entre eles, não.”

ROP em uma frase

O Return-Oriented Programming merece um parágrafo porque é a espinha dorsal da exploração moderna. A ideia: espalhados pelo binário existem milhares de pequenos trechos que terminam em ret — os gadgets (por exemplo, pop rdi; ret). Encadeando os endereços desses gadgets na pilha, o atacante monta um “programa” a partir de peças que já estavam ali — carregando registradores, chamando funções como system()sem nunca injetar código novo. É assim que se derrota o NX. Ferramentas como o ROPgadget catalogam as peças disponíveis.

A lição estratégica: as proteções modernas não eliminam o buffer overflow — elas exigem que o atacante encadeie várias condições (um estouro e um vazamento de informação, por exemplo). Cada muralha isolada cai; a segurança real está em ter todas ligadas ao mesmo tempo. É exatamente por isso que compilar com as proteções desativadas — comum em software embarcado e legado — é tão perigoso.

8. Por que isso não é história antiga

Junte os fios. A falha tem 50 anos, mas a causa raiz — linguagens que confiam no programador para nunca errar um limite de memória — continua na base de sistemas operacionais, navegadores, drivers e firmware. As proteções elevaram o custo do ataque, não o zeraram. E a chegada da IA ofensiva, capaz de rastrear esses erros em código que humanos revisaram por décadas, está desenterrando buffer overflows antigos em ritmo industrial.

Para quem desenvolve

Para empresas e gestores


Entender o buffer overflow é entender como uma linha de código descuidada vira controle de uma máquina — e por que profundidade técnica, não checklist, é o que separa segurança de teatro de segurança. É esse tipo de análise que a TreeSec leva para dentro dos sistemas dos nossos clientes.

Seu software aguenta uma análise de verdade?

Nossos testes vão do binário embarcado à API web, caçando as falhas que ferramentas automatizadas não alcançam — e provando o impacto real de cada uma.

Referências e para aprofundar

  • h41stur — série prática Linux Buffer Overflow (parte 2 · parte 3): walkthrough hands-on com GDB, offset e bypass de proteções.
  • Aleph One — Smashing the Stack for Fun and Profit (Phrack #49, 1996), o texto fundador.
  • The Chromium Projects — Memory safety (~70% das falhas graves são de memória).
  • Anthropic — Project Glasswing: IA encontrando buffer overflows de décadas (ex.: FFmpeg, 16 anos).