Magia negra con scapy
Esta receta en realidad es una traducción de un artículo de Rob Klein titulado Packet Wizardry: Ruling the Network with Python usando la magnífica herramienta scapy. Igual es un poco largo para receta, pero bueno, no tiene desperdicio.
Autor
By Rob klein Gunnewiek aka detach http://hackaholic.org/
Historial de cambios
- v2005-08-09 (This paper is subject to change.. new techniques will probably be added over time)
- Changes:
- v2005-08-09: Some fixes
- v2005-04-05: Typo’s and errata
- v2005-03-28: Initial paper
Prefacio
En este tutorial cubriré técnicas que involucran la construcción y manipulación de paquetes para ‘controlar’ la red desde la línea de comandos de Python. No se requieren conocimientos previos de Python, sin embargo supongo que cuando estés tan emocionado con el tema como lo estoy yo, querrás aprender inmediatamente. Sin embargo, si que recomiendo cierto conocimiento previo sobre problemas comunes de seguridad de redes.
Este tutorial es una zambullida práctica sobre seguridad en redes. La escasez de información práctica sobre la seguridad al nivel de red me lleva a creer que muchos curiosos tienen pocos conocimientos sobre el asunto. Puede que conozcas las bases de TCP/IP y sepas cómo usar Nmap. Puede que sepas incluso algunos trucos como el ‘escaneo suspendido’ usando Hping2. Pero ¿qué has programado usando Libnet? ¿Nunca has programado un sniffer básico usando Libpcap?
En cualquier caso, a pesar de tus conocimientos en el área del reconocimiento y los ataques de red, esta guía podría ser muy interesante
Introducción
Para que puedas decidir si deberías leer este tutorial, empezaré dando un ejemplo rápido para demostrar la potencia con la que estamos tratando aquí:
Quiero programar un escaneador de puertos, para escanear una red clase C completa para enumerar todos los hosts que tienen abierto el puerto 80. Ejecuto la shell de Python y empiezo a escribir comandos:
>>> p=IP(dst="hackaholic.org/24")/TCP(dport=80, flags="S") >>> sr(p)
¡Eso es todo! Ahora vemos qué hosts están escuchando en el puerto 80:
>>> results = _[0] >>> for pout, pin in results: ... if pin.flags == 2: ... print pout.dst ... 24.132.156.5 24.132.156.19 24.132.156.24 24.132.156.72 24.132.156.102 24.132.156.107 24.132.156.121 24.132.156.141 24.132.156.150 24.132.156.148 24.132.156.204 24.132.156.211 >>>
¡Bienvenido a esta caja de magia negra llamada Scapy! Qué hemos hecho. Primero cree un paquete que fue enviado a la subred /24 a la que está conectada hackaholic.org y puse el puerto destino 80 y el flag SYN en la cabecera TCP.
Después, como sabes, el frag SYN se utiliza para iniciar una conexión. Una respuesta SA (SYN/ACK) significa que el puerto está a la escucha, un RA (RESET/ACK) significa que está cerrado, y finalmente la ausencia de respuesta significa que el host está apagado o un firewall está descartando los paquetes.
Después de construir el paquete, le pido a Scapy que libere su magia negra y emita los paquetes. Los resultados se diseccionan en el bucle for y se lista la dirección IP destino de los hosts que respondieron SA
Scapy es una herramienta excelente escrita por Philippe Biondi. Aunque este tutorial debería facilitar la mayoría de la documentación que podrías necesitar, puedes encontrar más documentación ahí. Asegúrate de estudiar también su presentación sobre Scapy. Aunque scapy en si es bueno, su documentación no es fabulosa, mucho lo tendrás que aprender por ti mismo.
Puesta en marcha de Scapy
Bien, espero que el ejemplo de la introducción haya captado tu atención y te motive a través de esta sección en la que voy a explicar detalles aburridos sobre programación Python/Scapy
Primero, déjame decirte que yo no soy un experto en Python. Soy un tío práctico, no me gusta aprender cosas que no son prácticas desde el principio. Mi aprendizaje de Python es el resultado de mi intención de usar Scapy de forma efectiva. No tengo un libro de texto y puede que algunas cosas seas desconocidas para mi o estén equivocadas.
BIen, primero la configuración del entorno de Scapy. Instala la distribución binaria de Python de tu distribución GNU/Linux He visto que deberías tener al menos Python-2.2 o superior para que Scapy funcione. Escribe ‘python’ en un terminal y comprueba si funciona:
detach@luna:~$ python Python 2.3.5c1 (#2, Jan 27 2005, 10:49:01) [GCC 3.3.5 (Debian 1:3.3.5-6)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> if 1+1 == 2: ... print "Thank goodness!" ... Thank goodness! >>>
Lo que más me gusta de Python es que puedes escribir cualquier cosa que tú crees que debería funcionar y funciona. Hay algunas reglas básicas en Python que deberías saber:
- Los bloques no van dentro de llaves, o sentencias BEGIN-END, se distinguen por la indentación apropiada; 4 espacios y líneas vacías.
- No se necesita punto-y-coma como separador de sentencias (pero se puede usar si quieres poner varias sentencias en la misma línea)
- No se necesitan paréntesis en las sentencias condicionales como IF o WHILE, las sentencias condicionales acaban con ‘:’ después de poner la expresión.
La característica de Python más importante y probablemente más chula es que tiene un modo interactivo nativo. Sí, ese es el modo que acabas de usar. Tú ejecutas ‘python’ y tiene el indicador ‘>>>’.
Ahora que tenemos Python funcionando vamos con scapy. Descarga scapy de “”http://www.secdev.org/projects/scapy/“:http://www.secdev.org/projects/scapy/”>http://www.secdev.org/projects/scapy/":http://www.secdev.org/projects/scapy/, en el momento de escribir esto, la versión es 0.9.17beta. Extrae el fuente de Scapy y ejecuta el programa como root2.
detach@luna:~/lab/scapy-0.9.17$ sudo python ./scapy.py Welcome to Scapy (0.9.17.1beta) >>>
Pudes dejar que scapy guarde todo lo que escribas indicandole un nombre de fichero en la linea de comandos.
Scapy en una cáscara de nuez
Empezaré con una lista de lo que creo que son las ventajas más significativas de Scapy.
- Scapy tiene modos ‘envío’, ‘recepción’ y ‘envío&recepción’
- Scapy puede enviar paquetes en la capa 2 (enlace de datos) y en la capa 3 (red)
- Scapy tiene varias funciones de alto nivel como
p0f()
yarpcachepoison
que pueden hacer lo mismo que la mayoría de las aplicaciones de seguridad. - Es fácil diseccionar y reutilizar las respuestas.
- Es fácil
- Lo malo de Scapy es que es relativamente lento, lo que puede hacer imposible algunos usos. Por eso es más adecuado para reconocimiento, y no para un DoS por ejemplo.
Los comandos/funciones más importantes de scapy, los que necesitas recordar son ls()
y lsc()
. Los usarás mucho.
>>> ls() Dot11Elt : 802.11 Information Element Dot11 : 802.11 SNAP : SNAP IPerror : IP in ICMP BOOTP : BOOTP PrismHeader : abstract packet Ether : Ethernet TCP : TCP Dot11ProbeResp : 802.11 Probe Response TCPerror : TCP in ICMP Dot11AssoResp : 802.11 Association Response Dot11ReassoReq : 802.11 Reassociation Request Packet : abstract packet UDPerror : UDP in ICMP ISAKMP : ISAKMP Dot11ProbeReq : 802.11 Probe Request NTP : NTP Dot11Beacon : 802.11 Beacon DNSRR : DNS Resource Record STP : Spanning Tree Protocol ARP : ARP UDP : UDP Dot11ReassoResp : 802.11 Reassociation Response Dot1Q : 802.1Q ICMPerror : ICMP in ICMP Raw : Raw IKETransform : IKE Transform IKE_SA : IKE SA ISAKMP_payload : ISAKMP payload LLPPP : PPP Link Layer IP : IP LLC : LLC Dot11Deauth : 802.11 Deauthentication Dot11AssoReq : 802.11 Association Request ICMP : ICMP Dot3 : 802.3 EAPOL : EAPOL Dot11Disas : 802.11 Disassociation Padding : Padding DNS : DNS Dot11Auth : 802.11 Authentication Dot11ATIM : 802.11 ATIM DNSQR : DNS Question Record EAP : EAP IKE_proposal : IKE proposal >>>
Después explicaré cómo usar esta información
La función lsc()
lista todas las funciones disponibles (de Scapy):
>>> lsc() sr : Send and receive packets at layer 3 sr1 : Send packets at layer 3 and return only the first answer srp : Send and receive packets at layer 2 srp1 : Send and receive packets at layer 2 and return only the first answer srloop : Send a packet at layer 3 in loop and print the answer each time srploop : Send a packet at layer 2 in loop and print the answer each time sniff : Sniff packets p0f : Passive OS fingerprinting: which OS emitted this TCP SYN arpcachepoison : Poison target's cache with (your MAC,victim's IP) couple send : Send packets at layer 3 sendp : Send packets at layer 2 traceroute : Instant TCP traceroute arping : Send ARP who-has requests to determine which hosts are up ls : List available layers, or infos on a given layer lsc : List user commands queso : Queso OS fingerprinting nmap_fp : nmap fingerprinting report_ports : portscan a target and output a LaTeX table dyndns_add : Send a DNS add message to a nameserver for "name" to have a new "rdata" dyndns_del : Send a DNS delete message to a nameserver for "name" >>>
Otras funciones genéricas importantes:
- Net()
- IP, TCP, etc.
Estas funciones IP()
, ICMP()
, etc son muy interesantes3. Puedes verlas usando el comando ls()
y puedes usarlas para construir encabezados. Por ejemplo:
>>> ip = IP() >>> icmp = ICMP() >>> ip <IP |> >>> icmp <ICMP |> >>> ip.dst = "192.168.9.1" >>> icmp.display() ---[ ICMP ]--- type = echo-request code = 0 chksum = 0x0 id = 0x0 seq = 0x0 >>> sr1(ip/icmp) Begin emission: ...*Finished to send 1 packets. Received 4 packets, got 1 answers, remaining 0 packets <IP version=4L ihl=5L tos=0x0 len=28 id=16713 flags= frag=0L ttl=64 proto=ICMP chksum=0xa635 src=192.168.9.1 dst=192.168.9.17 options='' |<ICMP type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |<Padding load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|)\x0c\xa4' |>>> >>> _.display() ---[ IP ]--- version = 4L ihl = 5L tos = 0x0 len = 28 id = 16713 flags = frag = 0L ttl = 64 proto = ICMP chksum = 0xa635 src = 192.168.9.1 dst = 192.168.9.17 options = '' ---[ ICMP ]--- type = echo-reply code = 0 chksum = 0xffff id = 0x0 seq = 0x0 ---[ Padding ]--- load = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|)\x0c\xa4' >>>
Un paquete se puede crear de muchas formas. La más corta es algo como:
>>> p = IP(dst="192.168.9.1")/ICMP() >>> sr1(p) Begin emission: ...*Finished to send 1 packets. Received 4 packets, got 1 answers, remaining 0 packets <IP version=4L ihl=5L tos=0x0 len=28 id=16714 flags= frag=0L ttl=64 proto=ICMP chksum=0xa634 src=192.168.9.1 dst=192.168.9.17 options='' |<ICMP type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |<Padding load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16\x89\xdb\x88' |>>> >>>
Para encontrar qué campos puede tener cada protocolo, usa ls()
con un argumento:
>>> ls(TCP) sport : ShortField = (20) dport : ShortField = (80) seq : IntField = (0) ack : IntField = (0) dataofs : BitField = (None) reserved : BitField = (0) flags : FlagsField = (2) window : ShortField = (0) chksum : XShortField = (None) urgptr : ShortField = (0) options : TCPOptionsField = ({}) >>>
Así puedes asignar cualquiera de estos campos, también puedes ver cuales son sus valores por defecto. Por ejemplo, el puerto origen por defecto es 20, el puesto destino es 80.
Cuando se imprime un paquete solo se muestran los campos modficados. Como éste:
>>> i = IP() >>> i <IP |> >>> i.dst = "192.168.9.1" >>> i <IP dst=192.168.9.1 |> >>> i.src = "192.168.9.2" >>> del(i.dst) >>> i <IP src=192.168.9.2 |> >>>
Por supuesto, para mostrar todos los campos usa el método i.display()
. Me gusta mucho esto, puedes ver fácilmente lo que has modificado, no tienes que tratar con campos que no te interesan. Por ejemplo, yo no quiero ver qué opciones TCP están habilitadas, porque yo no uso opciones TCP en la mayoría de los ataques. Si las uso, entonces se muestran. Excelente.
También puedes usar ls()
para mostrar un paquete existente:
>>> ls(i) version : BitField = 4 (4) ihl : BitField = None (None) tos : XByteField = 0 (0) len : ShortField = None (None) id : ShortField = 1 (1) flags : FlagsField = 0 (0) frag : BitField = 0 (0) ttl : ByteField = 64 (64) proto : ByteEnumField = 0 (0) chksum : XShortField = None (None) src : SourceIPField = '192.168.9.2' (None) dst : IPField = '127.0.0.1' ('127.0.0.1') options : IPoptionsField = '' ('') >>>
Se muestra el valor por defecto, y el valor actual. Cuando construyes un paquete también puedes añadir una carga, como éste:
>>> p = IP(dst="192.168.9.1")/TCP(dport=22)/"AAAAAAAAAA" >>> p <IP proto=TCP dst=192.168.9.1 |<TCP dport=22 |<Raw load='AAAAAAAAAA' |>>> >>>
Para enviar paquetes en la capa 2, tienes que usar las funciones sendp
, srp
, srploop
y srp1
. La ‘p’ significa PF_PACKET, que es la interfaz de Linux que permite enviar paquetes en la capa 2.
Los paquetes están formados por cabeceras y el tipo de datos del paquete es una lista. Puedes comprobarlo usando la función type()
de Python.
Para ver el paquete crudo como una cadena puede ser útil entender la disección:
>>> packet = IP(dst="192.168.0.1")/TCP(dport=25) >>> raw_packet = str(packet) >>> type(raw_packet) <type 'str'> >>> IP(raw_packet) <IP version=4L ihl=5L tos=0x0 len=40 id=1 flags= frag=0L ttl=64 proto=TCP chksum=0xf36c src=192.168.6.17 dst=192.168.0.1 options='' |<TCP sport=20 dport=25 seq=0L ack=0L dataofs=5L reserved=16L flags=S window=0 chksum=0x2853 urgptr=0 |>> >>> TCP(raw_packet) <TCP sport=17664 dport=40 seq=65536L ack=1074197356L dataofs=12L reserved=0L flags=PUC window=1553 chksum=0xc0a8 urgptr=1 options=[] |> >>> dissected_tcp = TCP(raw_packet) >>> dissected_tcp <TCP sport=17664 dport=40 seq=65536L ack=1074197356L dataofs=12L reserved=0L flags=PUC window=1553 chksum=0xc0a8 urgptr=1 options=[] |> >>> raw_packet 'E\x00\x00(\x00\x01\x00\x00@\x06\xf3l\xc0\xa8\x06\x11\xc0\xa8\x00\x01\x00\x14\x00\x19\x00\x00\x00\x00\x00\x00\x00\x00P\x02\x00\x00(S\x00\x00' >>>
Construye tus propias herramientas con Scapy
Esta es la técnica de escaneo de puertos, que aquí aparece como un script, no interactivo:
detach@luna:~/lab/scapy-0.9.17$ cat pscan.py #!/usr/bin/env python import sys from scapy import * conf.verb=0 if len(sys.argv) != 2: print "Usage: ./pscan.py <target>" sys.exit(1) target=sys.argv[1] p=IP(dst=target)/TCP(dport=80, flags="S") ans,unans=sr(p, timeout=9) for a in ans: if a[1].flags == 2: print a[1].src
Vamos a probarla:
detach@luna:~/lab/scapy-0.9.17$ sudo ./pscan.py 192.168.9.0/24 192.168.9.1 192.168.9.2 192.168.9.11 192.168.9.14
¿Ves lo potente que es esto? A continuación construiré un programa tipo traceroute/firewalk que he tratado en Dealing with Firewalls. Lo que hacemos es jugar con el TTL y un puerto específico. De esta forma podemos ver si se usa NAT para los puertos redirigidos.
Lo que tenemos que hacer es:
- Detectar el TTL mínimo para alcanzar nuestro objetivo
- Encontrar un puerto para probar en nuestro host objetivo
- Descubrir si en ese puerto está escuchando el host objetivo o está NATeado.
Para esto necesitamos sr1()
ya que queremos enviar paquetes en un bucle hasta que tengamos una respuesta distinta de error ICMP. También necesitamos tener en cuenta el TTL actual. Entonces, ese TTL mínimo para alcanzar el host se consigue enviando un TCP SYN para un puerto específico. Si lo que conseguimos es un SYN/ACK (o quizá RST/ACK) asumimos que este puerto no está NATeado, de lo contrario sí lo está.
Bien, hagamos el programa para encontrar el TTL para alcanzar a nuestro objetivo:
$ sudo python ./scapy.py Welcome to Scapy (0.9.17.1beta) >>> ttl = 0 >>> def mkpacket(): ... global ttl ... ttl = ttl + 1 ... p = IP(dst="hackaholic.org", ttl=ttl)/ICMP() ... return p ... >>> res = sr1(mkpacket()) Begin emission: ...*Finished to send 1 packets. Received 4 packets, got 1 answers, remaining 0 packets >>> while res.type == 11: ... res = sr1(mkpacket()) ... Begin emission: .Finished to send 1 packets. * Received 2 packets, got 1 answers, remaining 0 packets Begin emission: .Finished to send 1 packets. * Received 2 packets, got 1 answers, remaining 0 packets Begin emission: .Finished to send 1 packets. * ****** Etcetera, >>> ttl 15 >>>
Esto significa que en el salto 15 hemos llegado al host, o que el TTL mínimo para llegar al host es 15. Fíjate en que el constructor de ICMP()
no requiere ningún parámetro porque por defecto es un icmp-echo-request. Si ICMP está bloqueado (algo que ocurre mucho hoy en día), puedes intentar con UDP o TCP. Pero recuerda que queremos averiguar la configuración del NAT, is usas TCP entonces utiliza un puerto cerrado. De otro modo, ¿cómo sabes si ese puerto está NATeado? (Nota: incluso los puertos cerrados puede estar NATeados).
Ahora que sabemos que la distancia es 15 podemos ver qué puertos están NATeados simplemente usando la misma técnica y viendo si hay diferencia en los TTLs que necesitamos.
Cambia el programa anterior para que use TCP()
en lugar de ICMP()
y deja que use el dport=80
. Al ejecutarlo probablemente fallará porque la última respuesta no fue ICMP, sino una respuesta TCP que no tiene el campo ‘type’. Pero eso no importa. Mira el valor del ‘ttl’ y si sigue siendo 15 (como es mi caso), el puerto no está NATeado.
Para hacerlo más automático, aquí está el script completo. Requiere los argumentos ‘host’ y ‘dport’:
Veamos cómo funciona:
$ sudo ./firewalk.py XX.XXX.XXX.XX 5900 + + ****** Etcetera This port is NATed. Firewall TTL is 10, TCP port TTL is 11 $ $ sudo ./firewalk.py google.com 80 + + ****** Etcetera Not NATed (16, 16) $
Esto es mucho más rápido que usar mi implementación de Hping3 (HTCL) :-D
Cuando este script detecta que un host está NATeado, es muy probable que así sea. Si no detecta que un puerto está redireccionado… eso no es una prueba. No es difícil engañar a esta técnica incrementando en uno el TTL de cada paquete entrante y de un puerto redireccionado. Aunque dudo que suceda habitualmente.
Lo siguiente que vamos a hacer es muy divertido. Probablemente, muchos no comprendáis porqué me parece tan excitante. Lo que vamos a hacer es crear una conexión TCP a un sistema local en la LAN desde una dirección IP ficticia. Esto significa que vamos a hacer spoofing de la conexión, sólo spoofing no-ciego desafortunadamente. El uso de esta técnica es únicamente educativo. Creo que es excitante porque esta teoría de TCP que aprendí hace mucho tiempo es el ejemplo más real de conexión TCP que jamás he visto. Creo que cualquier profesor que explique TCP/IP debería usarlo como un ejemplo en clase en lugar de toda esa teoría abstracta sobre el mecanismo de ventana deslizante y mierdas :-) 4.
Echa un vistazo a este script:
Cuando “spoofeas” tu dirección IP, debes asegurarte de que usas una dirección que está fuera de tu LAN, de otro modo, tu objetivo buscará la dirección MAC del emisor ficticio usando ARP. En nuestro caso él asume que los paquetes spoofeados vienen del enrutador y enviará las respuestas a la MAC del enrutador. Pero si necesitas usar una IP de tu LAN puede resolverlo poniendo el siguiente código después del envío “SYN”:
p = ARP() p.op = 2 p.hwsrc = "00:11:22:aa:bb:cc" p.psrc = spoofed_ip p.hwdst = "ff:ff:ff:ff:ff:ff" p.pdst = target send(p)
Esto es ARP poisoning. (Fíjate en que esto también podría ser útil si quieres spoofear una conexión desde una IP EXISTENTE, porque puedes mantener el envenenamiento de tu objetivo diciéndole que la dirección MAC ha cambiado; el host suplantado no podría responder porque las respuestas irían a una MAC inexistente. Eso implica que puede suplantar totalmente a un sistema conectado.
Vamos a probar:
$ sudo python ./spoof.py 192.168.9.14 123.123.123.123 22 Okay, SYN sent. Enter the sniffed sequence number now: 231823219 Okay, using sequence number 231823219 Okay, final ACK sent. Check netstat on your target :-) $
Ahora en mi objetivo ejecuto netstat
dos veces, antes y después de enviar el ACK.
tcp 0 0 devil.hengelo.gaast:ssh 123.123.123.123:5000 SYN_RECV tcp 0 0 devil.hengelo.gaast:ssh 123:123.123.123:5000 ESTABLISHED
¿Cómo es que funciona de todos modos? Bien, por supuesto funciona, poque la conexión TCP funciona. Estas son las reglas:
- El atacante envía al objetivo un segmento SYN con un ISN de 0 y número de reconocimiento también 0.
- El puerto del objetivo recibe un SYN, genera un número de secuencia y reconoce nuestro número de secuencia con seq+1, 0+1=1 y envía el segmento a la dirección IP “spoofeada”.
- Capturamos el paquete transmitido y escribimos en él el número de secuencia. Lo he capturado en el sistema objetivo, de otro modo, podría adaptar el script para capturar en la red y hacérselo saber. El número de secuencia se incrementa en 1 para convertirse en el número de reconocimiento. Este número es esencial en nuestro segmento ACK final para cambiar el estado TCP a ESTABLECIDO (sino la conexión estaría medio abierta).
Normalmente esto sería un problema de seguridad, como puedes ver, todo depende del número de secuencia generado en el lado del objetivo. Si pudiéramos predecir el número de secuencia que genera, podríamos explotar cualquier relación basada en la dirección y a veces incluso robar o terminar conexiones existentes. En el pasado, predecir el número de secuencia era trivial, pero hoy en día todos los sistemas operativos modernos generan ISN aleatorios decentes. Por supuesto, cualquier relación de confianza como esta que ocurra en una red local se podrá seguir capturando cuando sea necesario. Pero el ataque global está muerto. En especial, el secuestro de conexiónes ciegas es casi imposible. Necesitarías saber aún más, por ejemplo, si quieres terminar (RESET) una conexión existente.
- El SN de tu objetivo/víctima
- la tuplas destino/origen dirección/puerto
Sin embargo, algunos dispositivos modernos como enrutadores baratos y otro tipo de sistemas empotrados suelen tener pilas TCP/IP pobres. Dispositivos como cable-modems, modems DSL o puntos de acceso WLAN pueden ser vulnerables a ataques viejos. Yo tengo un punto de acceso US Robotics que exporta su tabla NAT a todo el mundo. Por ejemplo, http://ap/natlist.txt:
0) UDP 0.0.0.0:0 <-> 192.168.123.254:1212, out_port:60005, last_use:32 1) UDP 0.0.0.0:0 <-> 192.168.123.254:1211, out_port:60004, last_use:32 2) UDP 0.0.0.0:0 <-> 192.168.123.254:1210, out_port:60003, last_use:32 3) UDP 0.0.0.0:0 <-> 192.168.123.254:1209, out_port:60002, last_use:45 4) UDP 0.0.0.0:0 <-> 192.168.123.254:1207, out_port:60001, last_use:17
Horrible, ¿no? Sería trivial inyectar segmentos en una “conexión” UDP y mucho más fácil hacer ataques contra conexiones TCP ya que es fácil deducir los números de secuencia. Para terminar una conexión todo lo que necesitas es el número de secuencia de uno de los extremos de la conexión.
Pero en general, el spoofing ciego está muerto. Otras técnicas como la redirección de tráfico por medio de envenenamiento ARP, es decir, envenenamiento de tablas de conmutadores, son mucho más exitosas. Otra cosa muy efectiva es el spoofing DNS. Tal vez incluso ataques contra protocolos de enrutamiento, pero no los veo tan a menudo. Pero todo es posible usando Scapy.
Voy a explicar un último ejemplo de ataque de red. Esta vez haremos un envenenamiento DNS.
Primero, empezamos enviando una petición DNS. Lo intenté y me llevó un rato entender cómo funcionaba (era la primera vez que programaba un spoofer DNS). Lo que pasé por alto fue que DNS usa 0×03 para indicar el ‘.’ en “hackaholic.org”. Extraño.
Actualización: Philippe Biondi (autor de scapy) me envió un email explicándome el error que cometí aquí:
b1. Estás equivocado sobre DNS. Tú dices que se usa 0×3 en lugar de un punto. De hecho, DNS divide los nombres en listas de cadenas en los puntos, y añade la longitud antes de cada cadena o 0×80| (el desplazamiento del siguiente string). Suele ser 0×3 para la mayoría de los TLD, tal como ‘.org’.
En cualquier caso, Scapy dice esto sobre DNS:
>>> ls(DNS()) id : ShortField = 0 (0) qr : BitField = 0 (0) opcode : BitEnumField = 0 (0) aa : BitField = 0 (0) tc : BitField = 0 (0) rd : BitField = 0 (0) ra : BitField = 0 (0) z : BitField = 0 (0) rcode : BitEnumField = 0 (0) qdcount : DNSRRCountField = 0 (None) ancount : DNSRRCountField = 0 (None) nscount : DNSRRCountField = 0 (None) arcount : DNSRRCountField = 0 (None) qd : DNSQRField = None (None) an : DNSRRField = None (None) ns : DNSRRField = None (None) ar : DNSRRField = None (None) >>>
A la vista de la RFC-1035 veo que los siguientes campos son interesantes para enviar una consulta DNS:
- ID: Es un identificador de 16 bits que tu SO utiliza para distinguir entre consultas. De ese modo se puede saber con que consulta corresponde cada respuesta que llega (el ID de la respuesta es el mismo que el de la consulta)
- QR: Tipo de mensaje (0 significa pregunta, 1 significa respuesta)
- OPCODE: El tipo de consulta (4 bits). 0 significa consulta estándar, 1 es una consulta inversa, 2 es una petición de estado del servidor.
- QDCOUNT: Cuántas consultas contiene el mensaje (normalmente 1)
- QD: Campo de petición. Está formado a su vez por tres campos. Cada campo debe terminar con un byte NUL.
- QNAME: host/domainname (longitud variable), nota: reemplazar ‘.’ con 0×03. Por alguna razón, el QNAME debe empezar con un ‘\n’.
- QTYPE: Tipo de consulta (2 bytes). Fijado a 01
- QCLASS: Clase de consulta (2 bytes). Fijado a 01, Internet)
Vamos a hacerlo. Mi servidor de nombres local es 192.168.9.1. El protocolo de trasporte que uso es UDP:
>>> i = IP() >>> u = UDP() >>> d = DNS() >>> i.dst = "192.168.9.1" >>> u.dport = 53 >>> u.sport = 31337 >>> d.id = 31337 >>> d.qr = 0 >>> d.opcode = 0 >>> d.qdcount = 1 >>> d.qd = '\nhackaholic\x03org\x00\x00\x01\x00\x01' >>> packet = i/u/d >>> sr1(packet) Begin emission: ...*Finished to send 1 packets. Received 4 packets, got 1 answers, remaining 0 packets <IP version=4L ihl=5L tos=0x0 len=188 id=12111 flags=DF frag=0L ttl=64 proto= UDP chksum=0x777f src=192.168.9.1 dst=192.168.9.17 options='' |<UDP sport=53 dport=31337 len=168 chksum=0xab33 |<DNS id=31337 qr=1L opcode=16 aa=0L tc=0L rd=0L ra=1L z=8L rcode=ok qdcount=1 ancount=1 nscount=5 arcount=0 qd=<DNSQR q name='hackaholic.org.' qtype=A qclass=IN |> an=<DNSRR rrname='hackaholic.org. ' type=A rclass=IN ttl=661L rdata='24.132.169.84' |> ns=<DNSRR rrname='hackah olic.org.' type=NS rclass=IN ttl=1177L rdata='dns4.name-services.com.' |<DNSR R rrname='hackaholic.org.' type=NS rclass=IN ttl=1177L rdata='dns5.name-servi ces.com.' |<DNSRR rrname='hackaholic.org.' type=NS rclass=IN ttl=1177L rdata= 'dns1.name-services.com.' |<DNSRR rrname='hackaholic.org.' type=NS rclass=IN ttl=1177L rdata='dns2.name-services.com.' |<DNSRR rrname='hackaholic.org.' ty pe=NS rclass=IN ttl=1177L rdata='dns3.name-services.com.' |>>>>> ar=0 |<Paddi ng load='6g\xa3\xf8' |>>>> >>>
Ahora también puedes hacer esto:
>>> res =sr1(packet) Begin emission: .*Finished to send 1 packets. Received 2 packets, got 1 answers, remaining 0 packets >>> res.an.rdata '24.132.169.84' >>>
Chulo, ¿eh? Ahora que estamos seguros podemos intentar fabricar paquetes DNS.
Yo escribí un programa de spoofing DNS para este artículo que presupone el siguiente escenario:
Hay dos hosts, A y B, y un enrutador R. R es la pasarela a internet pero también es el servidor de nombres local. Nosotros somos el atacante en el host A, y el host B es nuestra víctima. Queremos conseguir que cualquier dirección que busque el host B sea resuelta con la dirección del host A. Si en el host B alguien arranca un navegador web y escribe una URL… cargará nuestra página del host A (por ejemplo un exploit para Internet Explorer para colarnos en el host B).
Antes de que empieces asegúrate de que tu host A tiene un navegador web. Puedes probar fijando por ejemplo ‘google.com’ con la dirección de A en /etc/hosts
, Windows (mi host objetivo) también tiene ese fichero (en %windir%\System32\Drivers\etc
IIRC).
Lo que hacemos es una técnica de envenenamiento DNS local (en la misma LAN). Asumimos las siguientes direcciones IP:
Host A: 192.168.123.100 Host B: 192.168.123.101 Host R: 192.168.123.254
Para “spoofear” DNS necesitamos construir una respuesta DNS que tenga sentido.. lo que significa que debe responder con el ID correcto a la petición correcta. Para lograrlo necesitamos capturar los paquetes DNS emitidos por B. El único modo de hacer esto es usar una técnica adicional para redirigir al host B el tráfico destinado a R. Lo haremos mediante envenenamiento ARP. Si enviamos un paquete ARP correctamente sintetizado antes de la consulta DNS, podremos capturar el paquete DNS y falsificar la respuesta. Generaremos una respuesta falsa con la dirección de A logrando que el navegador cargue y visualice nuestra página maliciosa!
Estudia y adapta el siguiente código:
Así es cómo funciona, antes de abrir cualquier página en el host B ejecuta algo como esto en el host A (asegurate de cambiar la variable mac_address
):
detach@luna:~/lab/scapy-0.9.17$ ./spoof.py Usage: ./spoof.py <dns_server> <victim> <impersonating_host> detach@luna:~/lab/scapy-0.9.17$ sudo ./spoof.py 192.168.123.254 192.168.123.101 192.168.123.100
Esto envenenará la cache ARP del host B (diciéndole que la MAC falsa del host R es la MAC real del host A) y entonces capturaré un paquete DNS. La información capturada se pasa a nuestra función mkdnsresponse()
que generará la respuesta DNS. ¡Un spoofer DNS funcional en menos de 100 líneas de código!
Vamos a intentarlo:
detach@luna:~/lab/scapy-0.9.17$ sudo ./spoof.py 192.168.123.254 192.168.123.101 192.168.123.100 WARNING: No IP underlayer to compute checksum. Leaving null. . Sent 1 packets. ---[ DNS ]--- id = 140 qr = 1 opcode = 16 aa = 0 tc = 0 rd = 0 ra = 1 z = 8 rcode = ok qdcount = 1 ancount = 1 nscount = 0 arcount = 0 qd = '\x05start\x07mozilla\x03org\x00\x00\x01\x00\x01' an = '\x05start\x07mozilla\x03org\x00\x00\x01\x00\x01\x00\x00\x07u\x00\x04\xc0\xa8{d' ns = 0 ar = 0 . Sent 1 packets. detach@luna:~/lab/scapy-0.9.17$
El paquete que se muestra es la respuesta spoofeada… y puedes ver que la dirección start.mozilla.org está spoofeada.
Debo decir que no he leído mucho sobre el protocolo DNS para programar esto. La mayoría de lo que he aprendido ha sido de Scapy y Ethereal.
Actualización: Una persona bondadosa me apunta que en la presentación de Philippe usa la función DNSRR()
de scapy para sintetizar respuestas DNS, que por supuesto es una función más sencilla de alto nivel. Así que deberías cambiar la función mkdnsresponse()
por algo como esto:
def mkdnsresponse(dr, malhost): d = DNS() d.id = dr.id d.qd = dr.qd d.qdcount = 1 d.qr = 1 d.opcode = 16 d.an = DNSRR(rrname=dr.qd.qname, ttl=10, rdata=malhost) return d
Otra cosa que se podría hacer más sencilla es la disección. Yo usé el método de bajo nivel ip = IP(str(packet)+len(Ether())
, pero podría haber usado:
ip = packet.getlayer(IP)
:-). Perdón por ello.
Actualización 2: Philippe Biondi (autor of scapy) menciona también en su email el uso de los mecanismos de respuesta de Scapy para spoofing DNS. Visita su website
Así que debería reemplazarlo con:
i = p[0].getlayer(IP) u = p[0].getlayer(UDP) d = p[0].getlayer(DNS)
Si tienes cualquier pregunta escríbeme (en inglés) a detach@REMOVEUPPERCASEhackaholic.org
2 (N. de T.) En sistemas Debian, scapy es un paquete oficial y se puede ejecutar simplemente escribiendo ‘scapy’ en un terminal.
3 (N. de T.) En realidad no son funciones, son clases.
4 (N. de T.) Se nota que este buen hombre no ha estudiado en la UCLM. Ya me gustaría a mi poder contar esto en clase y lograr que alguien se enterara.