My motivation was to start from a known good packet capture, for example, a DNS request and reply, and modify that request to create something interesting: an example to examine in Wireshark, or positive and negative test cases for an IDS software (Snort, Suricata).
I haven’t done much with Scapy before, but it seemed like the right tool for the task. My planned steps were as follows:
All the commands shown were run on an Ubuntu 18.04 LTS VM running on VirtualBox, but should work on any Linux host with Python3, Scapy, and tcpdump.
1. Take pcap (packet capture)
In one terminal I ran tcpdump
, capturing only port 53 traffic:
$ sudo tcpdump -i enp0s3 -w dns.pcap port 53
tcpdump: listening on enp0s3, link-type EN10MB (Ethernet), capture size 262144 bytes
^C2 packets captured
2 packets received by filter
0 packets dropped by kernel
In another terminal I generated a DNS request. I limited it to A records to reduce the number of packets generated:
$ host -t a mediacropolis.osric.com
mediacropolis.osric.com has address 216.154.220.53
I confirmed it worked:
$ tcpdump -n -r dns.pcap
reading from file dns.pcap, link-type EN10MB (Ethernet)
19:51:29.207334 IP 192.168.1.56.57241 > 192.168.1.1.53: 58127+ A? mediacropolis.osric.com. (41)
19:51:29.247780 IP 192.168.1.1.53 > 192.168.1.56.57241: 58127 1/0/0 A 216.154.220.53 (57)
2. Import pcap via scapy
First I set up a virtual environment. This was probably unnecessary, but is a habit I have when starting any new Python project:
mkdir mod_pcap
cd mod_pcap
python3 -m venv venv
source venv/bin/activate
pip install scapy
Then I ran Scapy and imported the packet capture:
$ scapy
>>> packets = rdpcap("/home/chris/dns.pcap")
3. Change pcap
First I looked at the packets in Scapy just to see what the objects looked like:
>>> packets[0]
<Ether dst=fc:ec:da:7b:02:cf src=08:00:27:f0:43:22 type=IPv4 |<IP version=4 ihl=5 tos=0x0 len=69 id=2453 flags=DF frag=0 ttl=64 proto=udp chksum=0xad89 src=192.168.1.56 dst=192.168.1.1 |<UDP sport=57241 dport=domain len=49 chksum=0x83cc |<DNS id=58127 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='mediacropolis.osric.com.' qtype=A qclass=IN |> an=None ns=None ar=None |>>>>
>>> packets[1]
<Ether dst=08:00:27:f0:43:22 src=fc:ec:da:7b:02:cf type=IPv4 |<IP version=4 ihl=5 tos=0x0 len=85 id=30039 flags=DF frag=0 ttl=64 proto=udp chksum=0x41b7 src=192.168.1.1 dst=192.168.1.56 |<UDP sport=domain dport=57241 len=65 chksum=0x1cff |<DNS id=58127 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='mediacropolis.osric.com.' qtype=A qclass=IN |> an=<DNSRR rrname='mediacropolis.osric.com.' type=A rclass=IN ttl=600 rdlen=None rdata=216.154.220.53 |> ns=None ar=None |>>>>
2 packets: one request, one reply.
What if I wanted to change the packets so that the request and reply are instead for supercalifragilisticexpialidocious.osric.net? And the record data is 10.0.100.7?
>>> request = packets[0]
>>> reply = packets[1]
>>> request["DNSQR"].qname = b'supercalifragilisticexpialidocious.osric.net'
>>> reply["DNSRR"].rrname = b'supercalifragilisticexpialidocious.osric.net'
>>> reply["DNSRR"].rdata = b'10.0.100.7'
>>> packets = [request, reply]
>>> wrpcap("/home/chris/dns-modified.pcap", packets)
>>> exit()
Can tcpdump
still read it?
$ tcpdump -n -r dns-modified.pcap
reading from file dns-modified.pcap, link-type EN10MB (Ethernet)
19:51:29.207334 IP 192.168.1.56.57241 > 192.168.1.1.53: 58127+[|domain]
19:51:29.247780 IP 192.168.1.1.53 > 192.168.1.56.57241: 58127 1/0/0 (57)
That doesn’t look quite right. What about tshark
?
$ tshark -r dns-modified.pcap
1 0.000000 192.168.1.56 → 192.168.1.1 DNS 83 Standard query 0xe30f[Malformed Packet]
2 0.040446 192.168.1.1 → 192.168.1.56 DNS 99 Standard query response 0xe30f A mediacropolis.osric.com[Malformed Packet]
It looks unhappy: Malformed Packet
. What went wrong?
Oh! The length and the checksum in both the IP header and the UDP header are incorrect.
Searching around for how to address this led me to How to calculate a packet checksum without sending it? on StackOverflow. It suggested deleting the checksums and rebuilding the packets, and Scapy would automatically calculate the checksums.
A tangent on using __class__
:
I thought it inadvisable to use “magic” objects (see https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles) like __class__
as in the example on StackOverflow. But it does make sense:
>>> type(reply)
<class 'scapy.layers.l2.Ether'>
>>> reply.__class__
<class 'scapy.layers.l2.Ether'>
>>> type(reply['IP'])
<class 'scapy.layers.inet.IP'>
>>> reply['IP'].__class__
<class 'scapy.layers.inet.IP'>
No matter what subclass of a packet layer you have, __class__ will give you the right subclass.
Back to the task at hand. I ended up deleting the length (len
) and checksum (chksum
) from both the IP layer and the UDP layer and rebuilt the packets:
>>> del request['IP'].len
>>> del request['IP'].chksum
>>> del request['UDP'].len
>>> del request['UDP'].chksum
>>> del reply['IP'].len
>>> del reply['IP'].chksum
>>> del reply['UDP'].len
>>> del reply['UDP'].chksum
>>> # rebuild packets
>>> request = Ether(request.build())
>>> reply = Ether(reply.build())
[Note: I also changed the host request from supercalifragilisticexpialidocious.osric.net to example.osric.net. I also included the trailing dot in the request and reply: b'example.osric.net.'
]
Then I re-built the PacketList
:
>>> packets = PacketList([request, reply])
4. Export pcap
Scapy’s wrpcap
function takes a destination filename and a PacketList
object (scapy.plist.PacketList
).
wrpcap("/home/chris/mod_dns.pcap", packets)
Did it work?
$ tcpdump -n -r mod_dns.pcap
reading from file mod_dns.pcap, link-type EN10MB (Ethernet)
14:16:40.005857 IP 192.168.1.56.57241 > 192.168.1.1.53: 58127+ A? example.osric.net. (35)
14:18:34.295393 IP 192.168.1.1.53 > 192.168.1.56.57241: 58127 1/0/0 A 10.0.100.7 (68)
$ tshark -r mod_dns.pcap
1 0.000000 192.168.1.56 → 192.168.1.1 DNS 77 Standard query 0xe30f A example.osric.net
2 114.289536 192.168.1.1 → 192.168.1.56 DNS 110 Standard query response 0xe30f A example.osric.net A 10.0.100.7
Success!
5. View pcap in Wireshark
If it worked in tcpdump and tshark, I expected it to work in Wireshark, but I wanted to make sure:
A note on packet timestamps:
I don’t see any timestamp data when I view the packets in Scapy, but tcpdump shows timestamps:
$ tcpdump -n -r mod_dns.pcap
reading from file mod_dns.pcap, link-type EN10MB (Ethernet)
14:16:40.005857 IP 192.168.1.56.57241 > 192.168.1.1.53: 58127+ A? example.osric.net. (35)
14:18:34.295393 IP 192.168.1.1.53 > 192.168.1.56.57241: 58127 1/0/0 A 10.0.100.7 (68)
How can I modify the timestamps? The timestamps appear to be the time the packet was created/rebuilt by Scapy. I would like to have better control of this, but I have not yet found a way to do that. Please leave a comment if you know of a way to do this!
Regarding timestamps, keep in mind that timestamps are not included in packets: they are part of the pcap file.
You can set
packet.time
. See Specify timestamp on each packet in Scapy?Also it could worth to check TCP timestamp on Scapy documentation:
https://scapy.readthedocs.io/en/latest/usage.html?highlight=timestamp#tcp-timestamp-filtering
pkt = (IP(dst=”72.14.207.99″)/TCP(dport=80,flags=”S”,options=[(‘Timestamp’,(1231234,1234123))]))
>>> pkt.show()
###[ IP ]###
version= 4
ihl= None
tos= 0x0
len= None
id= 1
flags=
frag= 0
ttl= 64
proto= tcp
chksum= None
src= 0.0.0.0
dst= 72.14.207.99
\options\
###[ TCP ]###
sport= ftp_data
dport= http
seq= 0
ack= 0
dataofs= None
reserved= 0
flags= S
window= 8192
chksum= None
urgptr= 0
options= [(‘Timestamp’, (1231234, 1234123))]