Neste estudo, veremos o princípio
de uma inundação de memória de buffer
(buffer oveflow). Existem dois tipos de buffer overflow, o stack overflow
que se refere à pilha; e o heap overflow que refere à memória
heap.
Quando um programa é executado, seus vários elementos (instruções, variáveis...) são mapeados para a memória de uma maneira estruturada.
Organização do processo de memória
A próxima parte da memória contém duas seções: a stack (a pilha) e a heap,que são alocadas em tempo de execução. A pilha é usada para armazenar arguementos de funçoes, variáveis locais ou alguma informação que permite obter o estado da pilha antes de uma chamada de função... Essa pilha é baseada em um sistema de acesso LIFO (Last in Último a entrar, First Out Primeiro a sair) e cresce através dos últimos endereços de memória. Variáveis dinamicamente alocadas são encontradas no heap; tipicamente, um ponteiro se refere a um endereço heap, se ele é retornado por chamada à função malloc. As seçoes .bss e .data são dedicadas a variáveis globais e são alocadas em tempo de compilação. A seção .data contém dados estáticos inicializados, enquanto dados não inicializados podem ser encontrados na .bss A última seção de memória, .text contém instruções (por exemplo: o código do programa) e pode incluir dados somente-leitura. Pequenos exemplos podem ser realmente bons para um melhor entendimento. Vamos ver onde cada tipo de variavel é salva:
Nós vamos, agora considerar como as chamadas a funções são representadas na memória (na pilha, para ser mais exato, já que esse é o objeto deste estudo) e tentar entender os mecanismos envolvidos. Em um sistema Unix, uma chamada de função pode ser quebrada em três passos: 1.Prólogo Um ponteiro
de frame é salvo. Um frame pode ser visto como uma unidade lógica
da pilha e contém todos os elementos relacionados a uma função.
A quantidade de memória necessária para a função
é reservada. Uma simples ilustrações nos ajuda a ver como tudo isso funciona e nos fornecerá um melhor entendimento das técnicas mais comuns utilizadas em buffers overflow.
Vamos considerar esse código: int test(int a, int b , int c) { int main (int argc, char **argv) { Agora nós, disassemblamos (vimos código assembler) o binário GDB para tentar obter mais detalhes sobre esses três. Dois registradores são mencionados aqui: ponteiros EBP para o frame atual (ponteiro de frame) e ESP para o topo da pilha. Primeiro, a função mairt. (gdb) disassemble main A seguir, veja o prólogo da função main. Para mais detalhes sobre o prólogo de uma função, veja antes o caso do teste (), dado como exemplo. 0x80483ea <main+6>: add $0xfffffffc, %esp 0x80483ed <main+9>: push $0x2 Seguindo o código em assembler: 0x80483f8 <main+20>: add $0x10 , %esp Essa instrução representa ao retornoda função teste() para a função main(), o ponteiro da pilha aponta para um endereço de retorno, então, ele deve ser incrementado para apontar antes dos parâmetros da função (a pilha cresce pelos endereços de memória baixa!). Então, nós retornamos ao ambiente inicial, como ele era antes de ser chamado teste(): 0x80483fb <main+23>: xor %eax, %eax 0x8048400 <main+28>: leave As últimas duas instruções são o passo de retorno a main(). Agora vamos dar uma olhada na nossa função teste(). (gdb) disassemble teste Esse é o prólogo da nossa funçã: %ebp inicialmente aponta para um ambiente; ele é pilhado (para salvar o atual ambiente), e a segunda instrução faz %ebp apontar para o topo da pilha, que agora contém o endereço inicial do ambiente. A terceira função reserva memória suficiente para as variáveis locais. 0x80483c6 <teste+6>: movl $0x4 , 0xfffffffc (%ebp) Essas são as funções da instrução 0x80483e0 <teste+32>: leave
Isto é possível quando uma função
retorna, o proximo endereço de instrução é
copiado da pilha para o ponteiro EIP (ele foi pilhado implicitamente pela
instrução call). Como esse endereço é gravado
na pilha é possível corrompê-la para acessar essa
zona e escrever um novo valor lá, e é possível especificar
um novo endereço de instrução, que contém
código malvado. |
|