86890704fd
todo: add documentation & wireshark dissector
484 lines
14 KiB
Python
Executable file
484 lines
14 KiB
Python
Executable file
# scapy.contrib.description = TaZmen Sniffer Protocol (TZSP)
|
|
# scapy.contrib.status = loads
|
|
|
|
"""
|
|
TZSP - TaZmen Sniffer Protocol
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
:author: Thomas Tannhaeuser, hecke@naberius.de
|
|
:license: GPLv2
|
|
|
|
This module 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 2
|
|
of the License, or (at your option) any later version.
|
|
|
|
This module 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.
|
|
|
|
:description:
|
|
|
|
This module provides Scapy layers for the TZSP protocol.
|
|
|
|
references:
|
|
- https://en.wikipedia.org/wiki/TZSP
|
|
- https://web.archive.org/web/20050404125022/http://www.networkchemistry.com/support/appnotes/an001_tzsp.html # noqa: E501
|
|
|
|
:NOTES:
|
|
- to allow Scapy to dissect this layer automatically, you need to bind the TZSP layer to UDP using # noqa: E501
|
|
the default TZSP port (0x9090), e.g.
|
|
|
|
bind_layers(UDP, TZSP, sport=TZSP_PORT_DEFAULT)
|
|
bind_layers(UDP, TZSP, dport=TZSP_PORT_DEFAULT)
|
|
|
|
- packet format definition from www.networkchemistry.com is different from the one given by wikipedia # noqa: E501
|
|
- seems Wireshark implements the wikipedia protocol version (didn't dive into their code) # noqa: E501
|
|
- observed (miss)behavior of Wireshark (2.2.6)
|
|
- fails to decode RSSI & SNR using short values - only one byte taken
|
|
- SNR is labeled as silence
|
|
- WlanRadioHdrSerial is labeled as Sensor MAC
|
|
- doesn't know the packet count tag (40 / 0x28)
|
|
|
|
"""
|
|
from scapy.compat import orb
|
|
from scapy.contrib.avs import AVSWLANHeader
|
|
from scapy.error import warning, Scapy_Exception
|
|
from scapy.fields import ByteField, ShortEnumField, IntField, FieldLenField, YesNoByteField # noqa: E501
|
|
from scapy.layers.dot11 import Packet, Dot11, PrismHeader
|
|
from scapy.layers.l2 import Ether
|
|
from scapy.fields import StrLenField, ByteEnumField, ShortField, XStrLenField
|
|
from scapy.packet import Raw
|
|
|
|
|
|
TZSP_PORT_DEFAULT = 0x9090
|
|
|
|
|
|
class TZSP(Packet):
|
|
TYPE_RX_PACKET = 0x00
|
|
TYPE_TX_PACKET = 0x01
|
|
TYPE_CONFIG = 0x03
|
|
TYPE_KEEPALIVE = TYPE_NULL = 0x04
|
|
TYPE_PORT = 0x05
|
|
|
|
TYPES = {
|
|
TYPE_RX_PACKET: 'RX_PACKET',
|
|
TYPE_TX_PACKET: 'TX_PACKET',
|
|
TYPE_CONFIG: 'CONFIG',
|
|
TYPE_NULL: 'KEEPALIVE/NULL',
|
|
TYPE_PORT: 'PORT',
|
|
}
|
|
|
|
ENCAPSULATED_ETHERNET = 0x01
|
|
ENCAPSULATED_IEEE_802_11 = 0x12
|
|
ENCAPSULATED_PRISM_HEADER = 0x77
|
|
ENCAPSULATED_WLAN_AVS = 0x7f
|
|
|
|
ENCAPSULATED_PROTOCOLS = {
|
|
ENCAPSULATED_ETHERNET: 'ETHERNET',
|
|
ENCAPSULATED_IEEE_802_11: 'IEEE 802.11',
|
|
ENCAPSULATED_PRISM_HEADER: 'PRISM HEADER',
|
|
ENCAPSULATED_WLAN_AVS: 'WLAN AVS'
|
|
}
|
|
|
|
ENCAPSULATED_PROTOCOL_CLASSES = {
|
|
ENCAPSULATED_ETHERNET: Ether,
|
|
ENCAPSULATED_IEEE_802_11: Dot11,
|
|
ENCAPSULATED_PRISM_HEADER: PrismHeader,
|
|
ENCAPSULATED_WLAN_AVS: AVSWLANHeader
|
|
}
|
|
|
|
fields_desc = [
|
|
ByteField('version', 0x01),
|
|
ByteEnumField('type', TYPE_RX_PACKET, TYPES),
|
|
ShortEnumField('encapsulated_protocol', ENCAPSULATED_ETHERNET, ENCAPSULATED_PROTOCOLS) # noqa: E501
|
|
]
|
|
|
|
def get_encapsulated_payload_class(self):
|
|
"""
|
|
get the class that holds the encapsulated payload of the TZSP packet
|
|
:return: class representing the payload, Raw() on error
|
|
"""
|
|
|
|
try:
|
|
return TZSP.ENCAPSULATED_PROTOCOL_CLASSES[self.encapsulated_protocol] # noqa: E501
|
|
except KeyError:
|
|
warning(
|
|
'unknown or invalid encapsulation type (%i) - returning payload as raw()' % self.encapsulated_protocol) # noqa: E501
|
|
return Raw
|
|
|
|
def guess_payload_class(self, payload):
|
|
if self.type == TZSP.TYPE_KEEPALIVE:
|
|
if len(payload):
|
|
warning('payload (%i bytes) in KEEPALIVE/NULL packet' % len(payload)) # noqa: E501
|
|
return Raw
|
|
else:
|
|
return _tzsp_guess_next_tag(payload)
|
|
|
|
def get_encapsulated_payload(self):
|
|
|
|
has_encapsulated_data = self.type == TZSP.TYPE_RX_PACKET or self.type == TZSP.TYPE_TX_PACKET # noqa: E501
|
|
|
|
if has_encapsulated_data:
|
|
end_tag_lyr = self.payload.getlayer(TZSPTagEnd)
|
|
if end_tag_lyr:
|
|
return end_tag_lyr.payload
|
|
else:
|
|
return None
|
|
|
|
|
|
def _tzsp_handle_unknown_tag(payload, tag_type):
|
|
|
|
payload_len = len(payload)
|
|
|
|
if payload_len < 2:
|
|
warning('invalid or unknown tag type (%i) and too short packet - treat remaining data as Raw' % tag_type) # noqa: E501
|
|
return Raw
|
|
|
|
tag_data_length = orb(payload[1])
|
|
|
|
tag_data_fits_in_payload = (tag_data_length + 2) <= payload_len
|
|
if not tag_data_fits_in_payload:
|
|
warning('invalid or unknown tag type (%i) and too short packet - treat remaining data as Raw' % tag_type) # noqa: E501
|
|
return Raw
|
|
|
|
warning('invalid or unknown tag type (%i)' % tag_type)
|
|
|
|
return TZSPTagUnknown
|
|
|
|
|
|
def _tzsp_guess_next_tag(payload):
|
|
"""
|
|
:return: class representing the next tag, Raw on error, None on missing payload # noqa: E501
|
|
"""
|
|
|
|
if not payload:
|
|
warning('missing payload')
|
|
return None
|
|
|
|
tag_type = orb(payload[0])
|
|
|
|
try:
|
|
tag_class_definition = _TZSP_TAG_CLASSES[tag_type]
|
|
|
|
except KeyError:
|
|
|
|
return _tzsp_handle_unknown_tag(payload, tag_type)
|
|
|
|
if type(tag_class_definition) is not dict:
|
|
return tag_class_definition
|
|
|
|
try:
|
|
length = orb(payload[1])
|
|
except IndexError:
|
|
length = None
|
|
|
|
if not length:
|
|
warning('no tag length given - packet to short')
|
|
return Raw
|
|
|
|
try:
|
|
return tag_class_definition[length]
|
|
except KeyError:
|
|
warning('invalid tag length {} for tag type {}'.format(length, tag_type)) # noqa: E501
|
|
return Raw
|
|
|
|
|
|
class _TZSPTag(Packet):
|
|
TAG_TYPE_PADDING = 0x00
|
|
TAG_TYPE_END = 0x01
|
|
TAG_TYPE_RAW_RSSI = 0x0a
|
|
TAG_TYPE_SNR = 0x0b
|
|
TAG_TYPE_DATA_RATE = 0x0c
|
|
TAG_TYPE_TIMESTAMP = 0x0d
|
|
TAG_TYPE_CONTENTION_FREE = 0x0f
|
|
TAG_TYPE_DECRYPTED = 0x10
|
|
TAG_TYPE_FCS_ERROR = 0x11
|
|
TAG_TYPE_RX_CHANNEL = 0x12
|
|
TAG_TYPE_PACKET_COUNT = 0x28
|
|
TAG_TYPE_RX_FRAME_LENGTH = 0x29
|
|
TAG_TYPE_WLAN_RADIO_HDR_SERIAL = 0x3c
|
|
|
|
TAG_TYPES = {
|
|
TAG_TYPE_PADDING: 'PADDING',
|
|
TAG_TYPE_END: 'END',
|
|
TAG_TYPE_RAW_RSSI: 'RAW_RSSI',
|
|
TAG_TYPE_SNR: 'SNR',
|
|
TAG_TYPE_DATA_RATE: 'DATA_RATE',
|
|
TAG_TYPE_TIMESTAMP: 'TIMESTAMP',
|
|
TAG_TYPE_CONTENTION_FREE: 'CONTENTION_FREE',
|
|
TAG_TYPE_DECRYPTED: 'DECRYPTED',
|
|
TAG_TYPE_FCS_ERROR: 'FCS_ERROR',
|
|
TAG_TYPE_RX_CHANNEL: 'RX_CHANNEL',
|
|
TAG_TYPE_PACKET_COUNT: 'PACKET_COUNT',
|
|
TAG_TYPE_RX_FRAME_LENGTH: 'RX_FRAME_LENGTH',
|
|
TAG_TYPE_WLAN_RADIO_HDR_SERIAL: 'WLAN_RADIO_HDR_SERIAL'
|
|
}
|
|
|
|
def guess_payload_class(self, payload):
|
|
return _tzsp_guess_next_tag(payload)
|
|
|
|
|
|
class TZSPStructureException(Scapy_Exception):
|
|
pass
|
|
|
|
|
|
class TZSPTagPadding(_TZSPTag):
|
|
"""
|
|
padding tag (should be ignored)
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_PADDING, _TZSPTag.TAG_TYPES),
|
|
]
|
|
|
|
|
|
class TZSPTagEnd(Packet):
|
|
"""
|
|
last tag
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_END, _TZSPTag.TAG_TYPES),
|
|
]
|
|
|
|
def guess_payload_class(self, payload):
|
|
"""
|
|
the type of the payload encapsulation is given be the outer TZSP layers attribute encapsulation_protocol # noqa: E501
|
|
"""
|
|
|
|
under_layer = self.underlayer
|
|
tzsp_header = None
|
|
|
|
while under_layer:
|
|
if isinstance(under_layer, TZSP):
|
|
tzsp_header = under_layer
|
|
break
|
|
under_layer = under_layer.underlayer
|
|
|
|
if tzsp_header:
|
|
|
|
return tzsp_header.get_encapsulated_payload_class()
|
|
else:
|
|
raise TZSPStructureException('missing parent TZSP header')
|
|
|
|
|
|
class TZSPTagRawRSSIByte(_TZSPTag):
|
|
"""
|
|
relative received signal strength - signed byte value
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_RAW_RSSI, _TZSPTag.TAG_TYPES),
|
|
ByteField('len', 1),
|
|
ByteField('raw_rssi', 0)
|
|
]
|
|
|
|
|
|
class TZSPTagRawRSSIShort(_TZSPTag):
|
|
"""
|
|
relative received signal strength - signed short value
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_RAW_RSSI, _TZSPTag.TAG_TYPES),
|
|
ByteField('len', 2),
|
|
ShortField('raw_rssi', 0)
|
|
]
|
|
|
|
|
|
class TZSPTagSNRByte(_TZSPTag):
|
|
"""
|
|
signal noise ratio - signed byte value
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_SNR, _TZSPTag.TAG_TYPES),
|
|
ByteField('len', 1),
|
|
ByteField('snr', 0)
|
|
]
|
|
|
|
|
|
class TZSPTagSNRShort(_TZSPTag):
|
|
"""
|
|
signal noise ratio - signed short value
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_SNR, _TZSPTag.TAG_TYPES),
|
|
ByteField('len', 2),
|
|
ShortField('snr', 0)
|
|
]
|
|
|
|
|
|
class TZSPTagDataRate(_TZSPTag):
|
|
"""
|
|
wireless link data rate
|
|
"""
|
|
DATA_RATE_UNKNOWN = 0x00
|
|
DATA_RATE_1 = 0x02
|
|
DATA_RATE_2 = 0x04
|
|
DATA_RATE_5_5 = 0x0B
|
|
DATA_RATE_6 = 0x0C
|
|
DATA_RATE_9 = 0x12
|
|
DATA_RATE_11 = 0x16
|
|
DATA_RATE_12 = 0x18
|
|
DATA_RATE_18 = 0x24
|
|
DATA_RATE_22 = 0x2C
|
|
DATA_RATE_24 = 0x30
|
|
DATA_RATE_33 = 0x42
|
|
DATA_RATE_36 = 0x48
|
|
DATA_RATE_48 = 0x60
|
|
DATA_RATE_54 = 0x6C
|
|
DATA_RATE_LEGACY_1 = 0x0A
|
|
DATA_RATE_LEGACY_2 = 0x14
|
|
DATA_RATE_LEGACY_5_5 = 0x37
|
|
DATA_RATE_LEGACY_11 = 0x6E
|
|
|
|
DATA_RATES = {
|
|
DATA_RATE_UNKNOWN: 'unknown',
|
|
DATA_RATE_1: '1 MB/s',
|
|
DATA_RATE_2: '2 MB/s',
|
|
DATA_RATE_5_5: '5.5 MB/s',
|
|
DATA_RATE_6: '6 MB/s',
|
|
DATA_RATE_9: '9 MB/s',
|
|
DATA_RATE_11: '11 MB/s',
|
|
DATA_RATE_12: '12 MB/s',
|
|
DATA_RATE_18: '18 MB/s',
|
|
DATA_RATE_22: '22 MB/s',
|
|
DATA_RATE_24: '24 MB/s',
|
|
DATA_RATE_33: '33 MB/s',
|
|
DATA_RATE_36: '36 MB/s',
|
|
DATA_RATE_48: '48 MB/s',
|
|
DATA_RATE_54: '54 MB/s',
|
|
DATA_RATE_LEGACY_1: '1 MB/s (legacy)',
|
|
DATA_RATE_LEGACY_2: '2 MB/s (legacy)',
|
|
DATA_RATE_LEGACY_5_5: '5.5 MB/s (legacy)',
|
|
DATA_RATE_LEGACY_11: '11 MB/s (legacy)',
|
|
}
|
|
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_DATA_RATE, _TZSPTag.TAG_TYPES),
|
|
ByteField('len', 1),
|
|
ByteEnumField('data_rate', DATA_RATE_UNKNOWN, DATA_RATES)
|
|
]
|
|
|
|
|
|
class TZSPTagTimestamp(_TZSPTag):
|
|
"""
|
|
MAC receive timestamp
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_TIMESTAMP, _TZSPTag.TAG_TYPES),
|
|
ByteField('len', 4),
|
|
IntField('timestamp', 0)
|
|
]
|
|
|
|
|
|
class TZSPTagContentionFree(_TZSPTag):
|
|
"""
|
|
packet received in contention free period
|
|
"""
|
|
NO = 0x00
|
|
YES = 0x01
|
|
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_CONTENTION_FREE, _TZSPTag.TAG_TYPES), # noqa: E501
|
|
ByteField('len', 1),
|
|
YesNoByteField('contention_free', NO)
|
|
]
|
|
|
|
|
|
class TZSPTagDecrypted(_TZSPTag):
|
|
"""
|
|
packet was decrypted
|
|
"""
|
|
YES = 0x00
|
|
NO = 0x01
|
|
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_DECRYPTED, _TZSPTag.TAG_TYPES),
|
|
ByteField('len', 1),
|
|
YesNoByteField('decrypted', NO, config={'yes': YES, 'no': (NO, 0xff)})
|
|
]
|
|
|
|
|
|
class TZSPTagError(_TZSPTag):
|
|
"""
|
|
frame checksum error
|
|
"""
|
|
NO = 0x00
|
|
YES = 0x01
|
|
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_FCS_ERROR, _TZSPTag.TAG_TYPES),
|
|
ByteField('len', 1),
|
|
YesNoByteField('fcs_error', NO, config={'no': NO, 'yes': YES, 'reserved': (YES + 1, 0xff)}) # noqa: E501
|
|
]
|
|
|
|
|
|
class TZSPTagRXChannel(_TZSPTag):
|
|
"""
|
|
channel the sensor was on while receiving the frame
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_RX_CHANNEL, _TZSPTag.TAG_TYPES), # noqa: E501
|
|
ByteField('len', 1),
|
|
ByteField('rx_channel', 0)
|
|
]
|
|
|
|
|
|
class TZSPTagPacketCount(_TZSPTag):
|
|
"""
|
|
packet counter
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_PACKET_COUNT, _TZSPTag.TAG_TYPES), # noqa: E501
|
|
ByteField('len', 4),
|
|
IntField('packet_count', 0)
|
|
]
|
|
|
|
|
|
class TZSPTagRXFrameLength(_TZSPTag):
|
|
"""
|
|
received packet length
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_RX_FRAME_LENGTH, _TZSPTag.TAG_TYPES), # noqa: E501
|
|
ByteField('len', 2),
|
|
ShortField('rx_frame_length', 0)
|
|
]
|
|
|
|
|
|
class TZSPTagWlanRadioHdrSerial(_TZSPTag):
|
|
"""
|
|
(vendor specific) unique capture device (sensor/AP) identifier
|
|
"""
|
|
fields_desc = [
|
|
ByteEnumField('type', _TZSPTag.TAG_TYPE_WLAN_RADIO_HDR_SERIAL, _TZSPTag.TAG_TYPES), # noqa: E501
|
|
FieldLenField('len', None, length_of='sensor_id', fmt='b'),
|
|
StrLenField('sensor_id', '', length_from=lambda pkt:pkt.len)
|
|
]
|
|
|
|
|
|
class TZSPTagUnknown(_TZSPTag):
|
|
"""
|
|
unknown tag type dummy
|
|
"""
|
|
fields_desc = [
|
|
ByteField('type', 0xff),
|
|
FieldLenField('len', None, length_of='data', fmt='b'),
|
|
XStrLenField('data', '', length_from=lambda pkt: pkt.len)
|
|
]
|
|
|
|
|
|
_TZSP_TAG_CLASSES = {
|
|
_TZSPTag.TAG_TYPE_PADDING: TZSPTagPadding,
|
|
_TZSPTag.TAG_TYPE_END: TZSPTagEnd,
|
|
_TZSPTag.TAG_TYPE_RAW_RSSI: {1: TZSPTagRawRSSIByte, 2: TZSPTagRawRSSIShort}, # noqa: E501
|
|
_TZSPTag.TAG_TYPE_SNR: {1: TZSPTagSNRByte, 2: TZSPTagSNRShort},
|
|
_TZSPTag.TAG_TYPE_DATA_RATE: TZSPTagDataRate,
|
|
_TZSPTag.TAG_TYPE_TIMESTAMP: TZSPTagTimestamp,
|
|
_TZSPTag.TAG_TYPE_CONTENTION_FREE: TZSPTagContentionFree,
|
|
_TZSPTag.TAG_TYPE_DECRYPTED: TZSPTagDecrypted,
|
|
_TZSPTag.TAG_TYPE_FCS_ERROR: TZSPTagError,
|
|
_TZSPTag.TAG_TYPE_RX_CHANNEL: TZSPTagRXChannel,
|
|
_TZSPTag.TAG_TYPE_PACKET_COUNT: TZSPTagPacketCount,
|
|
_TZSPTag.TAG_TYPE_RX_FRAME_LENGTH: TZSPTagRXFrameLength,
|
|
_TZSPTag.TAG_TYPE_WLAN_RADIO_HDR_SERIAL: TZSPTagWlanRadioHdrSerial
|
|
}
|