Como funciona a vulnerabilidade de Stack Buffer Overflow
Este tutorial faz parte do GUIA COMPLETO do professional em Segurança Ofensiva de Software, saiba mais.
Aula 21: Vulnerabilidade de estouro de buffer na pilha
Vulnerabilidade é uma falha de segurança em um sistema. O artefato utilizado para explorar uma vulnerabilidade leva o nome de exploit e o termo PoC se refere a prova de conceito utilizada para provar que a vulnerabilidade existe explorado de forma inofensiva para evitar que usuários maliciosos utilizem do código.
Figura 1: Dados consultados em Outubro 2022 |
A vulnerabilidade de Buffer Overflow apesar de ser antiga continua presente. Muito devido a uma quantidade maior de aplicações sendo desenvolvidas por pessoas que desconhecem o processo de programação segura (Figura 1).
Na computação, Pilha (Stack) é um segmento de memória utilizado para fazer o processo de chamadas e retorno de funções. O espaço de memória de uma função é criado quando esta está em execução e quando termina de executar o espaço é liberado. Já Buffer é um conceito de área de memória limitada e contínua e Overflow é um estouro, um transbordamento. Então quando se fala em Buffer Overflow, se refere ao estouro do armazenamento da memória.
Outros termos que você pode encontrar na literatura é que quando você escreve uma função e o compilador gera o código desta função, ele gera dois trechos de códigos um deles chamado de Prologo utilizado para abrir espaço na pilha para os dados da função e na hora de retornar ele libera o espaço da pilha para devolver à função chamadora a pilha como ela estava, essas instruções finais é chamado de Epílogo.
Figura 2: Memória |
Para facilitar o entendimento, a estrutura da memória será descrita pensando numa representação para um sistema Linux 32 bits (Figura 2). Para outros sistemas isso muda muito pouco, sistemas 64 bits basicamente a memória terá o dobro de tamanho.
Qualquer programa enxerga a memória do computador como sendo toda dele, indo do endereço 0x00000000 à 0xFFFFFFFF . Isso acontece devido ao sistema de memória virtual, no qual o sistema operacional faz a tradução do espaço real que o programa pode utilizar.
A memória começa com o segmento Text, que é onde fica as instruções do programa. É um segmento de tamanho fixo que depende da quantidade de instruções do programa para alocar seu espaço. Normalmente começa dos endereços baixo de memória após 0x00000000 pois antes disso o espaço é reservado para alguns metadados de processos que o sistema operacional utiliza como informações de controle de acesso, segurança, troca de contexto etc.
Depois do segmento Text, acima dois pedaços são destinados a variáveis estáticas. O segmento Data se refere a variáveis estáticas inicializadas explicitamente no código, como por exemplo inicialização de variáveis globais no código. Já o segmento BSS se refere a variáveis estáticas não inicializadas, o compilador costuma preencher elas com zeros.
Em seguida, temos dois segmentos que crescem a medida que o programa executa. O Heap que cresce do endereço menor para o maior e o Stack que cresce do endereço maior para o menor. O sistema operacional quando armazena esses espaços, ele limita o crescimento máximo do Heap, que armazena variáveis globais alocadas dinamicamente, e do Stack, que armazena variáveis locais e meta valores como o endereço de retorno, de forma que quando esse limite é ultrapassado pelo programa ocorre o transbordamento levando o sistema operacional a interromper a aplicação.
A ultima área, primeira de cima para baixo, é armazenada para o sistema operacional. Lá é armazenado APIs e bibliotecas que o sistema necessita chamar.
É bom saber a diferença entre Stack Overflow e Stack Buffer Overflow:
- Stack Overflow: ocorre quando o segmento de pilha é estourado, por exemplo em uma chamada recursiva com muitas chamadas vai acabar acabando com o espaço disponível para a pilha.
- Stack Buffer Overflow: ocorre quando uma área contínua dentro da pilha é estourado. Estudaremos isso abaixo!
[void func(char * entrada){char buffer[5];strcpy(buffer, entrada);return;}int main(int argc, char ** argv){func(argv[1]);return 0;}]
Figura 5: Pilha x86, visão F2 |
Figura 6: Estouro de buffer na pilha |
COMENTÁRIOS