diff --git a/doc/scapy/graphics/ble_ibeacon.png b/doc/scapy/graphics/ble_ibeacon.png new file mode 100644 index 00000000000..0f133b04343 Binary files /dev/null and b/doc/scapy/graphics/ble_ibeacon.png differ diff --git a/doc/scapy/layers/bluetooth.rst b/doc/scapy/layers/bluetooth.rst index a945f1529ff..4c56914d1d6 100644 --- a/doc/scapy/layers/bluetooth.rst +++ b/doc/scapy/layers/bluetooth.rst @@ -443,11 +443,51 @@ __ https://github.com/google/eddystone/tree/master/eddystone-url 'https://scapy.net').build_set_advertising_data()) Once :ref:`advertising has been started `, the beacon may then be -detected with the `Eddystone Validator`__ (Android): +detected with `Eddystone Validator`__ or `Beacon Locator`__ (Android): .. image:: ../graphics/ble_eddystone_url.png __ https://github.com/google/eddystone/tree/master/tools/eddystone-validator +__ https://github.com/vitas/beaconloc + +.. _adv-ibeacon: + +iBeacon +^^^^^^^ + +`iBeacon`__ is a proximity beacon protocol developed by Apple, which uses their +manufacturer-specific data field. :ref:`Apple/iBeacon framing ` +(below) describes this in more detail. + +__ https://en.wikipedia.org/wiki/IBeacon + +This example sets up a virtual iBeacon: + +.. code-block:: python3 + + # Load the contrib module for iBeacon + load_contrib('ibeacon') + + # Beacon data consists of a UUID, and two 16-bit integers: "major" and + # "minor". + # + # iBeacon sits ontop of Apple's BLE protocol. + p = Apple_BLE_Submessage()/IBeacon_Data( + uuid='fb0b57a2-8228-44cd-913a-94a122ba1206', + major=1, minor=2) + + # build_set_advertising_data() wraps an Apple_BLE_Submessage or + # Apple_BLE_Frame into a HCI_Cmd_LE_Set_Advertising_Data payload, that can + # be sent to the BLE controller. + bt.sr(p.build_set_advertising_data()) + +Once :ref:`advertising has been started `, the beacon may then be +detected with `Beacon Locator`__ (Android): + +.. image:: ../graphics/ble_ibeacon.png + +__ https://github.com/vitas/beaconloc + .. _le-adv-start: @@ -495,3 +535,104 @@ __ https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers __ https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile +.. _apple-ble: + +Apple/iBeacon broadcast frames +============================== + +.. note:: + + This describes the wire format for Apple's Bluetooth Low Energy + advertisements, based on (limited) publicly available information. It is not + specific to using Bluetooth on Apple operating systems. + +`iBeacon`__ is Apple's proximity beacon protocol. Scapy includes a contrib +module, ``ibeacon``, for working with Apple's :abbr:`BLE (Bluetooth Low Energy)` +broadcasts: + +__ https://en.wikipedia.org/wiki/IBeacon + +.. code-block:: pycon + + >>> load_contrib('ibeacon') + +:ref:`Setting up advertising for iBeacon ` (above) describes how to +broadcast a simple beacon. + +While this module is called ``ibeacon``, Apple has other "submessages" which are +also advertised within their manufacturer-specific data field, including: + + * `AirDrop`__ + * AirPlay + * AirPods + * `Handoff`__ + * Nearby + +__ https://en.wikipedia.org/wiki/AirDrop +__ https://en.wikipedia.org/wiki/OS_X_Yosemite#Continuity + +For compatibility with these other broadcasts, Apple BLE frames in Scapy are +layered on top of ``Apple_BLE_Submessage`` and ``Apple_BLE_Frame``: + + * ``HCI_Cmd_LE_Set_Advertising_Data``, ``HCI_LE_Meta_Advertising_Report``, + ``BTLE_ADV_IND``, ``BTLE_ADV_NONCONN_IND`` or ``BTLE_ADV_SCAN_IND`` contain + one or more... + * ``EIR_Hdr``, which may have a payload of one... + * ``EIR_Manufacturer_Specific_Data``, which may have a payload of one... + * ``Apple_BLE_Frame``, which contains one or more... + * ``Apple_BLE_Submessage``, which contains a payload of one... + * ``Raw`` (if not supported), or ``IBeacon_Data``. + +This module only presently supports ``IBeacon_Data`` submessages. Other +submessages are decoded as ``Raw``. + +One might sometimes see multiple submessages in a single broadcast, such as +Handoff and Nearby. This is not mandatory -- there are also Handoff-only and +Nearby-only broadcasts. + +Inspecting a raw BTLE advertisement frame from an Apple device: + +.. code-block:: python3 + + p = BTLE(hex_bytes('d6be898e4024320cfb574d5a02011a1aff4c000c0e009c6b8f40440f1583ec895148b410050318c0b525b8f7d4')) + p.show() + +Results in the output: + +.. code-block:: text + + ###[ BT4LE ]### + access_addr= 0x8e89bed6 + crc= 0xb8f7d4 + ###[ BTLE advertising header ]### + RxAdd= public + TxAdd= random + RFU= 0 + PDU_type= ADV_IND + unused= 0 + Length= 0x24 + ###[ BTLE ADV_IND ]### + AdvA= 5a:4d:57:fb:0c:32 + \data\ + |###[ EIR Header ]### + | len= 2 + | type= flags + |###[ Flags ]### + | flags= general_disc_mode+simul_le_br_edr_ctrl+simul_le_br_edr_host + |###[ EIR Header ]### + | len= 26 + | type= mfg_specific_data + |###[ EIR Manufacturer Specific Data ]### + | company_id= 0x4c + |###[ Apple BLE broadcast frame ]### + | \plist\ + | |###[ Apple BLE submessage ]### + | | subtype= handoff + | | len= 14 + | |###[ Raw ]### + | | load= '\x00\x9ck\x8f@D\x0f\x15\x83\xec\x89QH\xb4' + | |###[ Apple BLE submessage ]### + | | subtype= nearby + | | len= 5 + | |###[ Raw ]### + | | load= '\x03\x18\xc0\xb5%' diff --git a/scapy/contrib/ibeacon.py b/scapy/contrib/ibeacon.py new file mode 100644 index 00000000000..fddae43190a --- /dev/null +++ b/scapy/contrib/ibeacon.py @@ -0,0 +1,101 @@ +# -*- mode: python3; indent-tabs-mode: nil; tab-width: 4 -*- +# ibeacon.py - protocol handlers for iBeacons and other Apple devices +# +# 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 = iBeacon BLE proximity beacon +# scapy.contrib.status = loads +""" +scapy.contrib.ibeacon - Apple iBeacon Bluetooth LE proximity beacons. + +Packet format documentation can be found at at: + +* https://en.wikipedia.org/wiki/IBeacon#Packet_Structure_Byte_Map (public) +* https://developer.apple.com/ibeacon/ (official, requires license) + +""" + +from scapy.fields import ByteEnumField, LenField, PacketListField, \ + ShortField, SignedByteField, UUIDField +from scapy.layers.bluetooth import EIR_Hdr, EIR_Manufacturer_Specific_Data, \ + LowEnergyBeaconHelper +from scapy.packet import bind_layers, Packet + +APPLE_MFG = 0x004c + + +class Apple_BLE_Submessage(Packet, LowEnergyBeaconHelper): + """ + A basic Apple submessage. + """ + + name = "Apple BLE submessage" + fields_desc = [ + ByteEnumField("subtype", None, { + 0x02: "ibeacon", + 0x05: "airdrop", + 0x07: "airpods", + 0x09: "airplay_sink", + 0x0a: "airplay_src", + 0x0c: "handoff", + 0x10: "nearby", + }), + LenField("len", None, fmt="B") + ] + + def extract_padding(self, s): + # Needed to end each EIR_Element packet and make PacketListField work. + return s[:self.len], s[self.len:] + + # These methods are here in case you only want to send 1 submessage. + # It creates an Apple_BLE_Frame to wrap your (single) Apple_BLE_Submessage. + def build_frame(self): + """Wraps this submessage in a Apple_BLE_Frame.""" + return Apple_BLE_Frame(plist=[self]) + + def build_eir(self): + """See Apple_BLE_Frame.build_eir.""" + return self.build_frame().build_eir() + + +class Apple_BLE_Frame(Packet, LowEnergyBeaconHelper): + """ + The wrapper for a BLE manufacturer-specific data advertisement from Apple + devices. + + Each advertisement is composed of one or multiple submessages. + + The length of this field comes from the EIR_Hdr. + """ + name = "Apple BLE broadcast frame" + fields_desc = [ + PacketListField("plist", None, Apple_BLE_Submessage) + ] + + def build_eir(self): + """Builds a list of EIR messages to wrap this frame.""" + + return LowEnergyBeaconHelper.base_eir + [ + EIR_Hdr() / EIR_Manufacturer_Specific_Data() / self + ] + + +class IBeacon_Data(Packet): + """ + iBeacon broadcast data frame. Composed on top of an Apple_BLE_Submessage. + """ + name = "iBeacon data" + fields_desc = [ + UUIDField("uuid", None, uuid_fmt=UUIDField.FORMAT_BE), + ShortField("major", None), + ShortField("minor", None), + SignedByteField("tx_power", None), + ] + + +bind_layers(EIR_Manufacturer_Specific_Data, Apple_BLE_Frame, + company_id=APPLE_MFG) +bind_layers(Apple_BLE_Submessage, IBeacon_Data, subtype=2) diff --git a/scapy/contrib/ibeacon.uts b/scapy/contrib/ibeacon.uts new file mode 100644 index 00000000000..e5c67dbd74f --- /dev/null +++ b/scapy/contrib/ibeacon.uts @@ -0,0 +1,70 @@ +% iBeacon unit tests +# +# Type the following command to launch start the tests: +# $ test/run_tests -P "load_contrib('ibeacon')" -t scapy/contrib/ibeacon.uts + ++ iBeacon tests + += Presence check + +Apple_BLE_Frame +IBeacon_Data +Apple_BLE_Submessage + += Apple multiple submessages + +# Observed in the wild; handoff + nearby message. +# Meaning unknown. +d = hex_bytes('D6BE898E4024320CFB574D5A02011A1AFF4C000C0E009C6B8F40440F1583EC895148B410050318C0B525B8F7D4') +p = BTLE(d) + +assert len(p[Apple_BLE_Frame].plist) == 2 +assert p[Apple_BLE_Frame].plist[0].subtype == 0x0c # handoff +assert (raw(p[Apple_BLE_Frame].plist[0].payload) == + hex_bytes('009c6b8f40440f1583ec895148b4')) +assert p[Apple_BLE_Frame].plist[1].subtype == 0x10 # nearby +assert raw(p[Apple_BLE_Frame].plist[1].payload) == hex_bytes('0318c0b525') + += iBeacon (decode LE Set Advertising Data) + +# from https://en.wikipedia.org/wiki/IBeacon#Technical_details +d = hex_bytes('1E02011A1AFF4C000215FB0B57A2822844CD913A94A122BA120600010002D100') +p = HCI_Cmd_LE_Set_Advertising_Data(d) + +assert len(p[Apple_BLE_Frame].plist) == 1 +assert p[IBeacon_Data].uuid == UUID("fb0b57a2-8228-44cd-913a-94a122ba1206") +assert p[IBeacon_Data].major == 1 +assert p[IBeacon_Data].minor == 2 +assert p[IBeacon_Data].tx_power == -47 + +d2 = raw(p) +assert d == d2 + += iBeacon (encode LE Set Advertising Data) + +d = hex_bytes('1E0201021AFF4C000215FB0B57A2822844CD913A94A122BA120600010002D100') +p = Apple_BLE_Submessage()/IBeacon_Data( + uuid='fb0b57a2-8228-44cd-913a-94a122ba1206', + major=1, minor=2, tx_power=-47) + +sap = p.build_set_advertising_data()[HCI_Cmd_LE_Set_Advertising_Data] +assert d == raw(sap) + +pa = Apple_BLE_Frame(plist=[p]) +sapa = pa.build_set_advertising_data()[HCI_Cmd_LE_Set_Advertising_Data] +assert d == raw(sapa) + +# Also try to build with Submessage directly +sapa = p.build_set_advertising_data()[HCI_Cmd_LE_Set_Advertising_Data] +assert d == raw(sapa) + += iBeacon (decode advertising frame) + +# from https://en.wikipedia.org/wiki/IBeacon#Spoofing +d = hex_bytes('043E2A02010001FCED16D4EED61E0201061AFF4C000215B9407F30F5F8466EAFF925556B57FE6DEDFCD416B6B4') +p = HCI_Hdr(d) + +assert p[HCI_LE_Meta_Advertising_Report].addr == 'd6:ee:d4:16:ed:fc' +assert len(p[Apple_BLE_Frame].plist) == 1 +assert p[IBeacon_Data].uuid == UUID('b9407f30-f5f8-466e-aff9-25556b57fe6d') +