Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added doc/scapy/graphics/ble_ibeacon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
143 changes: 142 additions & 1 deletion doc/scapy/layers/bluetooth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <le-adv-start>`, 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 <apple-ble>`
(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 <le-adv-start>`, the beacon may then be
detected with `Beacon Locator`__ (Android):

.. image:: ../graphics/ble_ibeacon.png

__ https://github.com/vitas/beaconloc


.. _le-adv-start:

Expand Down Expand Up @@ -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 <adv-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%'
101 changes: 101 additions & 0 deletions scapy/contrib/ibeacon.py
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
# 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)
70 changes: 70 additions & 0 deletions scapy/contrib/ibeacon.uts
Original file line number Diff line number Diff line change
@@ -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')