Package killerbee :: Module dev_wislab
[hide private]
[frames] | no frames]

Source Code for Module killerbee.dev_wislab

  1  # KillerBee Device Support for: 
  2  # Wislab Sniffer Radio Client 
  3  # This sniffer is a remote IPv4 host. 
  4  #  
  5  # (C) 2013 Ryan Speers <ryan at riverloopsecurity.com> 
  6  # 
  7  # For documentation from the vendor, visit: 
  8  #   http://www.sniffer.wislab.cz/sniffer-configuration/ 
  9  # 
 10   
 11  import os 
 12  import time 
 13  import struct 
 14  import time 
 15  import urllib2 
 16  import re 
 17  from socket import socket, AF_INET, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR, timeout as error_timeout 
 18  from struct import unpack 
 19   
 20  from datetime import datetime, date, timedelta 
 21  from kbutils import KBCapabilities, makeFCS, isIpAddr, KBInterfaceError 
 22   
 23  DEFAULT_IP = "10.10.10.2"   #IP address of the sniffer 
 24  DEFAULT_GW = "10.10.10.1"   #IP address of the default gateway 
 25  DEFAULT_UDP = 17754         #"Remote UDP Port" 
 26  TESTED_FW_VERS = ["0.5"]    #Firmware versions tested with the current version of this client device connector 
 27   
 28  NTP_DELTA = 70*365*24*60*60 #datetime(1970, 1, 1, 0, 0, 0) - datetime(1900, 1, 1, 0, 0, 0) 
 29   
 30  ''' 
 31  Convert the two parts of an NTP timestamp to a datetime object. 
 32  Similar code from Wireshark source: 
 33  575     /* NTP_BASETIME is in fact epoch - ntp_start_time */ 
 34  576     #define NTP_BASETIME 2208988800ul 
 35  619     void 
 36  620     ntp_to_nstime(tvbuff_t *tvb, gint offset, nstime_t *nstime) 
 37  621     { 
 38  622     nstime->secs = tvb_get_ntohl(tvb, offset); 
 39  623     if (nstime->secs) 
 40  624     nstime->secs -= NTP_BASETIME; 
 41  625     nstime->nsecs = (int)(tvb_get_ntohl(tvb, offset+4)/(NTP_FLOAT_DENOM/1000000000.0)); 
 42  626     } 
 43  ''' 
44 -def ntp_to_system_time(secs, msecs):
45 """convert a NTP time to system time""" 46 print "Secs:", secs, msecs 47 print "\tUTC:", datetime.utcfromtimestamp(secs - 2208988800) 48 return datetime.utcfromtimestamp(secs - 2208988800)
49
50 -def getFirmwareVersion(ip):
51 try: 52 html = urllib2.urlopen("http://{0}/".format(ip)) 53 fw = re.search(r'Firmware version ([0-9.]+)', html.read()) 54 if fw is not None: 55 return fw.group(1) 56 except Exception as e: 57 print("Unable to connect to IP {0} (error: {1}).".format(ip, e)) 58 return None
59
60 -def getMacAddr(ip):
61 ''' 62 Returns a string for the MAC address of the sniffer. 63 ''' 64 try: 65 html = urllib2.urlopen("http://{0}/".format(ip)) 66 # Yup, we're going to have to steal the status out of a JavaScript variable 67 #var values = removeSSItag('<!--#pindex-->STOPPED,00:1a:b6:00:0a:a4,... 68 res = re.search(r'<!--#pindex-->[A-Z]+,((?:[0-9a-f]{2}:){5}[0-9a-f]{2})', html.read()) 69 if res is None: 70 raise KBInterfaceError("Unable to parse the sniffer's MAC address.") 71 return res.group(1) 72 except Exception as e: 73 print("Unable to connect to IP {0} (error: {1}).".format(ip, e)) 74 return None
75
76 -def isWislab(dev):
77 return ( isIpAddr(dev) and getFirmwareVersion(dev) != None )
78
79 -class WISLAB:
80 - def __init__(self, dev=DEFAULT_IP, recvport=DEFAULT_UDP, recvip=DEFAULT_GW):
81 ''' 82 Instantiates the KillerBee class for the Wislab Sniffer. 83 @type dev: String 84 @param dev: IP address (ex 10.10.10.2) 85 @type recvport: Integer 86 @param recvport: UDP port to listen for sniffed packets on. 87 @type recvip: String 88 @param recvip: IP address of the host, where the sniffer will send sniffed packets to. 89 @return: None 90 @rtype: None 91 ''' 92 self._channel = None 93 self._modulation = 0 #unknown, will be set by change channel currently 94 self.handle = None 95 self.dev = dev 96 97 #TODO The receive port and receive IP address are currently not 98 # obtained from or verified against the Wislab sniffer, nor are they 99 # used to change the settings on the sniffer. 100 self.udp_recv_port = recvport 101 self.udp_recv_ip = recvip 102 103 self.__revision_num = getFirmwareVersion(self.dev) 104 if self.__revision_num not in TESTED_FW_VERS: 105 print("Warning: Firmware revision {0} reported by the sniffer is not currently supported. Errors may occur and dev_wislab.py may need updating.".format(self.__revision_num)) 106 107 self.handle = socket(AF_INET, SOCK_DGRAM) 108 self.handle.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 109 self.handle.bind((self.udp_recv_ip, self.udp_recv_port)) 110 111 self.__stream_open = False 112 self.capabilities = KBCapabilities() 113 self.__set_capabilities()
114
115 - def close(self):
116 '''Actually close the receiving UDP socket.''' 117 self.sniffer_off() # turn sniffer off if it's currently running 118 self.handle.close() # socket.close() 119 self.handle = None
120
121 - def check_capability(self, capab):
122 return self.capabilities.check(capab)
123 - def get_capabilities(self):
124 return self.capabilities.getlist()
125 - def __set_capabilities(self):
126 ''' 127 Sets the capability information appropriate for the client and firmware version. 128 @rtype: None 129 @return: None 130 ''' 131 self.capabilities.setcapab(KBCapabilities.SNIFF, True) 132 self.capabilities.setcapab(KBCapabilities.SETCHAN, True) 133 self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) 134 self.capabilities.setcapab(KBCapabilities.FREQ_900, True) 135 return
136 137 # KillerBee expects the driver to implement this function
138 - def get_dev_info(self):
139 ''' 140 Returns device information in a list identifying the device. 141 @rtype: List 142 @return: List of 3 strings identifying device. 143 ''' 144 return [self.dev, "Wislab Sniffer v{0}".format(self.__revision_num), getMacAddr(self.dev)]
145
146 - def __make_rest_call(self, path, fetch=True):
147 ''' 148 Wrapper to the sniffer's RESTful services. 149 Reports URL/HTTP errors as KBInterfaceErrors. 150 @rtype: If fetch==True, returns a String of the page. Otherwise, it 151 returns True if an HTTP 200 code was received. 152 ''' 153 try: 154 html = urllib2.urlopen("http://{0}/{1}".format(self.dev, path)) 155 if fetch: 156 return html.read() 157 else: 158 return (html.getcode() == 200) 159 except Exception as e: 160 raise KBInterfaceError("Unable to preform a call to {0}/{1} (error: {2}).".format(self.dev, path, e))
161
162 - def __sniffer_status(self):
163 ''' 164 Because the firmware accepts only toggle commands for sniffer on/off, 165 we need to check what state it's in before taking action. It's also 166 useful to make sure our command worked. 167 @rtype: Boolean 168 ''' 169 html = self.__make_rest_call('') 170 # Yup, we're going to have to steal the status out of a JavaScript variable 171 res = re.search(r'<!--#pindex-->([A-Z]+),', html) 172 if res is None: 173 raise KBInterfaceError("Unable to parse the sniffer's current status.") 174 # RUNNING means it's sniffing, STOPPED means it's not. 175 return (res.group(1) == "RUNNING")
176
177 - def __sync_status(self):
178 ''' 179 This updates the standard self.__stream_open variable based on the 180 status as reported from asking the remote sniffer. 181 ''' 182 self.__stream_open = self.__sniffer_status()
183
184 - def __sniffer_channel(self):
185 ''' 186 Because the firmware accepts only toggle commands for sniffer on/off, 187 we need to check what state it's in before taking action. It's also 188 useful to make sure our command worked. 189 @rtype: Boolean 190 ''' 191 html = self.__make_rest_call('') 192 # Yup, we're going to have to steal the channel number out of a JavaScript variable 193 # var values = removeSSItag('<!--#pindex-->RUNNING,00:1a:b6:00:0a:a4,10.10.10.2,0,High,0x0000,OFF,0,0').split(","); 194 res = re.search(r'<!--#pindex-->[A-Z]+,[0-9a-f:]+,[0-9.]+,([0-9]+),', html) 195 if res is None: 196 raise KBInterfaceError("Unable to parse the sniffer's current channel.") 197 return int(res.group(1))
198 199 # KillerBee expects the driver to implement this function
200 - def sniffer_on(self, channel=None):
201 ''' 202 Turns the sniffer on such that pnext() will start returning observed 203 data. 204 @type channel: Integer 205 @param channel: Sets the channel, optional 206 @rtype: None 207 ''' 208 self.capabilities.require(KBCapabilities.SNIFF) 209 210 # Because the Wislab just toggles, we have to only hit the page 211 # if we need to go from off to on state. 212 self.__sync_status() 213 if self.__stream_open == False: 214 if channel != None: 215 self.set_channel(channel) 216 217 if not self.__make_rest_call('status.cgi?p=2', fetch=False): 218 raise KBInterfaceError("Error instructing sniffer to start capture.") 219 220 #This makes sure the change actually happened 221 self.__sync_status() 222 if not self.__stream_open: 223 raise KBInterfaceError("Sniffer did not turn on capture.")
224 225 # KillerBee expects the driver to implement this function
226 - def sniffer_off(self):
227 ''' 228 Turns the sniffer off. 229 @rtype: None 230 ''' 231 # Because the Wislab just toggles, we have to only hit the page 232 # if we need to go from on to off state. 233 self.__sync_status() 234 if self.__stream_open == True: 235 if not self.__make_rest_call('status.cgi?p=2', fetch=False): 236 raise KBInterfaceError("Error instructing sniffer to stop capture.") 237 238 #This makes sure the change actually happened 239 self.__sync_status() 240 if self.__stream_open: 241 raise KBInterfaceError("Sniffer did not turn off capture.")
242 243 @staticmethod
244 - def __get_default_modulation(channel):
245 ''' 246 Return the Wislab-specific integer representing the modulation which 247 should be choosen to be IEEE 802.15.4 complinating for a given channel 248 number. 249 Captured values from sniffing Wislab web interface, unsure why these 250 are done as such. 251 Available modulations are listed at: 252 http://www.sewio.net/open-sniffer/develop/http-rest-interface/ 253 @rtype: Integer, or None if unable to determine modulation 254 ''' 255 if channel >= 11 or channel <= 26: return '0' #O-QPSK 250 kb/s 2.4GHz 256 elif channel >= 1 or channel <= 10: return 'c' #O-QPSK 250 kb/s 915MHz 257 elif channel >= 128 or channel <= 131: return '1c' #O-QPSK 250 kb/s 760MHz 258 elif channel == 0: return '0' #O-QPSK 100 kb/s 868MHz 259 else: return None #Error status
260 261 # KillerBee expects the driver to implement this function
262 - def set_channel(self, channel):
263 ''' 264 Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) 265 @type channel: Integer 266 @param channel: Sets the channel, optional 267 @rtype: None 268 ''' 269 self.capabilities.require(KBCapabilities.SETCHAN) 270 271 if self.capabilities.is_valid_channel(channel): 272 # We only need to update our channel if it doesn't match the currently reported one. 273 curChannel = self.__sniffer_channel() 274 if channel != curChannel: 275 self.modulation = self.__get_default_modulation(channel) 276 print("Setting to channel {0}, modulation {1}.".format(channel, self.modulation)) 277 # Examples captured in fw v0.5 sniffing: 278 # channel 6, 250 compliant: http://10.10.10.2/settings.cgi?chn=6&modul=c&rxsens=0 279 # channel 12, 250 compliant: http://10.10.10.2/settings.cgi?chn=12&modul=0&rxsens=0 280 # chinese 0, 780 MHz, 250 compliant: http://10.10.10.2/settings.cgi?chn=128&modul=1c&rxsens=0 281 # chinese 3, 786 MHz, 250 compliant: http://10.10.10.2/settings.cgi?chn=131&modul=1c&rxsens=0 282 self.__make_rest_call("settings.cgi?chn={0}&modul={1}&rxsens=0".format(channel, self.modulation), fetch=False) 283 self._channel = self.__sniffer_channel() 284 else: 285 self._channel = curChannel 286 else: 287 raise Exception('Invalid channel number ({0}) was provided'.format(channel))
288 289 # KillerBee expects the driver to implement this function
290 - def inject(self, packet, channel=None, count=1, delay=0):
291 ''' 292 Not implemented. 293 ''' 294 self.capabilities.require(KBCapabilities.INJECT)
295 296 @staticmethod
297 - def __parse_zep_v2(data):
298 ''' 299 Parse the packet from the ZigBee encapsulation protocol version 2/3 and 300 return the fields desired for usage by pnext(). 301 There is support here for some oddities specific to the Wislab 302 implementation of ZEP and the packet, such as CC24xx format FCS 303 headers being expected. 304 305 The ZEP protocol parsing is mainly based on Wireshark source at: 306 http://anonsvn.wireshark.org/wireshark/trunk/epan/dissectors/packet-zep.c 307 * ZEP v2 Header will have the following format (if type=1/Data): 308 * |Preamble|Version| Type |Channel ID|Device ID|CRC/LQI Mode|LQI Val|NTP Timestamp|Sequence#|Reserved|Length| 309 * |2 bytes |1 byte |1 byte| 1 byte | 2 bytes | 1 byte |1 byte | 8 bytes | 4 bytes |10 bytes|1 byte| 310 * ZEP v2 Header will have the following format (if type=2/Ack): 311 * |Preamble|Version| Type |Sequence#| 312 * |2 bytes |1 byte |1 byte| 4 bytes | 313 #define ZEP_PREAMBLE "EX" 314 #define ZEP_V2_HEADER_LEN 32 315 #define ZEP_V2_ACK_LEN 8 316 #define ZEP_V2_TYPE_DATA 1 317 #define ZEP_V2_TYPE_ACK 2 318 #define ZEP_LENGTH_MASK 0x7F 319 ''' 320 # Unpack constant part of ZEPv2 321 (preamble, version, zeptype) = unpack('<HBB', data[:4]) 322 if preamble != 22597 or version < 2: # 'EX'==22597, and v3 is compat with v2 (I think??) 323 raise Exception("Can not parse provided data as ZEP due to incorrect preamble or unsupported version.") 324 if zeptype == 1: #data 325 (ch, devid, crcmode, lqival, ntpsec, ntpnsec, seqnum, length) = unpack(">BHBBIII10xB", data[4:32]) 326 #print "Data ZEP:", ch, devid, crcmode, lqival, ntpsec, ntpnsec, seqnum, length 327 #We could convert the NTP timestamp received to system time, but the 328 # Wislab firmware uses "relative timestamping" where it begins at 0 each time 329 # the sniffer is started. Thus, it isn't that useful to us, so we just add the 330 # time the packet is received at the host instead. 331 #print "\tConverted time:", ntp_to_system_time(ntpsec, ntpnsec) 332 recdtime = datetime.combine(date.today(), (datetime.now()).time()) #TODO address timezones by going to UTC everywhere 333 #The LQI comes in ZEP, but the RSSI comes in the first byte of the FCS, 334 # if the FCS was correct. If the byte is 0xb1, Wireshark appears to do 0xb1-256 = -79 dBm. 335 # It appears that if CRC/LQI Mode field == 1, then checksum was bad, so the RSSI isn't 336 # available, as the CRC is left in the packet. If it == 0, then the first byte of FCS is the RSSI. 337 # From Wireshark: 338 #define IEEE802154_CC24xx_CRC_OK 0x8000 339 #define IEEE802154_CC24xx_RSSI 0x00FF 340 frame = data[32:] 341 # A length vs len(frame) check is not used here but is an 342 # additional way to verify that all is good (length == len(frame)). 343 if crcmode == 0: 344 validcrc = ((ord(data[-1]) & 0x80) == 0x80) 345 rssi = ord(data[-2]) 346 # We have to trust the sniffer that the FCS was OK, so we compute 347 # what a good FCS should be and patch it back into the packet. 348 frame = frame[:-2] + makeFCS(frame[:-2]) 349 else: 350 validcrc = False 351 rssi = None 352 return (frame, ch, validcrc, rssi, lqival, recdtime) 353 elif zeptype == 2: #ack 354 frame = data[8:] 355 (seqnum) = unpack(">I", data[4:8]) 356 recdtime = datetime.combine(date.today(), (datetime.now()).time()) #TODO address timezones by going to UTC everywhere 357 validcrc = (frame[-2:] == makeFCS(frame[:-2])) 358 return (frame, None, validcrc, None, None, recdtime) 359 return None
360 361 # KillerBee expects the driver to implement this function
362 - def pnext(self, timeout=100):
363 ''' 364 Returns a dictionary containing packet data, else None. 365 @type timeout: Integer 366 @param timeout: Timeout to wait for packet reception in usec 367 @rtype: List 368 @return: Returns None is timeout expires and no packet received. When a packet is received, a dictionary is returned with the keys bytes (string of packet bytes), validcrc (boolean if a vaid CRC), rssi (unscaled RSSI), and location (may be set to None). For backwards compatibility, keys for 0,1,2 are provided such that it can be treated as if a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] 369 ''' 370 if self.__stream_open == False: 371 self.sniffer_on() #start sniffing 372 373 # Use socket timeouts to implement the timeout 374 self.handle.settimeout(timeout / 1000000.0) # it takes seconds 375 376 frame = None 377 donetime = datetime.now() + timedelta(microseconds=timeout) 378 while True: 379 try: 380 data, addr = self.handle.recvfrom(1024) 381 except error_timeout: 382 return None 383 # Ensure it's data coming from the right place, for now we just 384 # check the sending IP address. Ex: addr = ('10.10.10.2', 17754) 385 if addr[0] != self.dev: 386 continue 387 # Dissect the UDP packet 388 (frame, ch, validcrc, rssi, lqival, recdtime) = self.__parse_zep_v2(data) 389 print "Valid CRC", validcrc, "LQI", lqival, "RSSI", rssi 390 if frame == None or (ch is not None and ch != self._channel): 391 #TODO this maybe should be an error condition, instead of ignored? 392 print("ZEP parsing issue (bytes length={0}, channel={1}).".format(len(frame) if frame is not None else None, ch)) 393 continue 394 break 395 396 if frame is None: 397 return None 398 399 #Return in a nicer dictionary format, so we don't have to reference by number indicies. 400 #Note that 0,1,2 indicies inserted twice for backwards compatibility. 401 result = {0:frame, 1:validcrc, 2:rssi, 'bytes':frame, 'validcrc':validcrc, 'rssi':rssi, 'dbm':None, 'location':None, 'datetime':recdtime} 402 if rssi is not None: 403 result['dbm'] = rssi - 45 #TODO tune specifically to the platform (expect antenna varriances?) 404 return result
405
406 - def jammer_on(self, channel=None):
407 ''' 408 Not yet implemented. 409 @type channel: Integer 410 @param channel: Sets the channel, optional 411 @rtype: None 412 ''' 413 self.capabilities.require(KBCapabilities.PHYJAM)
414
415 - def jammer_off(self, channel=None):
416 ''' 417 Not yet implemented. 418 @return: None 419 @rtype: None 420 ''' 421 self.capabilities.require(KBCapabilities.PHYJAM)
422