We’ve been able to work with Ethernet, ARP, IP, ICMP, and TCP pretty easily so far thanks to Scapy’s built in protocol support. Next on our list of protocols to work with are UDP and DNS.

DNS Request and Response

Using the sr1() function, we can craft a DNS request and capture the returned DNS response. Since DNS runs over IP and UDP, we will need to use those in our packet:



The DNS layer summary is printed showing the IP address of the hostname requested

Without too much work we were able to write a short script to query some DNS name to IP address resolutions. Just think about what this means; we could read in a list of hostnames to resolve and send the IP addresses off to some function to do some more tests such as ping or TCP scans.

DNS Forwarding and Spoofing

Ok, so sending a DNS query was fun, but let’s build on that. How about hand building a DNS service that can handle DNS forwarding, but with the added functionality of handing out a custom IP address for a certain domain name. This is similar to how the DNS server part of the PlexConnect utility works to hijack some communications from the AppleTV. This is going to jump up in complexity quite a bit from our previous example but the process is still pretty simple:

  • Sniff with Scapy to listen for incoming DNS requests
    • Filtering for UDP port 53 destined to the server’s IP address
  • If the request is for our special domain name, send a spoofed DNS response
    • Swap source/dest UDP ports and IP addresses
    • Match DNS request ID
  • Otherwise, make a new DNS request and send the response back to the requesting host
    • Make a new request, and save the DNS Response
    • Send a response back to the client matching the same fields as above

We’re doing a lot of field replacements, especially on the handcrafted spoof response. All I did to figure out all those fields is capture a DNS response from a request I made and used the show() function to figure out what fields are expected in the DNS response.

With this running on a host, I used the DNS dig utility to make some DNS requests:


This example uses a lot of Python, so if you’re not familiar with that take some time to look at the code and look up anything you don’t know in the Python Docs.


Series Navigation<< Scapy p.08 – Making a Christmas Tree PacketScapy p.10 – Emulating nmap Functions >>

This post was originally published on

This article has 7 comments

  1. Pingback: Antonio Herraiz Sousa | Reverse Engineering a Custom Protocol and Performing MITM Packet Filtering with Scapy

  2. Jim M

    Hi. I love this walk through, just one question though. where is the pkt in getResponse(pkt) comming from? When you call it you just call getResponse without passing anything.


    1. Mat

      Hi Jim, I’m glad you like the post!

      I can see where the confusion is, I probably should add more comments to explain but I have another post that should help you. Check the example in this post out. In the sniff() function, the prn parameter receives the function object of customAction(). Then, when the sniff() function calls the prn function, it passes in the sniffed packet. This is passed in as the packet argument in the customAction() function.

      So, back to this example, the getResponse function is returned to sniff() as the prn function, which will pass in each sniffed packet (pkt).

      Did that help? If not let me know and I can try to explain further!

  3. Tomy

    Hi. I was wondering if we have to use ARP spoofing with this script.

    Indeed, why should the DNS query come to the attacker machine in a local network.
    I tried, and the query goes straight to the router (and then to the resolver). The attacker’s computer don’t see that query.

    So shouldn’t I use ARP spoofing? I’ve seen other tutorials, and they don’t mention it as needed.
    I tried using ARP spoofing: when the client send a DNS query using dig, it works (the query goes straight to the attacker’s computer). But if I tried using the browser, this doesn’t work anymore. (the spoofed response is sent though)

    1. Mat

      The send() function is the layer 3 version of the sendp() function, so send() will handle the ARP destination logic. If the destination IP is in the same subnet as the computer running the python script, the TCP/IP stack will look up the MAC address in the ARP table or send an ARP request to resolve the MAC to IP address. That means the DNS host and the client must be in the same subnet. Otherwise, if the client is not in the same subnet, the DNS host will send the packet to its gateway in order to route the packet to another subnet. 

      But now that I look back through the script, I think IP source spoofing is not setup correctly and that’s the important address that the client is looking for. If the source IP of the DNS response is not the one the client originally sent the query to, it will not process the DNS response.

      I will look over and test this script again.  Thanks for your feedback!

      1. Tomy

        Indeed, with dig, we get the warning :

        ;; reply from unexpected source: IP_ATTACKER#53, expected IP_RESOLVER#53
        ;; Warning: query response not set

        where IP_ATTACKER is the IP address of the computer running the script, and IP_RESOLVER the IP address of the resolver.

        I fixed it l.16 :
        spfResp = IP(dst=pkt[IP].src)\   —–>   spfResp = IP(dst=pkt[IP].src, src=pkt[IP].dst)\

        If I remember, this is the only fix needed, and everything worked with the dig command afterward.

        But the thing that troubles me doesn’t concern send(), but sniff().
        Imagine I have a local network, with a client, an attacker and my router (gateway).
        All the DNS queries generated by the client won’t go the the attacker computer, and the sniff() function won’t see anything.
        These queries will go straight to the router.

        So, when I was talking about ARP spoofing, it was to do a MITM between the client and the router, forcing the DNS traffic to go through the attacker computer. (so the sniff() function will see it)

        Am I wrong? :-/

        1. Tomy

          Moreover (I apologize for this second post), several things seem wrong in your forged response.

          1. First, you didn’t put the IP src (as mentioned above).

          2. You also create 2 DNSRR and 0 DNSQR.
          dig still works, but it showed you a warning:
          “WARNING: Messages has 34 extra bytes at end”

          3. You didnt set the QR bit, though scapy should do this by itself (and it did on your exemple).
          But on my computer, it didn’t set it, and I got the warning:
          “;; Warning: query response not set”
          and the qr bit wasn’t set in the flags section.

          Here’s the forged response that should be sent:

                          spfResp = IP(dst=pkt[IP].src, src=pkt[IP].dst)\
                              /UDP(dport=pkt[UDP].sport, sport=53)\
                              /DNS(id=pkt[DNS].id, \
                                        qd=DNSQR(qname=pkt[DNSQR].qname), \
                                        an=DNSRR(rrname=”trailers.apple.com”,rdata=localIP) \

          I think there is no mistake in that one. 🙂

Leave a Reply

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