C²S Consulting logo
C²S Consulting | Primers | Scapy

Deep dive into networking through Scapy

Last updated: 08-02-2021 12:24

Scapy VM (lubuntu)......

............

Scapy VM (xubuntu)......

............

............

Create a Scapy VM......

............

DNS Lookup Script......

  1. Python Scapy
  2. Install scapy
  3. Run the scapy interactive shell
  4. Address Resolution Protocol (ARP)
  5. Ethernet frames
  6. Create IP packet
  7. Segment headers at layer 4
  8. Create a combined frame with packet and segment
  9. Sending packets
  10. Domain Name System (DNS)
  11. Access an application
  12. A final simple browser program

1. Python Scapy

Scapy is a powerful interactive packet manipulation program and python module. It has an interactive shell through iPython. It can be used to build or decode packets, send them to the wire and trap responses. This tutorial is an introduction to scapy used to explain computer networking beyond the superficial level. On computer networking courses it is common to use tools like tcpdump, wireshark and tshark to allow students understand what passes on the wire between hosts. This tutorial takes this a step further and allows the student to build frames and packets, put them on the wire and observe the responses. The diagram maps the Open System Interconnection (OSI) 7 layer and Transmission Control Protocol/Internet Protocol (TCP/IP), Department of Defence (DoD), models to functions in scapy used to build frames and packets. As you can see these function names match the protocols expected at these layers.

2. Install scapy

This tutorial was developed on a GNU/Linux Ubuntu 18.04 platform and using Python3; however, as scapy is Python based it can be adapted for most platforms. Having said that it is probably best exercised on a GNU/Linux Virtual Machine (VM).

Installation of scapy on GNU/Linux is as simple as:

  ~$ sudo apt-get install texlive
  ~$ pip3 install --pre scapy[complete]
  ~$ pip3 install scapy-http
  

3. Run the scapy interactive shell

As the tutorial uses Python3, the version of scapy matches, so launch scapy3.

  ~$ scapy3
                                        
                       aSPY//YASa       
               apyyyyCY//////////YCa       |
              sY//////YSpcs  scpCY//Pp     | Welcome to Scapy
   ayp ayyyyyyySCP//Pp           syY//C    | Version 2.4.3
   AYAsAYYYYYYYY///Ps              cY//S   |
           pCCCCY//p          cSSps y//Y   | https://github.com/secdev/scapy
           SPPPP///a          pP///AC//Y   |
                A//A            cyP////C   | Have fun!
                p///Ac            sC///a   |
                P////YCpc           A//A   | Wanna support scapy? Rate it on
         scccccp///pSP///p          p//Y   | sectools!
        sY/////////y  caa           S//P   | http://sectools.org/tool/scapy/
         cayCyayP//Ya              pY/Ya   |             -- Satoshi Nakamoto
          sY/PsY////YCc          aC//Yp    |
           sc  sccaCY//PCypaapyCP//YSs  
                    spCPY//////YPSps    
                         ccaacs         
                                         using IPython 7.14.0
  >>>                  
  

4. Address Resolution Protocol (ARP)

Start the tutorial with a simple ARP request for a known IP address on the same network.

Creating a basic Ethernet frame at layer 2 and review.

  >>> my_frame = Ether()
  
  
  >>> my_frame.show()                                                                                          
  WARNING: Mac address to reach destination not found. Using broadcast.
  ###[ Ethernet ]### 
    dst= ff:ff:ff:ff:ff:ff
    src= 74:d8:3e:54:ca:22
    type= 0x9000
  

Now create a layer 2 ARP request for IP address 192.168.1.3 and review.

  
  >>> my_arp = ARP(pdst='192.168.1.3') 
  
  
  >>> my_arp.show()                                                                                              
  ###[ ARP ]### 
    hwtype= 0x1
    ptype= IPv4
    hwlen= None
    plen= None
    op= who-has
    hwsrc= 74:d8:3e:54:ca:22
    psrc= 192.168.1.7
    hwdst= 00:00:00:00:00:00
    pdst= 192.168.1.3
  

Bring the two together in a frame.

  >>> arp_req = my_frame/my_arp
  

Review the new loaded frame.

  >>> arp_req.summary()                                                                                          
  'Ether / ARP who has 192.168.1.3 says 192.168.1.7'
  
  
  >>> arp_req.show()                                                                                             
  ###[ Ethernet ]### 
    dst= 34:e6:ad:05:51:bd
    src= 74:d8:3e:54:ca:22
    type= ARP
  ###[ ARP ]### 
       hwtype= 0x1
       ptype= IPv4
       hwlen= None
       plen= None
       op= who-has
       hwsrc= 74:d8:3e:54:ca:22
       psrc= 192.168.1.7
       hwdst= 00:00:00:00:00:00
       pdst= 192.168.1.3
  

Send the frame on the wire and trap the response. To send layer 2 frames the sendp() function is employed. Review the help on the sendp function first.

  >>> help(sendp)
  
  Help on function sendp in module scapy.sendrecv:
  
  sendp(x, inter=0, loop=0, iface=None, iface_hint=None, count=None, verbose=None, realtime=None, return_packets=False, socket=None, *args, **kargs)
      Send packets at layer 2
      sendp(packets, [inter=0], [loop=0], [iface=None], [iface_hint=None], [count=None], [verbose=conf.verb],  # noqa: E501
            [realtime=None], [return_packets=False], [socket=None]) -> None
  

Send the frame on the wire, but in another terminal setup tcpdump to capture the frame on the wire.

  
  ~$ sudo tcpdump -n -i wlp82s0 arp
  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
  listening on wlp82s0, link-type EN10MB (Ethernet), capture size 262144 bytes
  
  

Now send the ARP query.

  >>> sendp(arp_req)                                                                                             
  .
  Sent 1 packets.
  

Review what happened in the tcpdump window.

  08:34:13.549255 ARP, Request who-has 192.168.1.3 tell 192.168.1.7, length 28
  08:34:13.551500 ARP, Reply 192.168.1.3 is-at 34:e6:ad:05:51:bd, length 28
  

Repeat the exercise but this time trap the output to a pcap file (arp_req.pcap) for later use. Use Control-C to stop when the frames have been sent.

  ~$ sudo tcpdump -w arp_req.pcap -n -i wlp82s0 arp
  tcpdump: listening on wlp82s0, link-type EN10MB (Ethernet), capture size 262144 bytes
  
  
  >>> sendp(arp_req)                                                                                             
  .
  Sent 1 packets.
  
  
  ^C4 packets captured
  4 packets received by filter
  0 packets dropped by kernel
  

Open the arp_req.pcap with wireshark to review. Run wireshark and File => Open:

4.1. Notes

While the tutorial has described multiple steps, building first an Ethernet frame, then ARP data at layer 2, it could have been made in a single step as follows:

  >>> arp_req = Ether()/ARP(pdst='192.168.1.3')                                                                  
  >>> arp_req.summary()                                                                                          
  'Ether / ARP who has 192.168.1.3 says 192.168.1.7'
  

or even as an empty shell of a frame and the detail added, or edited, later using a field name and value.

  >>> arp_req = Ether()/ARP()                                                                                    
  >>> arp_req.summary()                                                                                          
  'Ether / ARP who has 0.0.0.0 says 192.168.1.7'
  
  
  >>> arp_req[ARP].pdst = "192.168.1.3"                                                                          
  >>> arp_req.summary()                                                                                          
  'Ether / ARP who has 192.168.1.3 says 192.168.1.7'
  

5. Ethernet frames

After the ARP introduction the tutorial moves to focus on Ethernet frames at layer 2. Create an Ethernet frame as follows:

  >>> Ether(dst='74:d8:3e:55:cb:24', type=0x0800)
  <Ether  dst=74:d8:3e:55:cb:24 type=IPv4 |>
  

Assign the Ethernet frame to the variable my_frame.

  >>> my_frame = Ether(dst='74:d8:3e:55:cb:24', type=0x0800)
  
  
  >>> my_frame
  <Ether  dst=74:d8:3e:55:cb:24 type=IPv4 |>
  
  
  >>> ls(my_frame) 
  dst        : DestMACField                        = '74:d8:3e:55:cb:24' (None)
  src        : SourceMACField                      = '74:d8:3e:54:ca:22' (None)
  type       : XShortEnumField                     = 2048            (36864)
  

This creates a special Python scapy, Layer2, Ethernet variable type.

  >>> type(my_frame)                          
  scapy.layers.l2.Ether
  

Analyse the frame further.

  >>> my_frame.summary()                                                         
  '74:d8:3e:54:ca:22 > 74:d8:3e:55:cb:24 (IPv4)'
  
  
  >>> my_frame.show()
  ###[ Ethernet ]### 
    dst= 74:d8:3e:55:cb:24
    src= 74:d8:3e:54:ca:22
    type= IPv4
  
  
  >>> my_frame.show2() 
  ###[ Ethernet ]### 
    dst= 74:d8:3e:55:cb:24
    src= 74:d8:3e:54:ca:22
    type= IPv4
  
  
  >>> raw(my_frame)
  b't\xd8>U\xcb$t\xd8>T\xca"\x08\x00'
  
  
  >>> hexdump(my_frame)
  0000  74 D8 3E 55 CB 24 74 D8 3E 54 CA 22 08 00        t.>U.$t.>T."..
  >>>                                                                         
  

Get a visual display of the frame from scapy.

  
  >>> my_frame.pdfdump()    
  

6. Create IP packet

Moving to layer 3, create an IP packet as follows:

  >>> IP(dst='192.168.1.1')                                                                                            
  <IP  dst=192.168.1.1 |>
  

Assign it to a variable.

  >>> my_packet = IP(dst='192.168.1.1')
  

What type is the packet? Another special variable type for IP packets.

  >>> type(my_packet)                                                                                                  
  scapy.layers.inet.IP
  

Now analyse the packet in a similar way as was carried out on the frame earlier.

  >>> ls(my_packet)
  version    : BitField (4 bits)                   = 4               (4)
  ihl        : BitField (4 bits)                   = None            (None)
  tos        : XByteField                          = 0               (0)
  len        : ShortField                          = None            (None)
  id         : ShortField                          = 1               (1)
  flags      : FlagsField (3 bits)                 = <Flag 0 ()>     (<Flag 0 ()>)
  frag       : BitField (13 bits)                  = 0               (0)
  ttl        : ByteField                           = 64              (64)
  proto      : ByteEnumField                       = 0               (0)
  chksum     : XShortField                         = None            (None)
  src        : SourceIPField                       = '192.168.1.7'   (None)
  dst        : DestIPField                         = '192.168.1.1'   (None)
  options    : PacketListField                     = []              ([])
  
  
  >>> my_packet.summary()                                                         
  '192.168.1.7 > 192.168.1.1 hopopt'
  
  
  >>> my_packet.show()                                                                                                 
  ###[ IP ]### 
    version= 4
    ihl= None
    tos= 0x0
    len= None
    id= 1
    flags= 
    frag= 0
    ttl= 64
    proto= hopopt
    chksum= None
    src= 192.168.1.7
    dst= 192.168.1.1
    \options\
  
  >>> raw(my_packet)                                                                                                   
  b'E\x00\x00\x14\x00\x01\x00\x00@\x00\xf7\x90\xc0\xa8\x01\x07\xc0\xa8\x01\x01'
  
  >>> hexdump(my_packet) 
  0000  45 00 00 14 00 01 00 00 40 00 F7 90 C0 A8 01 07  E.......@.......
  0010  C0 A8 01 01 
  

7. Segment headers at layer 4

7.1. Create a TCP segment

Now moving further up the DoD model, create a TCP segment at layer 4, review it. Again there is a special Python scapy variable type for TCP segments.

  >>> TCP(dport=80)/"GET 192.168.1.3 HTTP/1.0 \n\n"                               
  <TCP  dport=http |<Raw  load='GET 192.168.1.3 HTTP/1.0 \n\n' |>>
  
  
  >>> my_tcp = TCP(dport=80)/"GET 192.168.1.3 HTTP/1.0 \n\n"                                                              
  
  
  >>> type(my_tcp)                                                                                                     
  scapy.layers.inet.TCP
  

Analyse the TCP segment.

  >>> my_tcp                                                                                                           
  <TCP  dport=http |<Raw  load='GET 192.168.1.3 HTTP/1.0 \n\n' |>>
  
  
  >>> ls(my_tcp)                                                                                                       
  sport      : ShortEnumField                      = 20              (20)
  dport      : ShortEnumField                      = 80              (80)
  seq        : IntField                            = 0               (0)
  ack        : IntField                            = 0               (0)
  dataofs    : BitField (4 bits)                   = None            (None)
  reserved   : BitField (3 bits)                   = 0               (0)
  flags      : FlagsField (9 bits)                 = <Flag 2 (S)>    (<Flag 2 (S)>)
  window     : ShortField                          = 8192            (8192)
  chksum     : XShortField                         = None            (None)
  urgptr     : ShortField                          = 0               (0)
  options    : TCPOptionsField                     = []              (b'')
  --
  load       : StrField                            = b'GET 192.168.1.3 HTTP/1.0 \n\n' (b'')
  
  
  >>> my_tcp.summary()                                                            
  'TCP ftp_data > http S / Raw'
  
  
  >>>  my_tcp.show()                                                              
  ###[ TCP ]### 
    sport= ftp_data
    dport= http
    seq= 0
    ack= 0
    dataofs= None
    reserved= 0
    flags= S
    window= 8192
    chksum= None
    urgptr= 0
    options= []
  ###[ Raw ]### 
       load= 'GET 192.168.1.3 HTTP/1.0 \n\n'
  
  
  >>> raw(my_tcp)                                                                                                      
  WARNING: No IP underlayer to compute checksum. Leaving null.
  b'\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x00\x00\x00\x00GET 192.168.1.3 HTTP/1.0 \n\n'
  
  
  >>> hexdump(my_tcp)                                                                                                  
  WARNING: No IP underlayer to compute checksum. Leaving null.
  0000  00 14 00 50 00 00 00 00 00 00 00 00 50 02 20 00  ...P........P. .
  0010  00 00 00 00 47 45 54 20 31 39 32 2E 31 36 38 2E  ....GET 192.168.
  0020  31 2E 33 20 48 54 54 50 2F 31 2E 30 20 0A 0A     1.3 HTTP/1.0 ..
                                            ..
  

7.2. Creating User Datagram Protocol (UDP) segments

In a similar way create a UDP segment and analyse it.

  
  >>> UDP(dport=1087)                                                                                                  
  <UDP  dport=1087 |>
  
  
  >>> my_udp = UDP(dport=1087)                                                                                         
  
  
  >>> ls(my_udp)                                                                                                       
  sport      : ShortEnumField                      = 53              (53)
  dport      : ShortEnumField                      = 1087            (53)
  len        : ShortField                          = None            (None)
  chksum     : XShortField                         = None            (None)
  
  
  >>> my_udp.summary()                                                            
  'UDP domain > 1087'
  
  
  >>> my_udp.show()                                                                                                    
  ###[ UDP ]### 
    sport= domain
    dport= 1087
    len= None
    chksum= None
  
  
  >>> raw(my_udp)                                                                                                      
  WARNING: No IP underlayer to compute checksum. Leaving null.
  b'\x005\x04?\x00\x08\x00\x00'
  
  
  >>> hexdump(my_udp)                                                                                                  
  WARNING: No IP underlayer to compute checksum. Leaving null.
  0000  00 35 04 3F 00 08 00 00                          .5.?....
  

8. Create a combined frame with packet and segment

This section of the tutorial will consider the Ethernet frame, embedding the IP header within it, the TCP segment header within that to create a complete unit.

Embed the TCP segment header in the IP packet and embed the packet in the Ethernet frame and analyse.

  >>> my_frame/my_packet/my_tcp
  <Ether  dst=74:d8:3e:55:cb:24 type=IPv4 |<IP  frag=0 proto=tcp dst=192.168.1.1 |<TCP  dport=http |<Raw  load='GET 192.168.1.3 HTTP/1.0 \n\n' |>>>>
  
  
  >>> full_frame=my_frame/my_packet/my_tcp 
  
  
  >>> ls(full_frame)                                                                                                   
  dst        : DestMACField                        = '74:d8:3e:55:cb:24' (None)
  src        : SourceMACField                      = '74:d8:3e:54:ca:22' (None)
  type       : XShortEnumField                     = 2048            (36864)
  --
  version    : BitField (4 bits)                   = 4               (4)
  ihl        : BitField (4 bits)                   = None            (None)
  tos        : XByteField                          = 0               (0)
  len        : ShortField                          = None            (None)
  id         : ShortField                          = 1               (1)
  flags      : FlagsField (3 bits)                 = <Flag 0 ()>     (<Flag 0 ()>)
  frag       : BitField (13 bits)                  = 0               (0)
  ttl        : ByteField                           = 64              (64)
  proto      : ByteEnumField                       = 6               (0)
  chksum     : XShortField                         = None            (None)
  src        : SourceIPField                       = '192.168.1.7'   (None)
  dst        : DestIPField                         = '192.168.1.1'   (None)
  options    : PacketListField                     = []              ([])
  --
  sport      : ShortEnumField                      = 20              (20)
  dport      : ShortEnumField                      = 80              (80)
  seq        : IntField                            = 0               (0)
  ack        : IntField                            = 0               (0)
  dataofs    : BitField (4 bits)                   = None            (None)
  reserved   : BitField (3 bits)                   = 0               (0)
  flags      : FlagsField (9 bits)                 = <Flag 2 (S)>    (<Flag 2 (S)>)
  window     : ShortField                          = 8192            (8192)
  chksum     : XShortField                         = None            (None)
  urgptr     : ShortField                          = 0               (0)
  options    : TCPOptionsField                     = []              (b'')
  --
  load       : StrField                            = b'GET 192.168.1.3 HTTP/1.0 \n\n' (b'')
  
  
  >>> full_frame.summary()                                                        
  'Ether / IP / TCP 192.168.1.7:ftp_data > 192.168.1.1:http S / Raw'
  
  
  >>> full_frame.show()                                                                                                
  ###[ Ethernet ]### 
    dst= 74:d8:3e:55:cb:24
    src= 74:d8:3e:54:ca:22
    type= IPv4
  ###[ IP ]### 
       version= 4
       ihl= None
       tos= 0x0
       len= None
       id= 1
       flags= 
       frag= 0
       ttl= 64
       proto= tcp
       chksum= None
       src= 192.168.1.7
       dst= 192.168.1.1
       \options\
  ###[ TCP ]### 
          sport= ftp_data
          dport= http
          seq= 0
          ack= 0
          dataofs= None
          reserved= 0
          flags= S
          window= 8192
          chksum= None
          urgptr= 0
          options= []
  ###[ Raw ]### 
             load= 'GET 192.168.1.3 HTTP/1.0 \n\n'
  
  
  >>> raw(full_frame)                                                                                                  
  b't\xd8>U\xcb$t\xd8>T\xca"\x08\x00E\x00\x00C\x00\x01\x00\x00@\x06\xf7[\xc0\xa8\x01\x07\xc0\xa8\x01\x01\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x1b{\x00\x00GET 192.168.1.3 HTTP/1.0 \n\n'
  
  
  >>> hexdump(full_frame)                                                                                              
  0000  74 D8 3E 55 CB 24 74 D8 3E 54 CA 22 08 00 45 00  t.>U.$t.>T."..E.
  0010  00 43 00 01 00 00 40 06 F7 5B C0 A8 01 07 C0 A8  .C....@..[......
  0020  01 01 00 14 00 50 00 00 00 00 00 00 00 00 50 02  .....P........P.
  0030  20 00 1B 7B 00 00 47 45 54 20 31 39 32 2E 31 36   ..{..GET 192.16
  0040  38 2E 31 2E 33 20 48 54 54 50 2F 31 2E 30 20 0A  8.1.3 HTTP/1.0 .
  0050  0A   
  

Dump a graphical view of the full frame.

  >>> full_frame.pdfdump()   
  

9. Sending packets

To send a packet to the wire a different set of functions, than the sendp() function used earlier, is required. sendp() is only used at layer 2. Look at the help messages for the send(), sr() and sr1() functions to understand the difference between them.

  >>> help(send)
  Help on function send in module scapy.sendrecv:
  
  send(x, inter=0, loop=0, count=None, verbose=None, realtime=None, return_packets=False, socket=None, *args, **kargs)
      Send packets at layer 3
      send(packets, [inter=0], [loop=0], [count=None], [verbose=conf.verb], [realtime=None], [return_packets=False],  # noqa: E501
           [socket=None]) -> None
  
  
  >>> help(sr)
  Help on function sr in module scapy.sendrecv:
  
  sr(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs)
      Send and receive packets at layer 3
      pks: SuperSocket instance to send/receive packets
      pkt: the packet to send
      rcv_pks: if set, will be used instead of pks to receive packets.
               packets will still be sent through pks
      nofilter: put 1 to avoid use of BPF filters
      retry:    if positive, how many times to resend unanswered packets
                if negative, how many times to retry when no more packets
                are answered
      timeout:  how much time to wait after the last packet has been sent
      verbose:  set verbosity level
      multi:    whether to accept multiple answers for the same stimulus
      store_unanswered: whether to store not-answered packets or not.
                        setting it to False will increase speed, and will return
                        None as the unans list.
      process:  if specified, only result from process(pkt) will be stored.
                the function should follow the following format:
                  lambda sent, received: (func(sent), func2(received))
                if the packet is unanswered, `received` will be None.
                if `store_unanswered` is False, the function won't be called on
                un-answered packets.
      prebuild: pre-build the packets before starting to send them. Automatically
                enabled when a generator is passed as the packet
  
  
  
  >>> help(sr1)
  Help on function sr1 in module scapy.sendrecv:
  
  sr1(x, promisc=None, filter=None, iface=None, nofilter=0, *args, **kargs)
      Send packets at layer 3 and return only the first answer
      pks: SuperSocket instance to send/receive packets
      pkt: the packet to send
      rcv_pks: if set, will be used instead of pks to receive packets.
               packets will still be sent through pks
      nofilter: put 1 to avoid use of BPF filters
      retry:    if positive, how many times to resend unanswered packets
                if negative, how many times to retry when no more packets
                are answered
      timeout:  how much time to wait after the last packet has been sent
      verbose:  set verbosity level
      multi:    whether to accept multiple answers for the same stimulus
      store_unanswered: whether to store not-answered packets or not.
                        setting it to False will increase speed, and will return
                        None as the unans list.
      process:  if specified, only result from process(pkt) will be stored.
                the function should follow the following format:
                  lambda sent, received: (func(sent), func2(received))
                if the packet is unanswered, `received` will be None.
                if `store_unanswered` is False, the function won't be called on
                un-answered packets.
      prebuild: pre-build the packets before starting to send them. Automatically
                enabled when a generator is passed as the packet
  

In summary:

Function Application
sendp() Send frames at layer 2 only
send() Send packets at layer 3, do not wait for reply
sr() Send and receive packets at layer 3
sr1() Send packets at layer 3 and return only the first answer

9.1. Create and send an ICMP request

Create a simple Internet Control Message Protocol (ICMP) request to send to the other computer.

  >>> my_icmp = IP(dst="192.168.1.3")/ICMP()
  
  
  >>> my_icmp.summary()                                                           
  'IP / ICMP 192.168.1.7 > 192.168.1.3 echo-request 0'
  
  
  >>> my_icmp.show()                                                                                                                              
  ###[ IP ]### 
    version= 4
    ihl= None
    tos= 0x0
    len= None
    id= 1
    flags= 
    frag= 0
    ttl= 64
    proto= icmp
    chksum= None
    src= 192.168.1.7
    dst= 192.168.1.3
    \options\
  ###[ ICMP ]### 
       type= echo-request
       code= 0
       chksum= None
       id= 0x0
       seq= 0x0
  

Try sending a packet on the wire with the send() function.

  >>> send(my_icmp)                                                                                                                                 
  PermissionError: [Errno 1] Operation not permitted
  

Ooops this results in a permissions error. The user doesn't have the right to access the interfaces directly. The Linux kernel will not allow access to raw sockets as the CAP_NET_RAW capability is required, which is normally only granted to the superuser. Run scapy3 as a superuser and try again.

  ~$ sudo scapy3
  >>> send(my_icmp)                                                                                                                                                                  
  .
  Sent 1 packets.
  

So what happened, I guess an ICMP packet was sent on the wire. Do it again but have tcpdump, tshark or wireshark monitoring for the packet this time. For example:

  ~$ sudo tcpdump -n -i wlp82s0 dst net 192.168.1.0/24 and icmp 
  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
  

Now send the packet again.

  >>> send(my_icmp)                                                                                                                                 
  .
  Sent 1 packets.
  

Notice tcpdump capturing the outgoing echo request as well as the returning echo reply.

  listening on wlp82s0, link-type EN10MB (Ethernet), capture size 262144 bytes
  17:33:29.908234 IP 192.168.1.7 > 192.168.1.3: ICMP echo request, id 0, seq 0, length 8
  17:33:29.911709 IP 192.168.1.3 > 192.168.1.7: ICMP echo reply, id 0, seq 0, length 8
  

9.2. Capture the reply

The reply can be captured directly by scapy and even assigned to a variable.

  >>> sr1(my_icmp)                                                                                                      
  Begin emission:
  .Finished sending 1 packets.
  *
  Received 2 packets, got 1 answers, remaining 0 packets
  <IP  version=4 ihl=5 tos=0x0 len=28 id=57251 flags= frag=0 ttl=64 proto=icmp chksum=0x17e3 src=192.168.1.3 dst=192.168.1.7 |<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |>>
  
  
  >>> echo_reply = sr1(my_icmp)                                                                                         
  Begin emission:
  Finished sending 1 packets.
  *
  Received 1 packets, got 1 answers, remaining 0 packets
  
  
  >>> type(echo_reply)                                                                                                  
  scapy.layers.inet.IP
  
  
  >>> echo_reply.show()                                                                                                 
  ###[ IP ]### 
    version= 4
    ihl= 5
    tos= 0x0
    len= 28
    id= 63003
    flags= 
    frag= 0
    ttl= 64
    proto= icmp
    chksum= 0x16b
    src= 192.168.1.3
    dst= 192.168.1.7
    \options\
  ###[ ICMP ]### 
       type= echo-reply
       code= 0
       chksum= 0xffff
       id= 0x0
       seq= 0x0
  

10. Domain Name System (DNS)

Making a DNS query and processing the response is simple with scapy. Consider the request and the handling of the response. The DNS query sends a Uniform Resource Locator (URL) to the DNS Server, which consults its A records for an associated IPv4 address. For example the A record in the DNS server would look something like this:

www.obriain.com. A 172.104.252.215

Now build a DNS query.

  >>> ip_packet = IP(dst="8.8.8.8") 


  >>> udp_segment = UDP(dport=53) 


  >>> dns_query = DNS(rd=1,qd=DNSQR(qname="www.obriain.com")) 


  >>> dns_packet = ip_packet/udp_segment/dns_query 
  
  
  >>> dns_packet.summary()                                                            
  'DNS Qry "b\'www.obriain.com\'" '
  
  
  >>> dns_packet[DNS].show()                                                                                                                                            
  ###[ DNS ]### 
    id= 0
    qr= 0
    opcode= QUERY
    aa= 0
    tc= 0
    rd= 1
    ra= 0
    z= 0
    ad= 0
    cd= 0
    rcode= ok
    qdcount= 1
    ancount= 0
    nscount= 0
    arcount= 0
    \qd\
     |###[ DNS Question Record ]### 
     |  qname= 'www.obriain.com'
     |  qtype= A
     |  qclass= IN
    an= None
    ns= None
    ar= None
  

Send the DNS query and analyse the response.

  >>> response = sr1(dns_packet)                                                                                                                                       
  Begin emission:
  ..Finished sending 1 packets.
  *
  Received 3 packets, got 1 answers, remaining 0 packets
  
  
  >>> response.summary()                                                                                                                                        
  IP / UDP / DNS Ans "172.104.252.215" 
  
  
  >>> response.show()                                                                                                                                           
  ###[ IP ]### 
    version= 4
    ihl= 5
    tos= 0x0
    len= 77
    id= 40939
    flags= 
    frag= 0
    ttl= 57
    proto= udp
    chksum= 0xff6
    src= 8.8.8.8
    dst= 192.168.1.7
    \options\
  ###[ UDP ]### 
       sport= domain
       dport= domain
       len= 57
       chksum= 0x8eec
  ###[ DNS ]### 
          id= 0
          qr= 1
          opcode= QUERY
          aa= 0
          tc= 0
          rd= 1
          ra= 1
          z= 0
          ad= 0
          cd= 0
          rcode= ok
          qdcount= 1
          ancount= 1
          nscount= 0
          arcount= 0
          \qd\
           |###[ DNS Question Record ]### 
           |  qname= 'www.obriain.com.'
           |  qtype= A
           |  qclass= IN
          \an\
           |###[ DNS Resource Record ]### 
           |  rrname= 'www.obriain.com.'
           |  type= A
           |  rclass= IN
           |  ttl= 2717
           |  rdlen= None
           |  rdata= 172.104.252.215
          ns= None
          ar= None
  

Extract only the DNS information from the response.

  
  >>> response[DNS].summary()                                                                                                                                          
  'DNS Ans "172.104.252.215" '
  
  
  >>> response[DNS].show()                                                                                                                                             
  ###[ DNS ]### 
    id= 0
    qr= 1
    opcode= QUERY
    aa= 0
    tc= 0
    rd= 1
    ra= 1
    z= 0
    ad= 0
    cd= 0
    rcode= ok
    qdcount= 1
    ancount= 1
    nscount= 0
    arcount= 0
    \qd\
     |###[ DNS Question Record ]### 
     |  qname= 'www.obriain.com.'
     |  qtype= A
     |  qclass= IN
    \an\
     |###[ DNS Resource Record ]### 
     |  rrname= 'www.obriain.com.'
     |  type= A
     |  rclass= IN
     |  ttl= 2717
     |  rdlen= None
     |  rdata= 172.104.252.215
    ns= None
    ar= None
  

Just extract the received IP address.

  >>> response[DNS].an.rdata                                                                                                                                    
  172.104.252.215
  

10.1. DNS lookup tool

Bring all this together to develop a simple DNS tool that can accept a simple Uniform Resource Locator (URL) and lookup the associated IP address, the A record, from Google (8.8.8.8).

  ~$ cat dns_lookup.py
  
  #!/usr/bin/env python3
  # -*- coding: utf-8 -*-
  
  __title__      = "dns_lookup.py"
  __author__      = "Diarmuid O'Briain"
  __copyright__   = "Copyright 2020, C²S Consulting"
  __license__     = "European Union Public Licence v1.2"
  __version__     = "Version 1.0"
  
  """
  Note: DNS lookup tool
  
  """
  from scapy.all import *
  
  # DNS Server IP address
  dns_svr = '8.8.8.8'
  dns_port = 53
  
  # Help and arguments
  if (len(sys.argv) == 1):
      sys.exit(f'Error! - Not enough arguments given, try $ {sys.argv[0]} help\n')
  
  elif (sys.argv[1].lower() in '--help'):
      sys.exit(f'Help! - ~$ sudo {sys.argv[0]} <URL>\n')
  
  else:
  	url = sys.argv[1]
  
  # Check for sudo privileges
  if os.geteuid() != 0:
      sys.exit("You require root privileges to run this script, please use 'sudo'\n")
  
  def dns_request(svr, dport, url):
  	# DNS packet
  	ip = IP(dst=svr)
  
  	# DNS Segment
  	udp = UDP(dport=dport)
  
  	# DNS Request
  	dns = DNS(rd=1,qd=DNSQR(qname=f"{url}"))                              
  
  	dns_req = sr1(ip/udp/dns)                                                             
  
  	return (dns_req)
  
  response = dns_request(dns_svr, dns_port, url)
  ip = response[DNS].an.rdata
  
  print(f"The IP address associated with URL: '{url}' is: {ip}\n")                                                  
                                                  
  

Make the new tool executable.

  ~$ chmod +x dns_lookup_tool.py
  

Test the new tool.

  ~$ sudo ./dns_lookup_tool.py www.obriain.com
  The IP address associated with URL: 'www.obriain.com' is: 172.104.252.215
  

11. Access an application

Before considering access to a daemon offering a service it is necessary to have such a daemon running. Build a webserver on one computer and access from the other via scrapy.

11.1. Create a simple test webserver

Create a simple test webserver daemon, if possible on another computer. Create a directory and add an index.html file. Then run up a webserver and test.

  ~$ mkdir webserver
  
  
  ~$ cd webserver/
  
  
  ~/webserver$ cat << EOF >> index.html
  <HTML>
  <TITLE>Test webserver</TITLE>
  <BODY>
  <P>This is a test webserver</P>
  </BODY>
  </HTML>
  EOF
  
  
  ~/webserver$ python3 -m http.server 8000
  Serving HTTP on 0.0.0.0 port 7800 (http://0.0.0.0:8000/) ...
  192.168.1.7 - - [27/May/2020 16:45:53] "GET / HTTP/1.1" 200 -
  192.168.1.7 - - [27/May/2020 16:46:22] "GET / HTTP/1.1" 200 -
  192.168.1.7 - - [27/May/2020 16:46:40] "GET / HTTP/1.1" 200 -
  

Browse to the webserver to confirm it is working.

11.2. The TCP 3-way handshake

So now the webserver daemon is running, using scapy, this tutorial will demonstrate how to access it. Before doing so consider the 3-way handshake process of TCP. The originator sends a synchronise, SYN message to the server hosting the daemon. The server responds with a Synchronise/acknowledge, SYN/ACK message. The originator responds with a ACK of its own as well as a Push, PSH to send embedded Hypertext Markup Language (HTML) payload data. The server responds with the payload HTML 200 OK response data using a TCP ACK flag. The TCP Finish, FIN flag indicates the end of data transmission to finish a TCP connection.

11.3. Access the application daemon

As demonstrated earlier create an IP packet header.

  >>> ip_packet = IP(dst='192.168.1.3')   
  

Create a TCP SYN segment header.

  >>> syn_segment = TCP(sport=1026, dport=8000, flags='S', seq=1000)
  

Assemble a complete IP packet with the SYN segment and review.

  >>> syn_packet = ip_packet/syn_segment
  
  
  >>> syn_packet.summary()                                                        
  'IP / TCP 192.168.1.7:1026 > 192.168.1.3:8000 S'
  
  
  >>> syn_packet.show()                                                           
  ###[ IP ]### 
    version= 4
    ihl= None
    tos= 0x0
    len= None
    id= 1
    flags= 
    frag= 0
    ttl= 64
    proto= tcp
    chksum= None
    src= 192.168.1.7
    dst= 192.168.1.3
    \options\
  ###[ TCP ]### 
       sport= 1026
       dport= 8000
       seq= 1000
       ack= 0
       dataofs= None
       reserved= 0
       flags= S
       window= 8192
       chksum= None
       urgptr= 0
       options= []
  
  

Send TCP SYN and trap using the sr1() function to grab the first response packet from webserver.

  >>> synack_response = sr1(syn_packet)  
  
  
  >>> synack_response.summary()                                                   
  'IP / TCP 192.168.1.3:8000 > 192.168.1.7:1026 SA'
  
  >>> synack_response.show()                                                      
  ###[ IP ]### 
    version= 4
    ihl= 5
    tos= 0x0
    len= 44
    id= 0
    flags= DF
    frag= 0
    ttl= 64
    proto= tcp
    chksum= 0xb771
    src= 192.168.1.3
    dst= 192.168.1.7
    \options\
  ###[ TCP ]### 
       sport= 8000
       dport= 1026
       seq= 1294044316
       ack= 1001
       dataofs= 6
       reserved= 0
       flags= SA
       window= 64240
       chksum= 0x18e2
       urgptr= 0
       options= [('MSS', 1460)]
  

Take the received ack value (1001), increment it to 1002 it and take the seq sent from the SYN/ACK (1294044316) and increment for the next ack value (1294044317). Now generate the ACK/PSH segment.

  >>> ackpsh_segment = TCP(sport=1026, dport=8000, flags='A' 'P', seq=1002, ack=1294044317)
  

Assemble an IP packet with TCP segment and the HTTP GET payload data.

  >>> ackpsh_packet = ip_packet/ackpsh_segment/"GET / HTTP/1.0\r\nHOST: 192.168.1.7\r\n\r\n"
  

Send the HTTP GET request, using the sr() function, along with the final ACK of the 3-way handshake, in payload and trap all responses, note the multi setting allows scapy to accept multiple answers for this request and timeout setting determines how much time scapy will wait after the last packet has been sent.

  >>> reply, err = sr(ackpsh_packet, multi=1, timeout=1)
  Begin emission:
  .Finished sending 1 packets.
  *
  Received 2 packets, got 1 answers, remaining 0 packets
  

11.4. Execute handshake at higher speed

From a programming perspective it is not good to manually take the ack and seq values to integrate them for the corresponding responding packets in the handshake. These two lines demonstrate how this step can be automated:

  next_seq = synack_response.ack
  next_ack = synack_response.seq + 1
  

The value synack_response.ack is extracted from the incoming packet and used as the seq of the outgoing packet as the other side expects its ack to be the seq of the next packet. The received synack_response.seq is the incoming seq and the other sided expects the ack of the next packet to be incremented. Therefore the outgoing ack is the incoming seq + 1.

Copy an paste all these lines, that have already been explained, into the scapy terminal together.

  ip_packet = IP(dst='192.168.1.3')   
  syn_segment = TCP(sport=1026, dport=8000, flags='S', seq=1000)
  syn_packet = ip_packet/syn_segment
  synack_response = sr1(syn_packet)  
  next_seq = synack_response.ack
  next_ack = synack_response.seq + 1  
  ackpsh_segment = TCP(sport=1026, dport=8000, flags='A' 'P', seq=next_seq, ack=next_ack)
  ackpsh_packet = ip_packet/ackpsh_segment/"GET / HTTP/1.0\r\nHOST: 192.168.1.7\r\n\r\n"
  reply, err = sr(ackpsh_packet, multi=1, timeout=1)
  

This will respond as follows:

  Begin emission:
  .Finished sending 1 packets.
  *
  Received 2 packets, got 1 answers, remaining 0 packets
  .
  Sent 1 packets.
  Begin emission:
  Finished sending 1 packets.
  **.*.
  Received 5 packets, got 3 answers, remaining 0 packets
  

Analyse the reply.

  >>> print(type(reply))                                                                                 
  <class 'scapy.plist.SndRcvList'>
  
  
  >>> print(len(reply))                                                                                  
  3
  >>> print(len(reply[0]))                                                                               
  2
  >>> print(len(reply[1]))                                                                               
  2
  >>> print(len(reply[2]))                                                                               
  2
  

Look at the second element in the tuple, which contains the payload if it exists.

  >>> print(reply[0][1])                                                                                 
  WARNING: Calling str(pkt) on Python 3 makes no sense!
  b'E\x00\x00(7S@\x00@\x06\x80"\xc0\xa8\x01\x03\xc0\xa8\x01\x07\x1f@\x04\x02 \xac0\xb4\x00\x00+\x8dP\x10\xfa\xcb\x91~\x00\x00'
  

Ooops it returns bytes so decode to a string value first.

  >>> print(r[0][1][Raw].load.decode('utf-8')) 
  GET / HTTP/1.0
  HOST: 192.168.1.7
  
  
  >>> print(r[1][1][Raw].load.decode('utf-8'))
  HTTP/1.0 200 OK
  Server: SimpleHTTP/0.6 Python/3.6.9
  Date: Wed, 27 May 2020 21:58:42 GMT
  Content-type: text/html
  Content-Length: 92
  Last-Modified: Wed, 27 May 2020 15:54:07 GMT
  
  <HTML>
  <TITLE>Test webserver</TITLE>
  <BODY>
  <P>This is a test webserver</P>
  </BODY>
  </HTML>
  
  
  >>> print(reply[2][1][Raw].load.decode('utf-8'))                                                       
  HTTP/1.0 200 OK
  Server: SimpleHTTP/0.6 Python/3.6.9
  Date: Wed, 27 May 2020 21:58:42 GMT
  Content-type: text/html
  Content-Length: 92
  Last-Modified: Wed, 27 May 2020 15:54:07 GMT
  
  <HTML>
  <TITLE>Test webserver</TITLE>
  <BODY>
  <P>This is a test webserver</P>
  </BODY>
  </HTML>
  

The webserver page has been accessed and is part of the payload of the responding message.

11.5. Bring this all together

Write a simple application in Python to bring this all together. The application will engage in a 3-way TCP handshake with a webserver running on the IP address 192.168.1.3 over TCP port 8000 and pull down the page there.

Create a script with the following content.

  ~$ cat http_retriever.py
  
  #!/usr/bin/env python3
  # -*- coding: utf-8 -*-
  
  __title__      = "http_retriever.py"
  __author__      = "Diarmuid O'Briain"
  __copyright__   = "Copyright 2020, C²S Consulting"
  __license__     = "European Union Public Licence v1.2"
  __version__     = "Version 1.0"
  
  """
  Note: 
  
  The Linux kernel doesn't originate the SYN so when it received a SNY/ACK
  it responds with an RST, prevent this my dropping using iptables.
  
  $ sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 192.168.1.7 -j DROP
  
  """
  from scapy.all import *
  
  seq = int(RandNum(10000, 19999))
  ipaddr = '192.168.1.3'
  sport = int(RandNum(1025, 65535))
  dport = 8000
  payload = f"GET / HTTP/1.0\r\nHOST: {ipaddr}\r\n\r\n"
  
  # IP packet header will be reused for multiple packets
  ip_packet = IP(dst=ipaddr)   
  
  # SYN segment 
  syn_segment = TCP(sport=sport, dport=dport, flags='S', seq=seq)
  
  # Assemble IP packet with SYN segment
  syn_packet = ip_packet/syn_segment
  
  # Send TCP SYN and trap, only the first, response from webserver
  synack_response = sr1(syn_packet)
  
  # Take the SEQ and ACK from the SYN/ACK and increment as the out ACK and SEQ
  next_seq = synack_response.ack          
  new_ack = synack_response.seq + 1 
  
  # Generate ACK, PSH and Payload segment
  ackpsh_segment = TCP(sport=sport, dport=dport, flags='A' 'P', seq=next_seq, ack=new_ack)
  
  # Assemble IP packet with TCP segment and Payload
  ackpsh_packet = ip_packet/ackpsh_segment/payload
  print (ackpsh_packet.show())
  # Send HTTP GET request in payload and trap all responses
  #  - multi: accept multiple answers for this request
  #  - timeout: how much time to wait after the last packet has been sent 
  reply, err = sr(ackpsh_packet, multi=1, timeout=1)
  
  # Loop through all the responses and show
  for r in reply:
      for p in range(0, len(r)):
          if Raw in r[p]:
              bytesload = r[p][Raw].load
              strload = bytesload.decode('utf-8')
              print(strload)
  

Now make the script executable and run it for the same effect as running the commands via the scapy ipython shell.

  ~$ chmod +x http_retriever.py
  
  
  ~$ sudo ./http_retriever.py
  Begin emission:
  .Finished sending 1 packets.
  *
  Received 2 packets, got 1 answers, remaining 0 packets
  .
  Sent 1 packets.
  Begin emission:
  Finished sending 1 packets.
  **.**
  Received 5 packets, got 4 answers, remaining 0 packets
  GET / HTTP/1.0
  HOST: 192.168.1.3
  
  
  GET / HTTP/1.0
  HOST: 192.168.1.3
  
  
  HTTP/1.0 200 OK
  Server: SimpleHTTP/0.6 Python/3.6.9
  Date: Wed, 27 May 2020 22:18:21 GMT
  Content-type: text/html
  Content-Length: 92
  Last-Modified: Wed, 27 May 2020 15:54:07 GMT
  
  
  GET / HTTP/1.0
  HOST: 192.168.1.3
  
  
  HTTP/1.0 200 OK
  Server: SimpleHTTP/0.6 Python/3.6.9
  Date: Wed, 27 May 2020 22:18:21 GMT
  Content-type: text/html
  Content-Length: 92
  Last-Modified: Wed, 27 May 2020 15:54:07 GMT
  
  <HTML>
  <TITLE>Test webserver</TITLE>
  <BODY>
  <P>This is a test webserver</P>
  </BODY>
  </HTML>
  
  GET / HTTP/1.0
  HOST: 192.168.1.3
  
  
  HTTP/1.0 200 OK
  Server: SimpleHTTP/0.6 Python/3.6.9
  Date: Wed, 27 May 2020 22:18:21 GMT
  Content-type: text/html
  Content-Length: 92
  Last-Modified: Wed, 27 May 2020 15:54:07 GMT
  
  <HTML>
  <TITLE>Test webserver</TITLE>
  <BODY>
  <P>This is a test webserver</P>
  </BODY>
  </HTML>
  

The received packets are of the following form.

12. A final simple browser program

Write a final Python program that will send a DNS query to resolve a given URL to the DNS Server (8.8.8.8). Once received the program should establish a 3-way handshake with the webserver followed by a HTTP GET request to which it will accept a HTTP 200 OK and output the received page data to the terminal.

The program should be of the form:

  ~$ ./simple_browser.py <URL> [<destination port>] [<source interface>]
  

You can download a sample program here. Running the program.

  ~$ ./simple_browser.py www.obriain.com 80
  You require root privileges to run this script, please use 'sudo'
  

Get some help.

  ~$ ./simple_browser.py help
  Help! - ~$ ./simple_browser.py <URL> [<destination port>] [<source interface>]
  

OK, it has to be run with root privileges. Actually all of the following gives the same output, assuming there is only a single interface with an IPv4 address on the host, otherwise if no interface name is supplied there maybe some difference depending on which interface that the program picks.

  ~$ sudo ./simple_browser.py www.obriain.com 
  ~$ sudo ./simple_browser.py www.obriain.com 80  
  ~$ sudo ./simple_browser.py www.obriain.com wlp82s0
  ~$ sudo ./simple_browser.py www.obriain.com 80 wlp82s0
  ~$ sudo ./simple_browser.py www.obriain.com wlp82s0 80 
  

Output generated by the simple_browser.py.

  
  Begin emission:
  .Finished sending 1 packets.
  *
  Received 2 packets, got 1 answers, remaining 0 packets
  Begin emission:
  Finished sending 1 packets.
  *
  Received 1 packets, got 1 answers, remaining 0 packets
  Begin emission:
  Finished sending 1 packets.
  **.....*
  Received 8 packets, got 3 answers, remaining 0 packets
  GET / HTTP/1.0
  HOST: 192.168.1.7
  
  
  GET / HTTP/1.0
  HOST: 192.168.1.7
  
  
  HTTP/1.1 200 OK
  Date: Fri, 29 May 2020 14:35:51 GMT
  Server: Apache/2.4.25 (Debian)
  Vary: Accept-Encoding
  Content-Length: 3966
  Connection: close
  Content-Type: text/html; charset=UTF-8
  
  
  <HTML>
  <HEAD>
  <TITLE>O'Briain | Home</TITLE>
  </HEAD>
  <BODY>
  <P>This is a test page</P>
  </BODY>
  </HTML>

Copyright © 2024 C²S Consulting