domingo, 24 de abril de 2011

Implementando ataques de fuerza bruta.

"En teoria, la práctica y la teoría son iguales. En la práctica, no lo son."
Bruce Schneier.

Y en efecto, en algunos casos, la teoría y la práctica, llegan a ser muy distintas. Especificamente, por ejemplo, muchos clientes de mis pruebas de penetración consideran que los ataques descritos en artículos anteriores son imprácticos pues: "cualquier firewall/IPS es capaz de bloquear tu dirección IP apenas se detecte el ataque de fuerza bruta". Sin embargo, esta medida de mitigación funciona sólo en teoría y sólo si tu atacante es un novicio.

En la práctica, la medida de bloquear la dirección IP de tu atacante no sería tan mala de no ser por la gran cantidad de proxies libres y pagos a la disposición de cualquiera que pueda ejecutar una busqueda en google. Si además agregamos la gran cantidad de servicios baratos en "las nubes" (ya sea de amazon, o las creadas por los criminales informáticos en lo que se conoce como "botnets"), nos damos cuenta que en realidad nuestro atacante dispone de una cantidad casi inagotable de direcciones IP desde donde puede lanzar su ataque! En la práctica, lo que sucederá al bloquear la dirección IP de nuestro atacante "no-novicio" es que simplemente se utilizará otra dirección IP casi instantaneamente. Antes de detener a tu atacante efectivamente mediante el bloqueo de su cada vez nueva dirección IP, será tu firewall/IPS quien muy probablemente tendrá serios problemas de desempeño al intentar bloquear tantas direcciones IP particulares. Esta consecuencia es muy bien conocida por los que nos hemos enfrentado a estos tipos de ataque de forma rutinaria y conocemos los intringulis de la bestia de forma práctica, no sólo teórica.

 
Pero ¿qué tan fácil es implementar un ataque de cambio de dirección IP automático luego que el administrador novicio intente bloquear nuestra dirección IP? la respuesta es: trivial. Por ejemplo, si estamos trabajando contra un sistema web, con nuestro propio script en perl y usando la libreria LWP::UserAgent, agregar la funcionalidad mencionada requiere una sola linea de código:

$ENV{HTTP_PROXY} = 'http://direccion.ip.del.proxy:puerto_del_proxy';

Y por supuesto, es necesario codificar el manejo de error cuando la conexión falle y cambiar la variable anterior. Este método tiene la ventaja que cualquier otro script que esté atento a la variable de entorno, continuará funcionando transparentemente luego de hacer este cambio en vivo. En caso que el proxy requiera autenticación, las modificaciones necesarias son igualmente triviales. El último ingrediente necesario es una lista de proxies que como hemos visto, es de muy fácil acceso hasta para el principiante.

DIGRESIÓN: Si no conocias la información anterior y estas pensando iniciar tu carrera criminal con las técnicas descritas aquí, te tengo malas noticias. Usar proxies de esta manera no provee la anonimidad que muchos probablemente piensan. Es muy fácil dejar pistas de tu identidad por muchos lados y si tu víctima tiene los recursos y tiempo para buscarte te van a atrapar. En mis trabajos de forénsica de redes hemos logrado atrapar a muchos atacantes novicios de forma muy rápida debido a errores muy tontos de su parte. Mi recomendación: olvida el mundo criminal y ejecuta este tipo de demostraciones de forma profesional, con permiso y ganando buen dinero por ello.

Esta digresión me lleva al siguiente punto equivocado que escucho con cierta frecuencia de mis clientes con un poco más de experiencia en estos temas.  El argumento es mas o menos el siguiente: "en la presencia de un ataque de fuerza bruta, nosotros no bloqueamos ninguna dirección IP, nosotros bloqueamos directamente al individuo que ejecuta el ataque, lo buscamos con ayuda de las autoridades y lo visitamos a su casa". Esta política es mucho más efectiva que la anterior pero sólo hasta cierto punto. Asumiendo que tu organización tiene los recursos y el tiempo para ejecutar tal respuesta en todos los casos, lo cual no es usual, y asumiendo también que tu atacante se encuentra en tu jurisdicción o una jurisdicción colaboradora, esta medida sólo será efectiva contra los atacantes menos sofisticados. Tu criminal informático "profesional", del tipo que va tras organizaciones con los recursos y tiempo que estamos considerando, muy probablemente también sepa contra-medidas para no ser encontrado. Hablamos no sólo de medidas técnicas, como encadenamiento de proxies, sino que incluso pueden tambien hacerle una visita mafiosa al investigador técnico a cargo de tu investigación. En otras palabras, tu atacante cuenta con exactamente las mismas posibilidades que tú, sólo que desde el punto de vista criminal y sin restricciones legales.

En cuanto a las técnicas avanzadas de anonimidad que usan estos "criminales informáticos avanzados", pienso que no tienen cabida en este tipo de artículos. Por un lado, porque para el pentester profesional tienen muy poca utilidad. En nuestras pruebas de penetracion nosotros no sólo tratamos de no cambiar IP. Por el contrario, intentamos ser lo más identificables posible. Esta situación es, por supuesto, con la idea de proveer a la organización que nos contrata de un método efectivo para diferenciar un ataque verdadero de las actividades de nuestro servicio. Por el otro lado, tampoco tienen cabida pues este blog trata de ser un punto de referencia para los profesionales de seguridad informática y no una enciclopedia de ayuda a los pichones de criminales informáticos.

La conclusión de este segundo error común con respecto a los ataques de fuerza bruta es: no siempre podrás identificar físicamente a tu atacante. En consecuencia, no puedes confiar enteramente en esta medida tampoco.

El tercer y último "error" que escucho, aunque no muy frecuentemente debido al detalle técnico que requiere, de mis clientes es el siguiente: "para una organización incluso tan pequeña como la mía, es relativamente barato hacer que el espacio de búsqueda de mi atacante sea lo suficientemente grande como para que su ataque requiera tanto tiempo que se genere un cambio de interés en sus objetivos". Este argumento es 100% correcto. Por supuesto, usualmente lo escucho de clientes con sistemas de nombres de usuario de 8 caracteres y un password de 4 dígitos. Obviamente, este espacio de búsqueda es realmente muy pequeño para los estándares de recursos actuales, pero no dejan de tener razón en la teoría.

El problema es el siguiente: ¿Cuándo es un espacio de busqueda lo suficientemente grande sin hacer que mis usuarios tengan que recordar credenciales inhumanamente grandes? En este tema intervienen demasiados factores como para dar una respuesta adecuada para todos los casos. En consecuencia, yo siempre recomiendo hacer lo que equivale a un "análisis de stress" a la aplicación. Simplemente se ejecuta un ataque de fuerza bruta controlado sobre los procesos importantes incluyendo el de "login" y se estudian los resultados. Al final de este trabajo no sólo se tiene un estimado de cúal es la capacidad de atención real del sistema, sino que también se tiene un estimado de la velocidad con que un atacante puede conseguir la información que necesita para continuar su ataque. Es con esta información que se deben tomar decisiones sobre controles de mitigación en lugar de abusar de los "estandares" y/o "buenas practicas" y/o ISO-XXXX como se quieran llamar. Siempre hay que recordar: los estandares no dejan de ser una recomendación meramente teorica y que se quedan, en la gran mayoría de casos, muy cortos en la práctica.

El tema es tán sutíl que hasta colegas pentesters con cierta trayectoria reportan una tasa muy pequeña de velocidad de revisión en los ataques de fuerza bruta ejecutados por sus scripts. Una de las formas en que pentesters (y criminales) profesionales hacen scripts de gran desempeño es mediante el uso de multi-threading. Hace un tiempo y para aprender algo de python decidí hacer un esqueleto de muti-threading para utilizarlo con mis scripts. Al ser mi primer programa en python está un poco áspero en los bordes, pero lleva a cabo el trabajo con efectividad:

class RequestThread(threading.Thread):
        def __init__(self, id, request_queue):
                threading.Thread.__init__(self, name ="ResquestThread-%d" %(id,))
                self.request_queue = request_queue
        def run(self):
                while 1:
                        pid = self.request_queue.get()
                        # trabajar con pid. Manejar errores de la transaccion web con cuidado.
                        # 3 posibles estados: exito->anotar exito y terminar, error->anotar error y terminar, else->reintentar.


class InputThread(threading.Thread):
        def __init__(self, id, salir, tope, startp, startt):
                threading.Thread.__init__(self, name ="InputThread-%d" %(id,))
                self.salir = salir
                self.tope = tope
                self.startp = startp
                self.startt = startt
        def run(self):
                while 1:
                        text = str(raw_input())
                        if text == "quit":
                                #salir
                                salir.put(1)
                                break
                        cur = tope.get()
                        print "pid: " + str(cur) + " Speed: " + str(abs(cur - self.startp)/abs(time.time() - self.startt)) + " thrs/sec",

if __name__ == "__main__":
        pid = 0 ### Inicio del ataque de fuerza bruta. Esto puede ser la secuencia de usernames/passwords/etc.

        N_compute_threads = 30 ## Numero de Threads concurrentes. 30 es un buen numero para iniciar las pruebas.
        request_queue = Queue.Queue(N_compute_threads)

        for i in range(N_compute_threads):
                RequestThread(i,request_queue).start()

        salir = Queue.Queue(1)
        tope = Queue.Queue(1)

        InputThread(1, salir, tope, pid, time.time()).start()

        while salir.full() == False:
                if request_queue.full() == False:
                        request_queue.put(pid)
                        try:
                                tope.get_nowait()
                        except:
                                pass
                        tope.put(pid)
                        pid = pid + 1

        for i in range(N_compute_threads):
                request_queue.put(None)
        print "Shuting down " + str(pid)
        while request_queue.empty() == False:
                print "!",
                time.sleep(1)






La clase InputThread se encarga de manejar la interacción con el usuario. Por el momento, escribir la palabra "quit" ocasiona la señal de parada para todos los threads y termina limpiamente. Cualquier otro input simplemente produce la impresión de estadisticas de velocidad, avance y status general del programa. Esta clase se podría mejorar para proveer otras acciones como aumento de threads, disminución de threads, etc. La clase RequestThread se encarga de hacer el trabajo bruto. En el loop "while 1" se ejecuta la tarea que se asigna en la cola request_queue y es necesario manejar todos los casos de respuesta. Para el caso de aplicaciones web, es necesario manejar una situación de éxito, una de fracaso y una de ninguna de las dos. En los comentarios del código hay un poco más de explicación.


Finalmente, a pesar de que este artículo está enfocado a aplicaciones web, no hay ninguna razón por la que estas ideas no se puedan aplicar a otro tipo de aplicaciones. Por supuesto, habrán detalles teóricos que hay que sortear. Pero eso nunca es problema en la práctica. Si estos detalles no los han descubiertos los expertos en seguridad informática, de seguro lo habrán hecho los criminales informáticos. En ambos casos, lo único que hay que hacer es aprender de éstos y adaptar nuestros sistemas.

No hay comentarios:

Publicar un comentario en la entrada