Parsing CDP Packets with Scapy
Scapy is a library for python designed for the manipulation of packets, in addition we can forge or decode packets of a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more. It is a Swiss army knife of packet manipulation in python. It can be ran interactively or as part of a script.
In this blog post I will cover how to use one of the new parsers to parse CDP packets included in version 2.2 of scapy. Cisco Discovery Protocol (CDP) is a proprietary Layer 2 Data Link Layer network protocol used to share device information with devices connected on the same subnet. Even do most new networks are migrating to Link Layer Discovery Protocol (LLDP) the Cisco Discovery Protocol is in used by many, even both protocols are enabled at the same time on cisco switches and routers to provide interoperability with third party equipment from HP and Juniper.
In our case we will focus on CDP. The first thing to do is to make sure that we are running the latest version of scapy since during my experimentation with Scapy and CDP I summited several bug reports and they where quickly fixed after the release of version 2.2. So do make sure you are running the latest dev version by downloading and installing from the Mecurial repository used by the project at http://trac.secdev.org/scapy
Once install we can just run from the command prompt in Linux the command scapy an enter in to the interactive shell so we can see what info we can gain from a capture CDP Packet in a pcap file. Lets start the shell:
carlos@infidel01:~/Development/scapy$ ./run_scapyWARNING: No route found for IPv6 destination :: (no default route?)Welcome to Scapy (2.2.0-dev)>>>
The next thing we need to do is list the contributed libraries that came with Scapy 2.2 this is achieved with the call list_contrib():
>>> list_contrib()vqp : VLAN Query Protocol status=loads
cdp : Cisco Discovery Protocol status=loads
ripng : RIPng status=loads
skinny : Skinny Call Control Protocol (SCCP) status=loads
igmpv3 : IGMPv3 status=loads
ubberlogger : Ubberlogger dissectors status=untesteddtp : DTP status=loads
bgp : BGP status=loads
rsvp : RSVP status=loads
wpa_eapol : WPA EAPOL dissector status=loads
mpls : MPLS status=loads
ospf : OSPF status=loads
chdlc : Cisco HDLC and SLARP status=loads
etherip : EtherIP status=loads
avs : AVS WLAN Monitor Header status=loads
ikev2 : IKEv2 status=loads
igmp : IGMP/IGMPv2 status=loads
vtp : VLAN Trunking Protocol (VTP) status=loads
eigrp : EIGRP status=loads
>>>
As it can been support for several new protocols was added. we can also see that some of them load and others are untested. This protocols are contributions by external developers to the project. To load the support for CDP we just issue the command load_contrib()
>>> load_contrib("cdp")
>>>
I have a pcap file on the same folder with CDP packets in it so we can have a look at how they look, to read the packets we use the rdpcap() call to read them in to a variable.
>>> cdp_pkts = rdpcap("cdp.cap")
>>> len(cdp_pkts)16
As it can be seen there are 16 packets in this capture. Lets take a look at the first packets:
>>> cdp_p = cdp_pkts[1]>>> cdp_p<Dot3 dst=01:00:0c:cc:cc:cc src=00:19:06:ea:b8:85 len=386 |<LLC dsap=0xaa ssap=0xaa ctrl=3 |<SNAP OUI=0xc code=0x2000
|<CDPv2_HDR vers=2 ttl=180 cksum=0xb0bd msg=[<CDPMsgDeviceID type=Device ID len=10 val='Switch' |>, <CDPMsgSoftwareVersiontype=Software Version len=196 val='Cisco IOS Software, C3560 Software (C3560-ADVIPSERVICESK9-M), Version 12.2(25)SEB4, RELEASESOFTWARE (fc1)\nCopyright (c) 1986-2005 by Cisco Systems, Inc.\nCompiled Tue 30-Aug-05 17:56 by yenanh' |>, <CDPMsgPlatformtype=Platform len=24 val='cisco WS-C3560G-24PS' |>, <CDPMsgAddr type=Addresses len=17 naddr=1 addr=[<CDPAddrRecordIPv4ptype=NLPID plen=1 proto='\xcc' addrlen=4 addr=192.168.0.1 |>] |>, <CDPMsgPortID type=Port ID len=22 iface='GigabitEthernet0/5'|>, <CDPMsgCapabilities type=Capabilities len=8 cap=Switch+IGMPCapable |>, <CDPMsgProtoHello type=Protocol Hello len=36val='\x00\x00\x0c\x01\x12\x00\x00\x00\x00\xff\xff\xff\xff\x01\x02!\xff\x00\x00\x00\x00\x00\x00\x00\x19\x06\xea\xb8\x80\xff\x00\x00'|>, <CDPMsgVTPMgmtDomain type=VTP Mangement Domain len=7 val='Lab' |>, <CDPMsgNativeVLAN type=Native VLAN len=6 vlan=1 |>,<CDPMsgDuplex type=Duplex len=5 duplex=Full |>, <CDPMsgGeneric type=Trust Bitmap len=5 val='\x00' |>, <CDPMsgGenerictype=Untrusted Port CoS len=5 val='\x00' |>, <CDPMsgMgmtAddr type=Management Address len=17 naddr=1 addr=[<CDPAddrRecordIPv4ptype=NLPID plen=1 proto='\xcc' addrlen=4 addr=192.168.0.1 |>] |>, <CDPMsgGeneric type=Power Availablelen=16 val='\x00\x00\x00\x01\x00\x00\x00\x00\xff\xff\xff\xff' |>] |>>>>>>>
We can see that each packet has each fields clearly defined. if we do an ls() on the packet we can get them in a more readable format:
>>> ls(cdp_p)dst : DestMACField = '01:00:0c:cc:cc:cc' (None)src : MACField = '00:19:06:ea:b8:85' ('00:00:00:00:00:00')len : LenField = 386 (None)--dsap : XByteField = 170 (0)ssap : XByteField = 170 (0)ctrl : ByteField = 3 (0)--OUI : X3BytesField = 12 (0)code : XShortEnumField = 8192 (0)
--vers : ByteField = 2 (2)ttl : ByteField = 180 (180)cksum : XShortField = 45245 (None)msg : PacketListField = [<CDPMsgDeviceID type=Device ID len=10 val='Switch' |>,<CDPMsgSoftwareVersion type=Software Version len=196 val='Cisco IOS Software, C3560 Software(C3560-ADVIPSERVICESK9-M), Version 12.2(25)SEB4, RELEASE SOFTWARE (fc1)\nCopyright (c) 1986-2005by Cisco Systems, Inc.\nCompiled Tue 30-Aug-05 17:56 by yenanh' |>, <CDPMsgPlatform type=Platform len=24 val='cisco WS-C3560G-24PS' |>, <CDPMsgAddr type=Addresses len=17 naddr=1 addr=[<CDPAddrRecordIPv4ptype=NLPID plen=1 proto='\xcc' addrlen=4 addr=192.168.0.1 |>] |>, <CDPMsgPortID type=Port ID len=22iface='GigabitEthernet0/5' |>, <CDPMsgCapabilities type=Capabilities len=8 cap=Switch+IGMPCapable|>, <CDPMsgProtoHello type=Protocol Hello len=36 val='\x00\x00\x0c\x01\x12\x00\x00\x00\x00\xff\xff\xff\xff\x01\x02!\xff\x00\x00\x00\x00\x00\x00\x00\x19\x06\xea\xb8\x80\xff\x00\x00' |>, <CDPMsgVTPMgmtDomain type=VTP Mangement Domain len=7 val='Lab' |>, <CDPMsgNativeVLAN type=Native VLAN len=6 vlan=1 |>, <CDPMsgDuplex type=Duplex len=5 duplex=Full |>, <CDPMsgGeneric type=Trust Bitmap len=5 val='\x00' |>, <CDPMsgGeneric type=Untrusted Port CoS len=5 val='\x00' |>, <CDPMsgMgmtAddr type=Management Address len=17 naddr=1 addr=[<CDPAddrRecordIPv4 ptype=NLPID plen=1 proto='\xcc' addrlen=4 addr=192.168.0.1 |>] |>, <CDPMsgGeneric type=Power Available len=16 val='\x00\x00\x00\x01\x00\x00\x00\x00\xff\xff\xff\xff' |>] ([])>>>
We can see that as it is expected the destination of all CDP packets is '01:00:0c:cc:cc:cc' so this will be the easiest way to identify this packets inside a pcap. The CDP fields are saved in the message, each containing a type and we can call each of the values in the type, they are following a TLV (Type Length Value) format.
With this information lets build a script to help us parse pcap files.
Lets start by making sure we have the proper libraries imported:
#!/usr/bin/python
import getopt
import logging
import reimport stringimport sys
Each one will server a different purpose for the script:
- getopt – Manage the script options that we will use.
- logging – Control any warning or error messages generated by the scapy library.
- re – Regular expression library.
- strings – Manage string objects
- sys – Provides access system specific parameters.
Next we will import the scapy 2.2.0-Dev library and set the logging lever to errors only, this will eliminate the “No IPv6 Route” warning message that may show for those running the script on systems without proper IPv6 configurations.
# suppress the no route warning in scapy when loadinglogging.getLogger("scapy.runtime").setLevel(logging.ERROR)
# import scapy
from scapy.all import *
I like the use of a usage function in my code so I can call it anytime a user enters a wrong parameter, no parameter or simply does –h for help on the script. We will create this function now:
def usage():
"""
Function for presenting usage of the tool.
"""
print "CDP Parse by Carlos Perez carlos_perez@darkoperator.com"print "Tool for printing to STDOUT information on CDP packets found capture"print "file. Will print all supported options.\n"print "cdp_parser.py <OPTIONS>"print "-F <dir> Directory containing pcaps."print "-f <pcap> pcap file."
Now lets create our function to process each packet and print the info to standard out:
1: def process_packets(pkts):
2: """3: Function for processing packets and printing information of CDP Packets4: """5:6: for p in pkts:7: # Check if the packet is a CDP Packet
8: if Dot3 in p and p.dst == '01:00:0c:cc:cc:cc':9:10: print "\n*******************************"11:12: print "Source MAC:", p.src13: # Process each field in the packet message
14: for f in p[CDPv2_HDR].fields["msg"]:15:16: # Check if the filed type is a known one
17: if f.type in _cdp_tlv_types:18:19: # Process each field according to type
20: f_type = _cdp_tlv_types[f.type]21:22: # Make sure we process each address in the message
23: if re.match(r"(Addresses|Management Address)", f_type):24: for ip in f.fields["addr"]:25: print f_type, ip.addr
26:27: elif f_type == "Software Version":
28: print f_type+":"29: print "\t" + string.replace(f.val, "\n", "\n\t")30:31: elif f_type == "Port ID":
32: print f_type, ":", f.iface33:34: elif f_type == "Capabilities":
35: # Ugly but works :)
36: print f_type, ":", "".join(re.findall(r"cap\s*=(\S*)", str(f.show)))37:38: elif re.match(r"Native VLAN|VoIP VLAN Reply",f_type):
39: print f_type, ":", f.vlan40:41: elif f_type == "Duplex":
42: print f_type, ":", _cdp_duplex[f.duplex]43:44: elif f_type == "IP Prefix":
45: print f_type, ":", f.defaultgw46:47: elif f_type == "Power":
48: print f_type, ":", f.power, " mW"49:50: # Fields not yet implemented in the current version of the
51: # contributed cdp module.
52: elif f_type == "Power Available":
53: # I know, this should provide the amount of power
54: print f_type, ": POE Enabled"55:56: elif f_type == "Protocol Hello":
57: pass58:59: else:
60: try:61: # Make sure we do not have an empty value and print
62: if f.val is not '\0' and len(f.val) != 0: print f_type, ":", f.val63:64: except Exception, e:65: print "ERROR!!!!:", f_type66: print e
67: print "Send error to: carlos_perez[at]darkoperator.com"68: pass
on line 1 we declare our function and we set the pkts variable as the input for the function. On line 6 we are going to iterate thru each of the packets found the in the packet list we give the function, next on line 8 we check the destination of each packet to see if they are '01:00:0c:cc:cc:cc' then they are CDP packets and we can proceed to parse them, on line 12 we will print the source MAC Address.
On line 17 we check if it is a know type that we can parse, if not we skip the type, In my testing I did not find any it could not do bust just in case Cisco adds one in the future or the packet has an error I added this line, specially since some vendors like HP had CDPv1 support and did some extensions. Next on line 20 we get from hex to text the type name of the field by checking against the _cdp_tlv_types dictionary that is part of the CDP library.
Now from line 22 to line 54 we parse each type for which we know the name of the field and do not follow the stand name of val like the rest.
From lines 56 and 57 we skip the Protocol Hello type since it just prints a bunch of garbage for this type, still working on how to dissect this type.
If the type is not known we try to parse the TLV data and if an exception occurs an error is raised and my email is provided to sent the error to so I can work on improving the script this happens from lines 59 to 68.
The next step is to create the main function that will handle options, open the pcap files and feed the packets to the function we just created.
1: def main():
2:3: try:4: # Check version
5: if not re.match(r"2\.[2-9]\.\S*", config.conf.version):6: print "You are not running the latest scapy release."7: print "Please go to http://trac.secdev.org/scapy and follow the"8: print "the instructions to download the latest versions."9: sys.exit(1)
10:11: # load the support for CDP Packets
12: load_contrib("cdp")
13:14: # Set Variables for Options
15: folder = None16: pcap_file = None17: pcap_files = []18:19: # Check that options are given
20: if len(sys.argv) == 1:
21: usage()22: sys.exit(1)
23:24: # Set Options
25: options, remainder = getopt.getopt(sys.argv[1:], 'F:f:h')26:27: # Parse Options
28: for opt, arg in options:29: if opt in ('-F'):30: folder = arg31: elif opt in ('-f'):
32: pcap_file = arg33: elif opt in ('-h'):
34: usage()35: sys.exit(0)
36: else:
37: usage()38: sys.exit(1)
39:40: # Process folder with pcap files
41: if folder:
42: if os.path.isdir(folder):
43: for item in os.listdir(arg):44: fullpath = os.path.join(arg, item)45: if os.path.isfile(fullpath) and ('.cap' in item or '.pcap' in item or '.dump' in item):46: pcap_files.append(fullpath)47: else:
48: print "ERROR:", folder, "does not exists!"49: sys.exit(1)
50:51: # Process single pcap file
52: if pcap_file:
53: if os.path.isfile(pcap_file):
54: pcap_files.appemd(pcap_file)55: else:
56: print "ERROR:",pcap_file,"does not exist!"57: sys.exit(1)
58:59: # Process all files selected and extract CDP Info
60: for f in pcap_files:61: pcap = rdpcap(f)62: process_packets(pcap)63: except Exception, e:64: print e
65: print "Send error to: carlos_perez[at]darkoperator.com"66: pass67:68: if __name__ == '__main__':
69: main()
In the main function at line 5 we do a scapy version check making sure we are running a version equal or above 2.2.x, if not we print a message indication that the wrong version is being used and to upgrade to the latest development version.
On line 11 we load the contributed CDP Parser. this has to be loaded before we read the packets since they will be ran against it when read.
In lines 14 to 17 we set the option variables that we will use for the script.
From lines 19 to 22 check that options are given, if none is given we print the usage message and exit.
From lines 24 to 38 we parse the options and set the variables, if an option does not match our list of options we exit with an usage message.
From lines 41 to 49 we check if the folder option is set, if it we check that the folder exists and if it does we list the content of the folder and save the full path of each capture file found in to a list for use.
From lines 52 to 57 we check if a pcap file is specified, if it is we check that the file actualy exist and we save the full path to it in to the the same list we we saved the files for the folder, so both options can be used at the same time.
From lines 60 to 62 we parse each file on the list of files collected, read the packets and pass those to the process_packet function to process them.
This is a very simple simple, I tried my best to explain each part of it so for those starting with python and playing with scapy can follow it and learn. You can download the whole script at cdp_parser.py