Skip to content

Commit 05f74b1

Browse files
micolousgpotter2
authored andcommitted
ibeacon: New layer!
- Decodes iBeacon as a submessage of Apple's BLE broadcast frame format. Does not implement support for other types of Apple BLE broadcasts. - Adds tests and example documentation.
1 parent 7c2c869 commit 05f74b1

File tree

4 files changed

+313
-1
lines changed

4 files changed

+313
-1
lines changed

doc/scapy/graphics/ble_ibeacon.png

8.23 KB
Loading

doc/scapy/layers/bluetooth.rst

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,11 +443,51 @@ __ https://github.com/google/eddystone/tree/master/eddystone-url
443443
'https://scapy.net').build_set_advertising_data())
444444
445445
Once :ref:`advertising has been started <le-adv-start>`, the beacon may then be
446-
detected with the `Eddystone Validator`__ (Android):
446+
detected with `Eddystone Validator`__ or `Beacon Locator`__ (Android):
447447

448448
.. image:: ../graphics/ble_eddystone_url.png
449449

450450
__ https://github.com/google/eddystone/tree/master/tools/eddystone-validator
451+
__ https://github.com/vitas/beaconloc
452+
453+
.. _adv-ibeacon:
454+
455+
iBeacon
456+
^^^^^^^
457+
458+
`iBeacon`__ is a proximity beacon protocol developed by Apple, which uses their
459+
manufacturer-specific data field. :ref:`Apple/iBeacon framing <apple-ble>`
460+
(below) describes this in more detail.
461+
462+
__ https://en.wikipedia.org/wiki/IBeacon
463+
464+
This example sets up a virtual iBeacon:
465+
466+
.. code-block:: python3
467+
468+
# Load the contrib module for iBeacon
469+
load_contrib('ibeacon')
470+
471+
# Beacon data consists of a UUID, and two 16-bit integers: "major" and
472+
# "minor".
473+
#
474+
# iBeacon sits ontop of Apple's BLE protocol.
475+
p = Apple_BLE_Submessage()/IBeacon_Data(
476+
uuid='fb0b57a2-8228-44cd-913a-94a122ba1206',
477+
major=1, minor=2)
478+
479+
# build_set_advertising_data() wraps an Apple_BLE_Submessage or
480+
# Apple_BLE_Frame into a HCI_Cmd_LE_Set_Advertising_Data payload, that can
481+
# be sent to the BLE controller.
482+
bt.sr(p.build_set_advertising_data())
483+
484+
Once :ref:`advertising has been started <le-adv-start>`, the beacon may then be
485+
detected with `Beacon Locator`__ (Android):
486+
487+
.. image:: ../graphics/ble_ibeacon.png
488+
489+
__ https://github.com/vitas/beaconloc
490+
451491

452492
.. _le-adv-start:
453493

@@ -495,3 +535,104 @@ __ https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers
495535

496536
__ https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile
497537

538+
.. _apple-ble:
539+
540+
Apple/iBeacon broadcast frames
541+
==============================
542+
543+
.. note::
544+
545+
This describes the wire format for Apple's Bluetooth Low Energy
546+
advertisements, based on (limited) publicly available information. It is not
547+
specific to using Bluetooth on Apple operating systems.
548+
549+
`iBeacon`__ is Apple's proximity beacon protocol. Scapy includes a contrib
550+
module, ``ibeacon``, for working with Apple's :abbr:`BLE (Bluetooth Low Energy)`
551+
broadcasts:
552+
553+
__ https://en.wikipedia.org/wiki/IBeacon
554+
555+
.. code-block:: pycon
556+
557+
>>> load_contrib('ibeacon')
558+
559+
:ref:`Setting up advertising for iBeacon <adv-ibeacon>` (above) describes how to
560+
broadcast a simple beacon.
561+
562+
While this module is called ``ibeacon``, Apple has other "submessages" which are
563+
also advertised within their manufacturer-specific data field, including:
564+
565+
* `AirDrop`__
566+
* AirPlay
567+
* AirPods
568+
* `Handoff`__
569+
* Nearby
570+
571+
__ https://en.wikipedia.org/wiki/AirDrop
572+
__ https://en.wikipedia.org/wiki/OS_X_Yosemite#Continuity
573+
574+
For compatibility with these other broadcasts, Apple BLE frames in Scapy are
575+
layered on top of ``Apple_BLE_Submessage`` and ``Apple_BLE_Frame``:
576+
577+
* ``HCI_Cmd_LE_Set_Advertising_Data``, ``HCI_LE_Meta_Advertising_Report``,
578+
``BTLE_ADV_IND``, ``BTLE_ADV_NONCONN_IND`` or ``BTLE_ADV_SCAN_IND`` contain
579+
one or more...
580+
* ``EIR_Hdr``, which may have a payload of one...
581+
* ``EIR_Manufacturer_Specific_Data``, which may have a payload of one...
582+
* ``Apple_BLE_Frame``, which contains one or more...
583+
* ``Apple_BLE_Submessage``, which contains a payload of one...
584+
* ``Raw`` (if not supported), or ``IBeacon_Data``.
585+
586+
This module only presently supports ``IBeacon_Data`` submessages. Other
587+
submessages are decoded as ``Raw``.
588+
589+
One might sometimes see multiple submessages in a single broadcast, such as
590+
Handoff and Nearby. This is not mandatory -- there are also Handoff-only and
591+
Nearby-only broadcasts.
592+
593+
Inspecting a raw BTLE advertisement frame from an Apple device:
594+
595+
.. code-block:: python3
596+
597+
p = BTLE(hex_bytes('d6be898e4024320cfb574d5a02011a1aff4c000c0e009c6b8f40440f1583ec895148b410050318c0b525b8f7d4'))
598+
p.show()
599+
600+
Results in the output:
601+
602+
.. code-block:: text
603+
604+
###[ BT4LE ]###
605+
access_addr= 0x8e89bed6
606+
crc= 0xb8f7d4
607+
###[ BTLE advertising header ]###
608+
RxAdd= public
609+
TxAdd= random
610+
RFU= 0
611+
PDU_type= ADV_IND
612+
unused= 0
613+
Length= 0x24
614+
###[ BTLE ADV_IND ]###
615+
AdvA= 5a:4d:57:fb:0c:32
616+
\data\
617+
|###[ EIR Header ]###
618+
| len= 2
619+
| type= flags
620+
|###[ Flags ]###
621+
| flags= general_disc_mode+simul_le_br_edr_ctrl+simul_le_br_edr_host
622+
|###[ EIR Header ]###
623+
| len= 26
624+
| type= mfg_specific_data
625+
|###[ EIR Manufacturer Specific Data ]###
626+
| company_id= 0x4c
627+
|###[ Apple BLE broadcast frame ]###
628+
| \plist\
629+
| |###[ Apple BLE submessage ]###
630+
| | subtype= handoff
631+
| | len= 14
632+
| |###[ Raw ]###
633+
| | load= '\x00\x9ck\x8f@D\x0f\x15\x83\xec\x89QH\xb4'
634+
| |###[ Apple BLE submessage ]###
635+
| | subtype= nearby
636+
| | len= 5
637+
| |###[ Raw ]###
638+
| | load= '\x03\x18\xc0\xb5%'

scapy/contrib/ibeacon.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# -*- mode: python3; indent-tabs-mode: nil; tab-width: 4 -*-
2+
# ibeacon.py - protocol handlers for iBeacons and other Apple devices
3+
#
4+
# This file is part of Scapy
5+
# See http://www.secdev.org/projects/scapy for more information
6+
# Copyright (C) Michael Farrell <[email protected]>
7+
# This program is published under a GPLv2 (or later) license
8+
#
9+
# scapy.contrib.description = iBeacon BLE proximity beacon
10+
# scapy.contrib.status = loads
11+
"""
12+
scapy.contrib.ibeacon - Apple iBeacon Bluetooth LE proximity beacons.
13+
14+
Packet format documentation can be found at at:
15+
16+
* https://en.wikipedia.org/wiki/IBeacon#Packet_Structure_Byte_Map (public)
17+
* https://developer.apple.com/ibeacon/ (official, requires license)
18+
19+
"""
20+
21+
from scapy.fields import ByteEnumField, LenField, PacketListField, \
22+
ShortField, SignedByteField, UUIDField
23+
from scapy.layers.bluetooth import EIR_Hdr, EIR_Manufacturer_Specific_Data, \
24+
LowEnergyBeaconHelper
25+
from scapy.packet import bind_layers, Packet
26+
27+
APPLE_MFG = 0x004c
28+
29+
30+
class Apple_BLE_Submessage(Packet, LowEnergyBeaconHelper):
31+
"""
32+
A basic Apple submessage.
33+
"""
34+
35+
name = "Apple BLE submessage"
36+
fields_desc = [
37+
ByteEnumField("subtype", None, {
38+
0x02: "ibeacon",
39+
0x05: "airdrop",
40+
0x07: "airpods",
41+
0x09: "airplay_sink",
42+
0x0a: "airplay_src",
43+
0x0c: "handoff",
44+
0x10: "nearby",
45+
}),
46+
LenField("len", None, fmt="B")
47+
]
48+
49+
def extract_padding(self, s):
50+
# Needed to end each EIR_Element packet and make PacketListField work.
51+
return s[:self.len], s[self.len:]
52+
53+
# These methods are here in case you only want to send 1 submessage.
54+
# It creates an Apple_BLE_Frame to wrap your (single) Apple_BLE_Submessage.
55+
def build_frame(self):
56+
"""Wraps this submessage in a Apple_BLE_Frame."""
57+
return Apple_BLE_Frame(plist=[self])
58+
59+
def build_eir(self):
60+
"""See Apple_BLE_Frame.build_eir."""
61+
return self.build_frame().build_eir()
62+
63+
64+
class Apple_BLE_Frame(Packet, LowEnergyBeaconHelper):
65+
"""
66+
The wrapper for a BLE manufacturer-specific data advertisement from Apple
67+
devices.
68+
69+
Each advertisement is composed of one or multiple submessages.
70+
71+
The length of this field comes from the EIR_Hdr.
72+
"""
73+
name = "Apple BLE broadcast frame"
74+
fields_desc = [
75+
PacketListField("plist", None, Apple_BLE_Submessage)
76+
]
77+
78+
def build_eir(self):
79+
"""Builds a list of EIR messages to wrap this frame."""
80+
81+
return LowEnergyBeaconHelper.base_eir + [
82+
EIR_Hdr() / EIR_Manufacturer_Specific_Data() / self
83+
]
84+
85+
86+
class IBeacon_Data(Packet):
87+
"""
88+
iBeacon broadcast data frame. Composed on top of an Apple_BLE_Submessage.
89+
"""
90+
name = "iBeacon data"
91+
fields_desc = [
92+
UUIDField("uuid", None, uuid_fmt=UUIDField.FORMAT_BE),
93+
ShortField("major", None),
94+
ShortField("minor", None),
95+
SignedByteField("tx_power", None),
96+
]
97+
98+
99+
bind_layers(EIR_Manufacturer_Specific_Data, Apple_BLE_Frame,
100+
company_id=APPLE_MFG)
101+
bind_layers(Apple_BLE_Submessage, IBeacon_Data, subtype=2)

scapy/contrib/ibeacon.uts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
% iBeacon unit tests
2+
#
3+
# Type the following command to launch start the tests:
4+
# $ test/run_tests -P "load_contrib('ibeacon')" -t scapy/contrib/ibeacon.uts
5+
6+
+ iBeacon tests
7+
8+
= Presence check
9+
10+
Apple_BLE_Frame
11+
IBeacon_Data
12+
Apple_BLE_Submessage
13+
14+
= Apple multiple submessages
15+
16+
# Observed in the wild; handoff + nearby message.
17+
# Meaning unknown.
18+
d = hex_bytes('D6BE898E4024320CFB574D5A02011A1AFF4C000C0E009C6B8F40440F1583EC895148B410050318C0B525B8F7D4')
19+
p = BTLE(d)
20+
21+
assert len(p[Apple_BLE_Frame].plist) == 2
22+
assert p[Apple_BLE_Frame].plist[0].subtype == 0x0c # handoff
23+
assert (raw(p[Apple_BLE_Frame].plist[0].payload) ==
24+
hex_bytes('009c6b8f40440f1583ec895148b4'))
25+
assert p[Apple_BLE_Frame].plist[1].subtype == 0x10 # nearby
26+
assert raw(p[Apple_BLE_Frame].plist[1].payload) == hex_bytes('0318c0b525')
27+
28+
= iBeacon (decode LE Set Advertising Data)
29+
30+
# from https://en.wikipedia.org/wiki/IBeacon#Technical_details
31+
d = hex_bytes('1E02011A1AFF4C000215FB0B57A2822844CD913A94A122BA120600010002D100')
32+
p = HCI_Cmd_LE_Set_Advertising_Data(d)
33+
34+
assert len(p[Apple_BLE_Frame].plist) == 1
35+
assert p[IBeacon_Data].uuid == UUID("fb0b57a2-8228-44cd-913a-94a122ba1206")
36+
assert p[IBeacon_Data].major == 1
37+
assert p[IBeacon_Data].minor == 2
38+
assert p[IBeacon_Data].tx_power == -47
39+
40+
d2 = raw(p)
41+
assert d == d2
42+
43+
= iBeacon (encode LE Set Advertising Data)
44+
45+
d = hex_bytes('1E0201021AFF4C000215FB0B57A2822844CD913A94A122BA120600010002D100')
46+
p = Apple_BLE_Submessage()/IBeacon_Data(
47+
uuid='fb0b57a2-8228-44cd-913a-94a122ba1206',
48+
major=1, minor=2, tx_power=-47)
49+
50+
sap = p.build_set_advertising_data()[HCI_Cmd_LE_Set_Advertising_Data]
51+
assert d == raw(sap)
52+
53+
pa = Apple_BLE_Frame(plist=[p])
54+
sapa = pa.build_set_advertising_data()[HCI_Cmd_LE_Set_Advertising_Data]
55+
assert d == raw(sapa)
56+
57+
# Also try to build with Submessage directly
58+
sapa = p.build_set_advertising_data()[HCI_Cmd_LE_Set_Advertising_Data]
59+
assert d == raw(sapa)
60+
61+
= iBeacon (decode advertising frame)
62+
63+
# from https://en.wikipedia.org/wiki/IBeacon#Spoofing
64+
d = hex_bytes('043E2A02010001FCED16D4EED61E0201061AFF4C000215B9407F30F5F8466EAFF925556B57FE6DEDFCD416B6B4')
65+
p = HCI_Hdr(d)
66+
67+
assert p[HCI_LE_Meta_Advertising_Report].addr == 'd6:ee:d4:16:ed:fc'
68+
assert len(p[Apple_BLE_Frame].plist) == 1
69+
assert p[IBeacon_Data].uuid == UUID('b9407f30-f5f8-466e-aff9-25556b57fe6d')
70+

0 commit comments

Comments
 (0)