Laboratório: o Attack Lab, Exploração de Buffer Overflow

Published on 2026-09-12·v1.0

Objetivos de Aprendizagem

  • Construir um ataque de buffer overflow por injeção de código que sobrescreve um endereço de retorno salvo na pilha com o endereço de um código de ataque feito à mão.
  • Explicar por que uma defesa que impede executar código na pilha (uma pilha não executável) derrota especificamente a injeção de código, e por que essa defesa sozinha não fecha a vulnerabilidade subjacente.
  • Construir um ataque de programação orientada a retorno (ROP) que encadeia sequências de instruções curtas e já existentes ("gadgets") já presentes no binário-alvo, sem injetar nenhum código novo.
  • Explicar, com precisão, o conhecimento do leiaute da pilha (exatamente onde o endereço de retorno fica em relação a um buffer) do qual os dois ataques dependem.

Contexto e Motivação

Stack Smashing e Buffer Overflows e A Pilha de Execução x86-64: Call e Ret já provaram, no papel, que uma escrita sem verificação além de um buffer de pilha de tamanho fixo consegue sobrescrever a memória de pilha adjacente, incluindo um endereço de retorno salvo, e que sobrescrever um endereço de retorno redireciona o fluxo de controle para onde o atacante escolher no momento em que a função atual retorna. Este laboratório é onde essa prova vira uma exploração real e funcional contra um binário real e compilado, o próprio Attack Lab da CMU, escalando por cinco níveis de um ataque direto de injeção de código até ataques mais difíceis de programação orientada a retorno, que funcionam mesmo quando a própria injeção de código passa a ser defendida.

Teoria Central

Nada sobre por que um buffer overflow de pilha consegue sobrescrever um endereço de retorno é rederivado aqui; esse argumento pertence a stack-smashing-and-buffer-overflows e a the-x86-64-runtime-stack-call-and-ret. Este laboratório é a disciplina de transformar esse argumento em bytes de exploração funcionais contra um leiaute de pilha específico e real, e de reconhecer por que a mais forte das duas famílias de ataque aqui, a ROP, ainda tem sucesso depois que a mais simples é defendida.

Exemplos Resolvidos

O leiaute de pilha do qual todo ataque deste laboratório depende

text
Endereços mais altos +-------------------------+ | endereço de retorno salvo | <- o alvo exato que este laboratório inteiro manipula +-------------------------+ | ponteiro base salvo | +-------------------------+ | buffer local (ex.: 32B) | <- o próprio array de tamanho fixo da função vulnerável +-------------------------+ Endereços mais baixos, a pilha cresce para baixo

Todo ataque deste laboratório depende do mesmo fato subjacente, já estabelecido na teoria: o endereço de retorno fica num deslocamento fixo e conhecido acima de um buffer vulnerável, então uma escrita que transborda esse buffer pelo número exato de bytes cai precisamente no endereço de retorno, e não em outro lugar da pilha.

Níveis 1 a 3: injeção de código

text
Entrada fornecida pelo atacante, correspondendo exatamente ao deslocamento de transbordamento do buffer: [32 bytes de preenchimento do buffer] [8 bytes: novo "endereço de retorno" apontando PARA DENTRO da própria entrada do atacante, especificamente para os bytes de código de ataque abaixo] [código de ataque: código de máquina x86-64 bruto que o atacante escreveu, por exemplo, instruções que chamam uma função específica com um argumento escolhido]

O endereço de retorno sobrescrito aponta não para alguma função existente, mas diretamente de volta para a própria entrada fornecida pelo atacante, para bytes que são, eles mesmos, instruções x86-64 válidas; quando a função vulnerável retorna, o processador começa a executar esses bytes fornecidos pelo atacante como código.

Níveis 4 a 5: programação orientada a retorno, quando a injeção de código é defendida

text
Uma pilha não executável (uma defesa real e padrão) torna o código de ataque acima inexecutável: o processador se recusa a rodar instruções buscadas na memória de pilha, seja qual for o conteúdo dos bytes. A resposta da ROP: em vez de injetar código NOVO, encadear endereços de pequenas sequências de instruções já existentes ("gadgets") JÁ presentes no próprio código executável do binário-alvo, cada uma terminando numa instrução `ret` que desempilha o endereço do PRÓXIMO gadget e salta para ele: [32 bytes de preenchimento do buffer] [endereço do gadget 1: ex.: "pop %rdi; ret" — desempilha um valor para %rdi] [o valor a desempilhar para %rdi: o argumento escolhido pelo atacante] [endereço do gadget 2: ex.: o endereço de uma função real a chamar]

O próprio ret de cada gadget é o que faz a cadeia funcionar: ele desempilha o próximo endereço nos dados de pilha forjados pelo atacante e salta para lá, de modo que o fluxo de controle pula de um pequeno trecho pré-existente do próprio código do binário para o seguinte, executando a lógica escolhida pelo atacante sem que uma única instrução nova seja injetada.

Passo 1: encontrando gadgets utilizáveis no binário-alvo

text
$ objdump -d ctarget | grep -B 1 'ret$' 4019a2: 5f pop %rdi 4019a3: c3 ret 4019c3: 58 pop %rax 4019c4: c3 ret

Ataques ROP reais são construídos exatamente a partir desse tipo de busca: varrer o próprio código compilado de um binário por sequências curtas de instruções, muitas vezes não intencionais, que por acaso ocorrem dentro de instruções mais longas e legítimas, terminando em ret, e então encadear seus endereços para realizar uma sequência escolhida de operações.

Equívocos Comuns e Armadilhas

  • "Uma pilha não executável fecha por completo a vulnerabilidade de buffer overflow." Ela derrota especificamente a injeção de código (Níveis 1 a 3), recusando-se a executar bytes recém-injetados, mas a vulnerabilidade subjacente (a capacidade de um atacante de sobrescrever o endereço de retorno e controlar o que executa em seguida) permanece inalterada; a ROP (Níveis 4 a 5) mostra que essa vulnerabilidade ainda é totalmente explorável usando só o próprio código já executável do alvo.
  • "A ROP exige escrever código malicioso novo, só que estruturado de forma diferente." Ela exige o contrário: nenhuma instrução nova é introduzida; toda instrução executada durante um ataque ROP já estava presente, e já marcada como executável, no binário-alvo antes de o ataque começar, e é precisamente isso que lhe permite contornar uma defesa de pilha não executável.
  • "O deslocamento exato do buffer overflow pode ser adivinhado por tentativa e erro, sem entender o leiaute da pilha." O deslocamento que cai precisamente no endereço de retorno é um número específico de bytes, determinado pelo leiaute real do quadro de pilha da função vulnerável; errá-lo sobrescreve a memória errada (muitas vezes travando o programa em vez de redirecioná-lo), e é por isso que o modelo de quadro de pilha de the-x86-64-runtime-stack-call-and-ret é um pré-requisito real, e não um contexto opcional, para este laboratório.

Resumo

Este laboratório transforma a prova teórica de stack-smashing-and-buffer-overflows numa exploração real e funcional contra o próprio Attack Lab da CMU: três ataques de injeção de código que sobrescrevem um endereço de retorno para redirecionar a execução para o código de ataque fornecido pelo atacante, seguidos de dois ataques mais difíceis de programação orientada a retorno que alcançam o mesmo sequestro de fluxo de controle usando só gadgets, sequências curtas de instruções terminando em ret, já presentes no próprio código compilado do alvo. A escalada da injeção de código para a ROP demonstra diretamente por que uma pilha não executável, uma defesa real e padrão, fecha um vetor de ataque específico sem fechar a vulnerabilidade subjacente, exatamente a distinção que o modelo de quadro de pilha de the-x86-64-runtime-stack-call-and-ret torna precisa.

Documentation Links