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.

Em 2 de novembro de 1998, uma nova forma de perigo apareceu com o Morris Worm, também conhecido como internet worm. Este famoso evento causou enormes prejuízos à internet, utilizando dois programs de Unix comuns, sendmail e fingerd. Isso foi possível exploitando um buffer overflow em fingerd. Este é, provavelmente um dos ataques mais devastadores baseado em overflow. Esse tipo de vulnerabilidade foi encontrado em larga escala e em deamons comumente usados como bind, wu-ftpd, ou várias implementações de telnetd, e também em aplicações como Oracle e MS Outlook Express.

O Buffer Overflow é uma grande ameaça possibilitando ao atacante conseguir uma shell na máquina remota ou até obtenha privilegios de super usuario root.

A maioria dos exploits baseados em buffers oveflow tenta forçar a execução de código malicioso, na maioria das vezes para conseguir um shell como root. O princípio é bem simples: instruções maliciosas são gravadas em um buffer, o qual é “inundado” (overflowed) para permitir um uso não-esperado do processo, alterando várias seções de memória.

Processamento da Memória

Quando um programa é executado, seus vários elementos (instruções, variáveis...) são mapeados para a memória de uma maneira estruturada.


Memória alta











Memória baixa

env strings
argv strings
env pointers
argv pointers
argc
stack
5

6
heap
.bss
.data
.text

Organização do processo de memória



Ás zonas mais altas contêm o ambiente de processamento, assim como seus argumentos: env strings (strings de ambiente), arg strings (strings de argumnetos), env pointes (ponteiros de ambiente), etc.

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:


Heap

int main () {
char * tata = malloc (3);
...
}

tata aponta para um endereço que está no heap.

.bss

char globais;
int main () {
...
}

int main () {
static int bss_var;
...
}

global e bss_var estarão em .bss

.data

char global = `a´;
int main () {
...
}

int main () {
static char data_var = `a´;
...
}
global e data_var estarão em .data.




Chamadas de Função

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.

2.Chamada – Os parâmetros da função são gravados na pilha e o ponteiro de instruções é salvo para saber quais instruções devem ser consideradas quando a função retornar.

3.Retorno (ou epílogo) – O antigo estado da pilha é restaurado.

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 i=4;
return (a+i)
}

int main (int argc, char **argv) {
teste (0, 1, 2) ;
return 0;
}

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
Dump of assembler code forfunction main:
0x80483e4 <main>: push %ebp
0x80483e5 <main+1>: mov %esp, %ebp
0x80483e7 <main+3>: sub $0x8, %esp

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
A chamada da função teste () é feita por esssa quatro instruções: seus parâmetros são pilhados (em ordem reversa) e afunção é invocada.

0x80483ed <main+9>: push $0x2
0x80483ef <main+11>: push $0x1
0x80483f1 <main+13>: push $0x0
0x80483f3 <main+15>: call 0x80483c0 <teste>

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
0x80483fd <main+25>: jmp 0x8048400 <main+28>
0x80483ff <main+27>: nop

0x8048400 <main+28>: leave
0x8048401 <main+29>: ret
End of assembler dump.

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
Dump of assembler code for function teste:
0x80483c0 <teste>: push %ebp
0x80483c1 <teste+1>: mov %esp, %ebp
0x80483c3 <teste+3>: sub $0x18, %esp

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)
0x80483cd <teste+13>: mov 0x8 (%ebp) , %eax
0x80483d0 <teste+16>: mov 0xfffffffc (%ebp). %ecx
0x80483d3 <teste+19>: lea (%ecx,, %eax, 1) , %edx
0x80483d6 <teste+22>: mov %edx, %eax
0x80483d8 <teste+24>: jmp 0x80483e0 <teste+32>
0x80483da <teste+26>: lea 0x0(%esi), %esi

Essas são as funções da instrução

0x80483e0 <teste+32>: leave
0x80483e1 <teste+32>: ret
End of assembler dump.
(gdb)


O passo de retorno (pelo menos sua fase interna) é feito com essas duas instruções. A primeira faz os ponteiros %ebp e %esp recuperarem o valor que tinham antes do prólogo (mas não antes da chamada da função, já que os ponteiros da pilha ainda apontam para um endereço que é menor do que a zona de memória onde nós encontramos os parâmetros de teste(), e nós acabamos de ver que ele recupera seu valor inicial na função main(). A segunda instrução lida com o registrador de instrução, o qual é visitado uma vez antes da chamada de função, para saber quais instruções devem ser executadas.
Esse pequeno exemplo mostra a organização da pilha quando as funções são chamadas. Se a seção de memória não é corretamente manipulada, ela pode prover oportunidades ao atacante de causar um distúrbio na organização da pilha e executar código arbitrário.

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.


fonte: Desafio Linux Hacker - Marcos Flávio Araújo Assunção - Visual Books


 

Imprimir Topo>>>
Hosted by www.Geocities.ws

1