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.2: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.2
02:42:ac:11:00:02 > None
172.17.0.2 > 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=105 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:4 Other:0>,
<Unanswered: TCP:0 UDP:2 ICMP:0 Other:0>)
sr()
sent a list of packets, and returns two variables, here r
and u
, where:
r
is a list of results (i.e tuples of the packet sent and its answer)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 / IPerror / UDPerror / DNS Qry "b'www.example.com.'" / Padding
<ICMP type=time-exceeded code=ttl-zero-during-transit chksum=0xe6b5 reserved=0 length=17 unused=0 |<IPerror version=4 ihl=5 tos=0x0 len=61 id=1 flags= frag=0 ttl=1 proto=udp chksum=0x36b7 src=192.168.178.64 dst=8.8.8.8 |<UDPerror sport=domain dport=domain len=41 chksum=0xc349 |<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 |<Padding load='\x00\x00\x00\x00\x00\x00\x00' |>>>>>
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=c8:0e:14:6d:a4:f0 src=14:ab:c5:16:d3:de type=IPv4 |<IP version=4 ihl=5 tos=0x0 len=61 id=1 flags= frag=0 ttl=5 proto=udp chksum=0x32b7 src=192.168.178.64 dst=8.8.8.8 |<UDP sport=domain dport=domain len=41 chksum=0x31dd |<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 |>>>>
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())
c8:0e:14:6d:a4:f0 > ff:ff:ff:ff:ff:ff (0x88e1) / Raw
c8:0e:14:6d:a4:f0 > ff:ff:ff:ff:ff:ff (0x8912) / Raw
<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*
<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=20238 rdlen=4 rdata=93.184.216.34 |> ns=None ar=None |>
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 0x7f7e2d79f3a0>],
[<matplotlib.lines.Line2D at 0x7f7e2d79f6a0>]]
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 = 1
ancount = 0
nscount = 0
arcount = 0
\qd \
|###[ DNS Question Record ]###
| qname = 'www.example.com'
| qtype = A
| qclass = IN
an = None
ns = None
ar = None
render a pretty and handy dissection of the packet
pkt.canvas_dump()
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())