diff --git a/doc/scapy/layers/bluetooth.rst b/doc/scapy/layers/bluetooth.rst index 091036dad92..a945f1529ff 100644 --- a/doc/scapy/layers/bluetooth.rst +++ b/doc/scapy/layers/bluetooth.rst @@ -384,8 +384,46 @@ Setting up advertising Changing advertisements may not take effect until advertisements have first been :ref:`stopped `. -Eddystone URL beacon -^^^^^^^^^^^^^^^^^^^^ +AltBeacon +^^^^^^^^^ + +`AltBeacon`__ is a proximity beacon protocol developed by Radius Networks. This +example sets up a virtual AltBeacon: + +__ https://github.com/AltBeacon/spec + +.. code-block:: python3 + + # Load the contrib module for AltBeacon + load_contrib('altbeacon') + + ab = AltBeacon( + id1='2f234454-cf6d-4a0f-adf2-f4911ba9ffa6', + id2=1, + id3=2, + tx_power=-59, + ) + + bt.sr(ab.build_set_advertising_data()) + +Once :ref:`advertising has been started `, the beacon may then be +detected with `Beacon Locator`__ (Android). + +.. note:: + + Beacon Locator v1.2.2 `incorrectly reports the beacon as being an + iBeacon`__, but the values are otherwise correct. + +__ https://github.com/vitas/beaconloc +__ https://github.com/vitas/beaconloc/issues/32 + +Eddystone +^^^^^^^^^ + +`Eddystone`__ is a proximity beacon protocol developed by Google. This uses an +Eddystone-specific service data field. + +__ https://github.com/google/eddystone/ This example sets up a virtual `Eddystone URL`__ beacon: diff --git a/scapy/contrib/altbeacon.py b/scapy/contrib/altbeacon.py new file mode 100644 index 00000000000..75ce3aeaf5d --- /dev/null +++ b/scapy/contrib/altbeacon.py @@ -0,0 +1,82 @@ +# -*- mode: python3; indent-tabs-mode: nil; tab-width: 4 -*- +# altbeacon.py - protocol handlers for AltBeacon +# +# This file is part of Scapy +# See http://www.secdev.org/projects/scapy for more information +# Copyright (C) Michael Farrell +# This program is published under a GPLv2 (or later) license +# +# scapy.contrib.description = AltBeacon BLE proximity beacon +# scapy.contrib.status = loads +""" +scapy.contrib.altbeacon - AltBeacon Bluetooth LE proximity beacons. + +The AltBeacon specification can be found at: https://github.com/AltBeacon/spec +""" + +from scapy.fields import ByteField, ShortField, SignedByteField, \ + StrFixedLenField +from scapy.layers.bluetooth import EIR_Hdr, EIR_Manufacturer_Specific_Data, \ + UUIDField, LowEnergyBeaconHelper +from scapy.packet import Packet + + +# When building beacon frames, one should use their own manufacturer ID. +# +# However, most software (including the AltBeacon SDK) requires explicitly +# registering particular manufacturer IDs to listen to, and the only ID used is +# that of Radius Networks (the developer of the specification). +# +# To maximise compatibility, Scapy's implementation of +# LowEnergyBeaconHelper.build_eir (for constructing frames) uses Radius +# Networks' manufacturer ID. +# +# Scapy's implementation of AltBeacon **does not** require a specific +# manufacturer ID to detect AltBeacons - it uses +# EIR_Manufacturer_Specific_Data.register_magic_payload. +RADIUS_NETWORKS_MFG = 0x0118 + + +class AltBeacon(Packet, LowEnergyBeaconHelper): + """ + AltBeacon broadcast frame type. + + https://github.com/AltBeacon/spec + """ + name = "AltBeacon" + magic = b"\xBE\xAC" + fields_desc = [ + StrFixedLenField("header", magic, len(magic)), + + # The spec says this is 20 bytes, with >=16 bytes being an + # organisational unit-specific identifier. However, the Android library + # treats this as UUID + uint16 + uint16. + UUIDField("id1", None), + + # Local identifier + ShortField("id2", None), + ShortField("id3", None), + + SignedByteField("tx_power", None), + ByteField("mfg_reserved", None), + ] + + @classmethod + def magic_check(cls, payload): + """ + Checks if the given payload is for us (starts with our magic string). + """ + return payload.startswith(cls.magic) + + def build_eir(self): + """Builds a list of EIR messages to wrap this frame.""" + + # Note: Company ID is not required by spec, but most tools only look + # for manufacturer-specific data with Radius Networks' manufacturer ID. + return LowEnergyBeaconHelper.base_eir + [ + EIR_Hdr() / EIR_Manufacturer_Specific_Data( + company_id=RADIUS_NETWORKS_MFG) / self + ] + + +EIR_Manufacturer_Specific_Data.register_magic_payload(AltBeacon) diff --git a/scapy/contrib/altbeacon.uts b/scapy/contrib/altbeacon.uts new file mode 100644 index 00000000000..1f17cb08bce --- /dev/null +++ b/scapy/contrib/altbeacon.uts @@ -0,0 +1,94 @@ +% AltBeacon unit tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('altbeacon')" -t scapy/contrib/altbeacon.uts +# +# AltBeaconParser tests adapted from: +# https://github.com/AltBeacon/android-beacon-library/blob/master/lib/src/test/java/org/altbeacon/beacon/AltBeaconParserTest.java + ++ AltBeacon tests + += Setup + +def next_eir(p): + return EIR_Hdr(p[Padding]) + += Presence check + +AltBeacon + += AltBeaconParserTest.testRecognizeBeacon + +d = hex_bytes('02011a1bff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600010002c509') +p = EIR_Hdr(d) + +# First is a flags header +assert EIR_Flags in p + +# Then the AltBeacon +p = next_eir(p) +assert p[EIR_Manufacturer_Specific_Data].company_id == RADIUS_NETWORKS_MFG +assert p[AltBeacon].mfg_reserved == 9 + + += AltBeaconParserTest.testDetectsDaveMHardwareBeacon + +d = hex_bytes('02011a1bff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600050003be020e09526164426561636f6e20555342020a03000000000000000000000000') +p = EIR_Hdr(d) + +# First is Flags +assert EIR_Flags in p + +# Then the AltBeacon +p = next_eir(p) +assert p[EIR_Manufacturer_Specific_Data].company_id == RADIUS_NETWORKS_MFG +assert AltBeacon in p + +# Then CompleteLocalName +p = next_eir(p) +assert p[EIR_CompleteLocalName].local_name == b'RadBeacon USB' + +# Then TX_Power_Level +p = next_eir(p) +assert p[EIR_TX_Power_Level].level == 3 + += AltBeaconParserTest.testParseWrongFormatReturnsNothing + +d = hex_bytes('02011a1aff1801ffff2f234454cf6d4a0fadf2f4911ba9ffa600010002c509') +p = EIR_Hdr(d) + +# First is Flags +assert EIR_Flags in p + +# Then the EIR_Manufacturer_Specific_Data +p = next_eir(p) +assert p[EIR_Manufacturer_Specific_Data].company_id == RADIUS_NETWORKS_MFG +assert AltBeacon not in p + += AltBeaconParserTest.testParsesBeaconMissingDataField + +d = hex_bytes('02011a1aff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600010002c50000') +p = EIR_Hdr(d) + +# First is Flags +assert EIR_Flags in p + +# Then the EIR_Manufacturer_Specific_Data +p = next_eir(p) +assert p[EIR_Manufacturer_Specific_Data].company_id == RADIUS_NETWORKS_MFG +assert p[AltBeacon].id1 == uuid.UUID('2f234454-cf6d-4a0f-adf2-f4911ba9ffa6') +assert p[AltBeacon].id2 == 1 +assert p[AltBeacon].id3 == 2 +assert p[AltBeacon].tx_power == -59 + += Build EIR + +p = AltBeacon( + id1=uuid.UUID('2f234454-cf6d-4a0f-adf2-f4911ba9ffa6'), + id2=1, + id3=2, + tx_power=-59, +) + +d = raw(p.build_eir()[-1]) +assert d == hex_bytes('1bff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600010002c500') diff --git a/scapy/contrib/eddystone.py b/scapy/contrib/eddystone.py index d6652e25a52..62406697e23 100644 --- a/scapy/contrib/eddystone.py +++ b/scapy/contrib/eddystone.py @@ -6,20 +6,29 @@ # Copyright (C) Michael Farrell # This program is published under a GPLv2 (or later) license # -# The Eddystone specification can be found at: -# https://github.com/google/eddystone/blob/master/protocol-specification.md -# # scapy.contrib.description = Eddystone BLE proximity beacon # scapy.contrib.status = loads +""" +scapy.contrib.eddystone - Google Eddystone Bluetooth LE proximity beacons. + +The Eddystone specification can be found at: +https://github.com/google/eddystone/blob/master/protocol-specification.md + +These beacons are used as building blocks for other systems: + +* Google's Physical Web +* RuuviTag +* Waze Beacons + +""" from scapy.compat import orb -from scapy.packet import bind_layers, Packet from scapy.fields import IntField, SignedByteField, StrField, BitField, \ StrFixedLenField, ShortField, FixedPointField, ByteEnumField from scapy.layers.bluetooth import EIR_Hdr, EIR_ServiceData16BitUUID, \ - EIR_Flags, EIR_CompleteList16BitServiceUUIDs, HCI_Hdr, HCI_Command_Hdr, \ - HCI_Cmd_LE_Set_Advertising_Data, HCI_LE_Meta_Advertising_Report + EIR_CompleteList16BitServiceUUIDs, LowEnergyBeaconHelper from scapy.modules import six +from scapy.packet import bind_layers, Packet EDDYSTONE_UUID = 0xfeaa @@ -93,8 +102,12 @@ def any2i(self, pkt, x): return x -class Eddystone_Frame(Packet): - # https://github.com/google/eddystone/blob/master/protocol-specification.md +class Eddystone_Frame(Packet, LowEnergyBeaconHelper): + """ + The base Eddystone frame on which all Eddystone messages are built. + + https://github.com/google/eddystone/blob/master/protocol-specification.md + """ name = "Eddystone Frame" fields_desc = [ BitField("type", None, 4), @@ -104,36 +117,19 @@ class Eddystone_Frame(Packet): def build_eir(self): """Builds a list of EIR messages to wrap this frame.""" - return [ - EIR_Hdr() / EIR_Flags(flags=[ - "general_disc_mode", "br_edr_not_supported"]), + return LowEnergyBeaconHelper.base_eir + [ EIR_Hdr() / EIR_CompleteList16BitServiceUUIDs(svc_uuids=[ EDDYSTONE_UUID]), EIR_Hdr() / EIR_ServiceData16BitUUID() / self ] - def build_advertising_report(self): - """Builds HCI_LE_Meta_Advertising_Report containing this frame.""" - - return HCI_LE_Meta_Advertising_Report( - type=0, # Undirected - atype=1, # Random address - data=self.build_eir() - ) - - def build_set_advertising_data(self): - """Builds a HCI_Cmd_LE_Set_Advertising_Data containing this frame. - - This includes the HCI_Hdr and HCI_Command_Hdr layers. - """ - - return HCI_Hdr() / HCI_Command_Hdr() / HCI_Cmd_LE_Set_Advertising_Data( - data=self.build_eir() - ) - class Eddystone_UID(Packet): - # https://github.com/google/eddystone/tree/master/eddystone-uid + """ + An Eddystone type for transmitting a unique identifier. + + https://github.com/google/eddystone/tree/master/eddystone-uid + """ name = "Eddystone UID" fields_desc = [ SignedByteField("tx_power", 0), @@ -144,7 +140,11 @@ class Eddystone_UID(Packet): class Eddystone_URL(Packet): - # https://github.com/google/eddystone/tree/master/eddystone-url + """ + An Eddystone type for transmitting a URL (to a web page). + + https://github.com/google/eddystone/tree/master/eddystone-url + """ name = "Eddystone URL" fields_desc = [ SignedByteField("tx_power", 0), @@ -174,7 +174,11 @@ def from_url(url): class Eddystone_TLM(Packet): - # https://github.com/google/eddystone/tree/master/eddystone-tlm + """ + An Eddystone type for transmitting beacon telemetry information. + + https://github.com/google/eddystone/tree/master/eddystone-tlm + """ name = "Eddystone TLM" fields_desc = [ ByteEnumField("version", None, { @@ -185,7 +189,11 @@ class Eddystone_TLM(Packet): class Eddystone_TLM_Unencrypted(Packet): - # https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-plain.md + """ + A subtype of Eddystone-TLM for transmitting telemetry in unencrypted form. + + https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-plain.md + """ name = "Eddystone TLM (Unencrypted)" fields_desc = [ ShortField("batt_mv", 0), @@ -196,7 +204,13 @@ class Eddystone_TLM_Unencrypted(Packet): class Eddystone_TLM_Encrypted(Packet): - # https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-encrypted.md + """ + A subtype of Eddystone-TLM for transmitting telemetry in encrypted form. + + This implementation does not support decrypting this data. + + https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-encrypted.md + """ name = "Eddystone TLM (Encrypted)" fields_desc = [ StrFixedLenField("etlm", None, 12), @@ -206,7 +220,13 @@ class Eddystone_TLM_Encrypted(Packet): class Eddystone_EID(Packet): - # https://github.com/google/eddystone/tree/master/eddystone-eid + """ + An Eddystone type for transmitting encrypted, ephemeral identifiers. + + This implementation does not support decrypting this data. + + https://github.com/google/eddystone/tree/master/eddystone-eid + """ name = "Eddystone EID" fields_desc = [ SignedByteField("tx_power", 0), diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index d84a59b321b..2c328aa7f32 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -701,6 +701,56 @@ class EIR_Manufacturer_Specific_Data(EIR_Element): XLEShortField("company_id", None), ] + registered_magic_payloads = {} + + @classmethod + def register_magic_payload(cls, payload_cls, magic_check=None): + """ + Registers a payload type that uses magic data. + + Traditional payloads require registration of a Bluetooth Company ID + (requires company membership of the Bluetooth SIG), or a Bluetooth + Short UUID (requires a once-off payment). + + There are alternatives which don't require registration (such as + 128-bit UUIDs), but the biggest consumer of energy in a beacon is the + radio -- so the energy consumption of a beacon is proportional to the + number of bytes in a beacon frame. + + Some beacon formats side-step this issue by using the Company ID of + their beacon hardware manufacturer, and adding a "magic data sequence" + at the start of the Manufacturer Specific Data field. + + Examples of this are AltBeacon and GeoBeacon. + + For an example of this method in use, see ``scapy.contrib.altbeacon``. + + :param Type[Packet] payload_cls: + A reference to a Packet subclass to register as a payload. + :param Callable[[bytes], bool] magic_check: + (optional) callable to use to if a payload should be associated + with this type. If not supplied, ``payload_cls.magic_check`` is + used instead. + :raises TypeError: If ``magic_check`` is not specified, + and ``payload_cls.magic_check`` is not implemented. + """ + if magic_check is None: + if hasattr(payload_cls, "magic_check"): + magic_check = payload_cls.magic_check + else: + raise TypeError("magic_check not specified, and {} has no " + "attribute magic_check".format(payload_cls)) + + cls.registered_magic_payloads[payload_cls] = magic_check + + def default_payload_class(self, payload): + for cls, check in six.iteritems( + EIR_Manufacturer_Specific_Data.registered_magic_payloads): + if check(payload): + return cls + + return Packet.default_payload_class(self, payload) + def extract_padding(self, s): # Needed to end each EIR_Element packet and make PacketListField work. plen = EIR_Element.length_from(self) - 2 @@ -1192,6 +1242,61 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(SM_Hdr, SM_Signing_Information, sm_command=0x0a) +########### +# Helpers # +########### + +class LowEnergyBeaconHelper: + """ + Helpers for building packets for Bluetooth Low Energy Beacons. + + Implementors provide a :meth:`build_eir` implementation. + + This is designed to be used as a mix-in -- see + ``scapy.contrib.eddystone`` and ``scapy.contrib.ibeacon`` for examples. + """ + + # Basic flags that should be used by most beacons. + base_eir = [EIR_Hdr() / EIR_Flags(flags=[ + "general_disc_mode", "br_edr_not_supported"]), ] + + def build_eir(self): + """ + Builds a list of EIR messages to wrap this frame. + + Users of this helper must implement this method. + + :returns: List of HCI_Hdr with payloads that describe this beacon type + :rtype: list[HCI_Hdr] + """ + raise NotImplementedError("build_eir") + + def build_advertising_report(self): + """ + Builds a HCI_LE_Meta_Advertising_Report containing this frame. + + :rtype: HCI_LE_Meta_Advertising_Report + """ + + return HCI_LE_Meta_Advertising_Report( + type=0, # Undirected + atype=1, # Random address + data=self.build_eir() + ) + + def build_set_advertising_data(self): + """Builds a HCI_Cmd_LE_Set_Advertising_Data containing this frame. + + This includes the :class:`HCI_Hdr` and :class:`HCI_Command_Hdr` layers. + + :rtype: HCI_Hdr + """ + + return HCI_Hdr() / HCI_Command_Hdr() / HCI_Cmd_LE_Set_Advertising_Data( + data=self.build_eir() + ) + + ########### # Sockets # ########### diff --git a/test/bluetooth.uts b/test/bluetooth.uts index 3d22a338351..9dbad26056d 100644 --- a/test/bluetooth.uts +++ b/test/bluetooth.uts @@ -152,6 +152,54 @@ scapy_packet = HCI_Hdr(scan_resp_raw_data) assert(raw(scapy_packet[EIR_Manufacturer_Specific_Data].payload) == b'\x00_B31147D2461\xfc\x00\x03\x0c\x00\x00') assert(scapy_packet[EIR_Manufacturer_Specific_Data].company_id == 0x154) += Parse EIR_Manufacturer_Specific_Data with magic + +class ScapyManufacturerPacket(Packet): + magic = b'SCAPY!' + fields_desc = [ + StrFixedLenField("header", magic, len(magic)), + ShortField("x", None), + ] + +class ScapyManufacturerPacket2(Packet): + magic = b'!SCAPY' + fields_desc = [ + StrFixedLenField("header", magic, len(magic)), + ShortField("y", None), + ] + @classmethod + def magic_check(cls, payload): + return payload.startswith(cls.magic) + +EIR_Manufacturer_Specific_Data.register_magic_payload( + ScapyManufacturerPacket, lambda p: p.startswith(ScapyManufacturerPacket.magic)) +EIR_Manufacturer_Specific_Data.register_magic_payload(ScapyManufacturerPacket2) + +# Test decode +p = EIR_Hdr(b'\x0b\xff\xff\xffSCAPY!\xab\x12') + +p.show() +assert p[EIR_Manufacturer_Specific_Data].company_id == 0xffff +assert p[ScapyManufacturerPacket].x == 0xab12 + +p = EIR_Hdr(b'\x0b\xff\xff\xff!SCAPY\x12\x34') + +p.show() +assert p[EIR_Manufacturer_Specific_Data].company_id == 0xffff +assert p[ScapyManufacturerPacket2].y == 0x1234 + +# Test encode +p = EIR_Hdr()/EIR_Manufacturer_Specific_Data(company_id=0xffff)/ScapyManufacturerPacket(x=0x5678) +assert raw(p) == b'\x0b\xff\xff\xffSCAPY!\x56\x78' + +# Test bad setup +try: + EIR_Manufacturer_Specific_Data.register_magic_payload(conf.raw_layer) +except TypeError: + pass +else: + assert False, "expected exception" + = Parse EIR_ServiceData16BitUUID d = hex_bytes("043e1902010001abcdef7da97f0d020102030350fe051650fee6c2ac")