Scapy in 15 minutes#

by Guillaume Valadon & Pierre Lalet The original source is part of Scapy and can be found here.

Scapy is a powerful Python-based interactive packet manipulation program and library. It can be used to forge or decode packets for a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more.

This iPython notebook provides a short tour of the main Scapy features. It assumes that you are familiar with networking terminology. All examples were built using the development version from secdev/scapy, and tested on Linux. They should work as well on OS X, and other BSD.

The current documentation is available on http://scapy.readthedocs.io/ !

Quick setup#

The easiest way to try Scapy is to clone the github repository, then launch the run_scapy script as root. The following examples can be pasted at the Scapy prompt. There is no need to install any external Python modules.

git clone https://github.com/secdev/scapy --depth=1
sudo ./run_scapy
Welcome to Scapy (2.4.0)
>>>

Note: iPython users must import scapy as follows

from scapy.all import *

First steps#

With Scapy, each network layer is a Python class. There are two important superclasses: Socket and Packet.

The '/' operator is used to bind layers together. Let’s put a TCP segment on top of IP and assign it to the packet variable, then stack it on top of Ethernet.

packet = IP()/TCP()
Ether()/packet
<Ether  type=IPv4 |<IP  frag=0 proto=tcp |<TCP  |>>>

This last output displays the packet summary. Here, Scapy automatically filled the Ethernet type as well as the IP protocol field.

Protocol fields can be listed using the ls() function:

>>> ls(IP, verbose=True)
version    : BitField  (4 bits)                  = ('4')
ihl        : BitField  (4 bits)                  = ('None')
tos        : XByteField                          = ('0')
len        : ShortField                          = ('None')
id         : ShortField                          = ('1')
flags      : FlagsField                          = ('<Flag 0 ()>')
               MF, DF, evil
frag       : BitField  (13 bits)                 = ('0')
ttl        : ByteField                           = ('64')
proto      : ByteEnumField                       = ('0')
chksum     : XShortField                         = ('None')
src        : SourceIPField                       = ('None')
dst        : DestIPField                         = ('None')
options    : PacketListField                     = ('[]')

Let’s create a new packet to a specific IP destination. With Scapy, each protocol field can be specified. As shown in the ls() output, the interesting field is dst.

Scapy packets are objects with some useful methods, such as summary().

p = Ether()/IP(dst="www.secdev.org")/TCP()
p.summary()
'Ether / IP / TCP 172.17.0.3:ftp_data > Net("www.secdev.org/32"):http S'

There are not many differences with the previous example. However, Scapy used the specific destination to perform some magic tricks !

Using internal mechanisms (such as DNS resolution, routing table and ARP resolution), Scapy has automatically set fields necessary to send the packet. These fields can of course be accessed and displayed.

print(p.dst)  # first layer that has an src field, here Ether
print(p[IP].src)  # explicitly access the src field of the IP layer

# sprintf() is a useful method to display fields
print(p.sprintf("%Ether.src% > %Ether.dst%\n%IP.src% > %IP.dst%"))
None
172.17.0.3
02:42:ac:11:00:03 > None
172.17.0.3 > Net("www.secdev.org/32")

Scapy uses default values that work most of the time. For example, TCP() is a SYN segment to port 80.

print(p.sprintf("%TCP.flags% %TCP.dport%"))
S http

Moreover, Scapy has implicit packets. For example, they are useful to make the TTL field value vary from 1 to 5 to mimic traceroute.

[p for p in IP(ttl=(1,5))/ICMP()]
[<IP  frag=0 ttl=1 proto=icmp |<ICMP  |>>,
 <IP  frag=0 ttl=2 proto=icmp |<ICMP  |>>,
 <IP  frag=0 ttl=3 proto=icmp |<ICMP  |>>,
 <IP  frag=0 ttl=4 proto=icmp |<ICMP  |>>,
 <IP  frag=0 ttl=5 proto=icmp |<ICMP  |>>]

Sending and receiving#

Currently, you know how to build packets with Scapy. The next step is to send them over the network !

The sr1() function sends a packet and returns the corresponding answer. srp1() does the same for layer two packets, i.e. Ethernet. If you are only interested in sending packets send() is your friend.

As an example, we can use the DNS protocol to get www.example.com IPv4 address.

p = sr1(IP(dst="8.8.8.8")/UDP()/DNS(qd=DNSQR()))
p[DNS].an
Begin emission
.
Finished sending 1 packets
*
Received 2 packets, got 1 answers, remaining 0 packets
[<DNSRR  rrname=b'www.example.com.' type=A cacheflush=0 rclass=IN ttl=381 rdata=93.184.215.14 |>]

Another alternative is the sr() function. Like srp1(), the sr1() function can be used for layer 2 packets.

r, u = srp(Ether()/IP(dst="8.8.8.8", ttl=(5,10))/UDP()/DNS(rd=1, qd=DNSQR(qname="www.example.com")), verbose=False)
r, u
(<Results: TCP:0 UDP:0 ICMP:6 Other:0>,
 <Unanswered: TCP:0 UDP:0 ICMP:0 Other:0>)

sr() sent a list of packets, and returns two variables, here r and u, where:

  1. r is a list of results (i.e tuples of the packet sent and its answer)

  2. u is a list of unanswered packets

# Access the first tuple
print(r[0][0].summary())  # the packet sent
print(r[0][1].summary())  # the answer received

# Access the ICMP layer
r[0][1][ICMP]
Ether / IP / UDP / DNS Qry b'www.example.com.'
Ether / IP / ICMP 192.168.5.1 > 172.17.0.3 time-exceeded ttl-zero-during-transit / IPerror / UDPerror
<ICMP  type=time-exceeded code=ttl-zero-during-transit chksum=0xfbba reserved=0 length=0 unused=0 extpad=b'' |<IPerror  version=4 ihl=5 tos=0x0 len=61 id=1 flags= frag=0 ttl=1 proto=udp chksum=0xfd8b src=172.17.0.3 dst=8.8.8.8 |<UDPerror  sport=domain dport=domain len=41 chksum=0xf8b1 |>>>

With Scapy, list of packets, such as r or u, can be easily written to, or read from PCAP files.

wrpcap("scapy.pcap", r)

pcap_p = rdpcap("scapy.pcap")
pcap_p[0]
<Ether  dst=02:42:01:7e:20:bf src=02:42:ac:11:00:03 type=IPv4 |<IP  version=4 ihl=5 tos=0x0 len=61 id=1 flags= frag=0 ttl=5 proto=udp chksum=0xf98b src=172.17.0.3 dst=8.8.8.8 |<UDP  sport=domain dport=domain len=41 chksum=0xf8b1 |<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=[<DNSQR  qname=b'www.example.com.' qtype=A unicastresponse=0 qclass=IN |>] |>>>>

Sniffing the network is as straightforward as sending and receiving packets. The sniff() function returns a list of Scapy packets, that can be manipulated as previously described.

s = sniff(count=2)
s
<Sniffed: TCP:0 UDP:0 ICMP:0 Other:2>

sniff() has many arguments. The prn one accepts a function name that will be called on received packets. Using the lambda keyword, Scapy could be used to mimic the tshark command behavior.

sniff(count=2, prn=lambda p: p.summary())
Ether / ARP who has 172.17.0.2 says 172.17.0.1
Ether / IPv6 / ICMPv6ND_RS / ICMPv6 Neighbor Discovery Option - Source Link-Layer Address 46:f0:e9:3c:89:90
<Sniffed: TCP:0 UDP:0 ICMP:0 Other:2>

Alternatively, Scapy can use OS sockets to send and receive packets. The following example assigns an UDP socket to a Scapy StreamSocket, which is then used to query www.example.com IPv4 address. Unlike other Scapy sockets, StreamSockets do not require root privileges.

import socket

sck = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # create an UDP socket
sck.connect(("8.8.8.8", 53))  # connect to 8.8.8.8 on 53/UDP

# Create the StreamSocket and gives the class used to decode the answer
ssck = StreamSocket(sck)
ssck.basecls = DNS

# Send the DNS query
ssck.sr1(DNS(rd=1, qd=DNSQR(qname="www.example.com")))
Begin emission

Finished sending 1 packets

Received 1 packets, got 1 answers, remaining 0 packets
*
<Raw  load=b'\x00\x00\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x01\\\x00\x04]\xb8\xd7\x0e' |>

Visualization#

Parts of the following examples require the matplotlib module.

With srloop(), we can send 100 ICMP packets to 8.8.8.8 and 8.8.4.4.

ans, unans = srloop(IP(dst=["8.8.8.8", "8.8.4.4"])/ICMP(), inter=.1, timeout=.1, count=100, verbose=False)

Then we can use the results to plot the IP id values.

%matplotlib inline
ans.multiplot(lambda x, y: (y[IP].src, (y.time, y[IP].id)), plot_xy=True)
[[<matplotlib.lines.Line2D at 0x7ff00ffb47d0>],
 [<matplotlib.lines.Line2D at 0x7ff0860df250>]]
../_images/43f2e2db3da6ed2189575f144f8c00b4c639a951f98e52edfba6616acc8c2a3c.png

The raw() constructor can be used to “build” the packet’s bytes as they would be sent on the wire.

pkt = IP() / UDP() / DNS(qd=DNSQR())
print(repr(raw(pkt)))
b'E\x00\x00=\x00\x01\x00\x00@\x11|\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\x005\x005\x00)\xb6\xd3\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01'

Since some people cannot read this representation, Scapy can:

  • give a summary for a packet

print(pkt.summary())
IP / UDP / DNS Qry b'www.example.com.'
  • “hexdump” the packet’s bytes

hexdump(pkt)
0000  45 00 00 3D 00 01 00 00 40 11 7C AD 7F 00 00 01  E..=....@.|.....
0010  7F 00 00 01 00 35 00 35 00 29 B6 D3 00 00 01 00  .....5.5.)......
0020  00 01 00 00 00 00 00 00 03 77 77 77 07 65 78 61  .........www.exa
0030  6D 70 6C 65 03 63 6F 6D 00 00 01 00 01           mple.com.....
  • dump the packet, layer by layer, with the values for each field

pkt.show()
###[ IP ]###
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     = 
  frag      = 0
  ttl       = 64
  proto     = udp
  chksum    = None
  src       = 127.0.0.1
  dst       = 127.0.0.1
  \options   \
###[ UDP ]###
     sport     = domain
     dport     = domain
     len       = None
     chksum    = None
###[ DNS ]###
        id        = 0
        qr        = 0
        opcode    = QUERY
        aa        = 0
        tc        = 0
        rd        = 1
        ra        = 0
        z         = 0
        ad        = 0
        cd        = 0
        rcode     = ok
        qdcount   = None
        ancount   = None
        nscount   = None
        arcount   = None
        \qd        \
         |###[ DNS Question Record ]###
         |  qname     = b'www.example.com.'
         |  qtype     = A
         |  unicastresponse= 0
         |  qclass    = IN
        \an        \
        \ns        \
        \ar        \
  • render a pretty and handy dissection of the packet

pkt.canvas_dump()
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[21], line 1
----> 1 pkt.canvas_dump()

File /usr/local/lib/python3.13/site-packages/scapy/packet.py:825, in Packet.canvas_dump(self, layer_shift, rebuild)
    822 def canvas_dump(self, layer_shift=0, rebuild=1):
    823     # type: (int, int) -> pyx.canvas.canvas
    824     if PYX == 0:
--> 825         raise ImportError("PyX and its dependencies must be installed")
    826     canvas = pyx.canvas.canvas()
    827     if rebuild:

ImportError: PyX and its dependencies must be installed

Scapy has a traceroute() function, which basically runs a sr(IP(ttl=(1..30)) and creates a TracerouteResult object, which is a specific subclass of SndRcvList().

ans, unans = traceroute('www.secdev.org', maxttl=15)
Begin emission:
Finished sending 15 packets.
***************
Received 15 packets, got 15 answers, remaining 0 packets
   217.25.178.5:tcp80 
1  192.168.178.1   11 
2  62.214.63.146   11 
3  62.214.36.181   11 
4  62.214.36.146   11 
5  195.66.224.21   11 
6  72.52.92.13     11 
7  184.105.81.166  11 
8  184.104.207.166 11 
9  217.25.178.5    SA 
10 217.25.178.5    SA 
11 217.25.178.5    SA 
12 217.25.178.5    SA 
13 217.25.178.5    SA 
14 217.25.178.5    SA 
15 217.25.178.5    SA 

The PacketList.make_table() function can be very helpful. Here is a simple “port scanner”:

ans = sr(IP(dst=["scanme.nmap.org", "nmap.org"])/TCP(dport=[22, 80, 443, 31337]), timeout=3, verbose=False)[0]
ans.extend(sr(IP(dst=["scanme.nmap.org", "nmap.org"])/UDP(dport=53)/DNS(qd=DNSQR()), timeout=3, verbose=False)[0])
ans.make_table(lambda x, y: (x[IP].dst, x.sprintf('%IP.proto%/{TCP:%r,TCP.dport%}{UDP:%r,UDP.dport%}'), y.sprintf('{TCP:%TCP.flags%}{ICMP:%ICMP.type%}')))
          45.33.32.156 45.33.49.119 
tcp/22    SA           SA           
tcp/31337 SA           RA           
tcp/443   RA           SA           
tcp/80    SA           SA           
udp/53    dest-unreach -            

Implementing a new protocol#

Scapy can be easily extended to support new protocols.

The following example defines DNS over TCP. The DNSTCP class inherits from Packet and defines two field: the length, and the real DNS message. The length_of and length_from arguments link the len and dns fields together. Scapy will be able to automatically compute the len value.

class DNSTCP(Packet):
    name = "DNS over TCP"
    
    fields_desc = [ FieldLenField("len", None, fmt="!H", length_of="dns"),
                    PacketLenField("dns", 0, DNS, length_from=lambda p: p.len)]
    
    # This method tells Scapy that the next packet must be decoded with DNSTCP
    def guess_payload_class(self, payload):
        return DNSTCP

This new packet definition can be direcly used to build a DNS message over TCP.

# Build then decode a DNS message over TCP
DNSTCP(raw(DNSTCP(dns=DNS())))
<DNSTCP  len=33 dns=<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=<DNSQR  qname='www.example.com.' qtype=A qclass=IN |> an=None ns=None ar=None |> |>

Modifying the previous StreamSocket example to use TCP allows to use the new DNSCTP layer easily.

import socket

sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # create an TCP socket
sck.connect(("8.8.8.8", 53))  # connect to 8.8.8.8 on 53/TCP

# Create the StreamSocket and gives the class used to decode the answer
ssck = StreamSocket(sck)
ssck.basecls = DNSTCP

# Send the DNS query
ssck.sr1(DNSTCP(dns=DNS(rd=1, qd=DNSQR(qname="www.example.com"))))
Begin emission:
Finished sending 1 packets.

Received 1 packets, got 1 answers, remaining 0 packets*
<DNSTCP  len=49 dns=<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=<DNSQR  qname='www.example.com.' qtype=A qclass=IN |> an=<DNSRR  rrname='www.example.com.' type=A rclass=IN ttl=15210 rdlen=4 rdata=93.184.216.34 |> ns=None ar=None |> |>

Scapy as a module#

So far, Scapy was only used from the command line. It is also a Python module than can be used to build specific network tools, such as ping6.py:

from scapy.all import *
import argparse

parser = argparse.ArgumentParser(description="A simple ping6")
parser.add_argument("ipv6_address", help="An IPv6 address")
args = parser.parse_args()

print(sr1(IPv6(dst=args.ipv6_address)/ICMPv6EchoRequest(), verbose=0).summary())