Como funcionam os exploits
COMO FUNCIONAM OS EXPLOITS Aléxis Rodrigues de Almeidal Resumo – Este documento descreve o que são e como funcionam os exploits, procurando mostrar, através de um exemplo real, os riscos que essas ferramentas de ataque representam para uma rede de computadores. Palavras chaves: exploit, buffer overflow, segurança. Introdução Nos dias atuais são por meio da interliga grande rede acessíve Internet, essa grande p eneficios obtidos m uma uruca e nto do globo. A computadores em torno do mundo, é uma conquista irreversivel que admite um único futuro: uma contínua e frequente expansão.
Entretanto, om o advento dessa incrível interconexão de máquinas em escala mundial, muitos ainda são os problemas que precisam ser resolvidos para que os usuários obtenham uma razoável segurança na utilização dos serviços disponibilizados na grande rede. Cada novo serviço ou funcionalidade implementada pelos fabricantes de softwares utilizados nas redes de computadores encontra, frequentemente, uma imediata resposta de hackers e crackers. Esses “usuários” utilizam seus conhecimentos avançados de programação de computadores para explorar falhas existentes nos códigos desenvolvidos para essas novas funcionalidades.
Esse é um problema do qual ninguém está totalmente livre. Conforme (FIREWALLS SECURITY CORPORATION) , até mesmo programas famosos e considerados vulnerabilidade. Essas investidas contra fraquezas nos sistemas operacionais e aplicativos são apoiadas por ferramentas conhecidas como exploits. O resultado desses ataques pode ser simplesmente uma momentânea indisponibilidade do serviço (DOS – Denial Of Service) ou, na pior situação, a abertura de um acesso privilegiado no computador hospedeiro do serviço que sofreu o ataque.
A partir desse acesso obtido, poderão ser provocados prejuízos imprevisíveis dentro da rede atacada. Este trabalho procura descrever como funcionam e quais os resultados do ataque desses exploits. O objetivo do trabalho é dar subsídios aos administradores de rede e desenvolvedores de aplicativos na difícil tarefa de tentar evitar ou, pelo menos, responder o mais rápido possivel a ataques desse tipo. Aluno do curso de especialização em Admininistração em Redes Linux. Turma ARL2003s1.
CJFLA — Universidade Federal de Lavras. E-mail: alexis. almeida@ig. com. br. 2 O que são exploits O termo exploit, que em português significa, literalmente, explorar, na linguagem da Internet é usado comumente para e refererir a pequenos códigos de programas desenvolvidos especialmente para explorar falhas introduzidas em aplicativos por erros Involuntários de programação. Esses exploits, que podem ser preparados para atacar um sistema local ou remotamente, variam muito quanto à sua forma e poder de ataque.
Pelo fato de serem peças de código especialmente preparadas para explorar falhas muito específicas, geralmente há um diferente exploit para cada tipo de aplicativo, para cada tipo de falha ou para cada tipo de sistem 20F cada tipo de aplicativo, para cada tipo de falha ou para cada tipo de sistema operacional. Os exploits podem existir como programas executáveis ou, quando usados remotamente, podem estar ocultos, por exemplo, dentro de uma mensagem de correio eletrônico ou dentro de determinado comando de um protocolo de rede. Como funcionam os exploits Os exploits quase sempre se aproveitam de uma falha conhecida como buffer overflow (estouro de buffer). O buffer overflow acontece quando um programa grava informação em uma certa variável, passando, porém, uma quantidade maior de dados do que estava previsto pelo programa. Essa situação possibilita que um código arbitrário seja executado, necessitando apenas que ste seja devidamente posicionado dentro da área de memória do processo. Na figura 3. pode ser visto um simples exemplo de um programa vulnerável a um ataque de buffer overflow. O problema está na segunda linha da função ProcessaParm, que não critica o tamanho do parâmetro recebido na variável arg- void ProcessaParm(char *arg); void main(int argc, char *argv[]) { if (argc 1) { printf(“Param: Processaparm(argv[l]); } } void Processaparm(char *arg) { char buffer[l O]; /* PROBLEMA: se a string contida em arg tiver mais que 10 carateres havera um “buffer overflow” printf(buffer); } Programa vulnerável a buffer overflow Figura 3. : O buffer overflow, quando ocorre de forma aleatória, normalmente causa um crash na a licação. No Linux, essa situação gera a conhecida fault com core dump. 3 3 conhecida segmentation fault com core dump. Porém, quando corretamente induzido pelo atacante, o buffer overflow pode permitir que se execute um código malicioso que terá os mesmos privilégios de execução do aplicativo atacado. Embora o problema do buffer overflow seja conhecido há multo tempo, somente nos últimos anos ele passou a ser amplamente explorado como ferramenta de ataque.
Para entender completamente como o buffer overflow é explorado para se obter acessos indevidos ao sistema, é necessário em primeiro lugar compreender como os processos são organizados em memória. Cada arquitetura de hardware, sistema operacional ou compilador pode organizar de forma diferente um processo em memória. Na figura 3. 2 é possível ver um diagrama que representa essa organização para um programa escrito na linguagem C em um sistema Linux/i386. Pilha Heap Variáveis globais/estáticas Programa Figura 3. 2: Organização dos processos em memória A área de programa armazena o código executável.
Na área e variáveis globais são alocadas todas as variáveis globais e estáticas; enquanto que a área de heap é reservada para alocação local e dinâmica de memória. Finalmente, a área de pilha é usada para salvar registradores, salvar o endereço de retorno de subrotinas, criar variáveis locais bem como para passar parâmetros na chamada de funções. Como pode ser observado na figura 3. 2, os ponteiros da pilha e do heap crescem em sentidos opostos, convergindo para o centro da área livre que é comum às duas estruturas de memória.
Esse artificio é usado para otimizar o uso da memória livre na área de dados do rocesso. Ent 40F Esse artificio é usado para otimizar o uso da memória livre na área de dados do processo. Entretanto, como será visto ainda nesta seção, essa característica possibilita que os ataques sejam feitos tanto pela pilha quanto pelo heap. Na figura 3. 3 é possível ver os elementos envolvidos no processo de chamada de uma função. Normalmente, quando uma função é chamada, os seguintes passos são executados: 1) Os parâmetros da função são colocados da pilha em ordem inversa. ) Quando a instrução call é executa, o endereço de retorno é armazenado para permitir retorno da função à instrução imediatamente seguinte àquela que a chamou. 3) Já dentro da função, o conteúdo do registrador EBP, que é usado como apontador do stack frame, é colocado da pilha para ser recuperado no final da função. 4) Registrador EBP é carregado com o valor atual do ponteiro de pilha (SP). 5) O ponteiro da pilha é decrementado em N bytes, onde N é a quantidade de bytes necessários para a criação das variáveis locais.
Base da pilha (SB) Parâmetro n Endereços altos Valores anteriores colocados na pilha Frame de chamada da função Valor anterior de EBP salvo na primeira instrução da unção Valor reservado para as variáveis locais (Ex: buffer) 123 Parâmetro 2 Parâmetro 1 Endereço de retorno Antigo EBP Variáveis locais NOVO EBP 4 5 multa paciência, persistência e algum conhecimento de assembly e C, é possível alterar o valor do endereço de retorno do programa e redirecioná-lo para um código malicioso.
A partir desse momento, o ponteiro de instruções do processo passa a ser inteiramente controlado pelo atacante, que poderá fazer qualquer chamada a funções disponíveis no sistema. A alteração do endereço de retorno pode ser feita tanto pelo “estouro” e uma variável local alocada na pilha quanto pelo “estouro” da área de heap. Da mesma forma, o código malicioso, para onde o programa será desviado, pode ser colocado tanto no heap quanto na pilha. Nas figuras 3. 4 e 3. 5 pode ser vista uma representação da memória durante um ataque de pilha e de heap, respectivamente.
Base da pilha (SB) Parâmetro n Parâmetro 2 Parâmetro 1 Toda essa área de memória será sobrescrita pelo código malicioso passado no parâmetro que provocará o buffer overflow. O endereço de retorno aponta para um ponto qualquer dentro dessa área. Endereço de retorno modificado Antigo EBP sobrescrito) Var 1 (sobrescrito) Var N (sobrescrito) Buffer (contem o código malicioso) Valores anteriores colocados na pilha Frame de chamada da função Valor anterior de EBP salvo na primeira instrução da função Valor reservado para as variáveis locais (Ex: buffer) Endereços baixos Figura 3. : Situação da pilha em um ataque de buffer overflow sobre a pilha Base da pilha (SB) Parâmetro n Parâmetro 2 Parâmetro 1 Endereço de retorno modificado Antigo EBP (sobrescrito) Toda essa area de memória será sobrescrita pelo código malicioso passado no parâm 6 OF (sobrescrito) Toda essa área de memória será sobrescrita pelo ódigo malicioso passado no parâmetro que provocará o buffer overflow. O endereço de retorno aponta para um ponto qualquer dentro dessa área.
Var 1 (sobrescrito) Var N (sobrescrito) função Valor reservado para as variáveis locais (Ex: buffer) Heap: onde são alocados buffers dinâmicos Área livre entre a pilha e o heap Área livre entre a pilha e o heap. Tamanho variável de difícil precisao. Buffer (contem o código malicioso) Outras variáveis alocadas dinamicamente Endereços baixos Figura 3. 5: Situação da pilha em um ataque de buffer overflow sobre o heap Como pode ser visto na figura 3. 5, os exploits baseados no eap são mais difíceis de se construir devido à dificuldade de se determinar com precisão o tamanho da área entre o heap e a pilha.
Recentemente, os sistemas operacionais têm implementado mecanismos de bloqueio de execução de códigos na área de pilha e de heap. Essa medida tem por objetivo evitar esses ataques. Porém, para contornar essa dificuldade, uma outra variante do ataque foi desenvolvida. Essa nova tática, conhecida como “retorno à libc”, descrita em (MCDONALD,1999), consiste em desviar o programa para uma função da libc (system(), por exemplo), portanto dentro da área de código, onde não há ualquer restrição de execução de programas. A criação de novas técnicas de ataque é a enas uma questão de tempo.
Por exemplo, uma técnica mai o buffer overflow, e muit 3 uma questão de tempo. Por exemplo, uma técnica mais recente que o buffer overflow, e muito mais complexa do que esta, é a exploração do Format String Bug, detalhada com muita precisão em (THIJEMMEL,2001). Na seçao 4 será apresentado, passo a passo, um exemplo de um exploit baseado no estouro da pilha. Essa variante de exploit foi escolhida para ser analisada aqui por ser, dentre as técnicas de explorações de buffer overflow, a e menor dificuldade de implementação e a que mais tem sido usada ultimamente.
Um exemplo de explolt baseado no buffer overflow de pilha Em um ataque de estouro da pilha, normalmente o atacante terá que responder as seguintes questões antes de poder construir o exploit propriamente dito: Qual o tamanho do buffer? : em softwares livres isso é facilmente conseguido pelo fato dos fontes do programas serem de domínio público. Aqui não há demérito algum para o software livre uma vez que, fazendo um paralelo com a criptografia, conforme (UCHOA,2003), a segurança baseada na obscuridade é restrita e deve ser evitada.
O que vai ser executado dentro do código malicioso? : para responder a essa pergunta o atacante deve conhecer uma linguagem de baixo niVel, preferencialmente C, que será utilizada para construir o exploit. Além disso, é necessário que se conheça também um pouco de Assembly e do programa de depuração gdb. A premissa utilizada aqui é fazer um programa tao poderoso que faça todo o trabalho necessário e tão pequeno que caiba dentro da área de buffer.
Normalmente, a seqüência é: criar o programa em C, compilá-lo, abri-lo com o gdb, “anotar” os códigos binários das 80F equência é: criar o programa em C, compilá-lo, abri-lo com o gdb, “anota”‘ os códigos binários das instruções referentes ao trecho necessário. Esses códigos anotados do gdb serão guardados em uma variável do exploit, que os utilizará na construção da mensagem que será enviada ao servidor. Como “estourar o buffer do servidor? : aqui, principalmente, é onde entra a especificidade de cada exploit.
Novamente o atacante se utiliza do conhecimento dos fontes dos programas para conhecer todos os fatos necessários ao ataque. Não fosse o conhecimento dos fontes, isso ainda seria possível pelo menos de duas formas iferentes: ou através de engenharia reversa, utilizando-se de uma ferramenta de depuração (gdb, por exemplo), ou através da tentativa e erro, enviando grandes strngs em qualquer parte do programa em que há entrada de dados por parte do usuário. A figura 4. 1 mostra um trecho do programa que será alvo do ataque.
Trata-se aqui de um programa muito simples que tem por finalidade apenas semir aos propósitos didáticos deste trabalho. O programa implementa apenas duas funções: a função main(), que é responsável por “ouvir” a porta UDP 1234 e a função TrataMensagem(), que é chamada a cada mensagem recebida elo servidor. O programa cliente será o exploit, que preparará uma mensagem de forma tal que provoque o buffer overflow no servidor. Esse ataque abrirá, no servidor, um backdoor que será usado em seguida pelo atacante para continuar seu “trabalho”.
Procurando responder a segunda questão colocada no início desta seção, foi desenvolvido o código apresentado na figura 4. 2. Neste trabalho, a única seção, foi desenvolvido o código apresentado na figura 4. 2. Neste trabalho, a unica ação do atacante será criar o arquivo /bin/sx. Outros comandos poderiam ser acrescentados ao código para fetuar outras ações, como, por exemplo, incluir um usuário no arquivo /etc/passwd. Para criar o arquivo /bin/sx foi usada a system call sys_creat, através da instrução int Ox80. Após criar o arquivo, o exploit simplesmente encerra a execução do servidor. isten(Sock, 1); while(l) { Tam = sizeof(struct sockaddr_in); (struct sockaddr exit(l); memset(Mens, O,strIen(Mens)); if(read(Novo,Mens,sizeof(Mens)) < 0) exit(2); TrataMensagem(Mens); close(Novo); } void TrataMensagem(char *Mens) { char Buffer[256]; } VULNERABILIDADE: caso Mens seja maior que 256, haverá o estouro*/ Figura 4. 1 : Trecho do programa servidor alvo do ataque oid main() jmp INICIO FUNCAO: pop %esi xor %eax, %eax movb %eax,7(%esi) mov %esi,%ebx movb $0x8,%al mov $Oxfffff1 ff,%ecx int $Ox80 movb $1 xorl %ebx,%ebx int $0x80 INICIO: CALL FUNCAO . tring V' } Figura 4. 2: Código malicioso em assembly Na figura 4. 2 pode ser visto o código Assembly para esse pequeno programa. Para compilar o programa, foi usado o comando: gcc -g -o prog prog. c -Idb. unsigned char cod[]={ Oxeb,oxlf, Ox5e, ox31 ,oxco, Ox89,Oxf3, OxbO,Ox08, Oxb9,Oxff, oxfl Oxcd,Ox80, OxbO,Ox01, ox31 ,Oxdb, Oxcd,Ox80, Figura 4. 3: Versão em byte code do código malicioso O atacante de 0 DF 13