You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
363 lines
9.7 KiB
363 lines
9.7 KiB
#!/usr/bin/python3
|
|
#
|
|
# Diff two or more pcap files and write a pcap file with different
|
|
# packets as result
|
|
#
|
|
#
|
|
# You need to install scapy to use this script
|
|
# -> pip install scapy-python3
|
|
#
|
|
# Copyright 2013-2018 ETH Zurich, ISGINF, Bastian Ballmann
|
|
# E-Mail: bastian.ballmann@inf.ethz.ch
|
|
# Web: http://www.isg.inf.ethz.ch
|
|
#
|
|
# This is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# It is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License.
|
|
# If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import sys
|
|
import getopt
|
|
from scapy.all import PcapReader, wrpcap, Packet, NoPayload
|
|
|
|
|
|
output_file = None
|
|
input_files = []
|
|
complete_diff = False
|
|
ignore_source = False
|
|
ignore_source_ip = False
|
|
ignore_dest_ip = False
|
|
ignore_source_mac = False
|
|
ignore_macs = False
|
|
ignore_seq_ack = False
|
|
ignore_ip_id = False
|
|
ignore_timestamp = False
|
|
ignore_ttl = False
|
|
ignore_ck = False
|
|
ignore_headers = []
|
|
first_layer = None
|
|
diff_only_left = False
|
|
diff_only_right = False
|
|
be_quiet = False
|
|
show_diffs = False
|
|
|
|
|
|
def usage():
|
|
print(sys.argv[0])
|
|
print("""
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
|
Diff two or more pcap files
|
|
Programmed by Bastian Ballmann <bastian.ballmann@inf.ethz.ch>
|
|
Version 1.3.0
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
|
|
|
|
-i <input_file> (use multiple times)
|
|
-o <output_file> (this is a pcap file)
|
|
-c (complete diff, dont ignore ttl, checksums and timestamps)
|
|
False by default
|
|
-l diff only left side (first pcap file)
|
|
-L <scapy_layer_name> ignores everything below the given layer
|
|
-r diff only right side (not first pcap file)
|
|
-f s (ignore src ip / mac)
|
|
-f sm (ignore src mac)
|
|
-f si (ignore src ip)
|
|
-f di (ignore dst ip)
|
|
-f m (ignore mac addresses)
|
|
-f sa (ignore tcp seq and ack num)
|
|
-f ii (ignore ip id)
|
|
-f ts (ignore timestamp)
|
|
-f ttl (ignore ttl)
|
|
-f ck (ignore checksum)
|
|
-f <scapy header name>
|
|
-q be quiet
|
|
-d show differences
|
|
|
|
Example usage with ignore mac addresses
|
|
pcap_diff.py -i client.dump -i server.dump -o diff.pcap -f m
|
|
|
|
""")
|
|
sys.exit(1)
|
|
|
|
|
|
try:
|
|
cmd_opts = "cf:i:L:lo:qrd"
|
|
opts, args = getopt.getopt(sys.argv[1:], cmd_opts)
|
|
except getopt.GetoptError:
|
|
usage()
|
|
|
|
for opt in opts:
|
|
if opt[0] == "-i":
|
|
input_files.append(opt[1])
|
|
elif opt[0] == "-o":
|
|
output_file = opt[1]
|
|
elif opt[0] == "-c":
|
|
complete_diff = True
|
|
elif opt[0] == "-d":
|
|
show_diffs = True
|
|
elif opt[0] == "-l":
|
|
diff_only_left = True
|
|
elif opt[0] == "-L":
|
|
first_layer = opt[1]
|
|
elif opt[0] == "-r":
|
|
diff_only_right = True
|
|
elif opt[0] == "-q":
|
|
be_quiet = True
|
|
elif opt[0] == "-f":
|
|
if opt[1] == "s":
|
|
ignore_source = True
|
|
elif opt[1] == "sm":
|
|
ignore_source_mac = True
|
|
elif opt[1] == "sa":
|
|
ignore_seq_ack = True
|
|
elif opt[1] == "ii":
|
|
ignore_ip_id = True
|
|
ignore_ck = True
|
|
elif opt[1] == "si":
|
|
ignore_source_ip = True
|
|
elif opt[1] == "di":
|
|
ignore_dest_ip = True
|
|
elif opt[1] == "m":
|
|
ignore_macs = True
|
|
elif opt[1] == "ts":
|
|
ignore_timestamp = True
|
|
elif opt[1] == "ttl":
|
|
ignore_ttl = True
|
|
ignore_ck = True
|
|
elif opt[1] == "ck":
|
|
ignore_ck = True
|
|
else:
|
|
ignore_headers.append(opt[1])
|
|
else:
|
|
usage()
|
|
|
|
if len(input_files) < 2:
|
|
print("Need at least 2 input files to diff")
|
|
sys.exit(1)
|
|
|
|
|
|
def flatten(d, parent_key=''):
|
|
"""
|
|
Flatten a packet to a dict
|
|
Remove checksums (can be different due to calculation in netdev firmware)
|
|
"""
|
|
items = []
|
|
|
|
# skip scapy internal fields
|
|
skip_fields = ['fieldtype', 'underlayer', 'initialized', 'fieldtype',
|
|
'default_fields', 'aliastypes', 'post_transforms',
|
|
'packetfields', 'overloaded_fields', 'sent_time']
|
|
|
|
hasPayload = 'payload' in d
|
|
|
|
for k, v in d.items():
|
|
fullk = "%s_%s" % (parent_key, k)
|
|
|
|
# ignore the original if we have payload.The payload will be expanded
|
|
if hasPayload and k == 'original':
|
|
continue
|
|
|
|
# No complete diff? Ignore checksum, ttl and time
|
|
if not complete_diff and (k == "chksum" or k == "ttl" or k == "time"):
|
|
continue
|
|
|
|
# ignore Timestamp
|
|
if ignore_timestamp and k == "time":
|
|
continue
|
|
|
|
# skip time value of deeper layers (they all get it from Packet)
|
|
if parent_key and k == "time":
|
|
continue
|
|
|
|
# Ignore source IP?
|
|
if (ignore_source or ignore_source_ip) and k == "src":
|
|
continue
|
|
|
|
# Ignore dest IP?
|
|
if ignore_dest_ip and k == "dst":
|
|
continue
|
|
|
|
# Ignore TCP seq and ack num?
|
|
if ignore_seq_ack and (k == "seq" or k == "ack"):
|
|
continue
|
|
|
|
# Ignore IP ID?
|
|
if ignore_ip_id and k == "id":
|
|
continue
|
|
|
|
# Ignore Time To Live (TTL) field in IPv4 Header
|
|
if ignore_ttl and k == "ttl":
|
|
continue
|
|
|
|
# Ignore Checksum field in IPv4 Header
|
|
if ignore_ck and k == "chksum":
|
|
continue
|
|
|
|
# Ignore custom header field?
|
|
if fullk in ignore_headers:
|
|
continue
|
|
|
|
new_key = parent_key + '_' + k if parent_key else k
|
|
|
|
# payload is Packet or str
|
|
# stop at NoPayload payload
|
|
if k == "payload" and isinstance(v, NoPayload):
|
|
continue
|
|
elif k == "payload" and isinstance(v, Packet):
|
|
new_key = v.__class__.__name__ + "_" + k
|
|
items.extend(flatten(v.__dict__, new_key).items())
|
|
elif k == "payload":
|
|
new_key = v.__class__.__name__ + "_" + k
|
|
items.append((new_key, v.payload))
|
|
elif k == "fields" and isinstance(v, Packet):
|
|
new_key = v.__class__.__name__ + "_" + k
|
|
items.extend(flatten(v, new_key).items())
|
|
elif k in skip_fields:
|
|
continue # skip internal, unneeded fields
|
|
elif isinstance(v, dict):
|
|
items.extend(flatten(v, new_key).items())
|
|
else:
|
|
items.append((new_key, v))
|
|
|
|
return dict(items)
|
|
|
|
|
|
def serialize(packet):
|
|
"""
|
|
Serialize flattened packet
|
|
"""
|
|
|
|
# remove mac addresses?
|
|
if ignore_macs and packet.fields:
|
|
if packet.fields.get("src"):
|
|
del packet.fields["src"]
|
|
if packet.fields.get("dst"):
|
|
del packet.fields["dst"]
|
|
|
|
if (ignore_source or ignore_source_mac) and packet.fields.get("fields"):
|
|
if packet.fields.get("src"):
|
|
del packet.fields["src"]
|
|
|
|
if first_layer and packet.haslayer(first_layer):
|
|
flat_packet = flatten(packet[first_layer].fields)
|
|
else:
|
|
flat_packet = flatten(packet.fields)
|
|
|
|
serial = ""
|
|
|
|
for key in sorted(flat_packet):
|
|
serial += str(key) + ": " + str(flat_packet[key]) + " | "
|
|
|
|
return serial
|
|
|
|
|
|
def read_dump(pcap_file):
|
|
"""
|
|
Read PCAP file
|
|
Return dict of packets with serialized flat packet as key
|
|
"""
|
|
dump = {}
|
|
count = 0
|
|
|
|
if not be_quiet:
|
|
sys.stdout.write("Reading file " + pcap_file + ":\n")
|
|
sys.stdout.flush()
|
|
|
|
with PcapReader(pcap_file) as pcap_reader:
|
|
for packet in pcap_reader:
|
|
count += 1
|
|
dump[serialize(packet)] = packet
|
|
|
|
if not be_quiet:
|
|
sys.stdout.write("Found " + str(count) + " packets\n\n")
|
|
sys.stdout.flush()
|
|
|
|
return dump
|
|
|
|
|
|
def compare_summary():
|
|
ret = ""
|
|
if complete_diff:
|
|
ret += "Complete, "
|
|
if ignore_source:
|
|
ret += "no src mac, no src ip, "
|
|
if ignore_source_mac:
|
|
ret += "no src mac, "
|
|
if ignore_source_ip:
|
|
ret += "no src ip, "
|
|
if ignore_dest_ip:
|
|
ret += "no dst ip, "
|
|
if ignore_seq_ack:
|
|
ret += "not seq ack, "
|
|
if ignore_ip_id:
|
|
ret += "no ip id, "
|
|
if ignore_ttl:
|
|
ret += "no ttl, "
|
|
if ignore_ck:
|
|
ret += "no checksum, "
|
|
if ignore_timestamp:
|
|
ret += "no timestamp, "
|
|
for h in ignore_headers:
|
|
ret += "no " + h + ", "
|
|
|
|
return ret
|
|
|
|
|
|
# Parse pcap files
|
|
dumps = []
|
|
|
|
for input_file in input_files:
|
|
dumps.append(read_dump(input_file))
|
|
|
|
# Diff the dumps
|
|
diff_counter = 0
|
|
diff_packets = []
|
|
base_dump = dumps.pop(0)
|
|
|
|
if not be_quiet:
|
|
print("Diffing packets: " + compare_summary())
|
|
|
|
for packet in base_dump.values():
|
|
serial_packet = serialize(packet)
|
|
found_packet = False
|
|
|
|
for dump in dumps:
|
|
if dump.get(serial_packet):
|
|
del dump[serial_packet]
|
|
found_packet = True
|
|
|
|
if not diff_only_right and not found_packet:
|
|
if show_diffs:
|
|
print(" <<< " + packet.summary())
|
|
diff_packets.append(packet)
|
|
|
|
if not diff_only_left:
|
|
for dump in dumps:
|
|
if len(dump.values()) > 0:
|
|
diff_packets.extend(dump.values())
|
|
|
|
if show_diffs:
|
|
for packet in dump.values():
|
|
packet.show()
|
|
print(" >>> " + packet.summary())
|
|
|
|
if not be_quiet:
|
|
print("\nFound " + str(len(diff_packets)) + " different packets\n")
|
|
|
|
# Write pcap diff file?
|
|
if output_file and diff_packets:
|
|
if not be_quiet:
|
|
print("Writing " + output_file)
|
|
wrpcap(output_file, diff_packets)
|
|
|
|
sys.exit(len(diff_packets))
|
|
else:
|
|
sys.exit(0)
|