12 de abril de 2011

Escrevendo um exploit para Stack Overflow - parte 1

Bem, esse é um primeiro tutorial onde escreveremos um pequeno exploit para uma aplicação Windows escolhida ao acaso, dentre as inúmeras que são vulneráveis a essa falha.

Em julho de 2009 alguém reportou uma vulnerabilidade no programa “Easy RM to MP3 Conversion Utility” no Windows XP SP2, através do packetstormsecurity.org (http://packetstormsecurity.org/files/view/79307/easyrmmp3-overflow.txt). O relatório da vulnerabilidade incluia um exploit para prova de conceito. Pouco tempo depois foi liberado um segundo exploit.

Nós poderíamos, nesse momento, baixar os exploits e testá-los na aplicação para vê-los funcionando, ou... podemos tentar entender o processo de construção do exploit para que possamos construir nosso próprio exploit a partir do zero.

De qualquer forma, a menos que uma pessoa saiba como fazer o disassembling, ler e compreender um shellcode ou exploit, nunca se deve baixar um exploit, especialmente se for um executável previamente compilado, e executá-lo. As consequências podem ser desastrosas!

As dúvidas que surgem em nossa mente sobre esse processo de criação de exploits normalmente são: como os desenvolvedores de exploits escrevem seus códigos? Qual e o processo a ser seguido desde a detecção de um possível problema até a criação de um exploit que funcione? Como podemos usar a informação sobre uma vulnerabilidade para desenvolver nossos próprios exploits?

A vulnerabilidade que utilizei para escrever esse tutorial foi escolhida por ser simples e me permitir demonstrar algumas das técnicas que são utilizadas para escrever um exploit que gere um estouro de pilha, ou stack overflow, como é conhecido por aí...

Então, independente do fato de que a vulnerabilidade mencionada já ter dois exploits desenvolvidos (funcionando ou não), ainda poderemos utilizar essa falha no “Easy RM to MP3” como um exemplo prático e seguir um passo a passo para criar um exploit funcional, sem copiar nada dos exploits originais. Faremos desde o início e ainda faremos funcionar em um Windows XP com SP3.

Antes de continuarmos, gostaria de dizer que esse artigo é puramente educacional e não tenho a intenção de que alguém utilize essas informações para hackear computadores ou realizar qualquer tipo de ação ilegal. Portanto, não me responsabilizo pelos atos de outras pessoas que, aproveitando-se de informações contidas nesse artigo, realizem atos ilegais.

De início, é importante que saibam, que a informação contida sobre a vulnerabilidade no relatório liberado em 2009, é a base para nosso trabalho. Nesse caso, o relatório diz que a vulnerabilidade é a seguinte:

Easy RM to MP3 Converter version 2.7.3.700 universal buffer overflow exploit that creates a malicious .m3u file.”

Em outras palavras, podemos criar um arquivo .m3u malicioso, carregá-lo no aplicativo e ativar o exploit. Esse tipo de relatório não é muito específico na maioria das vezes, mas pelo menos podemos ter uma ideia de como simular um travamento ou fazer com que a aplicação comporte-se de maneira diferente.

Do contrário, o pesquisador de vulnerabilidades também pode liberar o que encontrou para o fabricante, permitindo que ele faça a correção, ou simplesmente guardar o que descobriu para si mesmo.


Verificando o Bug

Antes de tudo, vamos verificar que a aplicação que vamos utilizar realmente trava quando abre um arquivo m3u mal formado. (ou encontre uma aplicação que trave quando você insere algum tipo de dado nela)

Pegue uma cópia da versão vulnerável do Easy RM to MP3 e instale-a em um computador rodando Windows XP. O relatório da vulnerabilidade diz que o exploit funciona em um XP SP2 (Inglês), mas eu utilizarei XP SP3 (Português).

É possível baixar uma cópia da versão vulnerável aqui, no meu HD virtual:

http://www.4shared.com/file/o1vQ7J12/EasyRMtoMP3Converter.html

Vamos utilizar o seguinte script perl para criar o arquivo m3u, que nos ajudará a descobrir mais informações sobre a vulnerabilidade (se criar o script no windows, retire a primeira linha abaixo):

my $file= "crash.m3u";
my $junk= "\x41" x 10000;
open($FILE,">$file");
print $FILE "$junk";
close($FILE);
print "Arquivo m3u criado com sucesso\n";

Execute o script perl para criar o arquivo m3u. Seu conteúdo será 10000 letras A (\x41 é o hexadecimal de A). Agora abra o arquivo com o Easy RM to MP3...

A aplicação exibe um erro, mas o erro parece q está sendo tratado corretamente e a aplicação não trava. Modifique o script para criar um arquivo m3u com 20000 A's e tente de novo. Mesmo comportamento (a exception é tratada corretamente, então podemos não ter escrito algo útil). Mas vamos tentar de novo. Mude o script para escrever 30000 A's, crie o arquivo m3u e abra-o com a aplicação.

Booommmm!!!! A aplicação morreu :-)

Bem, descobrimos que a aplicação trava se inserimos um arquivo que contenha entre 20000 e 30000 A's. Mas o que podemos fazer com isso?


Verifique o bug e veja se pode ser algo interessante

Obviamente nem todo travamento de uma aplicação. Em muitos casos, um travamento não nos levará à possível exploração... Mas algumas vezes sim. Com “exploração”, quero dizer que você pode fazer com que a aplicação execute algo diferente através do controle do seu fluxo (e redirecione-o para algum outro local). Isso pode ser feito através do controle do Instruction Pointer , que é um registrador da CPU que contem um ponteiro para onde a próxima instrução que precisa ser executada, está localizada.

Vamos supor que uma determinada aplicação chama uma função com um parâmetro. Antes de ir para a função, ele salva sua localização no instruction pointer (assim saberá para onde retornar quando a função for completada). Se podemos modificar o valor desse ponteiro, e apontá-lo para um local na memória que contenha o código desenvolvido por nós, então podemos mudar o fluxo da aplicação e fazê-la executar algo diferente (diverso do seu retorno original ao local inicial). O código que faremos a aplicação executar após controlar seu fluxo, é normalmente referido como “shellcode”.

Dessa forma, se conseguirmos fazer com que a aplicação execute nosso shellcode, podemos fazer com que ela chame um exploit! Na maioria dos casos, este ponteiro é conhecido pelo termo EIP. Este registrador possui 4 bytes de tamanho. Portanto, se pudermos modificar esse 4 bytes, controlaremos a aplicação (owned...rs).


Antes de continuarmos, um pouco de teoria

Há alguns termos que precisamos que entender. Toda aplicação Windows utiliza partes da memória. A memória de processos contém 3 grandes componente:

  • code segment (instruções que o processador executa. O EIP armazena o local da próxima instrução)

  • data segment (variáveis, buffers dinâmicos)

  • stack segment (utilizado para passar dados/argumentos para funções, e é utilizado como espaço para variáveis. A pilha (stack) começa (a parte inferior da pilha) do final da memória virtual, e vai até o início da mesma (para os endereços menores). Um PUSH adiciona algo ao topo da pilha, POP remove um ítem (4 bytes) da pilha e coloca no registrador.

Se queremos acessar a pilha diretamente, podemos utilizar o ESP (Stack Pointer), que aponta o topo (isso quer dizer o endereço mais baixo da memória) da pilha.

  • Depois de um PUSH, o ESP apontará para o endereço mais baixo da memória (o endereço é decrementado com o tamanho dos dados que são colocados na pilha, que possuem o tamanho de 4 bytes no caso de endereços/ponteiros). O decremento normalmente ocorre antes do ítem ser colocado na pilha (dependendo da implementação... se o ESP já aponta para o próximo local livre na pilha, o decremento ocorre após a colocação dos dados na pilha).

  • Após um POP, o ESP aponta para um endereço mais alto (o endereço é incrementado por 4 byes no caso de endereços/ponteiros) O incremento ocorre após um ítem ser removido da pilha.


Quando uma função/subrotina é iniciada, um frame é criado na pilha. Este frame mantém os parâmetros dos procedimentos paralelos e é utilizado para passar argumentos à subrotina. A localização atual da pilha, pode ser acessada através do stack pointer (ESP), a base atual da função estará contida no base pointer (EBP) (ou frame pointer).

Os registradores da CPU para operações em geral (Intel, x86) são:

  • EAX : accumulator : utilizado na realização de cálculos e armazenar valores retornados por chamadas de funções. Operações básicas como soma, subtração, utilizam esse registrador para propósito geral.

  • EBX : base (não tem nenhuma relação com o base pointer). Não tem nenhum propósito específico e pode ser utilizado par armazenar dados.

  • ECX : counter : usado para iterações. ECX realiza sua contagem de forma decrescente.

  • EDX : data : este é uma extensão do registrador EAX. Permite cálculos mais complexos (multiplicação, divisão) permitindo dados extras serem armazenados para facilitar tais cálculos.

  • ESP : stack pointer

  • EBP : base pointer

  • ESI : source index : armazena a localização da entrada de dados.

  • EDI : destination index : aponta para a localização de onde o resultado de operações co dados estão armazenados.

  • EIP : instruction pointer



Memória de Processo


Quando uma aplicação é iniciada em ambiente Win32, um processo é criado e memória virtual é alocada. Em um processo de 32 bits, o range de endereços é de 0x00000000 à 0xFFFFFFFF, onde de 0x00000000 à 0x7FFFFFFF é alocado para “user-land”, e de 0x800000000 à 0xFFFFFFFF é alocado para “kernel-land”. Windows utiliza o flat memory model, que significa que a CPU pode diretaente/sequencialmente/linearmente endereçar todos os locais da memória disponível, sem precisar utilizar o esquema de segmentação/paginação.

A memória de kernel land é acessível apenas pelo sistema operacional.

Quando um processo é criado, um PEB (Process Execution Block) e TEB (Thread Environment Block) são criados.

O PEB contém todos os parâmetros do tipo user land que estão associados com o processo corrente:

  • localização do principal executável

  • ponteiro para o carregador de dados (pode ser utilizado para listar todas as dll's/módulos que estão/podem ser carregados no processo)

  • ponteiro para informação sobre o heap


O TEB descreve o estado da thread, e inclui:

  • localização do PEB na memória

  • localização da pilha para a thread a qual pertence

  • ponteiro para a primeira entrada na SEH chain (veremos o que isso quer dizer em um próximo tutorial)


Cada thread dentro do processo tem um TEB.

O mapa da memória de processos Win32 se parece com o seguinte:

O segmento de texto de uma imagem/dll de um programa é apenas para leitura, já que contém apenas o código da aplicação. Isso previne que alguém modifique o código da aplicação. Este segmento da memória tem um tamanho fixo. O segmento de dados é utilizado para armazenar variáveis estáticas e globais do programa. O segmento de dados é utilizado para variáveis, strings e constantes inicializadas.

O segmento de dados é alterável e possui tamanho fixo. O segmento da heap é utilizado para o restante das variáveis do programa. Pode crescer mais ou menos como desejado. Toda a memória na heap é gerenciada por algoritmos de alocação (e desalocação). Uma região da memória é reservada para esse algoritmos. A heap crescerá em direção aos endereços mais altos.

Em uma dll, o código, imports (lista de funções utilizadas pela dll, de outra dll ou aplicação), e exports (funções disponíveis para outras dll's da aplicação) são partes do segmento .text.


A pilha

A pilha é uma peça da memória de processo, uma estrutura de dados que trabalha no esquema LIFO (Last in first out – último a entrar, primeiro a sair). Uma pilha é alocada pelo só, para cada thread (quando a thread é criada). Quando a thread é finalizada, a pilha também é liberada. O tamanho da pilha é definido quando ela é criada e não é alterado. Combinado com o LIFO e o fato de que não requer mecanismos/estruturas complexas de gerenciamento para ser controlada, a pilha é bem rápida, mas de tamanho limitado.

LIFO significa que o dado mais novo (resultado de uma instrução PUSH), é o primeiro que será removido da pilha novamente (por uma instrução POP).

Quando uma pilha é criada, o ponteiro da pilha aponta para o topo da pilha (o endereço mais alto na pilha). A medida que a informação é inserida na pilha, este ponteiro decrementa-se (vai para um endereço mais baixo). Então, em essência, a pilha sobe para um endereço mais baixo.

A pilha contém variáveis locais, chamadas de função e outras informações que não precisam ser armazenadas por um longo período de tempo. A medida que mais dados são adicionado à pilha, o ponteiro da pilha é decrementado e aponta para um endereço de valor menor.

Todas as vezes que uma função é chamada, os parâmetros da mesma são inseridos na pilha, assim como os valores salvos dos registradores (EBP, EIP). Quando uma função retorna, o valor salvo do EIP é recuperado da pilha e colocado de volta no EIP, assim o fluxo normal da aplicação pode ser seguido.

Vamos utilizar um código simples, de poucas linhas, para demonstrar o comportamento:

#include

void do_something(char *Buffer)

{

char MyVar[128];

strcpy(MyVar,Buffer);

}

int main (int argc, char **argv)

{

do_something(argv[1]);

}


Compile a aplicação com o Dev-C++ no Windows XP e execute-a com o comando:

pilha.exe AAAAA

Nada deve retornar como resultado.

Esta aplicação recebe um argumento (argv[1]) e transmite-o para a função do_something(). Nessa função, o argumento é copiado em uma variável local que suporte o máximo de 128 bytes. Bem... se o argumento for maior que 127 bytes (+ um byte nulo para finalizar a string), o buffer pode ser estourado.
Quando a função “do_something(param1)” é chamada de dentro do main(), o seguinte ocorre: um novo frame de pilha será criado, no topo da pilha correlata. O ponteiro da pilha (ESP) aponta para o endereço mais alto da nova pilha criada. Este é o “topo da pilha”.

Antes da função do_something ser chamada, um ponteiro para o(s) argumento(s) é inserido na pilha. Em nosso caso, este é um ponteiro para argv[1].



Pilha após a instrução MOV:


A seguir, a função do_something é chamada. A instrução CALL primeiramente colocará o ponteiro da instrução atual na pilha (assim saberá para onde retornar se a função finalizar) e pulará para o código da função.

Pilha após a instrução CALL:



Como resultado da inserção, o ESP decrementa em 4 bytes e agora aponta para um endereço mais baixo.


ESP aponta para 0022FF7C. Nesse endereço, podemos ver o EIP salvo (Returno to...), seguido de um indicador de ponteiro para o parâmetro (AAAA neste exemplo). Este ponteiro foi salvo na pilha antes da instrução CALL ser executada.


A seguir, o prólogo da função é executado. Ele basicamente salvo o ponteiro do frame (EBP) na pilha, assim pode ser restaurado quando a função retornar. A instrução para salvar o ponteiro do frame é “push EBP”. ESP é decrementado novamente com 4 bytes.


Seguindo ao push ebp, o ponteiro atual da pilha (ESP) é colocado em EBP. Nesse ponto, tanto ESP quanto EBP apontam para o topo da pilha. A partir desse ponto, a pilha será referenciada normalmente pelo ESP (topo da pilha em qualquer momento) e EBP (o ponteiro da base da pilha). Dessa forma, a aplicação pode referenciar variáveis usando um offset de EBP.

Obs.: A maioria das funções começam com essa sequência: PUSH EBP, seguido por MOV EBP,ESP.

Então, se você inserisse outros 4 bytes na pilha, ESP decrementaria novamente e EBP permaneceria onde estava. Você pode se referir a estes 4 bytes utilizando EBP-0x8.

A seguir, podemos ver como o espaço da pilha para a variável MyVar (128 bytes) é declarado/alocado. Para armazenar dados, algum espaço é alocado na pilha, de forma que possa guardar dados nesta variável... ESP é decrementado por um determinado número de bytes. Este número de bytes será semelhante a algo maior do que 128 bytes, devido a uma rotina de alocação determinada pelo compilador. No caso do Dev-C++, isto será de 0x98 bytes. Assim você verá uma instrução SUB ESP,0x98. Dessa forma, haverá espaço disponível para esta variável.


O código do disassembled da function parece com o que vemos a seguir:


Não se preocupe muito com o código. É possível ver claramente o prólogo da função (PUSH EBP e MOV EBP,ESP), pode-se ver também onde há espaços alocados para MyVar (SUB ESP,98), e pode-se ver algumas instruções MOV e LEA (que basicamente configuram os parâmetros para a função strcpy) controlando o ponteiro onde está argv[1] e utilizando-a para copiar dados dela, para dentro de MyVar.

Se não houvesse uma strcpy() nessa função, a função agora finalizaria e liberaria a pilha. Basicamente, apenas seria movido o ESP de volta para seu local onde o EIP salvo estava, e então se dirigiria à uma instrução RET. Uma RET, nesse caso, pegaria da pilha o ponteiro EIP salvo e faria um jump com ele (assim, ele retornará para a função principal – main – logo após a do_something() ter sido chamada). A instrução final é executada por uma instrução LEAVE (que restauraria, tanto o ponteiro do frame quanto o EIP).

Em meu exemplo, nós temos uma função strcpy().

Esta função lerá os dados, a partir do endereço apontado para [Buffer], e armazenará os mesmos no , lendo todos os dados até que surja um null byte (string de término). Enquanto copia os dados, ESP lê um byte e escreve-o na ilha, usando um índice (por exemplo, ESP, ESP+1, ESP+2, etc). Assim, após a cópia, ESP ainda apontará para o início da string.


O que significa... que se os dados no [Buffer] for maiores do que 0x98 bytes, a função strcpy() irá sobrescrever o EBP salvo e eventualmente o EIP salvo (e assim por diante). Depois de tudo, continua a ler e escrever ate alcançar um null byte (no caso de uma string).


O ESP ainda apontará para o início da string. O strcpy() finaliza como se nada estivesse errado. Após o strcpy(), a função termina. E é aqui que as coisas ficam interessantes. Basicamente, o final da função moverá o ESP de volta para o local onde o EIP salvo está armazenado, e endereçará à um RET. Pegará o ponteiro (AAAA ou 0x41414141 em nosso caso, desde que tenha sido sobrescrito), e fará um jump para aquele endereço.

Assim você controla o EIP.

E controlando o EIP, você basicamente alterará o endereço de retorno que a função utilizará para seguir adiante com o “fluxo normal”.

É claro, que se mudarmos o endereço de retorno com um estouro de buffer, isso deixará de ser um “fluxo normal”.

Então... supondo que você possa sobrescrever o buffer em MyVar, EBP, EIP e tem os A's (parâmetros passados por você) na área antes e depois do EIP salvo... pense sobre isso.

Após enviar o buffer ([MyVar][EBP][EIP][seus parâmetros]), o ESP irá, ou deveria, apontar para o início de seus parâmetros. Assim, se você puder fazer com que o EIP vá para os seus parâmetros, você está no controle.


Obs.: texto baseado no original em inglês http://www.corelan.be/index.php/2009/07/19/exploit-writing-tutorial-part-1-stack-based-overflows/

4 comentários:

  1. artigo bem tecnico e rico em detalhes de facil entendimento , muito bom luiz.

    abraços

    ResponderExcluir
  2. muito bom...Luiz

    obs:

    LIFO (Last in first out – último a entrar, primeiro a sair).

    ResponderExcluir
  3. Pronto, concertado o erro do LIFO, valeu!

    ResponderExcluir
  4. putz muito phoda esse artigo, didatica muito boa...
    valew luiz

    ResponderExcluir