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_scapy
WARNING: 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=untested
dtp                 : 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' |>, <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-2005 by 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=[<CDPAddrRecordIPv4  
ptype=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=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 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-2005 
by Cisco Systems, Inc.\nCompiled Tue 30-Aug-05 17:56 by yenanh' |>, <CDPMsgPlatform  type=Platform l
en=24 val='cisco WS-C3560G-24PS' |>, <CDPMsgAddr  type=Addresses len=17 naddr=1 addr=[<CDPAddrRecordIPv4  
ptype=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=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 re
import string
import 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 loading
logging.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 Packets
  4:     """
  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.src
 13:             # 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.iface
 33: 
 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.vlan
 40: 
 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.defaultgw
 46: 
 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:                         pass
 58: 
 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.val
 63: 
 64:                         except Exception, e:
 65:                             print "ERROR!!!!:", f_type
 66:                             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 = None
 16:         pcap_file = None
 17:         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 = arg
 31:             elif opt in ('-f'):
 32:                 pcap_file = arg
 33:             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:         pass
 67: 
 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