Buffer Overflow

Escrito por Denner Lopes - Revisado com uso do ChatGPT-4o

Definição

Contexto Geral

Buffer overflow é um dos erros ou vulnerabilidades no desenvolvimento de software que pode ser explorado por hackers para obter acesso não autorizado a sistemas. É uma das vulnerabilidades de segurança de software mais conhecidas, mas ainda bastante comum. Isso ocorre, em parte, porque buffer overflows podem surgir de várias maneiras, e as técnicas usadas para evitá-los costumam ser propensas a erros.

A falha se concentra nos buffers, que são seções sequenciais (vetores) da memória que armazenam dados temporariamente enquanto são transferidos entre locais. Também conhecido como estouro de buffer, a vulnerabilidade ocorre quando a quantidade de dados no buffer excede sua capacidade de armazenamento. Esses dados extras transbordam para locais de memória adjacentes e corrompem ou sobrescrevem os dados nesses locais.

Uma Introdução ao Ataque

Um ataque de buffer overflow ocorre quando um invasor manipula o tamanho dos dados que deseja armazenar na memória, de forma a posicionar estrategicamente um código malicioso em uma área importante da pilha. Esse ataque normalmente envolve a violação de linguagens de programação e a sobrescrita dos limites dos buffers. A maioria dos estouros de buffer é causada por uma combinação de manipulação incorreta da memória e suposições equivocadas sobre a composição ou o tamanho dos dados.

Quando Ocorre

Uma vulnerabilidade de estouro de buffer normalmente ocorre quando o código:

  • Depende de dados externos para controlar seu comportamento;

  • Depende de propriedades de dados que são aplicadas além de seu escopo imediato;

  • É tão complexo que os programadores não conseguem prever seu comportamento com precisão.

Resumo Histórico

Os buffer overflows foram compreendidos e parcialmente documentados publicamente já em 1972, quando o Computer Security Technology Planning Study o descreveu:

"O código que executa esta função não verifica os endereços de origem e destino corretamente, permitindo que partes do monitor sejam sobrepostas pelo usuário. Isso pode ser usado para injetar código no monitor que permitirá ao usuário assumir o controle da máquina."

Atualmente, o "monitor" seria chamado de kernel.

A primeira exploração hostil documentada de um estouro de buffer foi em 1988. Foi um dos vários exploits usados pelo worm Morris para se propagar pela Internet. O programa explorado foi um serviço no Unix chamado finger. Mais tarde, em 1995, Thomas Lopatic redescobriu independentemente o problema e publicou suas descobertas na lista de discussão de segurança Bugtraq.

Desde então, pelo menos dois grandes worms da Internet exploraram essa vulnerabilidade para comprometer um grande número de sistemas. Em 2001, o worm Code Red explorou um estouro de buffer no Internet Information Services (IIS) 5.0 da Microsoft e, em 2003, o worm SQL Slammer comprometeu máquinas que executavam o Microsoft SQL Server 2000. Ainda em 2003, estouros de buffer presentes em jogos licenciados do Xbox foram explorados para permitir que softwares não licenciados, incluindo jogos homebrew, rodassem no console sem a necessidade de modificações de hardware, conhecidas como modchips. O PlayStation 2 e o Nintendo Wii tiveram problemas similares.


Registros de Ativação

Para aprofundar mais no contexto de execução e nas proteções contra essa vulnerabilidade, devemos antes falar sobre a teoria por trás dos conceitos de pilha e registros de ativação.

Na implementação de uma linguagem baseada em compilação, o código-fonte é traduzido para um programa em linguagem de máquina. Normalmente, uma instrução no código-fonte corresponde a várias instruções em nível de máquina, e as variáveis da linguagem de origem são mapeadas em endereços de memória.

Linkagem Estática

A linkagem estática define uma associação direta entre as variáveis e suas localizações em tempo de compilação, de forma 1:1. Isso significa que, ao compilar um programa, será definido quantas posições de memória serão usadas e que cada variável terá um espaço reservado.

Um dos principais problemas dessa abordagem é lidar com a recursividade, que torna impossível saber, em tempo de compilação, quantas variáveis serão criadas em tempo de execução. Portanto, não podemos resolver todas as alocações antecipadamente. Além disso, buscamos eficiência no uso da memória: queremos alocar espaço apenas quando necessário e desalocar quando possível.

Linkagem Dinâmica

Para resolver o problema da recursividade, foi adotada uma abordagem onde variáveis globais continuam sendo alocadas em tempo de compilação, garantindo eficiência na execução. Já para cada função executada, um Registro de Ativação é empilhado em uma estrutura de dados gerenciadora.

Alocação de Pilha

O buffer overflow é mais comumente explorado nesse contexto. A cada função executada, um Registro de Ativação é empilhado com informações sobre parâmetros, endereço de retorno, endereço do RA anterior e variáveis.

Imagem iustrativa de um Registro de Ativação com pilhas

Ao encerrar uma função, seu registro de ativação é removido da pilha, liberando o espaço correspondente.

Alocação de Heap

Diferente da pilha, onde os registros de ativação são desalocados automaticamente, os registros no heap permanecem acessíveis até que sejam explicitamente liberados, geralmente via coletor de lixo ou por liberação manual. Esse modelo permite a criação de estruturas dinâmicas, como listas e árvores, que sobrevivem ao término da função.

Embora o buffer overflow também possa ocorrer nesse contexto, ele se torna mais complexo devido ao gerenciamento manual de memória e à atuação do coletor de lixo. É por conta desse último fato que a vulnerabilidade ocorre mais frequentemente em C e C++, porque essas linguagens permitem manipulação direta de memória sem verificações automáticas de limites de buffer, devido a alocação e o gerenciamento de memória serem responsabilidade do programador. Já Java e Python possuem esses mecanismos de gerenciamente embutidos, que reduzem significativamente a ocorrência desse problema.


O Ataque

Com esse cenário elaborado, podemos redefinir o ataque de estouro de buffer com mais detalhes.

O ataque envolve a manipulação do tamanho do espaço alocado para os parâmetros do Registro de Ativação. Ao fornecer uma quantidade excessiva de dados, a escrita na memória ultrapassa a área destinada aos parâmetros e sobrescreve o endereço de retorno, permitindo que o invasor redirecione a execução do programa.

O ataque pode ser ainda mais sofisticado: o atacante pode inserir funções maliciosas diretamente no espaço de memória dos parâmetros e fazer com que o endereço de retorno aponte para esse código injetado, em vez de uma função legítima.

Imagem iustrativa do estouro ocorrendo em um Registro de Ativação

As Proteções

Stack Canary

Inspirado no uso de canários em minas de carvão para detectar gases tóxicos, o Stack Canary é um número aleatório alocado pelo compilador entre os parâmetros e o endereço de retorno. Antes de retornar, o programa verifica se esse valor foi alterado. Caso tenha sido, o programa é encerrado para evitar a execução de código malicioso.

Essa verificação é geralmente implementada de forma hardcoded, com instruções de assembly e valores constantes no programa.

No-Execution Bit (NX)

Essa proteção desativa a execução de código em áreas da pilha. O Bit Não Executável (NX), ou DEP (Data Execution Prevention), impede a execução de código em regiões destinadas a dados. As páginas de memória são marcadas como apenas leitura e executáveis, ou graváveis e não executáveis, o que impede que código injetado em uma região de dados seja executado.

Essa abordagem depende de fatores como o suporte do processador, o sistema operacional e a linguagem de programação utilizada.

Address Space Layout Randomization (ASLR)

O ASLR é uma técnica de segurança que randomiza os endereços de memória de um programa a cada execução. Isso dificulta que um invasor preveja locais específicos para injeção ou redirecionamento de código malicioso.


Bypass

Apesar da eficácia dessas proteções, técnicas avançadas foram desenvolvidas para contorná-las.

  • O Stack Canary pode ser burlado com a ajuda de vulnerabilidades como Stack Leak, que vaza o conteúdo da pilha.

  • O NX bit não impede ataques que reutilizam trechos de código legítimo, como o ROP (Return-Oriented Programming), que encadeia instruções válidas da memória.

  • O ASLR pode ser neutralizado por técnicas que exploram entropia fraca ou vazamentos de endereços.

Ataques modernos frequentemente combinam essas estratégias, explorando falhas no sistema operacional, no compilador ou em aplicações específicas.


Capture The Flag

Essa vulnerabilidade é frequentemente abordada em competições e desafios CTF (Capture The Flag). Alguns desafios focam especificamente em burlar proteções ou explorar o estouro de buffer em sua forma pura.

Para obter informações sobre as proteções de um binário, é possível usar a ferramenta checksec. Já o pwndbg pode ser usado para inspecionar a pilha e visualizar a injeção de endereços de retorno.

Você pode testar seus conhecimentos com o desafio “Regularity” no HackTheBox.


Fontes

Last updated